import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { execFileSync, spawn } from "node:child_process"; import { closeSync, existsSync, openSync } from "node:fs"; import path from "node:path"; import { startManagerServer } from "../../src/mgr/server.js"; import { MemoryAgentRunStore } from "../../src/mgr/store.js"; import { ManagerClient } from "../../src/mgr/client.js"; import { runOnce } from "../../src/runner/run-once.js"; import { renderRunnerJobDryRun } from "../../src/runner/k8s-job.js"; import type { RunnerSessionPvcOptions } from "../../src/runner/k8s-job.js"; import { renderCodexProviderSecretPlan } from "./secret-render.js"; import type { BackendProfile, CommandRecord, JsonRecord, JsonValue, RunRecord, SessionSummary } from "../../src/common/types.js"; import { AgentRunError, errorToJson } from "../../src/common/errors.js"; import type { RunnerOnceOptions } from "../../src/runner/run-once.js"; import { backendProfileSpec, isBackendProfile } from "../../src/common/backend-profiles.js"; import { outputBytesFromPayload, outputTruncatedFromPayload } from "../../src/common/output.js"; import { redactJson, redactText } from "../../src/common/redaction.js"; interface ParsedArgs { positional: string[]; flags: Map; } const plainTextOutputMarker = Symbol("agentrun.plainTextOutput"); interface PlainTextOutput { [plainTextOutputMarker]: true; text: string; } type CliResult = JsonValue | PlainTextOutput; export async function runCli(argv: string[]): Promise { try { const result = await dispatch(parseArgs(argv)); if (isPlainTextOutput(result)) { process.stdout.write(result.text.endsWith("\n") ? result.text : `${result.text}\n`); return; } print({ ok: true, data: result }); } catch (error) { const status = error instanceof AgentRunError ? error.httpStatus : 1; print({ ok: false, ...(error instanceof AgentRunError ? { failureKind: error.failureKind, message: error.message } : { failureKind: "infra-failed", message: error instanceof Error ? error.message : String(error) }), error: errorToJson(error) }); process.exitCode = status === 0 ? 1 : status > 255 ? 1 : status; } } async function dispatch(args: ParsedArgs): Promise { const [group, command, id] = args.positional; if (!group || group === "help" || group === "--help") return help(args); if (args.flags.get("help") === true) return help(args, group); if (command === "help" || command === "--help") return help(args, group); if (group === "manager" && (command === "url" || command === "resolve-url" || command === "status")) return managerEndpoint(args); if (group === "server" && command === "start") return startServer(args); if (group === "server" && command === "status") return serverStatus(args); if (group === "server" && command === "logs") return serverLogs(args); if (group === "server" && command === "stop") return stopServer(args); if (group === "backends" && command === "list") return client(args).get("/api/v1/backends"); if (group === "provider-profiles" && command === "list") return client(args).get("/api/v1/provider-profiles"); if (group === "provider-profiles" && command === "show" && id) return client(args).get(`/api/v1/provider-profiles/${encodeURIComponent(normalizeProfile(id))}`); if (group === "provider-profiles" && command === "config" && id) return client(args).get(`/api/v1/provider-profiles/${encodeURIComponent(normalizeProfile(id))}/config`); if (group === "provider-profiles" && (command === "remove" || command === "delete" || command === "rm") && id) return removeProviderProfileCli(id, args); if (group === "provider-profiles" && command === "set-key" && id) return setProviderProfileKey(args, id); if (group === "provider-profiles" && command === "set-config" && id) return setProviderProfileConfig(args, id); if (group === "provider-profiles" && command === "validate" && id) return validateProviderProfileCli(args, id); if (group === "secrets" && command === "codex" && id === "render") return renderCodexSecret(args); if (group === "sessions" && command === "ps") return listSessions(args); if (group === "sessions" && command === "create") return sessionCreate(args, id ?? null); if (group === "sessions" && command === "storage" && id) return sessionStorageGet(args, id); if (group === "sessions" && command === "storage" && !id) throw new AgentRunError("schema-invalid", "sessions storage requires a sessionId", { httpStatus: 2 }); if (group === "sessions" && command === "show" && id) return client(args).get(`/api/v1/sessions/${encodeURIComponent(id)}${readerQuery(args)}`); if (group === "sessions" && command === "read" && id) return sessionRead(args, id); if (group === "sessions" && command === "trace" && id) return sessionEvents(args, id, "trace"); if (group === "sessions" && command === "output" && id) return sessionEvents(args, id, "output"); if (group === "sessions" && command === "turn") return sessionTurn(args, id ?? null); if (group === "sessions" && command === "steer" && id) return sessionSteer(args, id); if (group === "sessions" && command === "cancel" && id) return sessionCancel(args, id); const sessionStorageCmd = group === "sessions" && (command === "storage-delete" || (command === "storage" && id && optionalFlag(args, "delete") === "true")); if (sessionStorageCmd && id) return sessionStorageDelete(args, id); if (group === "queue" && command === "submit") return submitQueueTask(args); if (group === "queue" && command === "list") return listQueueTasks(args); if (group === "queue" && command === "show" && id) return showQueueTask(args, id); if (group === "queue" && command === "stats") return client(args).get(`/api/v1/queue/stats${queueQuery(args)}`); if (group === "queue" && command === "commander") return queueCommander(args); if (group === "queue" && command === "read" && id) return readQueueTask(args, id); if (group === "queue" && command === "cancel" && id) return cancelQueueTask(args, id); if (group === "queue" && command === "dispatch" && id) return dispatchQueueTask(args, id); if (group === "queue" && command === "refresh" && id) return refreshQueueTask(args, id); if (group === "runs" && command === "create") return client(args).post("/api/v1/runs", await jsonFile(args)); if (group === "runs" && command === "show" && id) return client(args).get(`/api/v1/runs/${encodeURIComponent(id)}`); if (group === "runs" && command === "events" && id) return runEvents(args, id); if (group === "runs" && command === "result" && id) { const commandId = optionalFlag(args, "command-id"); return client(args).get(`/api/v1/runs/${encodeURIComponent(id)}/result${commandId ? `?commandId=${encodeURIComponent(commandId)}` : ""}`); } if (group === "runs" && command === "cancel" && id) return client(args).post(`/api/v1/runs/${encodeURIComponent(id)}/cancel`, cancelBody(args)); if (group === "commands" && command === "create" && id) { const body = await jsonFile(args); if (!body.type) body.type = flag(args, "type", "turn"); const idempotencyKey = optionalFlag(args, "idempotency-key"); if (idempotencyKey) body.idempotencyKey = idempotencyKey; return client(args).post(`/api/v1/runs/${encodeURIComponent(id)}/commands`, body); } if (group === "commands" && command === "show" && id) { const runId = flag(args, "run-id", ""); if (!runId) throw new AgentRunError("schema-invalid", "commands show requires --run-id", { httpStatus: 2 }); return client(args).get(`/api/v1/runs/${encodeURIComponent(runId)}/commands/${encodeURIComponent(id)}`); } if (group === "commands" && command === "result" && id) { const runId = flag(args, "run-id", ""); if (!runId) throw new AgentRunError("schema-invalid", "commands result requires --run-id", { httpStatus: 2 }); return client(args).get(`/api/v1/runs/${encodeURIComponent(runId)}/commands/${encodeURIComponent(id)}/result`); } if (group === "commands" && command === "cancel" && id) return client(args).post(`/api/v1/commands/${encodeURIComponent(id)}/cancel`, cancelBody(args)); if (group === "runner" && command === "start") { const runId = flag(args, "run-id", ""); if (!runId) throw new AgentRunError("schema-invalid", "runner start requires --run-id", { httpStatus: 2 }); const options: RunnerOnceOptions = { managerUrl: managerUrl(args), runId, }; const runnerId = optionalFlag(args, "runner-id"); const backend = optionalFlag(args, "backend"); const codexCommand = optionalFlag(args, "codex-command"); const codexHome = optionalFlag(args, "codex-home") ?? process.env.CODEX_HOME; if (runnerId) options.runnerId = runnerId; if (backend) { if (!isBackendProfile(backend)) throw new AgentRunError("schema-invalid", `runner start --backend ${backend} is not supported in v0.1`, { httpStatus: 2 }); options.backendProfile = backend; } if (codexCommand) options.codexCommand = codexCommand; if (codexHome) options.codexHome = codexHome; const idleTimeoutMs = optionalFlag(args, "idle-timeout-ms"); const pollIntervalMs = optionalFlag(args, "poll-interval-ms"); if (idleTimeoutMs) options.idleTimeoutMs = Number(idleTimeoutMs); if (pollIntervalMs) options.pollIntervalMs = Number(pollIntervalMs); if (args.flags.get("one-shot") === true) options.oneShot = true; return runOnce(options) as unknown as JsonValue; } if (group === "runner" && command === "job") return renderRunnerJob(args); if (group === "runner" && command === "jobs") return listRunnerJobs(args); if (group === "runner" && command === "job-status") return showRunnerJobStatus(args); throw new AgentRunError("schema-invalid", `unsupported command: ${args.positional.join(" ")}`, { httpStatus: 2 }); } async function listRunnerJobs(args: ParsedArgs): Promise { const runId = flag(args, "run-id", ""); if (!runId) throw new AgentRunError("schema-invalid", "runner jobs requires --run-id", { httpStatus: 2 }); const commandId = optionalFlag(args, "command-id"); return client(args).get(`/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs${commandId ? `?commandId=${encodeURIComponent(commandId)}` : ""}`); } async function runEvents(args: ParsedArgs, runId: string): Promise { const afterSeq = integerFlag(args, "after-seq", 0, { min: 0 }); const limit = integerFlag(args, "limit", 100, { min: 1, max: 500 }); const page = await client(args).get(`/api/v1/runs/${encodeURIComponent(runId)}/events?afterSeq=${afterSeq}&limit=${limit}`); const format = optionalFlag(args, "format") ?? "json"; if (format !== "json" && format !== "tsv") throw new AgentRunError("schema-invalid", "runs events --format must be json or tsv", { httpStatus: 2 }); const tail = tailFlag(args, limit); const wantsSummary = args.flags.get("summary") === true || args.flags.get("tail-summary") === true || tail !== null || format === "tsv"; if (!wantsSummary) return page; const summary = summarizeRunEventPage(page, { runId, afterSeq, limit, tail: tail ?? (args.flags.get("tail-summary") === true ? Math.min(limit, 20) : null), summaryChars: integerFlag(args, "summary-chars", 900, { min: 1, max: 4_000 }), }); if (format === "tsv") return plainTextOutput(renderRunEventSummaryTsv(summary)); return summary; } interface RunEventSummaryOptions { runId: string; afterSeq: number; limit: number; tail: number | null; summaryChars: number; } interface SessionEventSummaryOptions { kind: "trace" | "output"; sessionId: string; afterSeq: number; limit: number; runId: string | null; tail: number | null; summaryChars: number; includeOutput: boolean; } interface SessionEventDetailFilter { seq: number | null; eventId: string | null; itemId: string | null; } export function summarizeRunEventPage(page: JsonValue, options: RunEventSummaryOptions): JsonRecord { const events = eventPageItems(page); const selected = options.tail === null ? events : events.slice(-options.tail); const items = selected.map((event) => summarizeRunEvent(event, options.summaryChars)); const lastSeq = items.length > 0 ? items[items.length - 1]?.seq ?? null : null; return { action: "runs-events-summary", runId: options.runId, afterSeq: options.afterSeq, limit: options.limit, tail: options.tail, sourceCount: events.length, count: items.length, lastSeq, nextAfterSeq: lastSeq, valuesPrinted: false, items, }; } export function summarizeSessionEventPage(page: JsonValue, options: SessionEventSummaryOptions): JsonRecord { const record = jsonRecordValue(page); if (!record) throw new AgentRunError("schema-invalid", "sessions event response must be an object", { httpStatus: 2 }); const events = eventPageItems(page); const selected = options.tail === null ? events : events.slice(-options.tail); const summarized = selected.map((event) => summarizeRunEvent(event, options.summaryChars)); const visible = options.includeOutput ? summarized : summarized.filter(isDefaultVisibleSessionEvent); const suppressed = summarizeSuppressedSessionEvents(summarized, visible); const runId = stringValue(record.runId) ?? options.runId; const items = withSessionDetailCommands(visible, options.kind, options.sessionId, runId); const lastSeq = summarized.length > 0 ? summarized[summarized.length - 1]?.seq ?? null : null; return { action: `session-${options.kind}-summary`, sessionId: stringValue(record.sessionId) ?? options.sessionId, runId, afterSeq: options.afterSeq, limit: options.limit, tail: options.tail, sourceCount: events.length, displayedCount: items.length, count: items.length, cursor: stringValue(record.cursor), lastSeq, nextAfterSeq: lastSeq, suppressedEvents: suppressed, valuesPrinted: false, items, drillDownCommands: sessionEventDrillDownCommands(options.kind, options.sessionId, { afterSeq: options.afterSeq, limit: options.limit, runId }), progressiveDisclosure: { default: "assistant messages plus tool summaries; command_output/backend_status chunks are counted, not displayed", includeOutputFlag: "--include-output", detailFlags: "--seq or --item-id --full", fullFlag: "--full", rawFlag: "--raw", }, }; } export function summarizeQueueDispatchResult(result: JsonValue, taskId: string): JsonRecord { const record = jsonRecordValue(result); if (!record) throw new AgentRunError("schema-invalid", "queue dispatch response must be an object", { httpStatus: 2 }); const task = jsonRecordValue(record.task); const run = jsonRecordValue(record.run); const command = jsonRecordValue(record.command); const runnerJob = jsonRecordValue(record.runnerJob); const latestAttempt = jsonRecordValue(record.latestAttempt) ?? jsonRecordValue(task?.latestAttempt); const runId = stringValue(run?.id) ?? stringValue(latestAttempt?.runId); const commandId = stringValue(command?.id) ?? stringValue(latestAttempt?.commandId); const sessionId = stringValue(latestAttempt?.sessionId) ?? stringValue(jsonRecordValue(run?.sessionRef)?.sessionId) ?? stringValue(jsonRecordValue(task?.sessionRef)?.sessionId); return { action: "queue-dispatch-summary", taskId: stringValue(task?.id) ?? taskId, mutation: record.mutation === true, task: summarizeQueueTaskRecord(task, taskId), latestAttempt: summarizeAttemptRecord(latestAttempt), run: summarizeRunRecord(run), command: summarizeCommandRecord(command), runnerJob: summarizeRunnerJobRecord(runnerJob), fullResponseBytes: jsonByteLength(result), valuesPrinted: false, pollCommands: { queue: `./scripts/agentrun queue show ${stringValue(task?.id) ?? taskId}`, ...(runId ? { run: `./scripts/agentrun runs show ${runId}` } : {}), ...(runId && commandId ? { command: `./scripts/agentrun commands show ${commandId} --run-id ${runId}` } : {}), ...(runId ? { events: `./scripts/agentrun runs events ${runId} --after-seq 0 --limit 100 --tail-summary` } : {}), ...(sessionId ? { trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100`, output: `./scripts/agentrun sessions output ${sessionId} --after-seq 0 --limit 100` } : {}), }, expandedOutput: { fullFlag: "--full", rawFlag: "--raw", note: "For mutating commands, request expanded output on the original invocation.", }, }; } function summarizeRunEvent(event: JsonRecord, summaryChars: number): JsonRecord { const payload = jsonRecordValue(event.payload) ?? {}; const type = stringValue(event.type) ?? "unknown"; const command = type === "assistant_message" ? null : boundedSummaryString(stringValue(payload.command), Math.min(summaryChars, 240)); const text = type === "assistant_message" ? boundedSummaryString(stringValue(payload.text), summaryChars) : null; const outputSummary = boundedSummaryString(stringValue(payload.outputSummary) ?? nestedSummaryText(payload), summaryChars); const message = boundedSummaryString(stringValue(payload.failureMessage) ?? stringValue(payload.message), summaryChars); const summary = text ?? outputSummary ?? command ?? message ?? ""; const summaryTruncated = [command, text, outputSummary, message].some((value) => value?.endsWith("...") === true); const runnerTrace = findNestedValue(payload, "runnerTrace"); return { eventId: stringValue(event.id), seq: numberValue(event.seq) ?? 0, type, itemId: stringValue(payload.itemId) ?? stringValue(payload.id), method: stringValue(payload.method), status: stringValue(payload.status) ?? stringValue(payload.terminalStatus) ?? stringValue(payload.phase), phase: stringValue(payload.phase), commandId: stringValue(payload.commandId), command, text, exitCode: numberValue(payload.exitCode), durationMs: numberValue(payload.durationMs), outputTruncated: outputTruncatedFromPayload(payload) || summaryTruncated, outputBytes: outputBytesFromPayload(payload), outputSummary, summary, payloadBytes: jsonByteLength(payload), payloadKeys: Object.keys(payload).sort().slice(0, 24), hasRunnerTrace: runnerTrace !== null, runnerTraceBytes: runnerTrace === null ? 0 : jsonByteLength(runnerTrace), hasRawEvent: hasNestedKey(payload, ["raw", "rawEvent", "raw_event", "event"]), }; } function isDefaultVisibleSessionEvent(item: JsonRecord): boolean { const type = stringValue(item.type); return type === "assistant_message" || type === "tool_call" || type === "error"; } function summarizeSuppressedSessionEvents(all: JsonRecord[], visible: JsonRecord[]): JsonRecord { const visibleSeqs = new Set(visible.map((item) => item.seq)); const suppressed = all.filter((item) => !visibleSeqs.has(item.seq)); const byType: JsonRecord = {}; for (const item of suppressed) { const type = stringValue(item.type) ?? "unknown"; byType[type] = Number(byType[type] ?? 0) + 1; } return { count: suppressed.length, byType, outputBytes: suppressed.reduce((total, item) => total + Number(item.outputBytes ?? 0), 0), outputTruncated: suppressed.some((item) => item.outputTruncated === true), valuesPrinted: false, }; } function withSessionDetailCommands(items: JsonRecord[], kind: "trace" | "output", sessionId: string, runId: string | null): JsonRecord[] { return items.map((item) => { const seq = numberValue(item.seq); const itemId = stringValue(item.itemId); const eventId = stringValue(item.eventId); const pageHint = seq === null ? "" : ` --after-seq ${Math.max(0, seq - 1)} --limit 1`; const runFlag = runId ? ` --run-id ${runId}` : ""; const detail = seq === null ? null : `./scripts/agentrun sessions ${kind} ${sessionId}${pageHint} --seq ${seq}${runFlag} --full`; return { ...item, ...(detail || itemId || eventId ? { detailCommands: { ...(detail ? { seq: detail } : {}), ...(itemId ? { item: `./scripts/agentrun sessions ${kind} ${sessionId}${pageHint} --item-id ${itemId}${runFlag} --full` } : {}), ...(eventId ? { event: `./scripts/agentrun sessions ${kind} ${sessionId}${pageHint} --event-id ${eventId}${runFlag} --full` } : {}), }, } : {}), }; }); } function sessionEventDetailFilter(args: ParsedArgs, seq: number | null): SessionEventDetailFilter | null { const eventId = optionalFlag(args, "event-id"); const itemId = optionalFlag(args, "item-id"); if (seq === null && !eventId && !itemId) return null; return { seq, eventId, itemId }; } function sessionEventDetailResult(page: JsonValue, options: { kind: "trace" | "output"; sessionId: string; runId: string | null; summaryChars: number; filter: SessionEventDetailFilter }): JsonRecord { const record = jsonRecordValue(page); if (!record) throw new AgentRunError("schema-invalid", "sessions event response must be an object", { httpStatus: 2 }); const events = eventPageItems(page); const matches = events.filter((event) => matchesSessionEventFilter(event, options.filter)); if (matches.length === 0) { throw new AgentRunError("schema-invalid", "no session event matched --seq/--event-id/--item-id in the fetched page", { httpStatus: 2, details: { filter: options.filter as unknown as JsonRecord, hint: "increase --limit or adjust --after-seq when looking up --event-id/--item-id outside the current page", }, }); } return sessionEventDetailResultFromMatches(page, matches, options); } function sessionEventDetailResultFromMatches(page: JsonValue, matches: JsonRecord[], options: { kind: "trace" | "output"; sessionId: string; runId: string | null; summaryChars: number; filter: SessionEventDetailFilter; pagesScanned?: number; eventsScanned?: number }): JsonRecord { const record = jsonRecordValue(page); if (!record) throw new AgentRunError("schema-invalid", "sessions event response must be an object", { httpStatus: 2 }); const events = eventPageItems(page); const runId = stringValue(record.runId) ?? options.runId; return { action: `session-${options.kind}-event-detail`, sessionId: stringValue(record.sessionId) ?? options.sessionId, runId, filter: options.filter as unknown as JsonRecord, sourceCount: events.length, count: matches.length, ...(options.pagesScanned === undefined ? {} : { pagesScanned: options.pagesScanned }), ...(options.eventsScanned === undefined ? {} : { eventsScanned: options.eventsScanned }), valuesPrinted: false, items: matches.map((event) => ({ summary: summarizeRunEvent(event, options.summaryChars), event: redactJson(event) as JsonRecord, eventBytes: jsonByteLength(event), valuesPrinted: false, })), }; } function matchesSessionEventFilter(event: JsonRecord, filter: SessionEventDetailFilter): boolean { const payload = jsonRecordValue(event.payload) ?? {}; const nestedItem = jsonRecordValue(payload.item); if (filter.seq !== null && numberValue(event.seq) === filter.seq) return true; if (filter.eventId && stringValue(event.id) === filter.eventId) return true; if (filter.itemId && (stringValue(payload.itemId) === filter.itemId || stringValue(payload.id) === filter.itemId || stringValue(nestedItem?.id) === filter.itemId)) return true; return false; } export function renderRunEventSummaryTsv(summary: JsonRecord): string { const items = Array.isArray(summary.items) ? summary.items.filter((item): item is JsonRecord => jsonRecordValue(item) !== null) : []; const rows = items.map((item) => [ item.seq, item.type, item.method, item.status, item.exitCode, item.durationMs, item.outputTruncated, item.outputBytes, item.summary, ].map(tsvCell).join("\t")); return ["seq\ttype\tmethod\tstatus\texitCode\tdurationMs\toutputTruncated\toutputBytes\tsummary", ...rows].join("\n"); } function eventPageItems(page: JsonValue): JsonRecord[] { const record = jsonRecordValue(page); if (!record) throw new AgentRunError("schema-invalid", "runs events response must be an object", { httpStatus: 2 }); if (!Array.isArray(record.items)) throw new AgentRunError("schema-invalid", "runs events response.items must be an array", { httpStatus: 2 }); return record.items.map((item) => { const event = jsonRecordValue(item); if (!event) throw new AgentRunError("schema-invalid", "runs events item must be an object", { httpStatus: 2 }); return event; }); } function nestedSummaryText(payload: JsonRecord): string | null { const summary = payload.summary; if (typeof summary === "string") return summary; const summaryRecord = jsonRecordValue(summary); return summaryRecord ? stringValue(summaryRecord.text) : null; } function boundedSummaryString(value: string | null, limit: number): string | null { if (!value) return null; const normalized = redactText(value).replace(/\s+/gu, " ").trim(); if (normalized.length === 0) return null; return normalized.length > limit ? `${normalized.slice(0, Math.max(0, limit - 3))}...` : normalized; } function integerFlag(args: ParsedArgs, name: string, fallback: number, options: { min: number; max?: number }): number { const raw = optionalFlag(args, name); const value = raw === null ? fallback : Number(raw); if (!Number.isInteger(value) || value < options.min || (options.max !== undefined && value > options.max)) { const maxText = options.max === undefined ? "" : ` and <= ${options.max}`; throw new AgentRunError("schema-invalid", `--${name} must be an integer >= ${options.min}${maxText}`, { httpStatus: 2 }); } return value; } function optionalIntegerFlag(args: ParsedArgs, name: string, options: { min: number; max?: number }): number | null { const raw = optionalFlag(args, name); if (raw === null) return null; const value = Number(raw); if (!Number.isInteger(value) || value < options.min || (options.max !== undefined && value > options.max)) { const maxText = options.max === undefined ? "" : ` and <= ${options.max}`; throw new AgentRunError("schema-invalid", `--${name} must be an integer >= ${options.min}${maxText}`, { httpStatus: 2 }); } return value; } function tailFlag(args: ParsedArgs, limit: number): number | null { const value = args.flags.get("tail"); if (value === undefined) return null; if (value === true) return Math.min(limit, 20); const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 1 || parsed > limit) throw new AgentRunError("schema-invalid", `--tail must be an integer between 1 and --limit (${limit})`, { httpStatus: 2 }); return parsed; } function stringValue(value: JsonValue | undefined): string | null { return typeof value === "string" && value.length > 0 ? value : null; } function numberValue(value: JsonValue | undefined): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } function jsonRecordValue(value: unknown): JsonRecord | null { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as JsonRecord : null; } function tsvCell(value: JsonValue | undefined): string { if (value === null || value === undefined) return ""; return String(value).replace(/[\t\r\n]+/gu, " "); } function plainTextOutput(text: string): PlainTextOutput { return { [plainTextOutputMarker]: true, text }; } function isPlainTextOutput(value: CliResult): value is PlainTextOutput { return typeof value === "object" && value !== null && plainTextOutputMarker in value; } function wantsExpandedOutput(args: ParsedArgs): boolean { return args.flags.get("full") === true || args.flags.get("raw") === true; } function summarizeSessionMutationResult(action: "session-cancel" | "session-read", sessionId: string, result: JsonValue, flags: JsonRecord): JsonRecord { const record = jsonRecordValue(result); return { action, sessionId, mutation: true, ...flags, result: summarizeGenericRecord(record), fullResponseBytes: jsonByteLength(result), valuesPrinted: false, drillDownCommands: { show: `./scripts/agentrun sessions show ${sessionId} --reader-id cli`, trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100`, output: `./scripts/agentrun sessions output ${sessionId} --after-seq 0 --limit 100`, traceFull: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100 --full`, outputFull: `./scripts/agentrun sessions output ${sessionId} --after-seq 0 --limit 100 --full`, }, expandedOutput: { fullFlag: "--full", rawFlag: "--raw", note: "For mutating commands, request expanded output on the original invocation.", }, }; } interface QueueSummaryOptions { limit: number; } export function summarizeQueueTaskListResult(result: JsonValue, options: QueueSummaryOptions): JsonRecord { const record = jsonRecordValue(result); if (!record) throw new AgentRunError("schema-invalid", "queue list response must be an object", { httpStatus: 2 }); const items = queueItems(record.items); const selected = items.slice(0, options.limit); return { action: "queue-list-summary", queue: stringValue(record.queue), state: stringValue(record.state), cursor: stringValue(record.cursor), nextCursor: stringValue(record.nextCursor), sourceCount: items.length, displayedCount: selected.length, limit: options.limit, items: selected.map((item) => summarizeQueueTaskWithAttempt(item, stringValue(item.id) ?? "unknown")), fullResponseBytes: jsonByteLength(result), valuesPrinted: false, drillDownCommands: { full: "./scripts/agentrun queue list --full", raw: "./scripts/agentrun queue list --raw", next: stringValue(record.nextCursor) ? `./scripts/agentrun queue list --cursor ${String(record.nextCursor)} --limit ${options.limit}` : null, }, }; } export function summarizeQueueTaskShowResult(result: JsonValue, taskId: string): JsonRecord { const record = jsonRecordValue(result); if (!record) throw new AgentRunError("schema-invalid", "queue show response must be an object", { httpStatus: 2 }); const sessionId = stringValue(jsonRecordValue(record.sessionRef)?.sessionId) ?? stringValue(jsonRecordValue(record.latestAttempt)?.sessionId); return { action: "queue-show-summary", task: summarizeQueueTaskWithAttempt(record, taskId), payloadBytes: jsonByteLength(record.payload), resourceBundleBytes: jsonByteLength(record.resourceBundleRef), referencesCount: Array.isArray(record.references) ? record.references.length : null, metadataKeys: Object.keys(jsonRecordValue(record.metadata) ?? {}).sort().slice(0, 24), fullResponseBytes: jsonByteLength(result), valuesPrinted: false, pollCommands: { full: `./scripts/agentrun queue show ${taskId} --full`, ...(sessionId ? { trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100`, output: `./scripts/agentrun sessions output ${sessionId} --after-seq 0 --limit 100` } : {}), }, }; } export function summarizeQueueCommanderSnapshot(result: JsonValue, options: QueueSummaryOptions): JsonRecord { const record = jsonRecordValue(result); if (!record) throw new AgentRunError("schema-invalid", "queue commander response must be an object", { httpStatus: 2 }); const items = queueItems(record.items); const selected = items.slice(0, options.limit); return { action: "queue-commander-summary", queue: stringValue(record.queue), readerId: stringValue(record.readerId), stats: summarizeQueueStats(jsonRecordValue(record.stats)), sourceCount: items.length, displayedCount: selected.length, limit: options.limit, unreadCount: items.filter((item) => item.unread === true).length, activeCount: items.filter((item) => item.active === true || stringValue(item.attentionState) === "active").length, items: selected.map((item) => summarizeQueueTaskWithAttempt(item, stringValue(item.id) ?? "unknown")), generatedAt: stringValue(record.generatedAt), fullResponseBytes: jsonByteLength(result), valuesPrinted: false, drillDownCommands: { full: "./scripts/agentrun queue commander --reader-id cli --full", raw: "./scripts/agentrun queue commander --reader-id cli --raw", item: "./scripts/agentrun queue show ", trace: "./scripts/agentrun sessions trace --after-seq 0 --limit 100", output: "./scripts/agentrun sessions output --after-seq 0 --limit 100", }, }; } function summarizeQueueTaskMutationResult(action: "queue-read" | "queue-cancel" | "queue-refresh", taskId: string, result: JsonValue, flags: JsonRecord): JsonRecord { const record = jsonRecordValue(result); return { action, taskId, mutation: true, ...flags, task: summarizeQueueTaskWithAttempt(record, taskId), fullResponseBytes: jsonByteLength(result), valuesPrinted: false, drillDownCommands: { show: `./scripts/agentrun queue show ${taskId}`, full: `./scripts/agentrun queue show ${taskId} --full`, }, }; } function queueMutationDryRunPlan(action: string, taskId: string | null, pathValue: string, body: JsonRecord, method: "POST", confirmCommand: string, task?: JsonValue): JsonRecord { return { action: `${action}-plan`, dryRun: true, mutation: false, taskId, request: { method, path: pathValue, body: summarizeMutationBody(body), bodyBytes: jsonByteLength(body), valuesPrinted: false, }, ...(task === undefined ? {} : { task: summarizeQueueTaskWithAttempt(jsonRecordValue(task), taskId ?? stringValue(jsonRecordValue(task)?.id) ?? "unknown") }), next: { confirm: confirmCommand, note: "Remove --dry-run to perform the mutation.", }, valuesPrinted: false, }; } function summarizeMutationBody(body: JsonRecord): JsonRecord { return { ...compactRecord(body, { keys: ["idempotencyKey", "image", "namespace", "attemptId", "runnerId", "sourceCommit", "managerUrl", "serviceAccountName", "readerId", "reason"] }), keys: Object.keys(body).sort().slice(0, 32), payloadBytes: jsonByteLength(body.payload), executionPolicyBytes: jsonByteLength(body.executionPolicy), resourceBundleBytes: jsonByteLength(body.resourceBundleRef), valuesPrinted: false, }; } function summarizeQueueStats(record: JsonRecord | null): JsonRecord | null { if (!record) return null; return { queue: stringValue(record.queue), total: numberValue(record.total), maxVersion: numberValue(record.maxVersion), byState: jsonRecordValue(record.byState) ?? {}, byLane: jsonRecordValue(record.byLane) ?? {}, byBackendProfile: jsonRecordValue(record.byBackendProfile) ?? {}, generatedAt: stringValue(record.generatedAt), fullRecordBytes: jsonByteLength(record), valuesPrinted: false, }; } function summarizeQueueTaskWithAttempt(record: JsonRecord | null, fallbackTaskId: string): JsonRecord { const summary = summarizeQueueTaskRecord(record, fallbackTaskId); const latestAttempt = summarizeAttemptRecord(jsonRecordValue(record?.latestAttempt)); const supervisor = summarizeSupervisorRecord(jsonRecordValue(record?.supervisor)); const sessionRef = jsonRecordValue(record?.sessionRef); if (latestAttempt) summary.latestAttempt = latestAttempt; if (supervisor) summary.supervisor = supervisor; const sessionId = stringValue(sessionRef?.sessionId) ?? stringValue(latestAttempt?.sessionId); if (sessionId) summary.sessionId = sessionId; if (record?.readCursor !== undefined) summary.read = record.readCursor !== null; return summary; } function queueItems(value: JsonValue | undefined): JsonRecord[] { if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "queue response.items must be an array", { httpStatus: 2 }); return value.map((item) => { const record = jsonRecordValue(item); if (!record) throw new AgentRunError("schema-invalid", "queue response item must be an object", { httpStatus: 2 }); return record; }); } function sessionEventDrillDownCommands(kind: "trace" | "output", sessionId: string, options: { afterSeq: number; limit: number; runId: string | null }): JsonRecord { const base = `./scripts/agentrun sessions ${kind} ${sessionId} --after-seq ${options.afterSeq} --limit ${options.limit}${options.runId ? ` --run-id ${options.runId}` : ""}`; return { full: `${base} --full`, raw: `${base} --raw`, next: `./scripts/agentrun sessions ${kind} ${sessionId} --after-seq --limit ${options.limit}${options.runId ? ` --run-id ${options.runId}` : ""}`, }; } function summarizeQueueTaskRecord(record: JsonRecord | null, fallbackTaskId: string): JsonRecord { return compactRecord(record, { fallback: { id: fallbackTaskId }, keys: ["id", "state", "queue", "lane", "title", "priority", "backendProfile", "providerId", "sessionPath", "version", "updatedAt", "cancelledAt", "cancelReason", "readerId", "attentionState", "unread", "active"], }); } function summarizeAttemptRecord(record: JsonRecord | null): JsonRecord | null { if (!record) return null; return compactRecord(record, { keys: ["attemptId", "state", "runId", "commandId", "runnerJobId", "sessionId", "sessionPath"] }); } function summarizeSupervisorRecord(record: JsonRecord | null): JsonRecord | null { if (!record) return null; const lastActivity = jsonRecordValue(record.lastActivity); const timeoutBudget = jsonRecordValue(record.timeoutBudget); return { ...withoutFullRecordBytes(compactRecord(record, { keys: ["phase", "active", "status", "terminalStatus", "failureKind", "runId", "commandId", "lastSeq"] })), lastActivity: lastActivity ? withoutFullRecordBytes(compactRecord(lastActivity, { keys: ["sourceSeq", "eventId", "activityKind", "type", "status", "toolName", "itemId", "ageMs", "summary"] })) : null, timeoutBudget: timeoutBudget ? withoutFullRecordBytes(compactRecord(timeoutBudget, { keys: ["state", "timeoutMs", "elapsedMs", "remainingMs", "startedAt", "source"] })) : null, recoveryActions: summarizeRecoveryActions(record.recoveryActions), valuesPrinted: false, }; } function summarizeRecoveryActions(value: JsonValue | undefined): JsonValue[] { if (!Array.isArray(value)) return []; return value.slice(0, 5).map((item) => withoutFullRecordBytes(compactRecord(jsonRecordValue(item), { keys: ["action", "reason", "runId", "commandId", "sessionId", "afterSeq", "hint"] }))); } function withoutFullRecordBytes(record: JsonRecord): JsonRecord { const { fullRecordBytes: _omitted, ...rest } = record; return rest; } function summarizeRunRecord(record: JsonRecord | null): JsonRecord | null { if (!record) return null; return compactRecord(record, { keys: ["id", "status", "terminalStatus", "failureKind", "failureMessage", "backendProfile", "providerId", "claimedBy", "leaseExpiresAt", "createdAt", "updatedAt"] }); } function summarizeCommandRecord(record: JsonRecord | null): JsonRecord | null { if (!record) return null; return compactRecord(record, { keys: ["id", "runId", "seq", "type", "state", "createdAt", "updatedAt", "acknowledgedAt"] }); } function summarizeRunnerJobRecord(record: JsonRecord | null): JsonRecord | null { if (!record) return null; const runner = jsonRecordValue(record.runner); const jobIdentity = jsonRecordValue(record.jobIdentity); const kubernetes = jsonRecordValue(record.kubernetes); return { ...compactRecord(record, { keys: ["action", "mutation", "runId", "commandId", "attemptId", "runnerId", "namespace", "jobName"] }), logPath: stringValue(runner?.logPath), backendProfile: stringValue(runner?.backendProfile), jobUid: stringValue(jobIdentity?.uid), created: kubernetes?.created === true, warnings: Array.isArray(record.warnings) ? record.warnings.map((item) => boundedSummaryString(typeof item === "string" ? item : JSON.stringify(item), 240)).filter((item): item is string => Boolean(item)) : [], fullResponseBytes: jsonByteLength(record), valuesPrinted: false, }; } function summarizeGenericRecord(record: JsonRecord | null): JsonRecord | null { if (!record) return null; return compactRecord(record, { keys: ["id", "runId", "commandId", "sessionId", "readerId", "sessionVersion", "taskId", "taskVersion", "status", "state", "terminalStatus", "failureKind", "failureMessage", "updatedAt", "readAt", "cancelledAt", "cancelReason"], }); } function compactRecord(record: JsonRecord | null, options: { keys: string[]; fallback?: JsonRecord }): JsonRecord { const result: JsonRecord = { ...(options.fallback ?? {}) }; if (!record) return result; for (const key of options.keys) { const value = record[key]; if (value === undefined) continue; if (typeof value === "string") result[key] = boundedSummaryString(value, key.toLowerCase().includes("message") ? 300 : 900) ?? ""; else if (typeof value === "number" || typeof value === "boolean" || value === null) result[key] = value; } result.fullRecordBytes = jsonByteLength(record); result.valuesPrinted = false; return result; } function jsonByteLength(value: unknown): number { try { return Buffer.byteLength(JSON.stringify(redactJson(value)) ?? "null", "utf8"); } catch { return 0; } } function findNestedValue(value: unknown, key: string, depth = 0): unknown | null { if (depth > 8 || value === null || value === undefined) return null; if (Array.isArray(value)) { for (const item of value) { const found = findNestedValue(item, key, depth + 1); if (found !== null) return found; } return null; } if (typeof value !== "object") return null; const record = value as Record; if (Object.prototype.hasOwnProperty.call(record, key)) return record[key] ?? null; for (const entry of Object.values(record)) { const found = findNestedValue(entry, key, depth + 1); if (found !== null) return found; } return null; } function hasNestedKey(value: unknown, keys: readonly string[], depth = 0): boolean { if (depth > 8 || value === null || value === undefined) return false; if (Array.isArray(value)) return value.some((item) => hasNestedKey(item, keys, depth + 1)); if (typeof value !== "object") return false; const record = value as Record; if (keys.some((key) => Object.prototype.hasOwnProperty.call(record, key))) return true; return Object.values(record).some((entry) => hasNestedKey(entry, keys, depth + 1)); } async function listSessions(args: ParsedArgs): Promise { const params = new URLSearchParams(); const state = optionalFlag(args, "state") ?? (args.flags.get("running") === true ? "running" : args.flags.get("unread") === true ? "unread" : args.flags.get("all") === true ? "all" : null); const profile = optionalFlag(args, "profile") ?? optionalFlag(args, "backend-profile"); const readerId = optionalFlag(args, "reader-id"); const cursor = optionalFlag(args, "cursor"); const limit = optionalFlag(args, "limit"); if (state) params.set("state", state); if (profile) params.set("profile", normalizeProfile(profile)); if (readerId) params.set("readerId", readerId); if (cursor) params.set("cursor", cursor); if (limit) params.set("limit", limit); const query = params.toString(); return client(args).get(`/api/v1/sessions${query ? `?${query}` : ""}`); } async function sessionEvents(args: ParsedArgs, sessionId: string, kind: "trace" | "output"): Promise { const params = new URLSearchParams(); const requestedSeq = optionalIntegerFlag(args, "seq", { min: 0 }); const hasExplicitAfterSeq = optionalFlag(args, "after-seq") !== null; const afterSeq = requestedSeq !== null && !hasExplicitAfterSeq ? Math.max(0, requestedSeq - 1) : integerFlag(args, "after-seq", 0, { min: 0 }); const limit = requestedSeq !== null && optionalFlag(args, "limit") === null ? 1 : integerFlag(args, "limit", 100, { min: 1, max: 500 }); const runId = optionalFlag(args, "run-id"); const detailFilter = sessionEventDetailFilter(args, requestedSeq); if (detailFilter && requestedSeq === null && !hasExplicitAfterSeq && (detailFilter.eventId || detailFilter.itemId)) { return scanSessionEventDetail(args, { kind, sessionId, runId, afterSeq, limit, summaryChars: integerFlag(args, "summary-chars", 1_200, { min: 1, max: 8_000 }), filter: detailFilter }); } params.set("afterSeq", String(afterSeq)); params.set("limit", String(limit)); if (runId) params.set("runId", runId); const query = params.toString(); const page = await client(args).get(`/api/v1/sessions/${encodeURIComponent(sessionId)}/${kind}${query ? `?${query}` : ""}`); if (detailFilter) return sessionEventDetailResult(page, { kind, sessionId, runId, summaryChars: integerFlag(args, "summary-chars", 1_200, { min: 1, max: 8_000 }), filter: detailFilter }); if (wantsExpandedOutput(args)) return page; return summarizeSessionEventPage(page, { kind, sessionId, afterSeq, limit, runId, tail: tailFlag(args, limit), summaryChars: integerFlag(args, "summary-chars", 900, { min: 1, max: 4_000 }), includeOutput: args.flags.get("include-output") === true, }); } async function scanSessionEventDetail(args: ParsedArgs, options: { kind: "trace" | "output"; sessionId: string; runId: string | null; afterSeq: number; limit: number; summaryChars: number; filter: SessionEventDetailFilter }): Promise { const maxPages = integerFlag(args, "detail-scan-pages", 20, { min: 1, max: 100 }); let afterSeq = options.afterSeq; let pagesScanned = 0; let eventsScanned = 0; while (pagesScanned < maxPages) { const params = new URLSearchParams(); params.set("afterSeq", String(afterSeq)); params.set("limit", String(options.limit)); if (options.runId) params.set("runId", options.runId); const query = params.toString(); const page = await client(args).get(`/api/v1/sessions/${encodeURIComponent(options.sessionId)}/${options.kind}${query ? `?${query}` : ""}`); const events = eventPageItems(page); pagesScanned += 1; eventsScanned += events.length; const matches = events.filter((event) => matchesSessionEventFilter(event, options.filter)); if (matches.length > 0) return sessionEventDetailResultFromMatches(page, matches, { kind: options.kind, sessionId: options.sessionId, runId: options.runId, summaryChars: options.summaryChars, filter: options.filter, pagesScanned, eventsScanned }); const lastSeq = events.length > 0 ? numberValue(events[events.length - 1]?.seq) : null; const cursorSeq = numberValue(jsonRecordValue(page)?.cursor); const nextSeq = cursorSeq ?? lastSeq; if (nextSeq === null || nextSeq <= afterSeq) break; afterSeq = nextSeq; } throw new AgentRunError("schema-invalid", "no session event matched --event-id/--item-id in scanned pages", { httpStatus: 2, details: { filter: options.filter as unknown as JsonRecord, pagesScanned, eventsScanned, nextAfterSeq: afterSeq, hint: "use the detailCommands from the summary, pass --seq, or add --after-seq/--limit near the event if the trace is longer than the scan window", }, }); } async function sessionCreate(args: ParsedArgs, positionalSessionId: string | null): Promise { const sessionId = positionalSessionId ?? optionalFlag(args, "session-id") ?? newSessionId(); const profile = normalizeProfile(optionalFlag(args, "profile") ?? optionalFlag(args, "backend-profile") ?? "codex"); const tenantId = optionalFlag(args, "tenant-id") ?? "unidesk"; const projectId = optionalFlag(args, "project-id") ?? "default"; const providerId = optionalFlag(args, "provider-id") ?? "G14"; const expiresInDays = Number(optionalFlag(args, "expires-in-days") ?? 30); const expiresAt = new Date(Date.now() + Math.max(1, expiresInDays) * 24 * 60 * 60 * 1000).toISOString(); const created = await client(args).post("/api/v1/sessions", { sessionId, tenantId, projectId, backendProfile: profile, expiresAt, }) as { action: string; pvc: { pvcName: string; pvcPhase: string }; session: { sessionId: string; storageKind: string; codexRolloutSubdir: string } }; const storage = await client(args).get(`/api/v1/sessions/${encodeURIComponent(sessionId)}/storage`) as { pvcName: string; pvcPhase: string; storageSizeBytes: number | null }; return { action: created.action, session: created.session, pvc: created.pvc, storage, pollCommands: { show: `./scripts/agentrun sessions show ${sessionId} --reader-id cli`, storage: `./scripts/agentrun sessions storage ${sessionId}`, trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100`, turn: `./scripts/agentrun sessions turn ${sessionId} --prompt "..."`, }, }; } async function sessionStorageGet(args: ParsedArgs, sessionId: string): Promise { return (await client(args).get(`/api/v1/sessions/${encodeURIComponent(sessionId)}/storage`)) as JsonRecord; } async function sessionStorageDelete(args: ParsedArgs, sessionId: string): Promise { return (await client(args).delete(`/api/v1/sessions/${encodeURIComponent(sessionId)}/storage`)) as JsonRecord; } async function sessionTurn(args: ParsedArgs, positionalSessionId: string | null): Promise { const body = await optionalJsonFile(args); const sessionId = positionalSessionId ?? optionalFlag(args, "session-id") ?? newSessionId(); const requestedProfile = optionalFlag(args, "profile") ?? optionalFlag(args, "backend-profile") ?? (typeof body.backendProfile === "string" ? String(body.backendProfile) : "codex"); const profile = normalizeProfile(requestedProfile); if (positionalSessionId || optionalFlag(args, "session-id")) { try { await client(args).get(`/api/v1/sessions/${encodeURIComponent(sessionId)}/storage`); } catch (error) { const expiresInDays = Number(optionalFlag(args, "expires-in-days") ?? 30); await client(args).post("/api/v1/sessions", { sessionId, tenantId: optionalFlag(args, "tenant-id") ?? "unidesk", projectId: optionalFlag(args, "project-id") ?? "default", backendProfile: profile, expiresAt: new Date(Date.now() + Math.max(1, expiresInDays) * 24 * 60 * 60 * 1000).toISOString(), }); } } const prompt = await readPrompt(args); body.tenantId = optionalFlag(args, "tenant-id") ?? stringField(body, "tenantId", "unidesk"); body.projectId = optionalFlag(args, "project-id") ?? stringField(body, "projectId", "default"); body.providerId = optionalFlag(args, "provider-id") ?? stringField(body, "providerId", "G14"); body.backendProfile = profile; body.workspaceRef = jsonObjectFlag(args, "workspace-json") ?? objectField(body, "workspaceRef", { kind: "opaque", path: "." }); body.executionPolicy = jsonObjectFlag(args, "execution-policy-json") ?? objectField(body, "executionPolicy", defaultExecutionPolicy(profile)); body.traceSink = body.traceSink ?? null; const sessionRef = objectField(body, "sessionRef", {}); const sessionMetadata = objectField(sessionRef, "metadata", {}); const title = optionalFlag(args, "title"); if (title) sessionMetadata.title = title; body.sessionRef = { ...sessionRef, sessionId, metadata: sessionMetadata }; const run = await client(args).post("/api/v1/runs", body) as RunRecord; const commandBody: JsonRecord = { type: "turn", payload: { prompt } }; const commandIdempotencyKey = optionalFlag(args, "command-idempotency-key") ?? optionalFlag(args, "idempotency-key"); if (commandIdempotencyKey) commandBody.idempotencyKey = commandIdempotencyKey; const command = await client(args).post(`/api/v1/runs/${encodeURIComponent(run.id)}/commands`, commandBody) as CommandRecord; let runnerJob: JsonValue = null; if (args.flags.get("no-runner-job") !== true) { const runnerBody = await optionalRunnerJsonFile(args); runnerBody.commandId = command.id; copyOptionalFlag(args, runnerBody, "image"); copyOptionalFlag(args, runnerBody, "namespace"); copyOptionalFlag(args, runnerBody, "attempt-id", "attemptId"); copyOptionalFlag(args, runnerBody, "runner-id", "runnerId"); copyOptionalFlag(args, runnerBody, "source-commit", "sourceCommit"); copyRunnerManagerUrlFlag(args, runnerBody); copyOptionalFlag(args, runnerBody, "service-account-name", "serviceAccountName"); const runnerIdempotencyKey = optionalFlag(args, "runner-idempotency-key"); if (runnerIdempotencyKey) runnerBody.idempotencyKey = runnerIdempotencyKey; runnerJob = await client(args).post(`/api/v1/runs/${encodeURIComponent(run.id)}/runner-jobs`, runnerBody); } return { action: "session-turn", sessionId, profile, run, command, runnerJob, pollCommands: { ps: `./scripts/agentrun sessions ps --reader-id cli --profile ${profile}`, show: `./scripts/agentrun sessions show ${sessionId} --reader-id cli`, trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100`, output: `./scripts/agentrun sessions output ${sessionId} --after-seq 0 --limit 100`, read: `./scripts/agentrun sessions read ${sessionId} --reader-id cli`, steer: `./scripts/agentrun sessions steer ${sessionId} --prompt-file `, cancel: `./scripts/agentrun sessions cancel ${sessionId}` } }; } async function sessionSteer(args: ParsedArgs, sessionId: string): Promise { const session = await client(args).get(`/api/v1/sessions/${encodeURIComponent(sessionId)}${readerQuery(args)}`) as SessionSummary; const runId = session.activeRunId ?? session.lastRunId; if (!runId) throw new AgentRunError("schema-invalid", `session ${sessionId} has no run to steer`, { httpStatus: 2 }); const prompt = await readPrompt(args); const body: JsonRecord = { type: "steer", payload: { prompt } }; const idempotencyKey = optionalFlag(args, "idempotency-key"); if (idempotencyKey) body.idempotencyKey = idempotencyKey; const command = await client(args).post(`/api/v1/runs/${encodeURIComponent(runId)}/commands`, body); return { action: "session-steer", sessionId, runId, command }; } async function sessionCancel(args: ParsedArgs, sessionId: string): Promise { const result = await client(args).post(`/api/v1/sessions/${encodeURIComponent(sessionId)}/control`, { action: "cancel", ...cancelBody(args) }); if (wantsExpandedOutput(args)) return { action: "session-cancel", sessionId, result: result as JsonValue }; return summarizeSessionMutationResult("session-cancel", sessionId, result, { cancelled: true }); } async function sessionRead(args: ParsedArgs, sessionId: string): Promise { const result = await client(args).post(`/api/v1/sessions/${encodeURIComponent(sessionId)}/read`, { readerId: optionalFlag(args, "reader-id") ?? "cli" }); if (wantsExpandedOutput(args)) return result; return summarizeSessionMutationResult("session-read", sessionId, result, { read: true }); } async function submitQueueTask(args: ParsedArgs): Promise { const body = await jsonFile(args); const idempotencyKey = optionalFlag(args, "idempotency-key"); if (idempotencyKey) body.idempotencyKey = idempotencyKey; if (args.flags.get("dry-run") === true) { return queueMutationDryRunPlan("queue-submit", null, "/api/v1/queue/tasks", body, "POST", `./scripts/agentrun queue submit ${jsonInputHelp(args, "")}`); } return client(args).post("/api/v1/queue/tasks", body); } async function listQueueTasks(args: ParsedArgs): Promise { const params = new URLSearchParams(); const queue = optionalFlag(args, "queue"); const state = optionalFlag(args, "state"); const cursor = optionalFlag(args, "cursor"); const limit = optionalFlag(args, "limit"); const updatedAfter = optionalFlag(args, "updated-after"); if (queue) params.set("queue", queue); if (state) params.set("state", state); if (cursor) params.set("cursor", cursor); if (limit) params.set("limit", limit); if (updatedAfter) params.set("updatedAfter", updatedAfter); const query = params.toString(); const result = await client(args).get(`/api/v1/queue/tasks${query ? `?${query}` : ""}`); if (wantsExpandedOutput(args)) return result; return summarizeQueueTaskListResult(result, { limit: integerFlag(args, "limit", 20, { min: 1, max: 100 }) }); } async function showQueueTask(args: ParsedArgs, taskId: string): Promise { const result = await client(args).get(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}`); if (wantsExpandedOutput(args)) return result; return summarizeQueueTaskShowResult(result, taskId); } async function queueCommander(args: ParsedArgs): Promise { const result = await client(args).get(`/api/v1/queue/commander${queueQuery(args, { readerId: true })}`); if (wantsExpandedOutput(args)) return result; return summarizeQueueCommanderSnapshot(result, { limit: integerFlag(args, "limit", 20, { min: 1, max: 100 }) }); } function queueQuery(args: ParsedArgs, options: { readerId?: boolean } = {}): string { const params = new URLSearchParams(); const queue = optionalFlag(args, "queue"); if (queue) params.set("queue", queue); if (options.readerId) { const readerId = optionalFlag(args, "reader-id"); if (readerId) params.set("readerId", readerId); } const query = params.toString(); return query ? `?${query}` : ""; } async function dispatchQueueTask(args: ParsedArgs, taskId: string): Promise { const body = await queueDispatchBody(args); if (args.flags.get("dry-run") === true) { const task = await client(args).get(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}`); return queueMutationDryRunPlan("queue-dispatch", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/dispatch`, body, "POST", `./scripts/agentrun queue dispatch ${taskId} ${jsonInputHelp(args, "")}`, task); } const result = await client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}/dispatch`, body); if (wantsExpandedOutput(args)) return result; return summarizeQueueDispatchResult(result, taskId); } async function queueDispatchBody(args: ParsedArgs): Promise { const body = await optionalJsonFile(args); const copy = (flagName: string, key = flagName.replace(/-([a-z])/gu, (_, letter: string) => letter.toUpperCase())): void => { const value = optionalFlag(args, flagName); if (value) body[key] = value; }; copy("idempotency-key", "idempotencyKey"); copy("image"); copy("namespace"); copy("attempt-id", "attemptId"); copy("runner-id", "runnerId"); copy("source-commit", "sourceCommit"); copyRunnerManagerUrlFlag(args, body); copy("service-account-name", "serviceAccountName"); return body; } async function readQueueTask(args: ParsedArgs, taskId: string): Promise { const body = { readerId: optionalFlag(args, "reader-id") ?? "cli" }; if (args.flags.get("dry-run") === true) return queueMutationDryRunPlan("queue-read", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/read`, body, "POST", `./scripts/agentrun queue read ${taskId}`); const result = await client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}/read`, body); if (wantsExpandedOutput(args)) return result; return summarizeQueueTaskMutationResult("queue-read", taskId, result, { read: true }); } async function cancelQueueTask(args: ParsedArgs, taskId: string): Promise { const body = cancelBody(args); if (args.flags.get("dry-run") === true) return queueMutationDryRunPlan("queue-cancel", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/cancel`, body, "POST", `./scripts/agentrun queue cancel ${taskId}${body.reason ? " --reason " : ""}`); const result = await client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}/cancel`, body); if (wantsExpandedOutput(args)) return result; return summarizeQueueTaskMutationResult("queue-cancel", taskId, result, { cancelled: true }); } async function refreshQueueTask(args: ParsedArgs, taskId: string): Promise { const body: JsonRecord = {}; if (args.flags.get("dry-run") === true) return queueMutationDryRunPlan("queue-refresh", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/refresh`, body, "POST", `./scripts/agentrun queue refresh ${taskId}`); const result = await client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}/refresh`, body); if (wantsExpandedOutput(args)) return result; return summarizeQueueTaskMutationResult("queue-refresh", taskId, result, { refreshed: true }); } async function showRunnerJobStatus(args: ParsedArgs): Promise { const runId = flag(args, "run-id", ""); if (!runId) throw new AgentRunError("schema-invalid", "runner job-status requires --run-id", { httpStatus: 2 }); const runnerJobId = args.positional[2] ?? optionalFlag(args, "runner-job-id"); if (!runnerJobId) return listRunnerJobs(args); return client(args).get(`/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs/${encodeURIComponent(runnerJobId)}`); } async function renderRunnerJob(args: ParsedArgs): Promise { const runId = flag(args, "run-id", ""); const commandId = flag(args, "command-id", ""); if (!runId) throw new AgentRunError("schema-invalid", "runner job requires --run-id", { httpStatus: 2 }); if (!commandId) throw new AgentRunError("schema-invalid", "runner job requires --command-id", { httpStatus: 2 }); const image = optionalFlag(args, "image"); if (args.flags.get("dry-run") !== true) { const body: JsonRecord = { commandId }; if (image) body.image = image; const namespace = optionalFlag(args, "namespace"); const attemptId = optionalFlag(args, "attempt-id"); const runnerId = optionalFlag(args, "runner-id"); const sourceCommit = optionalFlag(args, "source-commit"); const runnerManagerUrl = optionalFlag(args, "runner-manager-url"); const idempotencyKey = optionalFlag(args, "idempotency-key"); if (namespace) body.namespace = namespace; if (attemptId) body.attemptId = attemptId; if (runnerId) body.runnerId = runnerId; if (sourceCommit) body.sourceCommit = sourceCommit; if (runnerManagerUrl) body.managerUrl = resolveRunnerManagerUrlFlag(args, runnerManagerUrl); if (idempotencyKey) body.idempotencyKey = idempotencyKey; return await client(args).post(`/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs`, body) as JsonRecord; } if (!image) throw new AgentRunError("schema-invalid", "runner job --dry-run requires --image", { httpStatus: 2 }); const run = await client(args).get(`/api/v1/runs/${encodeURIComponent(runId)}`) as RunRecord; let sessionPvc: RunnerSessionPvcOptions | undefined; if (run.sessionRef?.sessionId) { try { const session = await client(args).get(`/api/v1/sessions/${encodeURIComponent(run.sessionRef.sessionId)}`) as { storageKind?: string; storagePvcName?: string; storageNamespace?: string; codexRolloutSubdir?: string }; if (session?.storageKind === "pvc" && session.storagePvcName) { const subdir = session.codexRolloutSubdir ?? "sessions"; sessionPvc = { pvcName: session.storagePvcName, namespace: session.storageNamespace ?? "agentrun-v01", mountPath: `/home/agentrun/.codex-${run.backendProfile}/${subdir}`, codexRolloutSubdir: subdir }; } } catch { /* session not found, skip */ } } const options = { run, commandId, image, managerUrl: managerUrl(args), namespace: optionalFlag(args, "namespace") ?? "agentrun-v01", ...(sessionPvc ? { sessionPvc } : {}), }; const attemptId = optionalFlag(args, "attempt-id"); const runnerId = optionalFlag(args, "runner-id"); const sourceCommit = optionalFlag(args, "source-commit"); return renderRunnerJobDryRun({ ...options, ...(attemptId ? { attemptId } : {}), ...(runnerId ? { runnerId } : {}), ...(sourceCommit ? { sourceCommit } : {}), }); } async function renderCodexSecret(args: ParsedArgs): Promise { if (args.flags.get("dry-run") !== true) { throw new AgentRunError("schema-invalid", "secrets codex render requires --dry-run", { httpStatus: 2 }); } const options: Parameters[0] = { dryRun: true }; const profile = optionalFlag(args, "profile"); const codexHome = optionalFlag(args, "codex-home"); const authFile = optionalFlag(args, "auth-file"); const configFile = optionalFlag(args, "config-file"); const modelCatalogFile = optionalFlag(args, "model-catalog-file"); const namespace = optionalFlag(args, "namespace"); const secretName = optionalFlag(args, "secret-name"); if (profile) options.profile = profile; if (codexHome) options.codexHome = codexHome; if (authFile) options.authFile = authFile; if (configFile) options.configFile = configFile; if (modelCatalogFile) options.modelCatalogFile = modelCatalogFile; if (namespace) options.namespace = namespace; if (secretName) options.secretName = secretName; return renderCodexProviderSecretPlan(options); } async function setProviderProfileKey(args: ParsedArgs, profileValue: string): Promise { const profile = normalizeProfile(profileValue); if (args.flags.get("key-stdin") !== true) throw new AgentRunError("schema-invalid", "provider-profiles set-key requires --key-stdin", { httpStatus: 2 }); const apiKey = (await readStdinText()).trim(); if (apiKey.length === 0) throw new AgentRunError("schema-invalid", "stdin api key is empty", { httpStatus: 2 }); const body: JsonRecord = { apiKey, reason: optionalFlag(args, "reason") ?? "operator-cli", delegatedBy: { system: optionalFlag(args, "delegated-system") ?? "operator-cli", userId: optionalFlag(args, "delegated-user-id") ?? null, username: optionalFlag(args, "delegated-username") ?? null, requestId: optionalFlag(args, "delegated-request-id") ?? null, }, }; const config: JsonRecord = {}; copyOptionalFlag(args, config, "model"); copyOptionalFlag(args, config, "base-url", "baseUrl"); copyOptionalFlag(args, config, "provider-name", "providerName"); copyOptionalFlag(args, config, "env-key", "envKey"); if (Object.keys(config).length > 0) body.config = config; return await client(args).put(`/api/v1/provider-profiles/${encodeURIComponent(profile)}/credential`, body) as JsonRecord; } async function setProviderProfileConfig(args: ParsedArgs, profileValue: string): Promise { const profile = normalizeProfile(profileValue); if (args.flags.get("config-stdin") !== true) throw new AgentRunError("schema-invalid", "provider-profiles set-config requires --config-stdin", { httpStatus: 2 }); const configToml = await readStdinText(); if (configToml.trim().length === 0) throw new AgentRunError("schema-invalid", "stdin config.toml is empty", { httpStatus: 2 }); return await client(args).put(`/api/v1/provider-profiles/${encodeURIComponent(profile)}/config`, { configToml, reason: optionalFlag(args, "reason") ?? "operator-cli", delegatedBy: { system: optionalFlag(args, "delegated-system") ?? "operator-cli", userId: optionalFlag(args, "delegated-user-id") ?? null, username: optionalFlag(args, "delegated-username") ?? null, requestId: optionalFlag(args, "delegated-request-id") ?? null, }, }) as JsonRecord; } async function removeProviderProfileCli(profileValue: string, args: ParsedArgs): Promise { const profile = normalizeProfile(profileValue); return await client(args).delete(`/api/v1/provider-profiles/${encodeURIComponent(profile)}`) as JsonRecord; } async function validateProviderProfileCli(args: ParsedArgs, profileValue: string): Promise { const profile = normalizeProfile(profileValue); const started = await client(args).post(`/api/v1/provider-profiles/${encodeURIComponent(profile)}/validate`, {}) as JsonRecord; if (args.flags.get("wait") !== true) return started; const validationId = typeof started.validationId === "string" ? started.validationId : ""; if (!validationId) throw new AgentRunError("infra-failed", "provider profile validate response omitted validationId", { httpStatus: 1, details: started }); const timeoutMs = Math.max(1, Number(optionalFlag(args, "timeout-ms") ?? 60_000)); const deadline = Date.now() + timeoutMs; let latest: JsonRecord = started; while (Date.now() < deadline) { await sleep(2_000); latest = await client(args).get(`/api/v1/provider-profiles/${encodeURIComponent(profile)}/validations/${encodeURIComponent(validationId)}`) as JsonRecord; if (latest.status === "completed" || latest.status === "failed" || latest.status === "cancelled") return { action: "provider-profile-validation", initial: started, latest, valuesPrinted: false }; } return { action: "provider-profile-validation", initial: started, latest, timedOut: true, timeoutMs, valuesPrinted: false }; } async function startServer(args: ParsedArgs): Promise { if (args.flags.get("foreground") === true) return startServerForeground(args); const port = Number(flag(args, "port", "8080")); const host = flag(args, "host", "0.0.0.0"); const state = await readServerState(port); if (state.pidAlive || state.portListening) { throw new AgentRunError("infra-failed", `agentrun-mgr already appears to be running on port ${port}; use server status or server stop first`, { httpStatus: 409, details: state as JsonRecord }); } await ensureDir(stateDir()); await ensureDir(logDir()); const logPath = serverLogPath(port); const argsForChild = [process.argv[1] ?? "scripts/agentrun-cli.ts", "server", "start", "--foreground", "--host", host, "--port", String(port)]; const store = optionalFlag(args, "store"); if (store) argsForChild.push("--store", store); const stdoutFd = openSync(logPath, "a"); const stderrFd = openSync(logPath, "a"); const child = spawn(process.execPath, argsForChild, { cwd: process.cwd(), env: process.env, detached: true, stdio: ["ignore", stdoutFd, stderrFd], }); closeSync(stdoutFd); closeSync(stderrFd); child.unref(); const pidFile = pidFilePath(port); await writeFile(pidFile, JSON.stringify({ pid: child.pid, port, host, logPath, startedAt: new Date().toISOString() }) + "\n", "utf8"); const localBaseUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`; return { action: "server-start", mode: "background", serviceId: "agentrun-mgr", pid: child.pid ?? null, port, host, baseUrl: localBaseUrl, pidFile, logPath, pollCommands: { status: `./scripts/agentrun server status --port ${port}`, logs: `./scripts/agentrun server logs --port ${port}`, stop: `./scripts/agentrun server stop --port ${port}` } }; } async function startServerForeground(args: ParsedArgs): Promise { const port = Number(flag(args, "port", "8080")); const host = flag(args, "host", "0.0.0.0"); const storeMode = optionalFlag(args, "store") ?? process.env.AGENTRUN_STORE ?? process.env.AGENTRUN_MGR_STORE; const started = await startManagerServer({ port, host, ...(storeMode === "memory" ? { store: new MemoryAgentRunStore() } : {}) }); const database = await started.store.health(); return { serviceId: "agentrun-mgr", baseUrl: started.baseUrl, pid: process.pid, database, mode: "foreground", note: "foreground process; use server start without --foreground for local background mode" }; } async function serverStatus(args: ParsedArgs): Promise { const explicitPort = optionalFlag(args, "port"); const baseUrl = explicitPort ? `http://127.0.0.1:${Number(explicitPort)}` : managerUrl(args); const port = Number(explicitPort ?? portFromUrl(baseUrl)); const state = await readServerState(port); let readiness: JsonValue = null; let readinessFailure: JsonRecord | null = null; if (explicitPort ? state.portListening : true) { try { readiness = await new ManagerClient(baseUrl).get("/health/readiness"); } catch (error) { readinessFailure = errorToJson(error); } } return { action: "server-status", serviceId: "agentrun-mgr", port, baseUrl, local: state as JsonRecord, readiness, readinessFailure, pollCommands: { status: `./scripts/agentrun server status --port ${port}`, logs: `./scripts/agentrun server logs --port ${port}`, stop: `./scripts/agentrun server stop --port ${port}` } }; } async function serverLogs(args: ParsedArgs): Promise { const port = Number(flag(args, "port", portFromManagerUrl(args))); const state = await readServerState(port); const logPath = optionalFlag(args, "log-file") ?? (typeof state.logPath === "string" ? state.logPath : null); const tailBytes = Number(flag(args, "tail-bytes", "12000")); if (!logPath) return { action: "server-logs", serviceId: "agentrun-mgr", port, logPath: null, exists: false, tail: "", bytes: 0, truncated: false, message: "no log file recorded for this port" }; if (!existsSync(logPath)) return { action: "server-logs", serviceId: "agentrun-mgr", port, logPath, exists: false, tail: "", bytes: 0, truncated: false, message: "log file does not exist" }; const bytes = await readFile(logPath); const start = Math.max(0, bytes.byteLength - tailBytes); return { action: "server-logs", serviceId: "agentrun-mgr", port, logPath, exists: true, bytes: bytes.byteLength, tailBytes, truncated: start > 0, tail: bytes.subarray(start).toString("utf8") }; } async function stopServer(args: ParsedArgs): Promise { const port = Number(flag(args, "port", portFromManagerUrl(args))); const before = await readServerState(port); let signalSent = false; const beforePid = typeof before.pid === "number" ? before.pid : null; const beforePortPid = typeof before.portPid === "number" ? before.portPid : null; if (before.pidAlive === true && beforePid !== null) { process.kill(beforePid, "SIGTERM"); signalSent = true; } else if (beforePortPid !== null) { process.kill(beforePortPid, "SIGTERM"); signalSent = true; } await sleep(500); const after = await readServerState(port); if (!after.pidAlive && !after.portListening && existsSync(pidFilePath(port))) await rm(pidFilePath(port), { force: true }); return { action: "server-stop", serviceId: "agentrun-mgr", port, signalSent, before: before as JsonRecord, after: after as JsonRecord, stopped: !after.pidAlive && !after.portListening }; } function client(args: ParsedArgs): ManagerClient { return new ManagerClient(managerUrl(args)); } function managerUrl(args: ParsedArgs): string { const explicit = optionalFlag(args, "manager-url") ?? process.env.AGENTRUN_MGR_URL; if (explicit && explicit !== "auto") return explicit; if (explicit === "auto") return resolveRuntimeManagerUrl(); return "http://127.0.0.1:8080"; } function managerEndpoint(args: ParsedArgs): JsonRecord { const url = managerUrl(args); const explicit = optionalFlag(args, "manager-url") ?? process.env.AGENTRUN_MGR_URL ?? null; return { action: "manager-endpoint", managerUrl: url, source: explicit === "auto" ? "auto" : explicit ? (optionalFlag(args, "manager-url") ? "--manager-url" : "AGENTRUN_MGR_URL") : "default-localhost", runtimeNamespace: "agentrun-v01", serviceName: "agentrun-mgr", valuesPrinted: false, examples: { commander: "./scripts/agentrun --manager-url auto queue commander", sessions: "./scripts/agentrun --manager-url auto sessions ps --state default --reader-id cli", explicit: "./scripts/agentrun --manager-url http://:8080 queue commander", }, }; } function resolveRuntimeManagerUrl(): string { const fromEnv = process.env.AGENTRUN_RUNTIME_MANAGER_URL; if (fromEnv) return fromEnv; try { const serviceIp = execFileSync("kubectl", ["-n", "agentrun-v01", "get", "svc", "agentrun-mgr", "-o", "jsonpath={.spec.clusterIP}"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); if (serviceIp.length > 0 && serviceIp !== "None") return `http://${serviceIp}:8080`; } catch { // Fall through to localhost so the following request produces a structured connection failure. } return "http://127.0.0.1:8080"; } function portFromManagerUrl(args: ParsedArgs): string { return portFromUrl(managerUrl(args)); } function portFromUrl(value: string): string { try { const url = new URL(value); return url.port || (url.protocol === "https:" ? "443" : "80"); } catch { return "8080"; } } function stateDir(): string { return path.join(process.cwd(), ".state"); } function logDir(): string { return path.join(process.cwd(), "logs", new Date().toISOString().slice(0, 10).replace(/-/gu, "")); } function pidFilePath(port: number): string { return path.join(stateDir(), `agentrun-mgr-${port}.pid.json`); } function serverLogPath(port: number): string { return path.join(logDir(), `agentrun-mgr-${port}-${new Date().toISOString().replace(/[:.]/gu, "-")}.jsonl`); } async function ensureDir(dir: string): Promise { await mkdir(dir, { recursive: true }); } async function readServerState(port: number): Promise { const pidFile = pidFilePath(port); const pidRecord = await readPidFile(pidFile); const pid = typeof pidRecord?.pid === "number" ? pidRecord.pid : null; const pidAlive = pid !== null && isPidAlive(pid); const portPid = await pidForPort(port); return { pidFile, pid, pidAlive, port, portListening: portPid !== null, portPid, logPath: typeof pidRecord?.logPath === "string" ? pidRecord.logPath : null, startedAt: typeof pidRecord?.startedAt === "string" ? pidRecord.startedAt : null }; } async function readPidFile(pidFile: string): Promise { try { return JSON.parse(await readFile(pidFile, "utf8")) as JsonRecord; } catch { return null; } } function isPidAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } } async function pidForPort(port: number): Promise { const proc = spawn("sh", ["-c", `command -v ss >/dev/null 2>&1 && ss -ltnp 'sport = :${port}' || true`], { stdio: ["ignore", "pipe", "ignore"] }); const chunks: Buffer[] = []; proc.stdout.on("data", (chunk: Buffer) => chunks.push(chunk)); await new Promise((resolve) => proc.on("close", () => resolve())); const text = Buffer.concat(chunks).toString("utf8"); const match = text.match(/pid=(\d+)/u); return match ? Number(match[1]) : null; } async function sleep(ms: number): Promise { await new Promise((resolve) => setTimeout(resolve, ms)); } async function jsonFile(args: ParsedArgs): Promise { if (args.flags.get("json-stdin") === true) return parseJsonObject(await readStdinText(), "stdin json"); const file = optionalFlag(args, "json-file"); if (!file) throw new AgentRunError("schema-invalid", "JSON input is required; use --json-file or --json-stdin", { httpStatus: 2 }); return parseJsonObject(await readFile(file, "utf8"), "json file"); } async function optionalJsonFile(args: ParsedArgs): Promise { if (args.flags.get("json-stdin") === true) return jsonFile(args); const file = optionalFlag(args, "json-file"); if (!file) return {}; return jsonFile(args); } async function optionalRunnerJsonFile(args: ParsedArgs): Promise { if (args.flags.get("runner-json-stdin") === true) return parseJsonObject(await readStdinText(), "runner stdin json"); const file = optionalFlag(args, "runner-json-file"); if (!file) return {}; return parseJsonObject(await readFile(file, "utf8"), "runner json file"); } function parseJsonObject(text: string, source: string): JsonRecord { const value = JSON.parse(text) as unknown; if (typeof value === "object" && value !== null && !Array.isArray(value)) return value as JsonRecord; throw new AgentRunError("schema-invalid", `${source} must contain an object`, { httpStatus: 2 }); } function jsonInputHelp(args: ParsedArgs, filePlaceholder: string): string { return args.flags.get("json-stdin") === true ? "--json-stdin" : `--json-file ${filePlaceholder}`; } async function readPrompt(args: ParsedArgs): Promise { const promptFlag = optionalFlag(args, "prompt"); if (promptFlag) return promptFlag; const promptFile = optionalFlag(args, "prompt-file"); if (promptFile) { const text = await readFile(promptFile, "utf8"); if (text.trim().length === 0) throw new AgentRunError("schema-invalid", "prompt file is empty", { httpStatus: 2 }); return text; } if (args.flags.get("prompt-stdin") === true) { const text = await readStdinText(); if (text.trim().length === 0) throw new AgentRunError("schema-invalid", "stdin prompt is empty", { httpStatus: 2 }); return text; } const inline = args.positional.slice(3).join(" ").trim(); if (inline.length > 0) return inline; throw new AgentRunError("schema-invalid", "prompt is required; use --prompt, --prompt-file, --prompt-stdin, or trailing text", { httpStatus: 2 }); } async function readStdinText(): Promise { const chunks: Buffer[] = []; for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); return Buffer.concat(chunks).toString("utf8"); } function normalizeProfile(value: string): BackendProfile { const normalized = value.trim().toLowerCase(); const profile = normalized === "m3" || normalized === "minimax" || normalized === "minimax_m3" ? "minimax-m3" : normalized; if (!isBackendProfile(profile)) throw new AgentRunError("schema-invalid", `backend profile ${value} is not supported in v0.1`, { httpStatus: 2 }); return profile; } function newSessionId(): string { return `ses_${randomUUID().replace(/-/gu, "")}`; } function stringField(record: JsonRecord, key: string, fallback: string): string { const value = record[key]; return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback; } function objectField(record: JsonRecord, key: string, fallback: JsonRecord): JsonRecord { const value = record[key]; return typeof value === "object" && value !== null && !Array.isArray(value) ? value as JsonRecord : fallback; } function jsonObjectFlag(args: ParsedArgs, name: string): JsonRecord | null { const value = optionalFlag(args, name); if (!value) return null; const parsed = JSON.parse(value) as unknown; if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed as JsonRecord; throw new AgentRunError("schema-invalid", `--${name} must be a JSON object`, { httpStatus: 2 }); } function defaultExecutionPolicy(profile: BackendProfile): JsonRecord { const keys = [...(backendProfileSpec(profile)?.requiredSecretKeys ?? ["auth.json", "config.toml"])] as string[]; return { sandbox: "workspace-write", approval: "never", timeoutMs: 900000, network: "enabled", secretScope: { allowCredentialEcho: false, providerCredentials: [{ profile, secretRef: { name: `agentrun-v01-provider-${profile}`, keys } }] } }; } function copyOptionalFlag(args: ParsedArgs, target: JsonRecord, flagName: string, key = flagName.replace(/-([a-z])/gu, (_, letter: string) => letter.toUpperCase())): void { const value = optionalFlag(args, flagName); if (value) target[key] = value; } function copyRunnerManagerUrlFlag(args: ParsedArgs, target: JsonRecord): void { const value = optionalFlag(args, "runner-manager-url"); if (value) target.managerUrl = resolveRunnerManagerUrlFlag(args, value); } function resolveRunnerManagerUrlFlag(args: ParsedArgs, value: string): string { return value === "auto" ? managerUrl(args) : value; } function readerQuery(args: ParsedArgs): string { const readerId = optionalFlag(args, "reader-id"); return readerId ? `?readerId=${encodeURIComponent(readerId)}` : ""; } function parseArgs(argv: string[]): ParsedArgs { const positional: string[] = []; const flags = new Map(); for (let index = 0; index < argv.length; index += 1) { const item = argv[index] ?? ""; if (!item.startsWith("--")) { positional.push(item); continue; } const key = item.slice(2); const next = argv[index + 1]; if (next === undefined || next.startsWith("--")) flags.set(key, true); else { flags.set(key, next); index += 1; } } return { positional, flags }; } function flag(args: ParsedArgs, name: string, fallback: string): string { const value = args.flags.get(name); return typeof value === "string" ? value : fallback; } function optionalFlag(args: ParsedArgs, name: string): string | null { const value = args.flags.get(name); return typeof value === "string" && value.length > 0 ? value : null; } function cancelBody(args: ParsedArgs): JsonRecord { const reason = optionalFlag(args, "reason"); return reason ? { reason } : {}; } function help(args: ParsedArgs, group?: string): JsonRecord { const commands = [ "runs create --json-file |--json-stdin", "runs show ", "runs events --after-seq --limit [--summary|--tail-summary] [--tail ] [--summary-chars ] [--format json|tsv]", "runs result [--command-id ]", "runs cancel [--reason ]", "sessions ps [--state default|running|unread|terminal|idle|all] [--profile codex|deepseek|minimax-m3|dsflash-go||M3] [--reader-id ]", "sessions create [sessionId] [--profile codex|deepseek|minimax-m3|dsflash-go||M3] [--expires-in-days ]", "sessions storage ", "sessions storage --delete", "sessions show [--reader-id ]", "sessions turn [sessionId] [--json-file |--json-stdin] [--prompt-file |--prompt-stdin|--prompt ] [--profile codex|deepseek|minimax-m3|dsflash-go||M3] [--runner-json-file |--runner-json-stdin]", "sessions steer [--prompt-file |--prompt-stdin|--prompt ]", "sessions cancel [--reason ] [--full|--raw]", "sessions trace [--after-seq ] [--limit ] [--run-id ] [--summary-chars ] [--include-output] [--seq |--event-id |--item-id ] [--detail-scan-pages ] [--full|--raw]", "sessions output [--after-seq ] [--limit ] [--run-id ] [--summary-chars ] [--include-output] [--seq |--event-id |--item-id ] [--detail-scan-pages ] [--full|--raw]", "sessions read [--reader-id ] [--full|--raw]", "commands create --type turn|steer|interrupt --json-file |--json-stdin", "commands show --run-id ", "commands result --run-id ", "commands cancel [--reason ]", "runner start --run-id [--backend codex|deepseek|minimax-m3|dsflash-go|]", "runner job --run-id --command-id [--image ] [--runner-manager-url ] [--idempotency-key ]", "runner job --dry-run --run-id --command-id --image ", "runner jobs --run-id [--command-id ]", "runner job-status [runnerJobId] --run-id ", "queue submit --json-file |--json-stdin [--idempotency-key ] [--dry-run]", "queue list [--queue ] [--state ] [--cursor ] [--limit ] [--updated-after ] [--full|--raw]", "queue show [--full|--raw]", "queue stats [--queue ]", "queue commander [--queue ] [--reader-id ] [--limit ] [--full|--raw]", "queue read [--reader-id ] [--dry-run] [--full|--raw]", "queue cancel [--reason ] [--dry-run] [--full|--raw]", "queue dispatch [--json-file |--json-stdin] [--idempotency-key ] [--image ] [--namespace ] [--dry-run] [--full|--raw]", "queue refresh [--dry-run] [--full|--raw]", "secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go|] [--codex-home ] [--model-catalog-file ] [--namespace agentrun-v01] [--secret-name ]", "provider-profiles list", "provider-profiles show ", "provider-profiles config ", "provider-profiles remove ", "provider-profiles set-key --key-stdin [--model ] [--base-url ]", "provider-profiles set-config --config-stdin", "provider-profiles validate [--wait] [--timeout-ms ]", "backends list", "server start [--port ] [--host ] [--foreground]", "server status [--port ]", "server logs [--port ] [--tail-bytes ] [--log-file ]", "server stop [--port ]", ]; if (group) { const prefix = `${group} `; const groupCommands = commands.filter((item) => item === group || item.startsWith(prefix)); return { group, commands: groupCommands, commandCount: groupCommands.length, supported: groupCommands.length > 0, manager: managerEndpoint(args) }; } return { groups: [...new Set(commands.map((item) => item.split(" ")[0]).filter(Boolean))].sort(), commands, manager: managerEndpoint(args), }; } function print(value: JsonRecord): void { process.stdout.write(`${JSON.stringify(value)}\n`); }