fix: 明确 AgentRun tool-call 错误归因
This commit is contained in:
@@ -744,10 +744,45 @@ function agentRunProviderTrace({ base = {}, result = {}, terminalStatus = null }
|
||||
threadId: result?.sessionRef?.threadId ?? base.agentRun?.threadId ?? base.threadId ?? null,
|
||||
turnId: result?.turnId ?? null,
|
||||
terminalStatus: terminalStatus ?? result?.terminalStatus ?? null,
|
||||
failureKind: result?.failureKind ?? null,
|
||||
failureMessage: result?.failureMessage ?? result?.blocker?.message ?? null,
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunFailureAttribution({ code, message, canceled = false } = {}) {
|
||||
if (canceled) {
|
||||
return {
|
||||
category: "canceled",
|
||||
retryable: true,
|
||||
userMessage: "AgentRun 请求已取消。",
|
||||
summary: message || "AgentRun command was canceled."
|
||||
};
|
||||
}
|
||||
if (code === "provider-invalid-tool-call" || /invalid function arguments json string|invalid_prompt|tool_call_id/iu.test(String(message ?? ""))) {
|
||||
return {
|
||||
category: "provider_invalid_tool_call",
|
||||
retryable: true,
|
||||
userMessage: "Codex app-server/provider 返回了无效 tool-call arguments JSON;这不是 HWLAB device-pod 或 Cloud API 端点失败,请查看 providerTrace.failureKind 和 runnerTrace 定位上游 tool_call_id。",
|
||||
summary: message || "Codex app-server/provider returned invalid tool-call arguments JSON."
|
||||
};
|
||||
}
|
||||
if (code === "thread-resume-failed") {
|
||||
return {
|
||||
category: "thread_resume_failed",
|
||||
retryable: true,
|
||||
userMessage: "AgentRun 复用的 Codex thread 已失效;当前标准是终止本轮并保留原 session 指针,不自动创建 replacement thread。请重新发起新会话。",
|
||||
summary: message || "AgentRun thread/resume failed for an existing thread."
|
||||
};
|
||||
}
|
||||
return {
|
||||
category: "agentrun_failed",
|
||||
retryable: true,
|
||||
userMessage: "AgentRun 请求失败;请查看 runnerTrace、providerTrace 和 agentRun 字段定位 runId/commandId/jobName。",
|
||||
summary: message || "AgentRun command failed."
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunLongLivedSessionGate(base = {}) {
|
||||
return longLivedSessionGate({
|
||||
provider: CODEX_STDIO_PROVIDER,
|
||||
@@ -823,20 +858,21 @@ function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId })
|
||||
};
|
||||
}
|
||||
const canceled = terminalStatus === "cancelled";
|
||||
const code = canceled ? "agentrun_canceled" : result?.failureKind ?? (terminalStatus === "blocked" ? "agentrun_blocked" : "agentrun_failed");
|
||||
const message = result?.failureMessage ?? result?.blocker?.message ?? (canceled ? "AgentRun command was canceled" : "AgentRun command failed");
|
||||
const attribution = agentRunFailureAttribution({ code, message, canceled });
|
||||
traceStore.append(traceId, {
|
||||
type: "result",
|
||||
status: canceled ? "canceled" : "failed",
|
||||
label: `agentrun:result:${canceled ? "canceled" : terminalStatus || "failed"}`,
|
||||
createdAt: terminalEventCreatedAt,
|
||||
errorCode: canceled ? "agentrun_canceled" : result?.failureKind ?? "agentrun_failed",
|
||||
message: result?.failureMessage ?? result?.blocker?.message ?? (canceled ? "AgentRun command was canceled." : "AgentRun command failed without a completed reply."),
|
||||
errorCode: code,
|
||||
message: attribution.summary,
|
||||
runId: base.agentRun.runId,
|
||||
commandId: base.agentRun.commandId,
|
||||
terminal: true,
|
||||
valuesPrinted: false
|
||||
});
|
||||
const code = canceled ? "agentrun_canceled" : result?.failureKind ?? (terminalStatus === "blocked" ? "agentrun_blocked" : "agentrun_failed");
|
||||
const message = result?.failureMessage ?? result?.blocker?.message ?? (canceled ? "AgentRun command was canceled" : "AgentRun command failed");
|
||||
const partialContext = partialAgentRunContext(runnerTrace);
|
||||
return {
|
||||
...base,
|
||||
@@ -853,10 +889,10 @@ function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId })
|
||||
error: {
|
||||
code,
|
||||
layer: "agentrun",
|
||||
category: canceled ? "canceled" : "agentrun_failed",
|
||||
retryable: true,
|
||||
category: attribution.category,
|
||||
retryable: attribution.retryable,
|
||||
message,
|
||||
userMessage: canceled ? "AgentRun 请求已取消。" : "AgentRun 请求失败;请查看 runnerTrace 和 agentRun 字段定位 runId/commandId/jobName。",
|
||||
userMessage: attribution.userMessage,
|
||||
traceId,
|
||||
route: "/v1/agent/chat",
|
||||
toolName: "agentrun.manual-dispatch"
|
||||
@@ -864,9 +900,9 @@ function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId })
|
||||
blocker: canceled ? null : {
|
||||
code,
|
||||
layer: "agentrun",
|
||||
category: "agentrun_failed",
|
||||
retryable: true,
|
||||
summary: message,
|
||||
category: attribution.category,
|
||||
retryable: attribution.retryable,
|
||||
summary: attribution.summary,
|
||||
traceId,
|
||||
route: "/v1/agent/chat",
|
||||
toolName: "agentrun.manual-dispatch"
|
||||
|
||||
@@ -454,6 +454,112 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api AgentRun adapter exposes invalid tool-call attribution in result payload", async () => {
|
||||
const agentRunServer = createHttpServer(async (request, response) => {
|
||||
const url = new URL(request.url || "/", "http://127.0.0.1");
|
||||
const chunks = [];
|
||||
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : null;
|
||||
const send = (data) => {
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_agentrun_invalid_tool" })}\n`);
|
||||
};
|
||||
if (request.method === "POST" && url.pathname === "/api/v1/runs") {
|
||||
return send({ id: "run_invalid_tool", status: "pending", backendProfile: "deepseek", sessionRef: body.sessionRef, resourceBundleRef: body.resourceBundleRef });
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_invalid_tool/commands") {
|
||||
return send({ id: "cmd_invalid_tool", runId: "run_invalid_tool", state: "pending", type: "turn", seq: 1 });
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_invalid_tool/runner-jobs") {
|
||||
return send({
|
||||
action: "create-kubernetes-job",
|
||||
runId: "run_invalid_tool",
|
||||
commandId: "cmd_invalid_tool",
|
||||
attemptId: "attempt_invalid_tool",
|
||||
runnerId: "runner_invalid_tool",
|
||||
namespace: "agentrun-v01",
|
||||
jobName: "agentrun-v01-runner-invalid-tool",
|
||||
runner: { attemptId: "attempt_invalid_tool", runnerId: "runner_invalid_tool" }
|
||||
});
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_invalid_tool/events") {
|
||||
return send({ items: [
|
||||
{ id: "evt_invalid_tool", runId: "run_invalid_tool", seq: 1, type: "error", payload: { commandId: "cmd_invalid_tool", failureKind: "provider-invalid-tool-call", message: "invalid params, invalid function arguments json string, tool_call_id: call_function_selftest_2 (2013)" }, createdAt: "2026-06-02T00:00:00.000Z" },
|
||||
{ id: "evt_invalid_tool_terminal", runId: "run_invalid_tool", seq: 2, type: "terminal_status", payload: { commandId: "cmd_invalid_tool", terminalStatus: "failed", failureKind: "provider-invalid-tool-call" }, createdAt: "2026-06-02T00:00:01.000Z" }
|
||||
] });
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_invalid_tool/commands/cmd_invalid_tool/result") {
|
||||
return send({
|
||||
runId: "run_invalid_tool",
|
||||
commandId: "cmd_invalid_tool",
|
||||
attemptId: "attempt_invalid_tool",
|
||||
runnerId: "runner_invalid_tool",
|
||||
jobName: "agentrun-v01-runner-invalid-tool",
|
||||
namespace: "agentrun-v01",
|
||||
status: "failed",
|
||||
runStatus: "failed",
|
||||
commandState: "failed",
|
||||
terminalStatus: "failed",
|
||||
failureKind: "provider-invalid-tool-call",
|
||||
failureMessage: "invalid params, invalid function arguments json string, tool_call_id: call_function_selftest_2 (2013)",
|
||||
completed: false,
|
||||
lastSeq: 2,
|
||||
eventCount: 2,
|
||||
sessionRef: { sessionId: "ses_agentrun_invalid_tool", conversationId: "cnv_invalid_tool", threadId: "thread_invalid_tool" }
|
||||
});
|
||||
}
|
||||
response.writeHead(404, { "content-type": "application/json" });
|
||||
response.end(`${JSON.stringify({ ok: false, failureKind: "schema-invalid", message: `unexpected ${request.method} ${url.pathname}`, traceId: "trc_fake_agentrun_invalid_tool" })}\n`);
|
||||
});
|
||||
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
|
||||
const agentRunPort = agentRunServer.address().port;
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
|
||||
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
|
||||
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
|
||||
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
||||
HWLAB_ENVIRONMENT: "v02",
|
||||
HWLAB_GITOPS_PROFILE: "v02"
|
||||
},
|
||||
accessController: {
|
||||
required: false,
|
||||
async authenticate() {
|
||||
return { ok: true, actor: null, session: null };
|
||||
},
|
||||
async createCodeAgentDevicePodSession() {
|
||||
return { token: "test-device-pod-session-token" };
|
||||
}
|
||||
}
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const traceId = "trc_server-test-agentrun-invalid-tool";
|
||||
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", "x-trace-id": traceId },
|
||||
body: JSON.stringify({ conversationId: "cnv_invalid_tool", message: "invalid tool-call attribution" })
|
||||
});
|
||||
assert.equal(submit.status, 202);
|
||||
const payload = await pollAgentResult(port, traceId);
|
||||
validateCodeAgentChatSchema(payload);
|
||||
assert.equal(payload.status, "failed");
|
||||
assert.equal(payload.providerTrace.failureKind, "provider-invalid-tool-call");
|
||||
assert.equal(payload.error.code, "provider-invalid-tool-call");
|
||||
assert.equal(payload.error.category, "provider_invalid_tool_call");
|
||||
assert.match(payload.error.userMessage, /无效 tool-call arguments JSON/u);
|
||||
assert.match(payload.error.userMessage, /不是 HWLAB device-pod/u);
|
||||
assert.equal(payload.blocker.category, "provider_invalid_tool_call");
|
||||
assert.match(payload.blocker.summary, /invalid function arguments json string/u);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api AgentRun adapter maps minimax-m3 provider profile to AgentRun backend", async () => {
|
||||
const calls = [];
|
||||
const agentRunServer = createHttpServer(async (request, response) => {
|
||||
|
||||
@@ -448,6 +448,7 @@ function codeAgentSummaryRows(summary) {
|
||||
codeAgentSummaryRow("implementationType", summary.implementationType, summary.runtimePathTone),
|
||||
codeAgentSummaryRow("providerTrace.command", summary.providerTraceCommand, summary.runtimePathTone),
|
||||
codeAgentSummaryRow("providerTrace.terminalStatus", summary.providerTraceTerminalStatus, summary.runtimePathTone),
|
||||
codeAgentSummaryRow("providerTrace.failureKind", summary.providerTraceFailureKind, summary.runtimePathTone),
|
||||
codeAgentSummaryRow("运行路径语义", summary.runtimePathLabel, summary.runtimePathTone),
|
||||
codeAgentSummaryRow("toolCalls", summary.toolCalls, summary.toolCalls === "none" ? "source" : summary.tone),
|
||||
codeAgentSummaryRow("skills", summary.skills, summary.skills === "none" ? "source" : summary.tone),
|
||||
|
||||
@@ -222,6 +222,43 @@ test("Codex app-server without providerTrace is degraded with structured missing
|
||||
assert.deepEqual(runtimePath.missingFields, ["protocol", "providerTrace.command", "providerTrace.terminalStatus"]);
|
||||
});
|
||||
|
||||
test("AgentRun invalid tool-call failure is attributed to Codex provider tool-call JSON", () => {
|
||||
const message = {
|
||||
role: "agent",
|
||||
status: "failed",
|
||||
provider: "codex-stdio",
|
||||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
runner: { kind: "codex-app-server-stdio-runner" },
|
||||
providerTrace: {
|
||||
transport: "stdio",
|
||||
protocol: "codex-app-server-jsonrpc-stdio",
|
||||
wireApi: "responses",
|
||||
command: "codex app-server --listen stdio://",
|
||||
terminalStatus: "failed",
|
||||
failureKind: "provider-invalid-tool-call",
|
||||
failureMessage: "invalid params, invalid function arguments json string, tool_call_id: call_function_selftest_2 (2013)"
|
||||
},
|
||||
error: {
|
||||
code: "provider-invalid-tool-call",
|
||||
category: "provider_invalid_tool_call"
|
||||
},
|
||||
traceId: "trc_invalid_tool_call"
|
||||
};
|
||||
const facts = codeAgentFactsFromMessage(message);
|
||||
const runtimePath = codeAgentRuntimePathFromMessage(message);
|
||||
|
||||
assert.equal(facts.kind, "codex-stdio-blocked");
|
||||
assert.equal(runtimePath.kind, "codex-app-server-stdio");
|
||||
assert.equal(runtimePath.terminalStatusOk, false);
|
||||
assert.equal(runtimePath.failureKind, "provider-invalid-tool-call");
|
||||
assert.equal(runtimePath.label, "BLOCKED:Codex tool-call JSON 无效");
|
||||
assert.match(runtimePath.summary, /不是 HWLAB device-pod/u);
|
||||
assert.match(runtimePath.summary, /tool-call arguments JSON/u);
|
||||
});
|
||||
|
||||
test("openai-responses fallback is explicitly text-chat-only", () => {
|
||||
const message = {
|
||||
status: "source",
|
||||
|
||||
@@ -211,6 +211,8 @@ export function codeAgentRuntimePathFromMessage(message) {
|
||||
const implementationType = firstText(message.implementationType, message.runner?.implementationType, runnerTrace?.implementationType);
|
||||
const command = firstText(providerTrace?.command);
|
||||
const terminalStatus = firstText(providerTrace?.terminalStatus);
|
||||
const failureKind = firstText(providerTrace?.failureKind, message.error?.code, message.blocker?.code);
|
||||
const failureMessage = firstText(providerTrace?.failureMessage, message.error?.message, message.blocker?.summary);
|
||||
const wireApi = firstText(providerTrace?.wireApi, isCodexAppServerPath({ provider, runnerKind, protocol, implementationType, command }) ? "responses" : null);
|
||||
const providerTraceMissing = !providerTrace;
|
||||
const required = {
|
||||
@@ -244,8 +246,16 @@ export function codeAgentRuntimePathFromMessage(message) {
|
||||
label = "DEGRADED:运行路径字段不完整";
|
||||
summary = `repo-owned Codex app-server stdio 已暴露部分字段,但仍缺少 ${missingFields.join(", ")}。`;
|
||||
} else if (!terminalStatusOk) {
|
||||
label = "BLOCKED:Codex app-server 终态未完成";
|
||||
summary = `repo-owned Codex app-server stdio 返回 terminalStatus=${terminalStatus};本次不标为完整完成。`;
|
||||
if (failureKind === "provider-invalid-tool-call") {
|
||||
label = "BLOCKED:Codex tool-call JSON 无效";
|
||||
summary = `Codex app-server/provider 返回 failureKind=provider-invalid-tool-call,表示上游 tool-call arguments JSON 无效;不是 HWLAB device-pod 或 Cloud API 端点失败。${failureMessage ? ` 原始摘要:${failureMessage}` : ""}`;
|
||||
} else if (failureKind === "thread-resume-failed") {
|
||||
label = "BLOCKED:Codex thread resume 失败";
|
||||
summary = `AgentRun 复用的 Codex thread 已失效,当前标准是不自动创建 replacement thread;本轮以 thread-resume-failed 终止。${failureMessage ? ` 原始摘要:${failureMessage}` : ""}`;
|
||||
} else {
|
||||
label = "BLOCKED:Codex app-server 终态未完成";
|
||||
summary = `repo-owned Codex app-server stdio 返回 terminalStatus=${terminalStatus};本次不标为完整完成。`;
|
||||
}
|
||||
tone = "blocked";
|
||||
} else {
|
||||
label = "repo-owned Codex app-server stdio";
|
||||
@@ -279,6 +289,8 @@ export function codeAgentRuntimePathFromMessage(message) {
|
||||
implementationType,
|
||||
command,
|
||||
terminalStatus,
|
||||
failureKind,
|
||||
failureMessage,
|
||||
wireApi,
|
||||
providerTraceMissing,
|
||||
criticalMissing,
|
||||
@@ -291,6 +303,7 @@ export function codeAgentRuntimePathFromMessage(message) {
|
||||
runtimePathRow("implementationType", implementationType),
|
||||
runtimePathRow("providerTrace.command", command),
|
||||
runtimePathRow("providerTrace.terminalStatus", terminalStatus),
|
||||
runtimePathRow("providerTrace.failureKind", failureKind),
|
||||
runtimePathRow("wireApi", wireApi)
|
||||
]
|
||||
};
|
||||
|
||||
@@ -119,6 +119,38 @@ test("summarizes Codex app-server stdio thread as reusable long-lived session",
|
||||
assert.equal(summary.fields.threadId, "thread_app_server");
|
||||
});
|
||||
|
||||
test("summarizes AgentRun invalid tool-call JSON as provider-side blocked status", () => {
|
||||
const summary = classifyCodeAgentStatusSummary({
|
||||
latestMessage: {
|
||||
status: "failed",
|
||||
provider: "codex-stdio",
|
||||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
runner: { kind: "codex-app-server-stdio-runner" },
|
||||
providerTrace: {
|
||||
protocol: "codex-app-server-jsonrpc-stdio",
|
||||
command: "codex app-server --listen stdio://",
|
||||
terminalStatus: "failed",
|
||||
failureKind: "provider-invalid-tool-call",
|
||||
failureMessage: "invalid params, invalid function arguments json string, tool_call_id: call_function_selftest_2 (2013)"
|
||||
},
|
||||
error: {
|
||||
code: "provider-invalid-tool-call",
|
||||
category: "provider_invalid_tool_call"
|
||||
},
|
||||
traceId: "trc_status_invalid_tool_call"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(summary.kind, "blocked");
|
||||
assert.equal(summary.tone, "blocked");
|
||||
assert.equal(summary.providerTraceFailureKind, "provider-invalid-tool-call");
|
||||
assert.equal(summary.runtimePathLabel, "BLOCKED:Codex tool-call JSON 无效");
|
||||
assert.match(summary.runtimePathSummary, /不是 HWLAB device-pod/u);
|
||||
});
|
||||
|
||||
test("maps degraded readonly session tools to warning, not green", () => {
|
||||
const summary = classifyCodeAgentStatusSummary({
|
||||
availability: {
|
||||
|
||||
@@ -43,6 +43,7 @@ export function classifyCodeAgentStatusSummary({ availability = null, latestMess
|
||||
const implementationType = safeField(runtimePath?.implementationType);
|
||||
const providerTraceCommand = safeField(runtimePath?.command);
|
||||
const providerTraceTerminalStatus = safeField(runtimePath?.terminalStatus);
|
||||
const providerTraceFailureKind = safeField(runtimePath?.failureKind);
|
||||
const runtimePathLabel = runtimePath?.label ?? "运行路径证据不足";
|
||||
const runtimePathSummary = runtimePath?.summary ?? "providerTrace 未观测;不会推断完整 Code Agent。";
|
||||
const runtimePathTone = runtimePath?.tone ?? "warn";
|
||||
@@ -82,6 +83,7 @@ export function classifyCodeAgentStatusSummary({ availability = null, latestMess
|
||||
implementationType,
|
||||
providerTraceCommand,
|
||||
providerTraceTerminalStatus,
|
||||
providerTraceFailureKind,
|
||||
runtimePathLabel,
|
||||
runtimePathSummary,
|
||||
runtimePathTone,
|
||||
@@ -112,6 +114,7 @@ export function classifyCodeAgentStatusSummary({ availability = null, latestMess
|
||||
implementationType,
|
||||
providerTraceCommand,
|
||||
providerTraceTerminalStatus,
|
||||
providerTraceFailureKind,
|
||||
runtimePathLabel,
|
||||
runtimePathSummary,
|
||||
runtimePathTone,
|
||||
|
||||
Reference in New Issue
Block a user