feat: expand preValidateToolCall rules, add traceId+batch summary, extend log protocol to all agents
Deploy LangGraph Server to Azure Web App / build-and-deploy (push) Failing after 23s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 37s

- 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:
gongzhiyong
2026-04-12 13:55:19 +08:00
co-authored by Claude Sonnet 4.6
parent b22d13415a
commit 2f201bf1f0
5 changed files with 144 additions and 0 deletions
@@ -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,
+2
View File
@@ -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,