fix: 回填 result envelope failureKind
This commit is contained in:
+34
-4
@@ -48,7 +48,7 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman
|
||||
const commandTerminal = command ? terminalFromCommand(command) : null;
|
||||
const terminalEventStatus = terminalFromEvents(scopedEvents);
|
||||
const preliminaryTerminal = commandTerminal ?? terminalEventStatus ?? run.terminalStatus;
|
||||
const failureKind = resultFailureKind(run, command, scopedEvents, preliminaryTerminal);
|
||||
const failureKind = resultFailureKind(run, command, scopedEvents, jobs, preliminaryTerminal);
|
||||
const terminal = resultTerminal(commandTerminal, terminalEventStatus, run.terminalStatus, failureKind);
|
||||
const terminalSource = resultTerminalSource(commandTerminal, terminalEventStatus, run.terminalStatus, failureKind);
|
||||
const failureMessage = resultFailureMessage(run, command, scopedEvents, terminal);
|
||||
@@ -540,10 +540,35 @@ function failureKindFromEvents(events: RunEvent[]): FailureKind | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function resultFailureKind(run: RunRecord, command: CommandRecord | null, events: RunEvent[], terminal: TerminalStatus | null): FailureKind | null {
|
||||
function resultFailureKind(run: RunRecord, command: CommandRecord | null, events: RunEvent[], jobs: RunnerJobRecord[], terminal: TerminalStatus | null): FailureKind | null {
|
||||
if (terminal === "completed") return null;
|
||||
if (command) return failureKindFromEvents(events);
|
||||
return run.failureKind ?? failureKindFromEvents(events);
|
||||
return failureKindValue(run.failureKind) ?? failureKindFromEvents(events) ?? failureKindFromRunnerJobs(jobs);
|
||||
}
|
||||
|
||||
function failureKindFromRunnerJobs(jobs: RunnerJobRecord[]): FailureKind | null {
|
||||
for (const job of [...jobs].reverse()) {
|
||||
const value = failureKindFromJson(job.result) ?? failureKindFromNestedRecords(job.result, ["result", "run", "command", "summary", "status", "terminal", "runner", "kubernetes"]);
|
||||
if (value) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function failureKindFromNestedRecords(record: JsonRecord, keys: string[]): FailureKind | null {
|
||||
for (const key of keys) {
|
||||
const nested = jsonRecordValue(record[key]);
|
||||
if (!nested) continue;
|
||||
const value = failureKindFromJson(nested) ?? failureKindFromNestedRecords(nested, keys);
|
||||
if (value) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function failureKindFromJson(record: JsonRecord): FailureKind | null {
|
||||
return failureKindValue(record.failureKind);
|
||||
}
|
||||
|
||||
function failureKindValue(value: unknown): FailureKind | null {
|
||||
return isFailureKind(value) ? value : null;
|
||||
}
|
||||
|
||||
function isFailureKind(value: unknown): value is FailureKind {
|
||||
@@ -557,10 +582,15 @@ function isFailureKind(value: unknown): value is FailureKind {
|
||||
"skill-unavailable",
|
||||
"runner-lease-conflict",
|
||||
"backend-failed",
|
||||
"backend-spawn-failed",
|
||||
"backend-timeout",
|
||||
"backend-response-invalid",
|
||||
"backend-json-parse-error",
|
||||
"backend-protocol-error",
|
||||
"thread-resume-failed",
|
||||
"session-store-evicted",
|
||||
"provider-auth-failed",
|
||||
"provider-http-error",
|
||||
"provider-invalid-tool-call",
|
||||
"provider-compact-unsupported",
|
||||
"provider-rate-limited",
|
||||
|
||||
@@ -58,6 +58,41 @@ async function assertLongResultUsesTerminalAssistant(client: ManagerClient, stor
|
||||
const finalResponse = result.finalResponse as JsonRecord;
|
||||
assert.equal(finalResponse.text, "final terminal assistant");
|
||||
assert.equal(finalResponse.seq, finalAssistant.seq);
|
||||
|
||||
const failureRun = store.createRun({
|
||||
tenantId: "unidesk",
|
||||
projectId: "pikasTech/agentrun",
|
||||
workspaceRef: { kind: "host-path", path: "/tmp/agentrun-selftest" },
|
||||
providerId: "G14",
|
||||
backendProfile: "codex",
|
||||
executionPolicy: {
|
||||
sandbox: "workspace-write",
|
||||
approval: "never",
|
||||
timeoutMs: 1000,
|
||||
network: "none",
|
||||
secretScope: { allowCredentialEcho: false, providerCredentials: [] },
|
||||
},
|
||||
traceSink: null,
|
||||
});
|
||||
const failureCommand = store.createCommand(failureRun.id, { type: "turn", payload: { prompt: "exercise runner job result failureKind" }, idempotencyKey: "manager-result-runner-job-failure" });
|
||||
store.saveRunnerJob({
|
||||
runId: failureRun.id,
|
||||
commandId: failureCommand.id,
|
||||
payloadHash: "runner-job-failure-hash",
|
||||
attemptId: "attempt_runner_job_failure",
|
||||
runnerId: "runner_runner_job_failure",
|
||||
namespace: "agentrun-v01",
|
||||
jobName: "agentrun-v01-runner-job-failure",
|
||||
managerUrl: "http://127.0.0.1",
|
||||
image: "runner-image",
|
||||
sourceCommit: "self-test",
|
||||
serviceAccountName: null,
|
||||
result: { terminalStatus: "failed", failureKind: "provider-http-error", runner: { logPath: "/tmp/runner.log" } },
|
||||
});
|
||||
const runnerJobFailureResult = await client.get(`/api/v1/runs/${encodeURIComponent(failureRun.id)}/result?commandId=${encodeURIComponent(failureCommand.id)}`) as JsonRecord;
|
||||
assert.equal(runnerJobFailureResult.failureKind, "provider-http-error");
|
||||
const runnerJobFailureCommandResult = await client.get(`/api/v1/runs/${encodeURIComponent(failureRun.id)}/commands/${encodeURIComponent(failureCommand.id)}/result`) as JsonRecord;
|
||||
assert.equal(runnerJobFailureCommandResult.failureKind, "provider-http-error");
|
||||
}
|
||||
|
||||
export default selfTest;
|
||||
|
||||
@@ -394,6 +394,10 @@ async function runFailureDoesNotTerminalRunCase(options: { client: ManagerClient
|
||||
assert.equal(["claimed", "running"].includes(String(run.status)), true, "command failure must keep the reusable run/session non-terminal");
|
||||
assert.equal(run.terminalStatus, null);
|
||||
assert.equal(run.failureKind, null);
|
||||
const runEnvelope = await options.client.get(`/api/v1/runs/${item.runId}/result?commandId=${item.commandId}`) as JsonRecord;
|
||||
assert.equal(runEnvelope.failureKind, "provider-http-error", "run result must inherit terminal command failureKind even when reusable run stays non-terminal");
|
||||
const commandEnvelope = await options.client.get(`/api/v1/runs/${item.runId}/commands/${item.commandId}/result`) as JsonRecord;
|
||||
assert.equal(commandEnvelope.failureKind, "provider-http-error", "command result must expose provider HTTP failureKind from terminal command/events");
|
||||
}
|
||||
|
||||
async function runFailureCase(options: { client: ManagerClient; managerUrl: string; context: SelfTestContext; mode: string; expectedStatus: TerminalStatus; expectedFailureKind: FailureKind; timeoutMs?: number; expectRetryError?: boolean }): Promise<void> {
|
||||
@@ -509,6 +513,10 @@ async function runSpawnFailureCase(options: { client: ManagerClient; managerUrl:
|
||||
assert.ok(events.items?.some((event) => event.type === "error"), "spawn failure");
|
||||
const command = await options.client.get(`/api/v1/runs/${item.runId}/commands/${item.commandId}`) as { state?: string };
|
||||
assert.equal(command.state, "failed", "spawn failure");
|
||||
const commandEnvelope = await options.client.get(`/api/v1/runs/${item.runId}/commands/${item.commandId}/result`) as JsonRecord;
|
||||
assert.equal(commandEnvelope.failureKind, "backend-spawn-failed", "spawn failure command result kind");
|
||||
const runEnvelope = await options.client.get(`/api/v1/runs/${item.runId}/result?commandId=${item.commandId}`) as JsonRecord;
|
||||
assert.equal(runEnvelope.failureKind, "backend-spawn-failed", "spawn failure run result kind");
|
||||
assertNoSecretLeak(events);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user