feat: expand preValidateToolCall rules, add traceId+batch summary, extend log protocol to all agents
- Add validation for web_search/google_search (query length), sandbox_run/code_execute (empty code + dangerous commands), doc_create/report_generate/reply_draft (title/topic required) - Add traceId field to ExecutionLogEntry, generate per-request traceId in enterprise tool-executor with batch summary log - Add structured console.log tool_exec events to searcher, coder, writer tool-executors (no state changes) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
b22d13415a
commit
2f201bf1f0
@@ -35,10 +35,13 @@ export async function toolExecutorNode(
|
||||
content: string;
|
||||
}> = [];
|
||||
|
||||
const traceId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
const executions = toolCalls.map(async (tc) => {
|
||||
const name = tc.name;
|
||||
const args = tc.args;
|
||||
const id = tc.id ?? "";
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
@@ -62,6 +65,13 @@ export async function toolExecutorNode(
|
||||
},
|
||||
{ message: lastAiMessage },
|
||||
);
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: "code_execute",
|
||||
status: result.exit_code === 0 ? "success" : "partial_success",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: `language: ${parsed.language ?? "python"}, code: ${parsed.code.slice(0, 150)}`,
|
||||
traceId,
|
||||
}));
|
||||
return {
|
||||
role: "tool" as const,
|
||||
tool_call_id: id,
|
||||
@@ -74,6 +84,13 @@ export async function toolExecutorNode(
|
||||
// Install packages by running pip/npm in sandbox
|
||||
const installCmd = `pip install ${parsed.packages.join(" ")}`;
|
||||
const result = await sandboxRun(installCmd, "bash");
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: "code_install",
|
||||
status: result.exit_code === 0 ? "success" : "error",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: `packages: ${parsed.packages.join(", ")}`.slice(0, 200),
|
||||
traceId,
|
||||
}));
|
||||
return {
|
||||
role: "tool" as const,
|
||||
tool_call_id: id,
|
||||
@@ -93,6 +110,13 @@ export async function toolExecutorNode(
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: name, status: "error",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: JSON.stringify(args).slice(0, 200),
|
||||
errorMessage: e instanceof Error ? e.message : String(e),
|
||||
traceId,
|
||||
}));
|
||||
// Push a friendly sandbox-result card showing the failure
|
||||
ui.push(
|
||||
{
|
||||
|
||||
@@ -184,6 +184,37 @@ function preValidateToolCall(
|
||||
}
|
||||
}
|
||||
|
||||
// web_search / google_search / web_search_deep: query must be non-empty and >= 3 chars
|
||||
if (["web_search", "google_search", "web_search_deep"].includes(name)) {
|
||||
const query = String(args.query ?? "").trim();
|
||||
if (!query || query.length < 3) {
|
||||
return `${name} 调用被拦截:搜索词过短或为空`;
|
||||
}
|
||||
}
|
||||
|
||||
// sandbox_run / code_execute: code must be non-empty, block dangerous commands
|
||||
if (["sandbox_run", "code_execute"].includes(name)) {
|
||||
const code = String(args.code ?? "").trim();
|
||||
if (!code) {
|
||||
return `${name} 调用被拦截:代码内容为空`;
|
||||
}
|
||||
const DANGEROUS = ["rm -rf", "dd if=", "mkfs", ":(){:|:&};:"];
|
||||
for (const d of DANGEROUS) {
|
||||
if (code.includes(d)) {
|
||||
return `${name} 调用被拦截:检测到危险命令 "${d}"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doc_create / report_generate / reply_draft: must have title or topic
|
||||
if (["doc_create", "report_generate", "reply_draft"].includes(name)) {
|
||||
const title = String(args.title ?? "").trim();
|
||||
const topic = String(args.topic ?? args.subject ?? "").trim();
|
||||
if (!title && !topic) {
|
||||
return `${name} 调用被拦截:文档标题或主题不能为空`;
|
||||
}
|
||||
}
|
||||
|
||||
// chart_generate: must have prior successful tool data in this conversation
|
||||
if (name === "chart_generate") {
|
||||
const hasData = state.execution_log?.some(
|
||||
@@ -225,6 +256,7 @@ export async function toolExecutorNode(
|
||||
content: string;
|
||||
}> = [];
|
||||
|
||||
const traceId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const statusList: ToolExecStatus[] = [];
|
||||
const executionLog: ExecutionLogEntry[] = [];
|
||||
|
||||
@@ -833,6 +865,24 @@ export async function toolExecutorNode(
|
||||
}
|
||||
}
|
||||
|
||||
// Stamp traceId on all execution log entries
|
||||
for (const entry of executionLog) {
|
||||
entry.traceId = traceId;
|
||||
}
|
||||
|
||||
// Emit aggregated batch summary for Azure log stream filtering
|
||||
const summary = {
|
||||
event: "tool_batch_summary",
|
||||
traceId,
|
||||
totalTools: executionLog.length,
|
||||
succeeded: executionLog.filter(e => e.status === "success" || e.status === "fallback_success").length,
|
||||
failed: executionLog.filter(e => e.status === "error").length,
|
||||
partial: executionLog.filter(e => e.status === "partial_success").length,
|
||||
totalDurationMs: executionLog.reduce((s, e) => s + (e.durationMs ?? 0), 0),
|
||||
tools: executionLog.map(e => e.tool),
|
||||
};
|
||||
console.log(JSON.stringify(summary));
|
||||
|
||||
return {
|
||||
messages: toolMessages,
|
||||
ui: ui.items,
|
||||
|
||||
@@ -32,6 +32,8 @@ export type ExecutionLogEntry = {
|
||||
errorMessage?: string;
|
||||
/** Natural-language summary for LLM consumption */
|
||||
summary: string;
|
||||
/** Shared trace ID for all tool calls within a single user request */
|
||||
traceId?: string;
|
||||
};
|
||||
|
||||
function executionLogReducer(
|
||||
|
||||
@@ -46,12 +46,14 @@ export async function toolExecutorNode(
|
||||
content: string;
|
||||
}> = [];
|
||||
|
||||
const traceId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const statusList: ToolExecStatus[] = [];
|
||||
|
||||
const executions = toolCalls.map(async (tc) => {
|
||||
const name = tc.name;
|
||||
const args = tc.args;
|
||||
const id = tc.id ?? "";
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
@@ -116,6 +118,13 @@ export async function toolExecutorNode(
|
||||
status: fallbackUsed ? "fallback" : "ok",
|
||||
...(fallbackUsed ? { message: "快速搜索不可用,已使用深度搜索替代" } : {}),
|
||||
});
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: "google_search",
|
||||
status: fallbackUsed ? "fallback_success" : "success",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: `query: ${parsed.query}`.slice(0, 200),
|
||||
resultCount: results.length, traceId,
|
||||
}));
|
||||
return {
|
||||
role: "tool" as const,
|
||||
tool_call_id: id,
|
||||
@@ -206,6 +215,13 @@ export async function toolExecutorNode(
|
||||
status: fallbackUsed ? "fallback" : "ok",
|
||||
...(fallbackUsed ? { message: "深度搜索不可用,已使用快速搜索替代" } : {}),
|
||||
});
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: "web_search_deep",
|
||||
status: fallbackUsed ? "fallback_success" : "success",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: `query: ${parsed.query}`.slice(0, 200),
|
||||
resultCount: enriched.length, traceId,
|
||||
}));
|
||||
return {
|
||||
role: "tool" as const,
|
||||
tool_call_id: id,
|
||||
@@ -223,6 +239,12 @@ export async function toolExecutorNode(
|
||||
// Truncate content to avoid token explosion
|
||||
const truncated = data.content.slice(0, 4000);
|
||||
statusList.push({ tool: "web_read", status: "ok" });
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: "web_read", status: "success",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: `url: ${parsed.url}`.slice(0, 200),
|
||||
resultCount: 1, traceId,
|
||||
}));
|
||||
return {
|
||||
role: "tool" as const,
|
||||
tool_call_id: id,
|
||||
@@ -245,6 +267,12 @@ export async function toolExecutorNode(
|
||||
} catch (e) {
|
||||
const errMsg = formatToolError(name, e);
|
||||
statusList.push({ tool: name, status: "error", message: errMsg });
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: name, status: "error",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: JSON.stringify(args).slice(0, 200),
|
||||
errorMessage: errMsg, traceId,
|
||||
}));
|
||||
return {
|
||||
role: "tool" as const,
|
||||
tool_call_id: id,
|
||||
|
||||
@@ -75,10 +75,13 @@ export async function writerToolExecutorNode(
|
||||
content: string;
|
||||
}> = [];
|
||||
|
||||
const traceId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
for (const tc of toolCalls) {
|
||||
const name = tc.name;
|
||||
const args = tc.args;
|
||||
const id = tc.id ?? "";
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
@@ -115,6 +118,12 @@ export async function writerToolExecutorNode(
|
||||
_doc_content: parsed.content,
|
||||
}),
|
||||
});
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: "doc_create", status: "success",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: `title: ${parsed.title}`.slice(0, 200),
|
||||
traceId,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -171,6 +180,12 @@ export async function writerToolExecutorNode(
|
||||
_doc_content: editedContent,
|
||||
}),
|
||||
});
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: "doc_edit", status: "success",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: `doc_id: ${parsed.doc_id}, instructions: ${parsed.instructions}`.slice(0, 200),
|
||||
traceId,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -228,6 +243,12 @@ export async function writerToolExecutorNode(
|
||||
_doc_content: translatedContent,
|
||||
}),
|
||||
});
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: "doc_translate", status: "success",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: `doc_id: ${parsed.doc_id}, target: ${parsed.target_language}`.slice(0, 200),
|
||||
traceId,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -273,6 +294,12 @@ export async function writerToolExecutorNode(
|
||||
_doc_content: reportContent,
|
||||
}),
|
||||
});
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: "report_generate", status: "success",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: `title: ${parsed.title}, type: ${parsed.report_type}`.slice(0, 200),
|
||||
traceId,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -323,6 +350,12 @@ export async function writerToolExecutorNode(
|
||||
content: draftContent,
|
||||
}),
|
||||
});
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: "reply_draft", status: "success",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: `subject: ${parsed.subject}, mode: ${parsed.mode}`.slice(0, 200),
|
||||
traceId,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -334,6 +367,13 @@ export async function writerToolExecutorNode(
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: name, status: "error",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: JSON.stringify(args).slice(0, 200),
|
||||
errorMessage: e instanceof Error ? e.message : String(e),
|
||||
traceId,
|
||||
}));
|
||||
toolMessages.push({
|
||||
role: "tool",
|
||||
tool_call_id: id,
|
||||
|
||||
Reference in New Issue
Block a user