Merge pull request #165 from pikasTech/fix/issue-162-result-failure-kind

fix: 回填 result envelope failureKind
This commit is contained in:
Lyon
2026-06-11 09:48:33 +08:00
committed by GitHub
3 changed files with 77 additions and 4 deletions
+34 -4
View File
@@ -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",
+35
View File
@@ -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;
+8
View File
@@ -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);
}