diff --git a/langgraph/src/agent/coder/nodes/tool-executor.ts b/langgraph/src/agent/coder/nodes/tool-executor.ts index f32b61d..d9811d1 100644 --- a/langgraph/src/agent/coder/nodes/tool-executor.ts +++ b/langgraph/src/agent/coder/nodes/tool-executor.ts @@ -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( { diff --git a/langgraph/src/agent/enterprise/nodes/tool-executor.ts b/langgraph/src/agent/enterprise/nodes/tool-executor.ts index 6c3e79b..0ee14b9 100644 --- a/langgraph/src/agent/enterprise/nodes/tool-executor.ts +++ b/langgraph/src/agent/enterprise/nodes/tool-executor.ts @@ -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, diff --git a/langgraph/src/agent/enterprise/types.ts b/langgraph/src/agent/enterprise/types.ts index ecc89ea..e037a08 100644 --- a/langgraph/src/agent/enterprise/types.ts +++ b/langgraph/src/agent/enterprise/types.ts @@ -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( diff --git a/langgraph/src/agent/searcher/nodes/tool-executor.ts b/langgraph/src/agent/searcher/nodes/tool-executor.ts index f28b09c..c3a8b58 100644 --- a/langgraph/src/agent/searcher/nodes/tool-executor.ts +++ b/langgraph/src/agent/searcher/nodes/tool-executor.ts @@ -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, diff --git a/langgraph/src/agent/writer/nodes/tool-executor.ts b/langgraph/src/agent/writer/nodes/tool-executor.ts index 24167ee..da13c8f 100644 --- a/langgraph/src/agent/writer/nodes/tool-executor.ts +++ b/langgraph/src/agent/writer/nodes/tool-executor.ts @@ -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,