import type { RenderedCliResult } from "../output"; import type { AgentRunResourceOptions } from "./utils"; import { record, stringOrNull } from "./utils"; type SessionSendProjection = { kind: "SessionSendPlan" | "SessionSendResult" | "SessionSendError"; ok: boolean; action: string; decision: string | null; internalCommandType: string | null; dryRun: boolean; mutation: boolean | null; partialWrite: boolean | null; reuseRun: boolean | null; createRunner: boolean | null; resources: Record; runnerAdmission: Record | null; request: Record | null; error: Record | null; next: { actions: Array<{ command: string; mutation: boolean; reason: string }>; note: string | null; }; valuesPrinted: false; }; const resourceStateKeys = ["state", "status", "phase", "terminalStatus", "type"] as const; export function renderSessionSendResult(command: string, raw: Record, options: AgentRunResourceOptions): RenderedCliResult { if (options.raw) return renderSessionSendMachine(command, raw, "json", raw.ok !== false); const projection = sessionSendSuccessProjection(command, raw); if (options.output === "json" || options.output === "yaml") return renderSessionSendMachine(command, projection, options.output, true); return renderSessionSendHuman(command, projection); } export function renderSessionSendError(command: string, raw: Record, options: AgentRunResourceOptions): RenderedCliResult { if (options.raw) return renderSessionSendMachine(command, raw, "json", false); const projection = sessionSendErrorProjection(raw); if (options.output === "json" || options.output === "yaml") return renderSessionSendMachine(command, projection, options.output, false); return renderSessionSendHuman(command, projection); } export function sessionSendSuccessProjection(command: string, raw: Record): SessionSendProjection { const data = record(innerData(raw)); const activeBefore = record(data.activeBefore); const reusedIdleRun = record(data.reusedIdleRun); const run = record(data.run); const commandResource = record(data.command); const runnerJob = record(data.runnerJob); const dispatchIntent = record(commandResource.dispatchIntent); const rawRunnerAdmission = record(data.runnerAdmission); const runnerAdmission = compactRunnerAdmission(rawRunnerAdmission, dispatchIntent); const request = compactRequest(record(data.request)); const decision = stringOrNull(data.decision); const dryRun = data.dryRun === true; const reuseRun = decision === null ? null : decision === "steer" || Object.keys(reusedIdleRun).length > 0; const createRunner = resolveCreateRunner({ decision, reuseRun, request: record(data.request), runnerJob, runnerAdmission: rawRunnerAdmission }); const sessionId = firstString(data.sessionId, record(run.sessionRef).sessionId, activeBefore.sessionId, reusedIdleRun.sessionId); const runId = firstString(run.id, data.runId, activeBefore.runId, reusedIdleRun.runId, rawRunnerAdmission.runId); const commandId = firstString(commandResource.id, data.commandId, activeBefore.commandId, reusedIdleRun.commandId, rawRunnerAdmission.commandId); const dispatchIntentId = firstString(dispatchIntent.id, rawRunnerAdmission.dispatchIntentId); const plannedRunnerJobId = firstString(rawRunnerAdmission.plannedRunnerJobId, dispatchIntent.runnerJobId, isPlannedRunnerJob(runnerJob) ? runnerJob.runnerJobId : null); const actualRunnerJobId = firstString( rawRunnerAdmission.actualRunnerJobId, dispatchIntent.actualRunnerJobId, isPlannedRunnerJob(runnerJob) ? null : runnerJob.id, isPlannedRunnerJob(runnerJob) ? null : runnerJob.runnerJobId, Object.keys(rawRunnerAdmission).length === 0 ? data.runnerJobId : null, ); const mutation = booleanOrNull(data.mutation) ?? booleanOrNull(raw.mutation) ?? booleanOrNull(rawRunnerAdmission.mutation); const partialWrite = booleanOrNull(rawRunnerAdmission.partialWrite) ?? false; const recoveryActions = mergeActionSources(rawRunnerAdmission.recoveryActions, data.pollActions); const nextActions = dryRun ? [{ command: sessionSendConfirmCommand(command, sessionId), mutation: true, reason: "复用同一 prompt stdin,仅在确认计划后提交一次。", }] : readOnlyNextActions({ sessionId, runId, commandId, runnerJobId: actualRunnerJobId }, recoveryActions); return { kind: dryRun ? "SessionSendPlan" : "SessionSendResult", ok: raw.ok !== false, action: firstString(data.action, raw.action) ?? (dryRun ? "session-send-plan" : "session-send"), decision, internalCommandType: stringOrNull(data.internalCommandType), dryRun, mutation, partialWrite, reuseRun, createRunner, resources: compactResources({ session: resourceIdentity("session", sessionId, record(data.session)), run: resourceIdentity("run", runId, run), command: resourceIdentity("command", commandId, commandResource), runnerJob: resourceIdentity("runnerjob", actualRunnerJobId, isPlannedRunnerJob(runnerJob) ? {} : runnerJob), plannedRunnerJob: plannedRunnerIdentity(plannedRunnerJobId, rawRunnerAdmission), dispatchIntent: resourceIdentity("dispatch-intent", dispatchIntentId, dispatchIntent), }), runnerAdmission, request, error: null, next: { actions: nextActions, note: dryRun ? "dry-run 未写入 durable facts;移除 --dry-run 会由 manager 再次按 session durable state 决定 steer 或 turn。" : runnerAdmission === null ? "只使用 manager 返回的资源 identity 做只读下钻;不要在客户端补做 runner 调度或状态修复。" : "plannedRunnerJobId 只是 durable planned identity,不证明 Kubernetes Job 已创建;只按 manager 返回的只读入口观察 durable state。", }, valuesPrinted: false, }; } export function sessionSendErrorProjection(raw: Record): SessionSendProjection { const agentrun = record(raw.agentrun); const nestedError = record(agentrun.error); const rootDetails = record(agentrun.details); const nestedDetails = record(nestedError.details); const details = { ...rootDetails, ...nestedDetails }; const mutationSummary = record(details.mutationSummary); const partialWriteSummary = record(details.partialWrite); const resources = record(details.resources); const resourceIdentities = record(details.resourceIdentities); const session = firstRecord(record(resources.session), record(resourceIdentities.session), record(details.session)); const run = firstRecord(record(resources.run), record(resourceIdentities.run), record(details.run)); const commandResource = firstRecord(record(resources.command), record(resourceIdentities.command), record(details.command)); const runnerJob = firstRecord(record(resources.runnerJob), record(resourceIdentities.runnerJob), record(details.runnerJob)); const dispatchIntent = firstRecord(record(resources.dispatchIntent), record(resourceIdentities.dispatchIntent), record(commandResource.dispatchIntent), record(details.dispatchIntent)); const rawRunnerAdmission = firstRecord(record(details.runnerAdmission), record(agentrun.runnerAdmission)); const runnerAdmission = compactRunnerAdmission(rawRunnerAdmission, dispatchIntent); const sessionId = firstString(details.sessionId, session.id, session.sessionId, record(runnerAdmission).sessionId); const runId = firstString(details.runId, run.id, run.runId, record(runnerAdmission).runId); const commandId = firstString(details.commandId, commandResource.id, commandResource.commandId, record(runnerAdmission).commandId); const plannedRunnerJobId = firstString(rawRunnerAdmission.plannedRunnerJobId, dispatchIntent.runnerJobId, isPlannedRunnerJob(runnerJob) ? runnerJob.runnerJobId : null); const actualRunnerJobId = firstString( rawRunnerAdmission.actualRunnerJobId, dispatchIntent.actualRunnerJobId, isPlannedRunnerJob(runnerJob) ? null : details.runnerJobId, isPlannedRunnerJob(runnerJob) ? null : runnerJob.id, isPlannedRunnerJob(runnerJob) ? null : runnerJob.runnerJobId, ); const dispatchIntentId = firstString(details.dispatchIntentId, dispatchIntent.id, rawRunnerAdmission.dispatchIntentId); const typedFailureKind = firstString(nestedError.failureKind, details.failureKind, agentrun.failureKind); const code = firstString(details.code, nestedError.code, agentrun.code); const reason = firstString(details.reason, details.degradedReason, details.changeReason, record(runnerAdmission).reason); const mutation = firstBoolean(details.mutation, mutationSummary.mutation, mutationSummary.occurred, record(runnerAdmission).mutation); const partialWrite = firstBoolean( typeof details.partialWrite === "boolean" ? details.partialWrite : null, partialWriteSummary.occurred, partialWriteSummary.value, mutationSummary.partialWrite, record(runnerAdmission).partialWrite, ); const recoveryActions = Array.isArray(details.recoveryActions) ? details.recoveryActions : Array.isArray(agentrun.recoveryActions) ? agentrun.recoveryActions : []; const nextActions = readOnlyNextActions({ sessionId, runId, commandId, runnerJobId: actualRunnerJobId }, mergeActionSources(rawRunnerAdmission.recoveryActions, recoveryActions)); const message = truncateOneLine(firstString(raw.message, nestedError.message, agentrun.message) ?? "AgentRun session send failed", 400); const errorRetryable = firstBoolean(details.retryable, nestedError.retryable, agentrun.retryable, record(runnerAdmission).retryable); const errorRetryAuthority = firstString(details.retryAuthority, nestedError.retryAuthority, agentrun.retryAuthority, record(runnerAdmission).retryAuthority); const errorRecoveryAction = firstString(details.recoveryAction, nestedError.recoveryAction, agentrun.recoveryAction, record(runnerAdmission).recoveryAction); const errorProjection: Record = { failureKind: stringOrNull(raw.failureKind), typedFailureKind, code, reason, message, httpStatus: numberOrNull(raw.httpStatus), traceId: firstString(details.traceId, agentrun.traceId), phase: firstString(details.phase, details.stage), durable: firstBoolean(details.durable, record(runnerAdmission).durable), valuesPrinted: false, }; if (errorRetryable !== null) errorProjection.retryable = errorRetryable; if (errorRetryAuthority !== null) errorProjection.retryAuthority = errorRetryAuthority; if (errorRecoveryAction !== null) errorProjection.recoveryAction = errorRecoveryAction; return { kind: "SessionSendError", ok: false, action: firstString(details.action, agentrun.action) ?? "session-send", decision: firstString(details.decision, agentrun.decision), internalCommandType: firstString(details.internalCommandType, agentrun.internalCommandType), dryRun: firstBoolean(details.dryRun, agentrun.dryRun) ?? false, mutation, partialWrite, reuseRun: firstBoolean(details.reuseRun, details.reusedRun), createRunner: firstBoolean(details.createRunner, details.createRunnerJob, record(runnerAdmission).state === "not-requested" ? false : null), resources: compactResources({ session: resourceIdentity("session", sessionId, session), run: resourceIdentity("run", runId, run), command: resourceIdentity("command", commandId, commandResource), runnerJob: resourceIdentity("runnerjob", actualRunnerJobId, isPlannedRunnerJob(runnerJob) ? {} : runnerJob), plannedRunnerJob: plannedRunnerIdentity(plannedRunnerJobId, rawRunnerAdmission), dispatchIntent: resourceIdentity("dispatch-intent", dispatchIntentId, dispatchIntent), }), runnerAdmission, request: null, error: errorProjection, next: { actions: nextActions, note: partialWrite === true ? "manager 已报告 partial write;先按资源 identity 只读核对 durable state,禁止盲目重发、客户端清理或另建 run。" : runnerAdmission === null ? "先按资源 identity 核对 manager durable state;只有 manager 明确 fail-closed 或给出可重试合同后才能重发。" : "plannedRunnerJobId 不是 Kubernetes Job 创建证据;先按只读入口核对 durable state,仅在 manager 明确给出可重试合同后重发。", }, valuesPrinted: false, }; } function renderSessionSendHuman(command: string, projection: SessionSendProjection): RenderedCliResult { const lines: string[] = []; if (projection.kind === "SessionSendError") { const error = record(projection.error); lines.push("Session send failed"); lines.push(`Error: ${display(error.failureKind)}`); if (error.typedFailureKind !== null) lines.push(`TypedFailure: ${display(error.typedFailureKind)}`); if (error.code !== null) lines.push(`Code: ${display(error.code)}`); if (error.reason !== null) lines.push(`Reason: ${display(error.reason)}`); lines.push(`Message: ${display(error.message)}`); if (error.httpStatus !== null) lines.push(`HTTP: ${display(error.httpStatus)}`); if (error.retryable !== null && error.retryable !== undefined) lines.push(`Retryable: ${display(error.retryable)}`); if (error.retryAuthority !== null && error.retryAuthority !== undefined) lines.push(`RetryAuthority: ${display(error.retryAuthority)}`); if (error.recoveryAction !== null && error.recoveryAction !== undefined) lines.push(`RecoveryAction: ${display(error.recoveryAction)}`); } else { lines.push(projection.kind === "SessionSendPlan" ? "Session send plan" : "Session send submitted"); lines.push(`OK: ${String(projection.ok)}`); } lines.push(`DryRun: ${String(projection.dryRun)}`); lines.push(`Decision: ${display(projection.decision)}`); lines.push(`CommandType: ${display(projection.internalCommandType)}`); lines.push(`ReuseRun: ${display(projection.reuseRun)}`); lines.push(`CreateRunner: ${display(projection.createRunner)}`); lines.push(`Mutation: ${display(projection.mutation)}`); lines.push(`PartialWrite: ${display(projection.partialWrite)}`); const resources = Object.values(projection.resources).filter((value) => value !== null).map(record); if (resources.length > 0) { lines.push("", "Resources:"); for (const resource of resources) { const states = resourceStateKeys .map((key) => resource[key] === null || resource[key] === undefined ? null : `${key}=${display(resource[key])}`) .filter((value): value is string => value !== null); lines.push(` ${display(resource.kind)}/${display(resource.id)}${states.length === 0 ? "" : ` ${states.join(" ")}`}`); } } if (projection.runnerAdmission !== null) { const admission = projection.runnerAdmission; lines.push("", "RunnerAdmission:"); lines.push(` mode=${display(admission.mode)} state=${display(admission.state)} disposition=${display(admission.disposition)} reason=${display(admission.reason)} durable=${display(admission.durable)}`); lines.push(` runId=${display(admission.runId)} commandId=${display(admission.commandId)} dispatchIntentId=${display(admission.dispatchIntentId)}`); lines.push(` plannedRunnerJobId=${display(admission.plannedRunnerJobId)} actualRunnerJobId=${display(admission.actualRunnerJobId)}`); lines.push(` mutation=${display(admission.mutation)} partialWrite=${display(admission.partialWrite)} recoveredPriorPartialWrite=${display(admission.recoveredPriorPartialWrite)}`); lines.push(` retryable=${display(admission.retryable)} retryAuthority=${display(admission.retryAuthority)} willRetry=${display(admission.willRetry)} recoveryAction=${display(admission.recoveryAction)}`); if (admission.plannedRunnerJobId !== null && admission.plannedRunnerJobId !== undefined) { lines.push(" Semantics: plannedRunnerJobId 是 durable planned identity;它本身不是 Kubernetes Job 已创建的证据。"); } if (admission.retryable === true && admission.retryAuthority === "dispatcher") { lines.push(" Retry: durable dispatcher 负责自动重试;这不是重复执行 session send 的授权。"); } } if (projection.next.actions.length > 0 || projection.next.note !== null) { lines.push("", "Next:"); for (const action of projection.next.actions) lines.push(` ${action.command}`); if (projection.next.note !== null) lines.push(` Note: ${projection.next.note}`); } return { ok: projection.ok, command, renderedText: lines.join("\n"), contentType: "text/plain" }; } function compactResources(resources: Record | null>): Record { const result: Record = { session: resources.session, run: resources.run, command: resources.command, runnerJob: resources.runnerJob, dispatchIntent: resources.dispatchIntent, }; if (resources.plannedRunnerJob !== null && resources.plannedRunnerJob !== undefined) result.plannedRunnerJob = resources.plannedRunnerJob; return result; } function resourceIdentity(kind: string, id: string | null, value: Record): Record | null { if (id === null) return null; const identity: Record = { kind, id }; for (const key of resourceStateKeys) { const field = value[key]; if (typeof field === "string" || typeof field === "number" || typeof field === "boolean") identity[key] = field; } if (kind === "dispatch-intent") { for (const key of ["runnerJobId", "dispatchOutcome", "actualRunnerJobId", "activeRunnerId", "attemptCount", "durable"] as const) { const field = value[key]; if (typeof field === "string" || typeof field === "number" || typeof field === "boolean" || field === null) identity[key] = field; } } return identity; } function compactRunnerAdmission(value: Record, dispatchIntent: Record = {}): Record | null { if (Object.keys(value).length === 0) return null; const result: Record = {}; for (const key of ["mode", "state", "reason", "disposition", "failureKind", "sessionId", "runId", "commandId", "dispatchIntentId", "plannedRunnerJobId", "actualRunnerJobId", "attemptCount", "mutation", "partialWrite", "recoveredPriorPartialWrite", "durable", "retryable", "retryAuthority", "willRetry", "retryAfterMs", "recoveryAction"] as const) { const field = value[key]; if (typeof field === "string" || typeof field === "number" || typeof field === "boolean" || field === null) result[key] = typeof field === "string" ? truncateOneLine(field, 240) : field; } const actualRunnerJobId = firstString(value.actualRunnerJobId, dispatchIntent.actualRunnerJobId); if (actualRunnerJobId !== null) result.actualRunnerJobId = actualRunnerJobId; result.valuesPrinted = false; return result; } function plannedRunnerIdentity(id: string | null, admission: Record): Record | null { if (id === null) return null; const result: Record = { kind: "planned-runnerjob", id, identityRole: "durable-planned", kubernetesJobCreated: null, kubernetesJobCreationEvidence: "not-asserted", }; const state = admission.state; if (typeof state === "string" || typeof state === "number" || typeof state === "boolean") result.state = state; const durable = admission.durable; if (typeof durable === "boolean") result.durable = durable; return result; } function isPlannedRunnerJob(value: Record): boolean { return value.action === "runner-dispatch-admitted"; } function mergeActionSources(...values: unknown[]): unknown[] { return values.flatMap((value) => Array.isArray(value) ? value.slice(0, 12) : []).slice(0, 12); } function compactRequest(value: Record): Record | null { if (Object.keys(value).length === 0) return null; const result: Record = {}; for (const key of ["method", "path", "commandType", "createRunnerJob", "reusedIdleRun"] as const) { const field = value[key]; if (typeof field === "string" || typeof field === "boolean" || field === null) result[key] = typeof field === "string" ? truncateOneLine(field, 240) : field; } result.valuesPrinted = false; return result; } function resolveCreateRunner(input: { decision: string | null; reuseRun: boolean | null; request: Record; runnerJob: Record; runnerAdmission: Record }): boolean | null { if (input.decision === "steer" || input.reuseRun === true) return false; const requested = booleanOrNull(input.request.createRunnerJob); if (requested !== null) return requested; const admissionState = stringOrNull(input.runnerAdmission.state); if (admissionState === "not-requested") return false; if (admissionState !== null) return true; if (Object.keys(input.runnerJob).length > 0) return true; return input.decision === null ? null : false; } function readOnlyNextActions(ids: { sessionId: string | null; runId: string | null; commandId: string | null; runnerJobId: string | null }, rawActions: unknown): Array<{ command: string; mutation: boolean; reason: string }> { const commands: string[] = []; const add = (command: string | null): void => { if (command !== null && !commands.includes(command)) commands.push(command); }; for (const rawAction of Array.isArray(rawActions) ? rawActions.slice(0, 12) : []) { if (typeof rawAction === "string") { if (isSafeReadCommand(rawAction)) add(rawAction); continue; } const action = record(rawAction); const operation = stringOrNull(action.operation); const resourceKind = stringOrNull(action.resourceKind); const resourceName = stringOrNull(action.resourceName); if (operation === "describe" && resourceKind !== null && resourceName !== null) add(`bun scripts/cli.ts agentrun describe ${resourceKind}/${resourceName}`); if (operation === "events") { const runId = firstString(action.runId, resourceKind === "run" ? resourceName : null); if (runId !== null) add(`bun scripts/cli.ts agentrun events run/${runId} --after-seq ${nonNegativeInteger(action.afterSeq) ?? 0} --limit ${positiveInteger(action.limit) ?? 100}`); } if (operation === "logs") { const sessionId = firstString(action.sessionId, resourceKind === "session" ? resourceName : null); if (sessionId !== null) add(`bun scripts/cli.ts agentrun logs session/${sessionId} --tail ${positiveInteger(action.limit) ?? 100}`); } } add(ids.sessionId === null ? null : `bun scripts/cli.ts agentrun describe session/${ids.sessionId}`); add(ids.runId === null ? null : `bun scripts/cli.ts agentrun events run/${ids.runId} --after-seq 0 --limit 100`); add(ids.runId === null || ids.commandId === null ? null : `bun scripts/cli.ts agentrun describe command/${ids.commandId} --run ${ids.runId}`); add(ids.runId === null || ids.runnerJobId === null ? null : `bun scripts/cli.ts agentrun describe runnerjob/${ids.runnerJobId} --run ${ids.runId}`); return commands.slice(0, 5).map((value) => ({ command: value, mutation: false, reason: "只读核对 manager durable facts。" })); } function isSafeReadCommand(value: string): boolean { return /^bun scripts\/cli\.ts agentrun (?:get|describe|events|logs|result)\b/u.test(value.trim()); } function sessionSendConfirmCommand(command: string, sessionId: string | null): string { const canonical = command .replace(/\s+--dry-run\b/gu, "") .replace(/\s+(?:-o|--output)\s+(?:json|yaml|name|wide)\b/gu, "") .replace(/\s+(?:-o|--output)=(?:json|yaml|name|wide)\b/gu, "") .replace(/\s+--(?:raw|full)\b/gu, "") .trim(); if (/(?:^|\s)--prompt-stdin(?:\s|$)/u.test(canonical)) return `bun scripts/cli.ts ${canonical}`; return `bun scripts/cli.ts agentrun send session/${sessionId ?? ""} --prompt-stdin`; } function innerData(value: Record): unknown { const data = value.data; if (typeof data === "object" && data !== null && !Array.isArray(data)) return data; return value; } function firstRecord(...values: Record[]): Record { return values.find((value) => Object.keys(value).length > 0) ?? {}; } function firstString(...values: unknown[]): string | null { for (const value of values) { const text = stringOrNull(value); if (text !== null) return text; } return null; } function firstBoolean(...values: unknown[]): boolean | null { for (const value of values) { const boolean = booleanOrNull(value); if (boolean !== null) return boolean; } return null; } function booleanOrNull(value: unknown): boolean | null { return typeof value === "boolean" ? value : null; } function numberOrNull(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } function nonNegativeInteger(value: unknown): number | null { return Number.isInteger(value) && Number(value) >= 0 ? Number(value) : null; } function positiveInteger(value: unknown): number | null { return Number.isInteger(value) && Number(value) > 0 ? Number(value) : null; } function truncateOneLine(value: string, max: number): string { const normalized = value.replace(/\s+/gu, " ").trim(); return normalized.length <= max ? normalized : `${normalized.slice(0, Math.max(0, max - 1))}…`; } function display(value: unknown): string { if (value === null || value === undefined || value === "") return "-"; return truncateOneLine(String(value), 400); } function renderSessionSendMachine(command: string, value: unknown, mode: "json" | "yaml", ok: boolean): RenderedCliResult { const renderedText = mode === "json" ? `${JSON.stringify(value, null, 2)}\n` : `${Bun.YAML.stringify(value)}\n`; return { ok, command, renderedText, contentType: mode === "json" ? "application/json" : "application/yaml" }; }