fix: 暴露 session follow-up 可见性

This commit is contained in:
AgentRun Codex
2026-06-12 04:26:14 +08:00
parent 216209ca95
commit 21ff548b86
3 changed files with 197 additions and 68 deletions
+49 -40
View File
@@ -213,7 +213,7 @@ function summarizeCommandShowResult(command: JsonValue, result: JsonValue, runId
function summarizeResultEnvelope(record: JsonRecord | null): JsonRecord | null { function summarizeResultEnvelope(record: JsonRecord | null): JsonRecord | null {
if (!record) return null; if (!record) return null;
return withoutFullRecordBytes(compactRecord(record, { return withoutFullRecordBytes(compactRecord(record, {
keys: ["runId", "commandId", "attemptId", "runnerId", "jobName", "namespace", "status", "runStatus", "commandState", "terminalStatus", "terminalSource", "completed", "failureKind", "failureMessage", "lastSeq", "eventCount", "scopedEventCount", "scopedLastSeq", "runnerJobCount"], keys: ["ok", "executionOk", "runId", "commandId", "attemptId", "runnerId", "jobName", "namespace", "status", "runStatus", "commandState", "terminalStatus", "terminalSource", "providerTerminalFailure", "recoverableViaSession", "completed", "finalResponseAuthority", "finalResponseFallback", "needsContinuation", "failureKind", "failureMessage", "lastSeq", "eventCount", "scopedEventCount", "scopedLastSeq", "runnerJobCount"],
})); }));
} }
@@ -262,17 +262,16 @@ function summarizeDiagnosisRecord(record: JsonRecord | null): JsonRecord | null
function summarizeFinalResponseRecord(record: JsonRecord | null): JsonRecord | null { function summarizeFinalResponseRecord(record: JsonRecord | null): JsonRecord | null {
if (!record) return null; if (!record) return null;
return withoutFullRecordBytes(compactRecord(record, { keys: ["seq", "source", "final", "replyAuthority", "textTruncated", "outputTruncated", "text"] })); return withoutFullRecordBytes(compactRecord(record, { keys: ["seq", "source", "final", "replyAuthority", "authority", "fallback", "needsContinuation", "textTruncated", "outputTruncated", "text"] }));
} }
function runCommandDrillDown(runId: string, commandId: string | null, sessionId: string | null, lastSeq: number): JsonRecord { function runCommandDrillDown(runId: string, commandId: string | null, sessionId: string | null, lastSeq: number): JsonRecord {
return { return {
run: `./scripts/agentrun runs show ${runId}`, run: `describe run/${runId}`,
runFull: `./scripts/agentrun runs show ${runId} --full`, result: `result run/${runId}${commandId ? ` --command ${commandId}` : ""}`,
result: `./scripts/agentrun runs result ${runId}${commandId ? ` --command-id ${commandId}` : ""}`, events: `events run/${runId} --after-seq ${lastSeq} --limit 100`,
events: `./scripts/agentrun runs events ${runId} --after-seq ${lastSeq} --limit 100 --tail-summary`, ...(commandId ? { command: `describe command/${commandId} --run ${runId}`, commandResult: `result command/${commandId} --run ${runId}` } : {}),
...(commandId ? { command: `./scripts/agentrun commands show ${commandId} --run-id ${runId}`, commandResult: `./scripts/agentrun commands result ${commandId} --run-id ${runId}` } : {}), ...(sessionId ? { logs: `logs session/${sessionId} --tail 100`, send: `send session/${sessionId} --prompt-stdin` } : {}),
...(sessionId ? { trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq ${lastSeq} --limit 100 --run-id ${runId}`, output: `./scripts/agentrun sessions output ${sessionId} --after-seq ${lastSeq} --limit 100 --run-id ${runId}` } : {}),
valuesPrinted: false, valuesPrinted: false,
}; };
} }
@@ -405,11 +404,10 @@ export function summarizeQueueDispatchResult(result: JsonValue, taskId: string):
fullResponseBytes: jsonByteLength(result), fullResponseBytes: jsonByteLength(result),
valuesPrinted: false, valuesPrinted: false,
pollCommands: { pollCommands: {
queue: `./scripts/agentrun queue show ${stringValue(task?.id) ?? taskId}`, queue: `describe task/${stringValue(task?.id) ?? taskId}`,
...(runId ? { run: `./scripts/agentrun runs show ${runId}` } : {}), ...(runId ? { run: `describe run/${runId}`, result: `result run/${runId}${commandId ? ` --command ${commandId}` : ""}`, events: `events run/${runId} --after-seq 0 --limit 100` } : {}),
...(runId && commandId ? { command: `./scripts/agentrun commands show ${commandId} --run-id ${runId}` } : {}), ...(runId && commandId ? { command: `describe command/${commandId} --run ${runId}` } : {}),
...(runId ? { events: `./scripts/agentrun runs events ${runId} --after-seq 0 --limit 100 --tail-summary` } : {}), ...(sessionId ? { logs: `logs session/${sessionId} --tail 100`, send: `send session/${sessionId} --prompt-stdin` } : {}),
...(sessionId ? { trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100`, output: `./scripts/agentrun sessions output ${sessionId} --after-seq 0 --limit 100` } : {}),
}, },
expandedOutput: { expandedOutput: {
fullFlag: "--full", fullFlag: "--full",
@@ -665,11 +663,11 @@ function summarizeSessionMutationResult(action: "session-cancel" | "session-read
fullResponseBytes: jsonByteLength(result), fullResponseBytes: jsonByteLength(result),
valuesPrinted: false, valuesPrinted: false,
drillDownCommands: { drillDownCommands: {
show: `./scripts/agentrun sessions show ${sessionId} --reader-id cli`, show: `describe session/${sessionId}`,
trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100`, logs: `logs session/${sessionId} --tail 100`,
output: `./scripts/agentrun sessions output ${sessionId} --after-seq 0 --limit 100`, send: `send session/${sessionId} --prompt-stdin`,
traceFull: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100 --full`, read: `ack session/${sessionId}`,
outputFull: `./scripts/agentrun sessions output ${sessionId} --after-seq 0 --limit 100 --full`, cancel: `cancel session/${sessionId}`,
}, },
expandedOutput: { expandedOutput: {
fullFlag: "--full", fullFlag: "--full",
@@ -703,11 +701,13 @@ function summarizeSessionSendResult(result: JsonValue, sessionId: string, profil
fullResponseBytes: jsonByteLength(result), fullResponseBytes: jsonByteLength(result),
valuesPrinted: false, valuesPrinted: false,
drillDownCommands: { drillDownCommands: {
show: `./scripts/agentrun sessions show ${sessionId} --reader-id cli`, show: `describe session/${sessionId}`,
trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq ${afterSeq} --limit 100`, logs: `logs session/${sessionId} --tail 100`,
output: `./scripts/agentrun sessions output ${sessionId} --after-seq ${afterSeq} --limit 100`, ...(stringValue(run?.id) ? { events: `events run/${String(run?.id)} --after-seq ${afterSeq} --limit 100` } : {}),
read: `./scripts/agentrun sessions read ${sessionId} --reader-id cli`, ...(stringValue(run?.id) && stringValue(command?.id) ? { result: `result run/${String(run?.id)} --command ${String(command?.id)}` } : {}),
cancel: `./scripts/agentrun sessions cancel ${sessionId}`, send: `send session/${sessionId} --prompt-stdin`,
read: `ack session/${sessionId}`,
cancel: `cancel session/${sessionId}`,
}, },
expandedOutput: { expandedOutput: {
fullFlag: "--full", fullFlag: "--full",
@@ -796,10 +796,10 @@ export function summarizeQueueTaskShowResult(result: JsonValue, taskId: string):
fullResponseBytes: jsonByteLength(result), fullResponseBytes: jsonByteLength(result),
valuesPrinted: false, valuesPrinted: false,
pollCommands: { pollCommands: {
full: `./scripts/agentrun queue show ${taskId} --full`, full: `describe task/${taskId} --full`,
...(runId ? { run: `./scripts/agentrun runs show ${runId}`, events: `./scripts/agentrun runs events ${runId} --after-seq ${afterSeq} --limit 100 --tail-summary` } : {}), ...(runId ? { run: `describe run/${runId}`, result: `result run/${runId}${commandId ? ` --command ${commandId}` : ""}`, events: `events run/${runId} --after-seq ${afterSeq} --limit 100` } : {}),
...(runId && commandId ? { command: `./scripts/agentrun commands show ${commandId} --run-id ${runId}` } : {}), ...(runId && commandId ? { command: `describe command/${commandId} --run ${runId}` } : {}),
...(sessionId ? { trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100`, output: `./scripts/agentrun sessions output ${sessionId} --after-seq 0 --limit 100` } : {}), ...(sessionId ? { logs: `logs session/${sessionId} --tail 100`, send: `send session/${sessionId} --prompt-stdin` } : {}),
}, },
}; };
} }
@@ -814,6 +814,7 @@ export function summarizeQueueCommanderSnapshot(result: JsonValue, options: Queu
queue: stringValue(record.queue), queue: stringValue(record.queue),
readerId: stringValue(record.readerId), readerId: stringValue(record.readerId),
stats: summarizeQueueStats(jsonRecordValue(record.stats)), stats: summarizeQueueStats(jsonRecordValue(record.stats)),
activeSessionCount: numberValue(record.activeSessionCount),
sourceCount: items.length, sourceCount: items.length,
displayedCount: selected.length, displayedCount: selected.length,
limit: options.limit, limit: options.limit,
@@ -824,13 +825,15 @@ export function summarizeQueueCommanderSnapshot(result: JsonValue, options: Queu
fullResponseBytes: jsonByteLength(result), fullResponseBytes: jsonByteLength(result),
valuesPrinted: false, valuesPrinted: false,
drillDownCommands: { drillDownCommands: {
full: "./scripts/agentrun queue commander --reader-id cli --full", full: "get tasks --queue commander -o wide",
raw: "./scripts/agentrun queue commander --reader-id cli --raw", item: "describe task/<taskId>",
item: "./scripts/agentrun queue show <taskId>", run: "describe run/<runId>",
run: "./scripts/agentrun runs show <runId>", events: "events run/<runId> --after-seq <lastSeq> --limit 100",
events: "./scripts/agentrun runs events <runId> --after-seq <lastSeq> --limit 100 --tail-summary", result: "result run/<runId> --command <commandId>",
command: "./scripts/agentrun commands show <commandId> --run-id <runId>", command: "describe command/<commandId> --run <runId>",
sessionHint: "session trace/output 只在 queue show <taskId> 的 per-task pollCommands 中按实际 sessionId 输出", logs: "logs session/<sessionId> --tail 100",
send: "send session/<sessionId> --prompt-stdin",
sessionHint: "细节按实际 run/session 走 result/events/logs;续跑只用 send session/<sessionId>",
}, },
}; };
} }
@@ -948,9 +951,11 @@ function summarizeQueueTaskWithAttempt(record: JsonRecord | null, fallbackTaskId
const summary = summarizeQueueTaskRecord(record, fallbackTaskId); const summary = summarizeQueueTaskRecord(record, fallbackTaskId);
const latestAttempt = summarizeAttemptRecord(jsonRecordValue(record?.latestAttempt)); const latestAttempt = summarizeAttemptRecord(jsonRecordValue(record?.latestAttempt));
const supervisor = summarizeSupervisorRecord(jsonRecordValue(record?.supervisor)); const supervisor = summarizeSupervisorRecord(jsonRecordValue(record?.supervisor));
const activeSession = summarizeActiveSessionRecord(jsonRecordValue(record?.activeSession));
const sessionRef = jsonRecordValue(record?.sessionRef); const sessionRef = jsonRecordValue(record?.sessionRef);
if (latestAttempt) summary.latestAttempt = latestAttempt; if (latestAttempt) summary.latestAttempt = latestAttempt;
if (supervisor) summary.supervisor = supervisor; if (supervisor) summary.supervisor = supervisor;
if (activeSession) summary.activeSession = activeSession;
const sessionId = stringValue(sessionRef?.sessionId) ?? stringValue(latestAttempt?.sessionId); const sessionId = stringValue(sessionRef?.sessionId) ?? stringValue(latestAttempt?.sessionId);
if (sessionId) summary.sessionId = sessionId; if (sessionId) summary.sessionId = sessionId;
if (record?.readCursor !== undefined) summary.read = record.readCursor !== null; if (record?.readCursor !== undefined) summary.read = record.readCursor !== null;
@@ -994,7 +999,7 @@ function summarizeSupervisorRecord(record: JsonRecord | null): JsonRecord | null
const terminalClassification = jsonRecordValue(record.terminalClassification); const terminalClassification = jsonRecordValue(record.terminalClassification);
const diagnosis = jsonRecordValue(record.diagnosis); const diagnosis = jsonRecordValue(record.diagnosis);
return { return {
...withoutFullRecordBytes(compactRecord(record, { keys: ["phase", "active", "status", "terminalStatus", "failureKind", "runId", "commandId", "lastSeq", "lastEventAt", "lastEventAgeMs", "leaseRemainingMs", "leaseExpired"] })), ...withoutFullRecordBytes(compactRecord(record, { keys: ["source", "sessionId", "executionState", "attentionState", "active", "activeRunId", "activeCommandId", "attemptRunId", "attemptCommandId", "phase", "status", "terminalStatus", "failureKind", "runId", "commandId", "lastSeq", "lastEventAt", "lastEventAgeMs", "leaseRemainingMs", "leaseExpired"] })),
diagnosis: diagnosis ? summarizeDiagnosisRecord(diagnosis) : null, diagnosis: diagnosis ? summarizeDiagnosisRecord(diagnosis) : null,
terminalClassification: terminalClassification ? summarizeTerminalClassification(terminalClassification) : null, terminalClassification: terminalClassification ? summarizeTerminalClassification(terminalClassification) : null,
lastActivity: lastActivity ? withoutFullRecordBytes(compactRecord(lastActivity, { keys: ["sourceSeq", "eventId", "activityKind", "type", "status", "toolName", "itemId", "ageMs", "summary"] })) : null, lastActivity: lastActivity ? withoutFullRecordBytes(compactRecord(lastActivity, { keys: ["sourceSeq", "eventId", "activityKind", "type", "status", "toolName", "itemId", "ageMs", "summary"] })) : null,
@@ -1004,8 +1009,13 @@ function summarizeSupervisorRecord(record: JsonRecord | null): JsonRecord | null
}; };
} }
function summarizeActiveSessionRecord(record: JsonRecord | null): JsonRecord | null {
if (!record) return null;
return withoutFullRecordBytes(compactRecord(record, { keys: ["sessionId", "sessionPath", "executionState", "attentionState", "active", "activeRunId", "activeCommandId", "lastRunId", "lastCommandId", "terminalStatus", "failureKind", "lastActivityAt", "updatedAt"] }));
}
function summarizeTerminalClassification(record: JsonRecord): JsonRecord { function summarizeTerminalClassification(record: JsonRecord): JsonRecord {
return withoutFullRecordBytes(compactRecord(record, { keys: ["category", "confidence", "providerEvidence", "providerInterruption", "providerInterruptionKnown", "providerInterruptionReason", "retryInterruptionObserved", "retryInterruptionSeq", "retryInterruptionKind", "hardTimeout", "idleTimeout", "timeoutKind", "timeoutState", "transportDisconnectObserved", "transportDisconnectSeq", "reason"] })); return withoutFullRecordBytes(compactRecord(record, { keys: ["category", "confidence", "terminalStatus", "failureKind", "providerEvidence", "providerInterruption", "providerInterruptionKnown", "providerInterruptionReason", "retryInterruptionObserved", "retryInterruptionSeq", "retryInterruptionKind", "hardTimeout", "idleTimeout", "timeoutKind", "timeoutState", "transportDisconnectObserved", "transportDisconnectSeq", "reason"] }));
} }
function summarizeRecoveryActions(value: JsonValue | undefined): JsonValue[] { function summarizeRecoveryActions(value: JsonValue | undefined): JsonValue[] {
@@ -1215,10 +1225,9 @@ async function sessionCreate(args: ParsedArgs, positionalSessionId: string | nul
pvc: created.pvc, pvc: created.pvc,
storage, storage,
pollCommands: { pollCommands: {
show: `./scripts/agentrun sessions show ${sessionId} --reader-id cli`, show: `describe session/${sessionId}`,
storage: `./scripts/agentrun sessions storage ${sessionId}`, logs: `logs session/${sessionId} --tail 100`,
trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100`, send: `send session/${sessionId} --prompt-stdin`,
send: `./scripts/agentrun sessions send ${sessionId} --prompt "..."`,
}, },
}; };
} }
+37 -18
View File
@@ -57,14 +57,20 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman
const failureDetails = resultFailureDetails(scopedEvents, terminal); const failureDetails = resultFailureDetails(scopedEvents, terminal);
const reply = assistantReply(scopedEvents); const reply = assistantReply(scopedEvents);
const responseAuthority = finalResponseAuthority(reply); const responseAuthority = finalResponseAuthority(reply);
const needsContinuation = terminal === "completed" && responseAuthority !== "authoritative"; const providerTerminalFailure = terminal === "failed" && providerFailureCategory(failureKind) !== null;
const completionEvidence = completionEvidenceSummary({ terminal, terminalSource, reply, responseAuthority, needsContinuation, sessionId: run.sessionRef?.sessionId ?? null }); const stableResponseAuthority = providerTerminalFailure ? "provider-failure" : responseAuthority;
const finalResponseFallback = !providerTerminalFailure && responseAuthority === "fallback";
const needsContinuation = (terminal === "completed" && responseAuthority !== "authoritative") || (providerTerminalFailure && Boolean(run.sessionRef?.sessionId));
const completionEvidence = completionEvidenceSummary({ terminal, terminalSource, reply, responseAuthority, needsContinuation, sessionId: run.sessionRef?.sessionId ?? null, providerTerminalFailure, failureKind });
const blocker = terminal === "blocked" || terminal === "failed" ? { failureKind, message: failureMessage, details: failureDetails } : null; const blocker = terminal === "blocked" || terminal === "failed" ? { failureKind, message: failureMessage, details: failureDetails } : null;
const liveness = livenessSnapshot(run, command, events, scopedEvents, terminal, failureKind, failureMessage, { responseAuthority, needsContinuation }); const liveness = livenessSnapshot(run, command, events, scopedEvents, terminal, failureKind, failureMessage, { responseAuthority, needsContinuation });
const terminalClassification = terminalClassificationSummary({ terminal, terminalSource, failureKind, failureMessage, liveness }); const terminalClassification = terminalClassificationSummary({ terminal, terminalSource, failureKind, failureMessage, liveness });
const diagnosis = runDiagnosis({ run, command, latestJob, events, terminalClassification, liveness, terminalStatus: terminal, failureKind, failureMessage }); const diagnosis = runDiagnosis({ run, command, latestJob, events, terminalClassification, liveness, terminalStatus: terminal, failureKind, failureMessage });
const steerDelivery = command?.type === "steer" ? steerDeliverySummary(events, command.id) : null; const steerDelivery = command?.type === "steer" ? steerDeliverySummary(events, command.id) : null;
const executionOk = terminal === null ? null : terminal === "completed" && !providerTerminalFailure;
return { return {
ok: executionOk === true,
executionOk,
runId: run.id, runId: run.id,
commandId: command?.id ?? commandId ?? null, commandId: command?.id ?? commandId ?? null,
attemptId: latestJob?.attemptId ?? attemptFromEvents(events), attemptId: latestJob?.attemptId ?? attemptFromEvents(events),
@@ -76,9 +82,11 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman
commandState: command?.state ?? null, commandState: command?.state ?? null,
terminalStatus: terminal, terminalStatus: terminal,
terminalSource, terminalSource,
providerTerminalFailure,
recoverableViaSession: Boolean(run.sessionRef?.sessionId && (needsContinuation || terminal === "failed" || terminal === "blocked" || terminal === "cancelled")),
completed: terminal === "completed", completed: terminal === "completed",
finalResponseAuthority: responseAuthority, finalResponseAuthority: stableResponseAuthority,
finalResponseFallback: responseAuthority === "fallback", finalResponseFallback,
needsContinuation, needsContinuation,
completionEvidence, completionEvidence,
reply: reply.text, reply: reply.text,
@@ -88,8 +96,8 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman
source: reply.source, source: reply.source,
final: reply.final, final: reply.final,
replyAuthority: reply.replyAuthority, replyAuthority: reply.replyAuthority,
authority: responseAuthority, authority: stableResponseAuthority,
fallback: responseAuthority === "fallback", fallback: finalResponseFallback,
needsContinuation, needsContinuation,
textTruncated: reply.textTruncated, textTruncated: reply.textTruncated,
outputTruncated: reply.outputTruncated, outputTruncated: reply.outputTruncated,
@@ -447,21 +455,22 @@ function recoveryActions(input: { run: RunRecord; command: CommandRecord | null;
]; ];
if (sessionId) actions.push(recoveryDescriptor({ action: "poll-output", operation: "logs", resourceKind: "session", resourceName: sessionId, runId: run.id, commandId: command?.id ?? null, sessionId, afterSeq, limit: 100 })); if (sessionId) actions.push(recoveryDescriptor({ action: "poll-output", operation: "logs", resourceKind: "session", resourceName: sessionId, runId: run.id, commandId: command?.id ?? null, sessionId, afterSeq, limit: 100 }));
if (active) { if (active) {
if (sessionId) actions.push(recoveryDescriptor({ action: "send-session", operation: "send", resourceKind: "session", resourceName: sessionId, runId: run.id, commandId: command?.id ?? null, sessionId, inputKind: "prompt", reasonHint: "manager 会按当前 session 状态自动决定内部 steer 或新 turn" })); if (sessionId) actions.push(recoveryDescriptor({ action: "send-session", operation: "send", resourceKind: "session", resourceName: sessionId, runId: run.id, commandId: command?.id ?? null, sessionId, inputKind: "prompt", reasonHint: "manager 会按当前 session 状态自动路由后续 prompt;用户入口保持 send session/<sessionId>" }));
if (command) actions.push(recoveryDescriptor({ action: "cancel-command", operation: "cancel", resourceKind: "command", resourceName: command.id, runId: run.id, commandId: command.id, sessionId, reasonRequired: true, reasonHint: "operator supplied cancel reason" })); if (command) actions.push(recoveryDescriptor({ action: "cancel-command", operation: "cancel", resourceKind: "command", resourceName: command.id, runId: run.id, commandId: command.id, sessionId, reasonRequired: true, reasonHint: "operator supplied cancel reason" }));
else actions.push(recoveryDescriptor({ action: "cancel-run", operation: "cancel", resourceKind: "run", resourceName: run.id, runId: run.id, sessionId, reasonRequired: true, reasonHint: "operator supplied cancel reason" })); else actions.push(recoveryDescriptor({ action: "cancel-run", operation: "cancel", resourceKind: "run", resourceName: run.id, runId: run.id, sessionId, reasonRequired: true, reasonHint: "operator supplied cancel reason" }));
return actions; return actions;
} }
if (needsContinuation && sessionId) { if (needsContinuation && sessionId) {
if (command) actions.push(recoveryDescriptor({ action: "inspect-result", operation: "result", resourceKind: "command", resourceName: command.id, runId: run.id, commandId: command.id, sessionId })); if (command) actions.push(recoveryDescriptor({ action: "inspect-result", operation: "result", resourceKind: "command", resourceName: command.id, runId: run.id, commandId: command.id, sessionId }));
actions.push(recoveryDescriptor({ action: "continue-session", operation: "send", resourceKind: "session", resourceName: sessionId, runId: run.id, commandId: command?.id ?? null, sessionId, reason: `final-response-${finalResponseAuthority}`, inputKind: "prompt", reasonHint: "命令已 terminal completed,但没有 authoritative final response;管理者应先读 trace/output,再用同一 session 发送后续 prompt。" })); const providerTerminalFailure = terminal === "failed" && providerFailureCategory(failureKind) !== null;
actions.push(recoveryDescriptor({ action: "continue-session", operation: "send", resourceKind: "session", resourceName: sessionId, runId: run.id, commandId: command?.id ?? null, sessionId, reason: providerTerminalFailure ? `provider-terminal-${failureKind ?? "failure"}` : `final-response-${finalResponseAuthority}`, inputKind: "prompt", reasonHint: providerTerminalFailure ? "终态 provider failure;管理者应先读 result/events/logs,确认 provider 错误与最新上下文,再用同一 session 发送后续 prompt。" : "命令已 terminal completed,但没有 authoritative final response;管理者应先读 result/events/logs,再用同一 session 发送后续 prompt。" }));
return actions; return actions;
} }
if (terminal === "failed" || terminal === "blocked" || terminal === "cancelled") { if (terminal === "failed" || terminal === "blocked" || terminal === "cancelled") {
if (command) actions.push(recoveryDescriptor({ action: "inspect-result", operation: "result", resourceKind: "command", resourceName: command.id, runId: run.id, commandId: command.id, sessionId })); if (command) actions.push(recoveryDescriptor({ action: "inspect-result", operation: "result", resourceKind: "command", resourceName: command.id, runId: run.id, commandId: command.id, sessionId }));
if (sessionId) actions.push(recoveryDescriptor({ action: "continue-session", operation: "send", resourceKind: "session", resourceName: sessionId, runId: run.id, commandId: command?.id ?? null, sessionId, inputKind: "prompt" })); if (sessionId) actions.push(recoveryDescriptor({ action: "continue-session", operation: "send", resourceKind: "session", resourceName: sessionId, runId: run.id, commandId: command?.id ?? null, sessionId, inputKind: "prompt" }));
if (failureKind === "backend-timeout") actions.push(recoveryDescriptor({ action: "split-task", operation: "operator-decision", resourceKind: sessionId ? "session" : "run", resourceName: sessionId ?? run.id, runId: run.id, commandId: command?.id ?? null, sessionId, reason: "backend-timeout", reasonHint: "先由管理者读取 trace/result,总结下一步,再把后续 prompt 发到同一 session;必要时把大 patch / 长工具链拆成更短 turn。", failureMessage: failureMessage ? boundedTextSummary(failureMessage, { limitChars: 200 }).text as string : null })); if (failureKind === "backend-timeout") actions.push(recoveryDescriptor({ action: "split-task", operation: "operator-decision", resourceKind: sessionId ? "session" : "run", resourceName: sessionId ?? run.id, runId: run.id, commandId: command?.id ?? null, sessionId, reason: "backend-timeout", reasonHint: "先由管理者读取 result/events/logs,总结下一步,再把后续 prompt 发到同一 session;必要时把大 patch / 长工具链拆成更短步骤。", failureMessage: failureMessage ? boundedTextSummary(failureMessage, { limitChars: 200 }).text as string : null }));
else actions.push(recoveryDescriptor({ action: "retry-or-split", operation: "operator-decision", resourceKind: sessionId ? "session" : "run", resourceName: sessionId ?? run.id, runId: run.id, commandId: command?.id ?? null, sessionId, reason: failureKind ?? "terminal", reasonHint: "先读 trace/output 的 detail id,再决定继续同 session、重跑或拆分" })); else actions.push(recoveryDescriptor({ action: "retry-or-split", operation: "operator-decision", resourceKind: sessionId ? "session" : "run", resourceName: sessionId ?? run.id, runId: run.id, commandId: command?.id ?? null, sessionId, reason: failureKind ?? "terminal", reasonHint: "先读 result/events/logs,再决定继续同 session、重跑或拆分" }));
} }
return actions; return actions;
} }
@@ -491,25 +500,29 @@ function finalResponseAuthority(reply: AssistantReplySummary): "authoritative" |
return reply.text.length > 0 ? "fallback" : "missing"; return reply.text.length > 0 ? "fallback" : "missing";
} }
function completionEvidenceSummary(input: { terminal: TerminalStatus | null; terminalSource: string; reply: AssistantReplySummary; responseAuthority: string; needsContinuation: boolean; sessionId: string | null }): JsonRecord { function completionEvidenceSummary(input: { terminal: TerminalStatus | null; terminalSource: string; reply: AssistantReplySummary; responseAuthority: string; needsContinuation: boolean; sessionId: string | null; providerTerminalFailure: boolean; failureKind: FailureKind | null }): JsonRecord {
const recommendedAction = input.needsContinuation && input.sessionId ? recoveryDescriptor({ action: "continue-session", operation: "send", resourceKind: "session", resourceName: input.sessionId, sessionId: input.sessionId, inputKind: "prompt", reason: `final-response-${input.responseAuthority}` }) : null; const recommendedAction = input.needsContinuation && input.sessionId ? recoveryDescriptor({ action: "continue-session", operation: "send", resourceKind: "session", resourceName: input.sessionId, sessionId: input.sessionId, inputKind: "prompt", reason: input.providerTerminalFailure ? `provider-terminal-${input.failureKind ?? "failure"}` : `final-response-${input.responseAuthority}` }) : null;
const stableResponseAuthority = input.providerTerminalFailure ? "provider-failure" : input.responseAuthority;
return { return {
terminalStatus: input.terminal, terminalStatus: input.terminal,
terminalSource: input.terminalSource, terminalSource: input.terminalSource,
finalResponseAuthority: input.responseAuthority, providerTerminalFailure: input.providerTerminalFailure,
failureKind: input.failureKind,
finalResponseAuthority: stableResponseAuthority,
finalResponseSeq: input.reply.seq, finalResponseSeq: input.reply.seq,
finalResponseSource: input.reply.source, finalResponseSource: input.reply.source,
finalResponseFinal: input.reply.final, finalResponseFinal: input.reply.final,
finalResponseReplyAuthority: input.reply.replyAuthority, finalResponseReplyAuthority: input.reply.replyAuthority,
finalResponseFallback: input.responseAuthority === "fallback", finalResponseFallback: !input.providerTerminalFailure && input.responseAuthority === "fallback",
needsContinuation: input.needsContinuation, needsContinuation: input.needsContinuation,
reason: completionEvidenceReason(input.responseAuthority, input.terminal), reason: completionEvidenceReason(input.responseAuthority, input.terminal, input.providerTerminalFailure),
recommendedAction, recommendedAction,
valuesPrinted: false, valuesPrinted: false,
}; };
} }
function completionEvidenceReason(responseAuthority: string, terminal: TerminalStatus | null): string { function completionEvidenceReason(responseAuthority: string, terminal: TerminalStatus | null, providerTerminalFailure: boolean): string {
if (providerTerminalFailure) return "terminal provider failure; any assistant text is non-authoritative recovery context until the operator reads result/events/logs and sends the next prompt to the same session";
if (terminal !== "completed") return "command is not completed"; if (terminal !== "completed") return "command is not completed";
if (responseAuthority === "authoritative") return "terminal completed with authoritative assistant final response"; if (responseAuthority === "authoritative") return "terminal completed with authoritative assistant final response";
if (responseAuthority === "fallback") return "terminal completed but only a non-authoritative assistant progress/output fallback was available"; if (responseAuthority === "fallback") return "terminal completed but only a non-authoritative assistant progress/output fallback was available";
@@ -626,11 +639,13 @@ function terminalFromCommand(command: CommandRecord): TerminalStatus | null {
function resultTerminal(commandTerminal: TerminalStatus | null, terminalEventStatus: TerminalStatus | null, runTerminalStatus: TerminalStatus | null, failureKind: FailureKind | null): TerminalStatus | null { function resultTerminal(commandTerminal: TerminalStatus | null, terminalEventStatus: TerminalStatus | null, runTerminalStatus: TerminalStatus | null, failureKind: FailureKind | null): TerminalStatus | null {
if (commandTerminal === "failed" && terminalEventStatus === "blocked" && failureKind === "required-skill-unavailable") return "blocked"; if (commandTerminal === "failed" && terminalEventStatus === "blocked" && failureKind === "required-skill-unavailable") return "blocked";
if (providerFailureCategory(failureKind) && (commandTerminal === "completed" || terminalEventStatus === "completed" || runTerminalStatus === "completed")) return "failed";
return commandTerminal ?? terminalEventStatus ?? runTerminalStatus; return commandTerminal ?? terminalEventStatus ?? runTerminalStatus;
} }
function resultTerminalSource(commandTerminal: TerminalStatus | null, terminalEventStatus: TerminalStatus | null, runTerminalStatus: TerminalStatus | null, failureKind: FailureKind | null): string { function resultTerminalSource(commandTerminal: TerminalStatus | null, terminalEventStatus: TerminalStatus | null, runTerminalStatus: TerminalStatus | null, failureKind: FailureKind | null): string {
if (commandTerminal === "failed" && terminalEventStatus === "blocked" && failureKind === "required-skill-unavailable") return "terminal_status-event"; if (commandTerminal === "failed" && terminalEventStatus === "blocked" && failureKind === "required-skill-unavailable") return "terminal_status-event";
if (providerFailureCategory(failureKind) && (commandTerminal === "completed" || terminalEventStatus === "completed" || runTerminalStatus === "completed")) return "provider-failure-evidence";
if (commandTerminal) return "command-record"; if (commandTerminal) return "command-record";
if (terminalEventStatus) return "terminal_status-event"; if (terminalEventStatus) return "terminal_status-event";
if (runTerminalStatus) return "run-record"; if (runTerminalStatus) return "run-record";
@@ -652,8 +667,12 @@ function failureKindFromEvents(events: RunEvent[], options: { includeRetry: bool
} }
function resultFailureKind(run: RunRecord, command: CommandRecord | null, events: RunEvent[], jobs: RunnerJobRecord[], 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; const runFailureKind = failureKindValue(run.failureKind);
return failureKindValue(run.failureKind) ?? failureKindFromEvents(events, { includeRetry: terminal !== null }) ?? failureKindFromRunnerJobs(jobs); if (terminal === "completed") {
const nonRetryFailureKind = runFailureKind ?? failureKindFromEvents(events, { includeRetry: false }) ?? failureKindFromRunnerJobs(jobs);
return providerFailureCategory(nonRetryFailureKind) ? nonRetryFailureKind : null;
}
return runFailureKind ?? failureKindFromEvents(events, { includeRetry: terminal !== null }) ?? failureKindFromRunnerJobs(jobs);
} }
function failureKindFromRunnerJobs(jobs: RunnerJobRecord[]): FailureKind | null { function failureKindFromRunnerJobs(jobs: RunnerJobRecord[]): FailureKind | null {
+111 -10
View File
@@ -115,9 +115,10 @@ async function readBody(req: import("node:http").IncomingMessage): Promise<unkno
return JSON.parse(text) as unknown; return JSON.parse(text) as unknown;
} }
async function refreshQueueTaskForRead(store: AgentRunStore, taskId: string): Promise<QueueTaskRecord> { async function refreshQueueTaskForRead(store: AgentRunStore, taskId: string): Promise<JsonRecord> {
const task = await store.getQueueTask(taskId); const task = await store.getQueueTask(taskId);
return await refreshQueueTaskRecordForRead(store, task); const refreshed = await refreshQueueTaskRecordForRead(store, task);
return await enrichQueueTaskForRead(store, refreshed as unknown as JsonRecord);
} }
async function refreshQueuePageForRead(store: AgentRunStore, input: ListQueueTasksInput): Promise<void> { async function refreshQueuePageForRead(store: AgentRunStore, input: ListQueueTasksInput): Promise<void> {
@@ -148,27 +149,69 @@ async function queueCommanderForRead(store: AgentRunStore, queue: string | undef
const items = Array.isArray(snapshot.items) ? snapshot.items : []; const items = Array.isArray(snapshot.items) ? snapshot.items : [];
const enrichedItems = await Promise.all(items.map(async (item) => { const enrichedItems = await Promise.all(items.map(async (item) => {
const task = asJsonRecord(item); const task = asJsonRecord(item);
if (!task) return item as JsonValue; return task ? await enrichQueueTaskForRead(store, task) as JsonValue : item as JsonValue;
const supervisor = await queueTaskSupervisor(store, task);
return supervisor ? { ...task, supervisor, valuesPrinted: false } : task;
})); }));
return { ...snapshot, items: enrichedItems, valuesPrinted: false } as JsonValue; const byId = new Map<string, JsonValue>();
for (const item of enrichedItems) {
const task = asJsonRecord(item);
const id = stringJsonValue(task?.id);
if (id) byId.set(id, item);
}
if (readerId) {
const unfiltered = await store.queueCommander(queue, null) as unknown as JsonRecord;
const unfilteredItems = Array.isArray(unfiltered.items) ? unfiltered.items : [];
for (const item of unfilteredItems) {
const task = asJsonRecord(item);
const id = stringJsonValue(task?.id);
if (!task || !id || byId.has(id)) continue;
const enriched = await enrichQueueTaskForRead(store, task);
if (queueTaskHasActiveSession(enriched)) byId.set(id, enriched as unknown as JsonValue);
}
}
const visibleItems = Array.from(byId.values());
const activeSessionCount = visibleItems.filter((item) => queueTaskHasActiveSession(asJsonRecord(item))).length;
return { ...snapshot, items: visibleItems, activeSessionCount, valuesPrinted: false } as JsonValue;
} }
async function queueTaskSupervisor(store: AgentRunStore, task: JsonRecord): Promise<JsonRecord | null> { async function enrichQueueTaskForRead(store: AgentRunStore, task: JsonRecord): Promise<JsonRecord> {
const activeSession = await queueTaskActiveSession(store, task);
const supervisor = await queueTaskSupervisor(store, task, activeSession);
const activeBySession = activeSession?.active === true || stringJsonValue(activeSession?.activeRunId) !== null || stringJsonValue(activeSession?.activeCommandId) !== null;
return {
...task,
...(activeSession ? { activeSession } : {}),
...(supervisor ? { supervisor } : {}),
...(activeBySession ? { active: true, attentionState: "active-session" } : {}),
valuesPrinted: false,
};
}
async function queueTaskSupervisor(store: AgentRunStore, task: JsonRecord, activeSession: JsonRecord | null = null): Promise<JsonRecord | null> {
const attempt = asJsonRecord(task.latestAttempt); const attempt = asJsonRecord(task.latestAttempt);
const runId = stringJsonValue(attempt?.runId); const attemptRunId = stringJsonValue(attempt?.runId);
const attemptCommandId = stringJsonValue(attempt?.commandId);
const activeRunId = stringJsonValue(activeSession?.activeRunId);
const activeCommandId = stringJsonValue(activeSession?.activeCommandId);
const runId = activeRunId ?? attemptRunId;
if (!runId) return null; if (!runId) return null;
const commandId = activeCommandId ?? attemptCommandId ?? undefined;
try { try {
const result = await buildRunResult(store, runId, stringJsonValue(attempt?.commandId) ?? undefined); const result = await buildRunResult(store, runId, commandId);
const liveness = asJsonRecord(result.liveness); const liveness = asJsonRecord(result.liveness);
const lastActivity = asJsonRecord(liveness?.lastActivity ?? liveness?.lastCommandActivity); const lastActivity = asJsonRecord(liveness?.lastActivity ?? liveness?.lastCommandActivity);
const timeoutBudget = asJsonRecord(liveness?.timeoutBudget); const timeoutBudget = asJsonRecord(liveness?.timeoutBudget);
const terminalClassification = asJsonRecord(result.terminalClassification ?? liveness?.terminalClassification); const terminalClassification = asJsonRecord(result.terminalClassification ?? liveness?.terminalClassification);
const lease = asJsonRecord(liveness?.lease); const lease = asJsonRecord(liveness?.lease);
return { return {
source: activeRunId ? "active-session" : "latest-attempt",
attemptRunId,
attemptCommandId,
activeSession,
runId: stringJsonValue(result.runId), runId: stringJsonValue(result.runId),
commandId: stringJsonValue(result.commandId), commandId: stringJsonValue(result.commandId),
executionState: stringJsonValue(activeSession?.executionState),
activeRunId,
activeCommandId,
status: stringJsonValue(result.status), status: stringJsonValue(result.status),
terminalStatus: stringJsonValue(result.terminalStatus), terminalStatus: stringJsonValue(result.terminalStatus),
failureKind: stringJsonValue(result.failureKind), failureKind: stringJsonValue(result.failureKind),
@@ -192,6 +235,46 @@ async function queueTaskSupervisor(store: AgentRunStore, task: JsonRecord): Prom
} }
} }
async function queueTaskActiveSession(store: AgentRunStore, task: JsonRecord): Promise<JsonRecord | null> {
const sessionId = queueTaskSessionId(task);
if (!sessionId) return null;
try {
const session = await store.getSessionSummary(sessionId, null) as unknown as JsonRecord;
const activeRunId = stringJsonValue(session.activeRunId);
const activeCommandId = stringJsonValue(session.activeCommandId);
const active = session.active === true || activeRunId !== null || activeCommandId !== null || stringJsonValue(session.executionState) === "running";
if (!active) return null;
return {
sessionId,
sessionPath: stringJsonValue(session.sessionPath) ?? `/api/v1/sessions/${sessionId}`,
executionState: stringJsonValue(session.executionState),
attentionState: stringJsonValue(session.attentionState),
active,
activeRunId,
activeCommandId,
lastRunId: stringJsonValue(session.lastRunId),
lastCommandId: stringJsonValue(session.lastCommandId),
terminalStatus: stringJsonValue(session.terminalStatus),
failureKind: stringJsonValue(session.failureKind),
lastActivityAt: stringJsonValue(session.lastActivityAt),
updatedAt: stringJsonValue(session.updatedAt),
valuesPrinted: false,
};
} catch {
return null;
}
}
function queueTaskSessionId(task: JsonRecord): string | null {
return stringJsonValue(asJsonRecord(task.sessionRef)?.sessionId) ?? stringJsonValue(asJsonRecord(task.latestAttempt)?.sessionId) ?? null;
}
function queueTaskHasActiveSession(task: JsonRecord | null): boolean {
const activeSession = asJsonRecord(task?.activeSession);
const supervisor = asJsonRecord(task?.supervisor);
return activeSession?.active === true || supervisor?.source === "active-session" || supervisor?.active === true;
}
function compactActivity(activity: JsonRecord): JsonRecord { function compactActivity(activity: JsonRecord): JsonRecord {
return { return {
sourceSeq: numberJsonValue(activity.sourceSeq ?? activity.seq), sourceSeq: numberJsonValue(activity.sourceSeq ?? activity.seq),
@@ -337,17 +420,35 @@ async function route({ method, url, body, store, sourceCommit, authSummary, runn
const commandId = summary.activeCommandId ?? summary.lastCommandId ?? undefined; const commandId = summary.activeCommandId ?? summary.lastCommandId ?? undefined;
try { try {
const result = await buildRunResult(store, runId, commandId); const result = await buildRunResult(store, runId, commandId);
const liveness = asJsonRecord(result.liveness);
const lastActivity = asJsonRecord(liveness?.lastActivity ?? liveness?.lastCommandActivity);
const timeoutBudget = asJsonRecord(liveness?.timeoutBudget);
const terminalClassification = asJsonRecord(result.terminalClassification ?? liveness?.terminalClassification);
return { return {
...summary, ...summary,
liveness: result.liveness ?? null, liveness: result.liveness ?? null,
supervisor: { supervisor: {
sessionId: summary.sessionId,
executionState: summary.executionState,
attentionState: summary.attentionState,
active: summary.active,
activeRunId: summary.activeRunId,
activeCommandId: summary.activeCommandId,
lastRunId: summary.lastRunId,
lastCommandId: summary.lastCommandId,
runId: result.runId, runId: result.runId,
commandId: result.commandId, commandId: result.commandId,
status: result.status, status: result.status,
terminalStatus: result.terminalStatus, terminalStatus: result.terminalStatus,
failureKind: result.failureKind,
terminalClassification: terminalClassification ? compactTerminalClassification(terminalClassification) : null,
phase: stringJsonValue(liveness?.phase),
lastEventAgeMs: numberJsonValue(liveness?.lastEventAgeMs),
lastActivity: lastActivity ? compactActivity(lastActivity) : null,
timeoutBudget: timeoutBudget ? compactTimeoutBudget(timeoutBudget) : null,
lastSeq: result.lastSeq, lastSeq: result.lastSeq,
liveness: result.liveness ?? null, liveness: result.liveness ?? null,
recoveryActions: typeof result.liveness === "object" && result.liveness !== null && !Array.isArray(result.liveness) ? (result.liveness as JsonRecord).recoveryActions ?? [] : [], recoveryActions: compactRecoveryActions(liveness?.recoveryActions),
...(result.steerDelivery ? { steerDelivery: result.steerDelivery } : {}), ...(result.steerDelivery ? { steerDelivery: result.steerDelivery } : {}),
valuesPrinted: false, valuesPrinted: false,
}, },