fix: refine tool summaries and scope discord tool
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
|
||||
### Fixes
|
||||
- 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
|
||||
- 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.
|
||||
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.*`).
|
||||
- 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.
|
||||
|
||||
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).
|
||||
- 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.
|
||||
- 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
|
||||
- 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).
|
||||
- `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).
|
||||
- The tool is only exposed when the current surface is Discord.
|
||||
|
||||
## Parameters (common)
|
||||
|
||||
|
||||
@@ -310,6 +310,7 @@ function resolvePromptSkills(
|
||||
export async function runEmbeddedPiAgent(params: {
|
||||
sessionId: string;
|
||||
sessionKey?: string;
|
||||
surface?: string;
|
||||
sessionFile: string;
|
||||
workspaceDir: string;
|
||||
config?: ClawdisConfig;
|
||||
@@ -414,6 +415,7 @@ export async function runEmbeddedPiAgent(params: {
|
||||
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
|
||||
const tools = createClawdisCodingTools({
|
||||
bash: params.config?.agent?.bash,
|
||||
surface: params.surface,
|
||||
});
|
||||
const machineName = await getMachineDisplayName();
|
||||
const runtimeInfo = {
|
||||
|
||||
@@ -83,6 +83,14 @@ describe("createClawdisCodingTools", () => {
|
||||
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 () => {
|
||||
const tools = createClawdisCodingTools();
|
||||
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?: {
|
||||
bash?: BashToolDefaults & ProcessToolDefaults;
|
||||
surface?: string;
|
||||
}): AnyAgentTool[] {
|
||||
const bashToolName = "bash";
|
||||
const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => {
|
||||
@@ -314,5 +326,9 @@ export function createClawdisCodingTools(options?: {
|
||||
createWhatsAppLoginTool(),
|
||||
...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: {
|
||||
sessionId: string;
|
||||
sessionKey?: string;
|
||||
surface?: string;
|
||||
sessionFile: string;
|
||||
workspaceDir: string;
|
||||
config: ClawdisConfig;
|
||||
@@ -1871,6 +1872,7 @@ export async function getReplyFromConfig(
|
||||
run: {
|
||||
sessionId: sessionIdFinal,
|
||||
sessionKey,
|
||||
surface: sessionCtx.Surface?.trim().toLowerCase() || undefined,
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
@@ -1942,6 +1944,7 @@ export async function getReplyFromConfig(
|
||||
runResult = await runEmbeddedPiAgent({
|
||||
sessionId: queued.run.sessionId,
|
||||
sessionKey: queued.run.sessionKey,
|
||||
surface: queued.run.surface,
|
||||
sessionFile: queued.run.sessionFile,
|
||||
workspaceDir: queued.run.workspaceDir,
|
||||
config: queued.run.config,
|
||||
@@ -2061,6 +2064,7 @@ export async function getReplyFromConfig(
|
||||
runResult = await runEmbeddedPiAgent({
|
||||
sessionId: sessionIdFinal,
|
||||
sessionKey,
|
||||
surface: sessionCtx.Surface?.trim().toLowerCase() || undefined,
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("tool meta formatting", () => {
|
||||
"note",
|
||||
"a→b",
|
||||
]);
|
||||
expect(out).toMatch(/^\[🛠️ fs]/);
|
||||
expect(out).toMatch(/^🛠️ fs/);
|
||||
expect(out).toContain("~/dir/{a.txt, b.txt}");
|
||||
expect(out).toContain("note");
|
||||
expect(out).toContain("a→b");
|
||||
@@ -43,8 +43,8 @@ describe("tool meta formatting", () => {
|
||||
|
||||
it("formats prefixes with default labels", () => {
|
||||
vi.stubEnv("HOME", "/Users/test");
|
||||
expect(formatToolPrefix(undefined, undefined)).toBe("[🛠️ tool]");
|
||||
expect(formatToolPrefix("x", "/Users/test/a.txt")).toBe("[🛠️ x ~/a.txt]");
|
||||
expect(formatToolPrefix(undefined, undefined)).toBe("🛠️ tool");
|
||||
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_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 {
|
||||
const home = process.env.HOME;
|
||||
if (home && (p === home || p.startsWith(`${home}/`)))
|
||||
@@ -23,7 +45,7 @@ export function formatToolAggregate(
|
||||
): string {
|
||||
const filtered = (metas ?? []).filter(Boolean).map(shortenMeta);
|
||||
const label = toolName?.trim() || "tool";
|
||||
const prefix = `[🛠️ ${label}]`;
|
||||
const prefix = `${resolveToolEmoji(label)} ${label}`;
|
||||
if (!filtered.length) return prefix;
|
||||
|
||||
const rawSegments: string[] = [];
|
||||
@@ -53,13 +75,14 @@ export function formatToolAggregate(
|
||||
});
|
||||
|
||||
const allSegments = [...rawSegments, ...segments];
|
||||
return `${prefix} ${allSegments.join("; ")}`;
|
||||
return `${prefix}: ${allSegments.join("; ")}`;
|
||||
}
|
||||
|
||||
export function formatToolPrefix(toolName?: string, meta?: string) {
|
||||
const label = toolName?.trim() || "tool";
|
||||
const emoji = resolveToolEmoji(label);
|
||||
const extra = meta?.trim() ? shortenMeta(meta) : undefined;
|
||||
return extra ? `[🛠️ ${label} ${extra}]` : `[🛠️ ${label}]`;
|
||||
return extra ? `${emoji} ${label}: ${extra}` : `${emoji} ${label}`;
|
||||
}
|
||||
|
||||
export function createToolDebouncer(
|
||||
|
||||
@@ -329,9 +329,17 @@ export async function agentCommand(
|
||||
|
||||
let result: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
||||
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({
|
||||
sessionId,
|
||||
sessionKey,
|
||||
surface,
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
|
||||
@@ -255,9 +255,16 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
registerAgentRunContext(cronSession.sessionEntry.sessionId, {
|
||||
sessionKey: params.sessionKey,
|
||||
});
|
||||
const surface =
|
||||
resolvedDelivery.channel &&
|
||||
resolvedDelivery.channel !== "last" &&
|
||||
resolvedDelivery.channel !== "none"
|
||||
? resolvedDelivery.channel
|
||||
: undefined;
|
||||
runResult = await runEmbeddedPiAgent({
|
||||
sessionId: cronSession.sessionEntry.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
surface,
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
config: params.cfg,
|
||||
|
||||
@@ -1591,8 +1591,8 @@ describe("web auto-reply", () => {
|
||||
_ctx,
|
||||
opts?: { onToolResult?: (r: { text: string }) => Promise<void> },
|
||||
) => {
|
||||
await opts?.onToolResult?.({ text: "[🛠️ tool1]" });
|
||||
await opts?.onToolResult?.({ text: "[🛠️ tool2]" });
|
||||
await opts?.onToolResult?.({ text: "🛠️ tool1" });
|
||||
await opts?.onToolResult?.({ text: "🛠️ tool2" });
|
||||
return { text: "final" };
|
||||
},
|
||||
);
|
||||
@@ -1611,7 +1611,7 @@ describe("web auto-reply", () => {
|
||||
});
|
||||
|
||||
const replies = reply.mock.calls.map((call) => call[0]);
|
||||
expect(replies).toEqual(["🦞 [🛠️ tool1]", "🦞 [🛠️ tool2]", "🦞 final"]);
|
||||
expect(replies).toEqual(["🦞 🛠️ tool1", "🦞 🛠️ tool2", "🦞 final"]);
|
||||
resetLoadConfigMock();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user