fix: refine tool summaries and scope discord tool
This commit is contained in:
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Telegram: chunk block-stream replies to avoid “message is too long” errors (#124) — thanks @mukhtharcm.
|
- Telegram: chunk block-stream replies to avoid “message is too long” errors (#124) — thanks @mukhtharcm.
|
||||||
|
- Agent tools: scope the Discord tool to Discord surface runs.
|
||||||
|
- Agent tools: format verbose tool summaries without brackets, with unique emojis and `tool: detail` style.
|
||||||
|
|
||||||
### Docs
|
### Docs
|
||||||
- Skills: add Sheets/Docs examples to gog skill (#128) — thanks @mbelinky.
|
- Skills: add Sheets/Docs examples to gog skill (#128) — thanks @mbelinky.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa
|
|||||||
9. Optional slash commands: enable `discord.slashCommand` to accept user-installed app commands (ephemeral replies). Slash invocations respect the same DM/guild allowlists.
|
9. Optional slash commands: enable `discord.slashCommand` to accept user-installed app commands (ephemeral replies). Slash invocations respect the same DM/guild allowlists.
|
||||||
10. Optional guild context history: set `discord.historyLimit` (default 20) to include the last N guild messages as context when replying to a mention. Set `0` to disable.
|
10. Optional guild context history: set `discord.historyLimit` (default 20) to include the last N guild messages as context when replying to a mention. Set `0` to disable.
|
||||||
11. Reactions: the agent can trigger reactions via the `discord` tool (gated by `discord.actions.*`).
|
11. Reactions: the agent can trigger reactions via the `discord` tool (gated by `discord.actions.*`).
|
||||||
|
- The `discord` tool is only exposed when the current surface is Discord.
|
||||||
12. Slash commands use isolated session keys (`${sessionPrefix}:${userId}`) rather than the shared `main` session.
|
12. Slash commands use isolated session keys (`${sessionPrefix}:${userId}`) rather than the shared `main` session.
|
||||||
|
|
||||||
Note: Discord does not provide a simple username → id lookup without extra guild context, so prefer ids or `<@id>` mentions for DM delivery targets.
|
Note: Discord does not provide a simple username → id lookup without extra guild context, so prefer ids or `<@id>` mentions for DM delivery targets.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ read_when:
|
|||||||
- Levels: `on|full` or `off` (default).
|
- Levels: `on|full` or `off` (default).
|
||||||
- Directive-only message toggles session verbose and replies `Verbose logging enabled.` / `Verbose logging disabled.`; invalid levels return a hint without changing state.
|
- Directive-only message toggles session verbose and replies `Verbose logging enabled.` / `Verbose logging disabled.`; invalid levels return a hint without changing state.
|
||||||
- Inline directive affects only that message; session/global defaults apply otherwise.
|
- Inline directive affects only that message; session/global defaults apply otherwise.
|
||||||
- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool result back as its own metadata-only message, prefixed with `[🛠️ <tool-name> <arg>]` when available (path/command); the tool output itself is not forwarded. These tool summaries are sent as soon as each tool finishes (separate bubbles), not as streaming deltas. If you toggle `/verbose on|off` while a run is in-flight, subsequent tool bubbles honor the new setting.
|
- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool result back as its own metadata-only message, prefixed with `<emoji> <tool-name>: <arg>` when available (path/command); the tool output itself is not forwarded. These tool summaries are sent as soon as each tool finishes (separate bubbles), not as streaming deltas. If you toggle `/verbose on|off` while a run is in-flight, subsequent tool bubbles honor the new setting.
|
||||||
|
|
||||||
## Heartbeats
|
## Heartbeats
|
||||||
- Heartbeat probe body is `HEARTBEAT`. Inline directives in a heartbeat message apply as usual (but avoid changing session defaults from heartbeats).
|
- Heartbeat probe body is `HEARTBEAT`. Inline directives in a heartbeat message apply as usual (but avoid changing session defaults from heartbeats).
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ Notes:
|
|||||||
- `reactions` returns per-emoji user lists (limited to 100 per reaction).
|
- `reactions` returns per-emoji user lists (limited to 100 per reaction).
|
||||||
- `discord.actions.*` gates Discord tool actions; `roles` + `moderation` default to `false`.
|
- `discord.actions.*` gates Discord tool actions; `roles` + `moderation` default to `false`.
|
||||||
- `searchMessages` follows the Discord preview spec (limit max 25, channel/author filters accept arrays).
|
- `searchMessages` follows the Discord preview spec (limit max 25, channel/author filters accept arrays).
|
||||||
|
- The tool is only exposed when the current surface is Discord.
|
||||||
|
|
||||||
## Parameters (common)
|
## Parameters (common)
|
||||||
|
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ function resolvePromptSkills(
|
|||||||
export async function runEmbeddedPiAgent(params: {
|
export async function runEmbeddedPiAgent(params: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
|
surface?: string;
|
||||||
sessionFile: string;
|
sessionFile: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
config?: ClawdisConfig;
|
config?: ClawdisConfig;
|
||||||
@@ -414,6 +415,7 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
|
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
|
||||||
const tools = createClawdisCodingTools({
|
const tools = createClawdisCodingTools({
|
||||||
bash: params.config?.agent?.bash,
|
bash: params.config?.agent?.bash,
|
||||||
|
surface: params.surface,
|
||||||
});
|
});
|
||||||
const machineName = await getMachineDisplayName();
|
const machineName = await getMachineDisplayName();
|
||||||
const runtimeInfo = {
|
const runtimeInfo = {
|
||||||
|
|||||||
@@ -83,6 +83,14 @@ describe("createClawdisCodingTools", () => {
|
|||||||
expect(tools.some((tool) => tool.name === "process")).toBe(true);
|
expect(tools.some((tool) => tool.name === "process")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("scopes discord tool to discord surface", () => {
|
||||||
|
const other = createClawdisCodingTools({ surface: "whatsapp" });
|
||||||
|
expect(other.some((tool) => tool.name === "discord")).toBe(false);
|
||||||
|
|
||||||
|
const discord = createClawdisCodingTools({ surface: "discord" });
|
||||||
|
expect(discord.some((tool) => tool.name === "discord")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps read tool image metadata intact", async () => {
|
it("keeps read tool image metadata intact", async () => {
|
||||||
const tools = createClawdisCodingTools();
|
const tools = createClawdisCodingTools();
|
||||||
const readTool = tools.find((tool) => tool.name === "read");
|
const readTool = tools.find((tool) => tool.name === "read");
|
||||||
|
|||||||
@@ -294,8 +294,20 @@ function createClawdisReadTool(base: AnyAgentTool): AnyAgentTool {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSurface(surface?: string): string | undefined {
|
||||||
|
const trimmed = surface?.trim().toLowerCase();
|
||||||
|
return trimmed ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIncludeDiscordTool(surface?: string): boolean {
|
||||||
|
const normalized = normalizeSurface(surface);
|
||||||
|
if (!normalized) return false;
|
||||||
|
return normalized === "discord" || normalized.startsWith("discord:");
|
||||||
|
}
|
||||||
|
|
||||||
export function createClawdisCodingTools(options?: {
|
export function createClawdisCodingTools(options?: {
|
||||||
bash?: BashToolDefaults & ProcessToolDefaults;
|
bash?: BashToolDefaults & ProcessToolDefaults;
|
||||||
|
surface?: string;
|
||||||
}): AnyAgentTool[] {
|
}): AnyAgentTool[] {
|
||||||
const bashToolName = "bash";
|
const bashToolName = "bash";
|
||||||
const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => {
|
const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => {
|
||||||
@@ -314,5 +326,9 @@ export function createClawdisCodingTools(options?: {
|
|||||||
createWhatsAppLoginTool(),
|
createWhatsAppLoginTool(),
|
||||||
...createClawdisTools(),
|
...createClawdisTools(),
|
||||||
];
|
];
|
||||||
return tools.map(normalizeToolParameters);
|
const allowDiscord = shouldIncludeDiscordTool(options?.surface);
|
||||||
|
const filtered = allowDiscord
|
||||||
|
? tools
|
||||||
|
: tools.filter((tool) => tool.name !== "discord");
|
||||||
|
return filtered.map(normalizeToolParameters);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ type FollowupRun = {
|
|||||||
run: {
|
run: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
|
surface?: string;
|
||||||
sessionFile: string;
|
sessionFile: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
config: ClawdisConfig;
|
config: ClawdisConfig;
|
||||||
@@ -1871,6 +1872,7 @@ export async function getReplyFromConfig(
|
|||||||
run: {
|
run: {
|
||||||
sessionId: sessionIdFinal,
|
sessionId: sessionIdFinal,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
|
surface: sessionCtx.Surface?.trim().toLowerCase() || undefined,
|
||||||
sessionFile,
|
sessionFile,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@@ -1942,6 +1944,7 @@ export async function getReplyFromConfig(
|
|||||||
runResult = await runEmbeddedPiAgent({
|
runResult = await runEmbeddedPiAgent({
|
||||||
sessionId: queued.run.sessionId,
|
sessionId: queued.run.sessionId,
|
||||||
sessionKey: queued.run.sessionKey,
|
sessionKey: queued.run.sessionKey,
|
||||||
|
surface: queued.run.surface,
|
||||||
sessionFile: queued.run.sessionFile,
|
sessionFile: queued.run.sessionFile,
|
||||||
workspaceDir: queued.run.workspaceDir,
|
workspaceDir: queued.run.workspaceDir,
|
||||||
config: queued.run.config,
|
config: queued.run.config,
|
||||||
@@ -2061,6 +2064,7 @@ export async function getReplyFromConfig(
|
|||||||
runResult = await runEmbeddedPiAgent({
|
runResult = await runEmbeddedPiAgent({
|
||||||
sessionId: sessionIdFinal,
|
sessionId: sessionIdFinal,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
|
surface: sessionCtx.Surface?.trim().toLowerCase() || undefined,
|
||||||
sessionFile,
|
sessionFile,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ describe("tool meta formatting", () => {
|
|||||||
"note",
|
"note",
|
||||||
"a→b",
|
"a→b",
|
||||||
]);
|
]);
|
||||||
expect(out).toMatch(/^\[🛠️ fs]/);
|
expect(out).toMatch(/^🛠️ fs/);
|
||||||
expect(out).toContain("~/dir/{a.txt, b.txt}");
|
expect(out).toContain("~/dir/{a.txt, b.txt}");
|
||||||
expect(out).toContain("note");
|
expect(out).toContain("note");
|
||||||
expect(out).toContain("a→b");
|
expect(out).toContain("a→b");
|
||||||
@@ -43,8 +43,8 @@ describe("tool meta formatting", () => {
|
|||||||
|
|
||||||
it("formats prefixes with default labels", () => {
|
it("formats prefixes with default labels", () => {
|
||||||
vi.stubEnv("HOME", "/Users/test");
|
vi.stubEnv("HOME", "/Users/test");
|
||||||
expect(formatToolPrefix(undefined, undefined)).toBe("[🛠️ tool]");
|
expect(formatToolPrefix(undefined, undefined)).toBe("🛠️ tool");
|
||||||
expect(formatToolPrefix("x", "/Users/test/a.txt")).toBe("[🛠️ x ~/a.txt]");
|
expect(formatToolPrefix("x", "/Users/test/a.txt")).toBe("🛠️ x: ~/a.txt");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,28 @@
|
|||||||
export const TOOL_RESULT_DEBOUNCE_MS = 500;
|
export const TOOL_RESULT_DEBOUNCE_MS = 500;
|
||||||
export const TOOL_RESULT_FLUSH_COUNT = 5;
|
export const TOOL_RESULT_FLUSH_COUNT = 5;
|
||||||
|
|
||||||
|
const TOOL_EMOJI_BY_NAME: Record<string, string> = {
|
||||||
|
bash: "💻",
|
||||||
|
process: "🧰",
|
||||||
|
read: "📖",
|
||||||
|
write: "✍️",
|
||||||
|
edit: "📝",
|
||||||
|
attach: "📎",
|
||||||
|
clawdis_browser: "🌐",
|
||||||
|
clawdis_canvas: "🖼️",
|
||||||
|
clawdis_nodes: "📱",
|
||||||
|
clawdis_cron: "⏰",
|
||||||
|
clawdis_gateway: "🔌",
|
||||||
|
whatsapp_login: "🟢",
|
||||||
|
discord: "💬",
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveToolEmoji(toolName?: string): string {
|
||||||
|
const key = toolName?.trim().toLowerCase();
|
||||||
|
if (key && TOOL_EMOJI_BY_NAME[key]) return TOOL_EMOJI_BY_NAME[key];
|
||||||
|
return "🛠️";
|
||||||
|
}
|
||||||
|
|
||||||
export function shortenPath(p: string): string {
|
export function shortenPath(p: string): string {
|
||||||
const home = process.env.HOME;
|
const home = process.env.HOME;
|
||||||
if (home && (p === home || p.startsWith(`${home}/`)))
|
if (home && (p === home || p.startsWith(`${home}/`)))
|
||||||
@@ -23,7 +45,7 @@ export function formatToolAggregate(
|
|||||||
): string {
|
): string {
|
||||||
const filtered = (metas ?? []).filter(Boolean).map(shortenMeta);
|
const filtered = (metas ?? []).filter(Boolean).map(shortenMeta);
|
||||||
const label = toolName?.trim() || "tool";
|
const label = toolName?.trim() || "tool";
|
||||||
const prefix = `[🛠️ ${label}]`;
|
const prefix = `${resolveToolEmoji(label)} ${label}`;
|
||||||
if (!filtered.length) return prefix;
|
if (!filtered.length) return prefix;
|
||||||
|
|
||||||
const rawSegments: string[] = [];
|
const rawSegments: string[] = [];
|
||||||
@@ -53,13 +75,14 @@ export function formatToolAggregate(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const allSegments = [...rawSegments, ...segments];
|
const allSegments = [...rawSegments, ...segments];
|
||||||
return `${prefix} ${allSegments.join("; ")}`;
|
return `${prefix}: ${allSegments.join("; ")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatToolPrefix(toolName?: string, meta?: string) {
|
export function formatToolPrefix(toolName?: string, meta?: string) {
|
||||||
const label = toolName?.trim() || "tool";
|
const label = toolName?.trim() || "tool";
|
||||||
|
const emoji = resolveToolEmoji(label);
|
||||||
const extra = meta?.trim() ? shortenMeta(meta) : undefined;
|
const extra = meta?.trim() ? shortenMeta(meta) : undefined;
|
||||||
return extra ? `[🛠️ ${label} ${extra}]` : `[🛠️ ${label}]`;
|
return extra ? `${emoji} ${label}: ${extra}` : `${emoji} ${label}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createToolDebouncer(
|
export function createToolDebouncer(
|
||||||
|
|||||||
@@ -329,9 +329,17 @@ export async function agentCommand(
|
|||||||
|
|
||||||
let result: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
let result: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
||||||
try {
|
try {
|
||||||
|
const surface =
|
||||||
|
opts.surface?.trim().toLowerCase() ||
|
||||||
|
(() => {
|
||||||
|
const raw = opts.provider?.trim().toLowerCase();
|
||||||
|
if (!raw) return undefined;
|
||||||
|
return raw === "imsg" ? "imessage" : raw;
|
||||||
|
})();
|
||||||
result = await runEmbeddedPiAgent({
|
result = await runEmbeddedPiAgent({
|
||||||
sessionId,
|
sessionId,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
|
surface,
|
||||||
sessionFile,
|
sessionFile,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
|||||||
@@ -255,9 +255,16 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
registerAgentRunContext(cronSession.sessionEntry.sessionId, {
|
registerAgentRunContext(cronSession.sessionEntry.sessionId, {
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
});
|
});
|
||||||
|
const surface =
|
||||||
|
resolvedDelivery.channel &&
|
||||||
|
resolvedDelivery.channel !== "last" &&
|
||||||
|
resolvedDelivery.channel !== "none"
|
||||||
|
? resolvedDelivery.channel
|
||||||
|
: undefined;
|
||||||
runResult = await runEmbeddedPiAgent({
|
runResult = await runEmbeddedPiAgent({
|
||||||
sessionId: cronSession.sessionEntry.sessionId,
|
sessionId: cronSession.sessionEntry.sessionId,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
|
surface,
|
||||||
sessionFile,
|
sessionFile,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
config: params.cfg,
|
config: params.cfg,
|
||||||
|
|||||||
@@ -1591,8 +1591,8 @@ describe("web auto-reply", () => {
|
|||||||
_ctx,
|
_ctx,
|
||||||
opts?: { onToolResult?: (r: { text: string }) => Promise<void> },
|
opts?: { onToolResult?: (r: { text: string }) => Promise<void> },
|
||||||
) => {
|
) => {
|
||||||
await opts?.onToolResult?.({ text: "[🛠️ tool1]" });
|
await opts?.onToolResult?.({ text: "🛠️ tool1" });
|
||||||
await opts?.onToolResult?.({ text: "[🛠️ tool2]" });
|
await opts?.onToolResult?.({ text: "🛠️ tool2" });
|
||||||
return { text: "final" };
|
return { text: "final" };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1611,7 +1611,7 @@ describe("web auto-reply", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const replies = reply.mock.calls.map((call) => call[0]);
|
const replies = reply.mock.calls.map((call) => call[0]);
|
||||||
expect(replies).toEqual(["🦞 [🛠️ tool1]", "🦞 [🛠️ tool2]", "🦞 final"]);
|
expect(replies).toEqual(["🦞 🛠️ tool1", "🦞 🛠️ tool2", "🦞 final"]);
|
||||||
resetLoadConfigMock();
|
resetLoadConfigMock();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user