Verbose: send tool result metadata only
This commit is contained in:
@@ -67,6 +67,15 @@ describe("agent buildArgs + parseOutput helpers", () => {
|
||||
expect((parsed.meta?.usage as { output?: number })?.output).toBe(5);
|
||||
});
|
||||
|
||||
it("piSpec carries tool names when present", () => {
|
||||
const stdout =
|
||||
'{"type":"message_end","message":{"role":"tool_result","name":"bash","content":[{"type":"text","text":"ls output"}]}}';
|
||||
const parsed = piSpec.parseOutput(stdout);
|
||||
const tool = parsed.toolResults?.[0] as { text?: string; toolName?: string };
|
||||
expect(tool?.text).toBe("ls output");
|
||||
expect(tool?.toolName).toBe("bash");
|
||||
});
|
||||
|
||||
it("codexSpec parses agent_message and aggregates usage", () => {
|
||||
const stdout = [
|
||||
'{"type":"item.completed","item":{"type":"agent_message","text":"hi there"}}',
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import path from "node:path";
|
||||
|
||||
import type { AgentMeta, AgentParseResult, AgentSpec } from "./types.js";
|
||||
import type {
|
||||
AgentMeta,
|
||||
AgentParseResult,
|
||||
AgentSpec,
|
||||
AgentToolResult,
|
||||
} from "./types.js";
|
||||
|
||||
type PiAssistantMessage = {
|
||||
role?: string;
|
||||
@@ -9,15 +14,37 @@ type PiAssistantMessage = {
|
||||
model?: string;
|
||||
provider?: string;
|
||||
stopReason?: string;
|
||||
name?: string;
|
||||
toolName?: string;
|
||||
tool_call_id?: string;
|
||||
toolCallId?: string;
|
||||
};
|
||||
|
||||
function inferToolName(msg: PiAssistantMessage): string | undefined {
|
||||
const candidates = [
|
||||
msg.toolName,
|
||||
msg.name,
|
||||
msg.toolCallId,
|
||||
msg.tool_call_id,
|
||||
]
|
||||
.map((c) => (typeof c === "string" ? c.trim() : ""))
|
||||
.filter(Boolean);
|
||||
if (candidates.length) return candidates[0];
|
||||
|
||||
if (msg.role && msg.role.includes(":")) {
|
||||
const suffix = msg.role.split(":").slice(1).join(":").trim();
|
||||
if (suffix) return suffix;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parsePiJson(raw: string): AgentParseResult {
|
||||
const lines = raw.split(/\n+/).filter((l) => l.trim().startsWith("{"));
|
||||
|
||||
// Collect only completed assistant messages (skip streaming updates/toolcalls).
|
||||
const texts: string[] = [];
|
||||
const toolResults: string[] = [];
|
||||
const toolResults: AgentToolResult[] = [];
|
||||
let lastAssistant: PiAssistantMessage | undefined;
|
||||
let lastPushed: string | undefined;
|
||||
|
||||
@@ -59,7 +86,9 @@ function parsePiJson(raw: string): AgentParseResult {
|
||||
.map((c) => c.text)
|
||||
.join("\n")
|
||||
.trim();
|
||||
if (toolText) toolResults.push(toolText);
|
||||
if (toolText) {
|
||||
toolResults.push({ text: toolText, toolName: inferToolName(msg) });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed lines
|
||||
|
||||
@@ -15,11 +15,16 @@ export type AgentMeta = {
|
||||
extra?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type AgentToolResult = {
|
||||
text: string;
|
||||
toolName?: string;
|
||||
};
|
||||
|
||||
export type AgentParseResult = {
|
||||
// Plural to support agents that emit multiple assistant turns per prompt.
|
||||
texts?: string[];
|
||||
mediaUrls?: string[];
|
||||
toolResults?: string[];
|
||||
toolResults?: Array<string | AgentToolResult>;
|
||||
meta?: AgentMeta;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { type AgentKind, getAgentSpec } from "../agents/index.js";
|
||||
import type { AgentMeta } from "../agents/types.js";
|
||||
import type { AgentMeta, AgentToolResult } from "../agents/types.js";
|
||||
import type { WarelayConfig } from "../config/config.js";
|
||||
import { isVerbose, logVerbose } from "../globals.js";
|
||||
import { logError } from "../logger.js";
|
||||
@@ -53,6 +53,51 @@ export type CommandReplyResult = {
|
||||
meta: CommandReplyMeta;
|
||||
};
|
||||
|
||||
type ToolMessageLike = {
|
||||
name?: string;
|
||||
toolName?: string;
|
||||
tool_call_id?: string;
|
||||
toolCallId?: string;
|
||||
role?: string;
|
||||
};
|
||||
|
||||
function inferToolName(message?: ToolMessageLike): string | undefined {
|
||||
if (!message) return undefined;
|
||||
const candidates = [
|
||||
message.toolName,
|
||||
message.name,
|
||||
message.toolCallId,
|
||||
message.tool_call_id,
|
||||
]
|
||||
.map((c) => (typeof c === "string" ? c.trim() : ""))
|
||||
.filter(Boolean);
|
||||
if (candidates.length) return candidates[0];
|
||||
|
||||
if (message.role && message.role.includes(":")) {
|
||||
const suffix = message.role.split(":").slice(1).join(":").trim();
|
||||
if (suffix) return suffix;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeToolResults(
|
||||
toolResults?: Array<string | AgentToolResult>,
|
||||
): AgentToolResult[] {
|
||||
if (!toolResults) return [];
|
||||
return toolResults
|
||||
.map((tr) => (typeof tr === "string" ? { text: tr } : tr))
|
||||
.map((tr) => ({
|
||||
text: (tr.text ?? "").trim(),
|
||||
toolName: tr.toolName?.trim() || undefined,
|
||||
}))
|
||||
.filter((tr) => tr.text.length > 0);
|
||||
}
|
||||
|
||||
function formatToolPrefix(toolName?: string) {
|
||||
const label = toolName?.trim() || "tool";
|
||||
return `[🛠️ ${label}]`;
|
||||
}
|
||||
|
||||
export function summarizeClaudeMetadata(payload: unknown): string | undefined {
|
||||
if (!payload || typeof payload !== "object") return undefined;
|
||||
const obj = payload as Record<string, unknown>;
|
||||
@@ -289,23 +334,14 @@ export async function runCommandReply(
|
||||
ev.message?.role === "tool_result" &&
|
||||
Array.isArray(ev.message.content)
|
||||
) {
|
||||
const text = (
|
||||
ev.message.content as Array<{ text?: string }>
|
||||
)
|
||||
.map((c) => c.text)
|
||||
.filter((t): t is string => !!t)
|
||||
.join("\n")
|
||||
.trim();
|
||||
if (text) {
|
||||
const { text: cleanedText, mediaUrls: mediaFound } =
|
||||
splitMediaFromOutput(`🛠️ ${text}`);
|
||||
void onPartialReply({
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaFound?.length
|
||||
? mediaFound
|
||||
: undefined,
|
||||
} as ReplyPayload);
|
||||
}
|
||||
const toolName = inferToolName(ev.message);
|
||||
const prefix = formatToolPrefix(toolName);
|
||||
const { text: cleanedText, mediaUrls: mediaFound } =
|
||||
splitMediaFromOutput(prefix);
|
||||
void onPartialReply({
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaFound?.length ? mediaFound : undefined,
|
||||
} as ReplyPayload);
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed lines
|
||||
@@ -341,8 +377,7 @@ export async function runCommandReply(
|
||||
// Collect assistant texts and tool results from parseOutput (tau RPC can emit many).
|
||||
const parsedTexts =
|
||||
parsed?.texts?.map((t) => t.trim()).filter(Boolean) ?? [];
|
||||
const parsedToolResults =
|
||||
parsed?.toolResults?.map((t) => t.trim()).filter(Boolean) ?? [];
|
||||
const parsedToolResults = normalizeToolResults(parsed?.toolResults);
|
||||
|
||||
type ReplyItem = { text: string; media?: string[] };
|
||||
const replyItems: ReplyItem[] = [];
|
||||
@@ -352,7 +387,7 @@ export async function runCommandReply(
|
||||
|
||||
if (includeToolResultsInline) {
|
||||
for (const tr of parsedToolResults) {
|
||||
const prefixed = `🛠️ ${tr}`;
|
||||
const prefixed = formatToolPrefix(tr.toolName);
|
||||
const { text: cleanedText, mediaUrls: mediaFound } =
|
||||
splitMediaFromOutput(prefixed);
|
||||
replyItems.push({
|
||||
|
||||
@@ -719,7 +719,7 @@ describe("config and templating", () => {
|
||||
const rpcSpy = vi.spyOn(tauRpc, "runPiRpc").mockResolvedValue({
|
||||
stdout:
|
||||
'{"type":"message","message":{"role":"assistant","content":[{"type":"text","text":"summary"}]}}\n' +
|
||||
'{"type":"message_end","message":{"role":"tool_result","content":[{"type":"text","text":"ls output"}]}}',
|
||||
'{"type":"message_end","message":{"role":"tool_result","name":"bash","content":[{"type":"text","text":"ls output"}]}}',
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
@@ -744,8 +744,7 @@ describe("config and templating", () => {
|
||||
expect(rpcSpy).toHaveBeenCalled();
|
||||
const payloads = Array.isArray(res) ? res : res ? [res] : [];
|
||||
expect(payloads.length).toBeGreaterThanOrEqual(2);
|
||||
expect(payloads[0]?.text).toContain("🛠️");
|
||||
expect(payloads[0]?.text).toContain("ls output");
|
||||
expect(payloads[0]?.text).toBe("[🛠️ bash]");
|
||||
expect(payloads[1]?.text).toContain("summary");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user