1892 lines
89 KiB
TypeScript
1892 lines
89 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. render module for scripts/src/agentrun.ts.
|
|
|
|
// Moved mechanically from scripts/src/agentrun.ts:687-1652 for #903.
|
|
|
|
// SPEC: PJ2026-01020108 cancel lifecycle + PJ2026-01020205 AipodSpec binding + PJ2026-01020302 session policy + PJ2026-01020305 cancel control + PJ2026-01060305/06 YAML execution policy and bounded output draft-2026-06-25-p0.
|
|
// Exposes AgentRun lane-scoped policy, AipodSpec SecretRef binding, cancel lifecycle, and bounded default output in the UniDesk CLI.
|
|
import { chmodSync, copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
import { rootPath, type UniDeskConfig } from "../config";
|
|
import type { RenderedCliResult } from "../output";
|
|
import { applyLocalCaddyManagedSite } from "../pk01-caddy";
|
|
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
|
|
import { runRemoteSshCommandCapture } from "../remote";
|
|
import { startJob } from "../jobs";
|
|
import {
|
|
AGENTRUN_CONFIG_PATH,
|
|
agentRunLaneSummary,
|
|
agentRunPipelineRunName,
|
|
agentRunProviderCredentialRefs,
|
|
resolveAgentRunLaneTarget,
|
|
type AgentRunCancelLifecycleSpec,
|
|
type AgentRunLaneSpec,
|
|
} from "../agentrun-lanes";
|
|
import {
|
|
agentRunImageArtifact,
|
|
placeholderAgentRunImage,
|
|
renderedFilesDigest,
|
|
renderedObjectsDigest,
|
|
renderAgentRunControlPlaneManifests,
|
|
renderAgentRunGitopsFiles,
|
|
type AgentRunArtifactService,
|
|
} from "../agentrun-manifests";
|
|
import { sha256Fingerprint } from "../platform-infra-ops-library";
|
|
|
|
import type { AgentRunResourceKind, AgentRunResourceOptions, AgentRunResourceRef } from "./utils";
|
|
import { agentRunDryRunPlan, readAgentRunClientConfig } from "./config";
|
|
import { status } from "./control-plane";
|
|
import { arrayRecords, displayValue, isRecord, nextPagedResourceCommand, parseTaskManifest, pickCompact, relativeAge, renderFailureLines, renderResourceNextLines, renderTable, resourceName, shortId, stringOrDash, truncateMultiline, truncateOneLine } from "./options";
|
|
import { activeAgentRunRestTarget, agentRunRestRequest, runAgentRunRestCommand } from "./rest-bridge";
|
|
import { record, stringOrNull } from "./utils";
|
|
|
|
export function resolveAgentRunCancelPolicyTarget(config: UniDeskConfig | null, options: AgentRunResourceOptions): { configPath: string; spec: AgentRunLaneSpec; source: "selected-lane" | "default-lane" } | null {
|
|
if (activeAgentRunRestTarget !== null) return { configPath: activeAgentRunRestTarget.configPath, spec: activeAgentRunRestTarget.spec, source: "selected-lane" };
|
|
if (config === null) return null;
|
|
const { configPath, spec } = resolveAgentRunLaneTarget({ node: options.node, lane: options.lane });
|
|
return { configPath, spec, source: options.node !== null || options.lane !== null ? "selected-lane" : "default-lane" };
|
|
}
|
|
|
|
export function agentRunCancelAuthorityDisclosure(target: { configPath: string; spec: AgentRunLaneSpec; source: "selected-lane" | "default-lane" } | null): Record<string, unknown> {
|
|
const laneTarget = activeAgentRunRestTarget !== null;
|
|
return {
|
|
transport: laneTarget ? "lane-k8s-service-proxy" : "direct-http",
|
|
policySource: target?.source ?? "unavailable",
|
|
node: target?.spec.nodeId ?? null,
|
|
lane: target?.spec.lane ?? null,
|
|
namespace: target?.spec.runtime.namespace ?? null,
|
|
managerDeployment: target?.spec.runtime.managerDeployment ?? null,
|
|
baseUrl: laneTarget ? target?.spec.runtime.internalBaseUrl ?? null : agentRunDirectManagerBaseUrl(),
|
|
laneConfigPath: target?.configPath ?? null,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function agentRunDirectManagerBaseUrl(): string | null {
|
|
try {
|
|
return readAgentRunClientConfig().manager.baseUrl;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function agentRunCancelRunnerAbortDisclosure(policy: AgentRunCancelLifecycleSpec): Record<string, unknown> {
|
|
return {
|
|
deliveryMode: policy.deliveryMode,
|
|
gracefulAbortMs: policy.gracefulAbortMs,
|
|
killEscalationMs: policy.killEscalationMs,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function agentRunCancelFencingDisclosure(policy: AgentRunCancelLifecycleSpec | null): Record<string, unknown> {
|
|
if (policy === null) return { cancelEpoch: true, policySource: "unavailable", valuesPrinted: false };
|
|
return {
|
|
cancelEpoch: true,
|
|
staleHeartbeatFencingMs: policy.staleHeartbeatFencingMs,
|
|
lateWriteFencing: policy.lateWriteFencing.enabled,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function agentRunCancelCascadeScope(kind: AgentRunResourceKind): string {
|
|
if (kind === "task") return "current task attempt -> run -> active command -> runner job";
|
|
if (kind === "session") return "session active work -> active run/command -> session-scoped background work";
|
|
if (kind === "run") return "run active commands -> runner jobs -> run terminal";
|
|
if (kind === "command") return "single command -> current runner job; session remains reusable";
|
|
return "unsupported cancel target";
|
|
}
|
|
|
|
export async function resourceDispatch(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
|
|
const ref = parseResourceRef(action, args, "task");
|
|
if (ref.kind !== "task") throw new Error("dispatch supports task/<taskId>");
|
|
const result = await runAgentRunRestCommand(config, "queue", ["dispatch", ref.name, ...stripLeadingResource(args, ref.name)]);
|
|
return renderMutationSummary(command, result, options, `Task dispatch submitted ${shortId(ref.name)}`, [
|
|
`bun scripts/cli.ts agentrun describe task/${ref.name}`,
|
|
]);
|
|
}
|
|
|
|
export async function resourceCreate(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
|
|
const kind = parseResourceKind(action);
|
|
if (kind !== "task") throw new Error("create currently supports: create task");
|
|
const submitArgs = ["submit", ...stripLeadingResource(args, "task")];
|
|
const result = await runAgentRunRestCommand(config, "queue", submitArgs);
|
|
return renderMutationSummary(command, result, options, options.dryRun ? "Task create plan" : "Task create submitted", options.dryRun ? [rerunWithoutDryRun(command)] : undefined);
|
|
}
|
|
|
|
export async function resourceApply(config: UniDeskConfig | null, command: string, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
|
|
void config;
|
|
const file = options.file ?? requiredContext("apply", "-f <file>|-");
|
|
const raw = file === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8");
|
|
const parsed = parseTaskManifest(raw, file);
|
|
const result = options.dryRun
|
|
? agentRunDryRunPlan("queue-submit", "/api/v1/queue/tasks", parsed, `bun scripts/cli.ts ${command.replace(/\s+--dry-run\b/gu, "").trim()}`)
|
|
: await agentRunRestRequest("agentrun queue submit", "POST", "/api/v1/queue/tasks", parsed);
|
|
return renderMutationSummary(command, result, options, `${options.dryRun ? "Dry-run applied" : "Applied"} task manifest`, options.dryRun ? [rerunWithoutDryRun(command)] : undefined);
|
|
}
|
|
|
|
export async function resourceSessionPromptCommand(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
|
|
const ref = parseResourceRef(action, args, "session");
|
|
if (ref.kind !== "session") throw new Error("send requires session/<sessionId>");
|
|
const sessionArgs = ["send", ref.name, ...stripLeadingResource(args, ref.name)];
|
|
const result = await runAgentRunRestCommand(config, "sessions", sessionArgs);
|
|
return renderMutationSummary(command, result, options, options.dryRun ? "Session send plan" : "Session send submitted");
|
|
}
|
|
|
|
export function renderedCliResult(ok: boolean, command: string, renderedText: string, contentType: RenderedCliResult["contentType"] = "text/plain"): RenderedCliResult {
|
|
return { ok, command, renderedText, contentType };
|
|
}
|
|
|
|
export function renderResourceResult(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, kindLabel: string, items: Record<string, unknown>[]): RenderedCliResult {
|
|
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
|
|
const payload = { kind: `${kindLabel}List`, count: items.length, items };
|
|
if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output);
|
|
if (options.output === "name") return renderedCliResult(raw.ok !== false, command, items.map((item) => `${String(item.kind ?? kindLabel).toLowerCase()}/${String(item.name ?? "")}`).join("\n"));
|
|
if (items.length === 0) return renderedCliResult(raw.ok !== false, command, `No ${kindLabel.toLowerCase()} resources found.`);
|
|
return renderedCliResult(raw.ok !== false, command, renderResourceTable(items, options.output === "wide"));
|
|
}
|
|
|
|
export function renderQueueAttemptList(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, requestedTaskId: string, items: Record<string, unknown>[]): RenderedCliResult {
|
|
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
|
|
const data = record(innerData(raw));
|
|
const taskId = stringOrNull(data.taskId) ?? requestedTaskId;
|
|
const cursor = typeof data.cursor === "number" ? String(data.cursor) : stringOrNull(data.cursor);
|
|
const rawItems = arrayRecords(data.items);
|
|
if (options.full) {
|
|
return renderMachine(command, {
|
|
kind: "AttemptList",
|
|
taskId,
|
|
count: data.count ?? rawItems.length,
|
|
cursor,
|
|
items: rawItems,
|
|
}, options.output === "yaml" ? "yaml" : "json", raw.ok !== false);
|
|
}
|
|
const payload = { kind: "AttemptList", taskId, count: items.length, cursor, items };
|
|
if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output, raw.ok !== false);
|
|
if (options.output === "name") return renderedCliResult(raw.ok !== false, command, items.map((item) => `attempt/${String(item.name ?? "")}`).join("\n"));
|
|
if (items.length === 0) return renderedCliResult(raw.ok !== false, command, `No attempt resources found for task/${taskId}.`);
|
|
const lines = [
|
|
`Attempts for task/${taskId}`,
|
|
renderTable(["NAME", "INDEX", "STATE", "PREVIOUS", "RUN", "CMD", "RJOB", "AGE", "FAILURE", "REASON"], items.map((item) => [
|
|
resourceName(item),
|
|
displayValue(item.retryIndex),
|
|
displayValue(item.state),
|
|
shortId(stringOrDash(item.previousAttemptId)),
|
|
shortId(stringOrDash(item.runId)),
|
|
shortId(stringOrDash(item.commandId)),
|
|
shortId(stringOrDash(item.runnerJobId)),
|
|
displayValue(item.age),
|
|
queueAttemptFailureSummary(item),
|
|
truncateOneLine(displayValue(item.reason), 48),
|
|
])),
|
|
];
|
|
if (cursor !== null) lines.push(`Next: ${nextQueueAttemptPageCommand(command, cursor, options.limit)}`);
|
|
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
|
|
}
|
|
|
|
export function renderQueueRetrySummary(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, requestedTaskId: string): RenderedCliResult {
|
|
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
|
|
const data = record(innerData(raw));
|
|
const task = record(data.task);
|
|
const attempt = record(data.attempt);
|
|
const previousAttempt = record(data.previousAttempt);
|
|
const allowedTransition = record(data.allowedTransition);
|
|
const taskId = stringOrNull(task.id) ?? requestedTaskId;
|
|
if (options.full) {
|
|
return renderMachine(command, { kind: "TaskRetry", taskId, ...data }, options.output === "yaml" ? "yaml" : "json", raw.ok !== false);
|
|
}
|
|
const normalizedAttempt = compactQueueRetryAttempt(attempt);
|
|
const normalizedPreviousAttempt = compactQueueRetryAttempt(previousAttempt);
|
|
const run = record(data.run);
|
|
const runId = stringOrNull(run.id) ?? stringOrNull(attempt.runId);
|
|
const commandResource = record(data.command);
|
|
const commandId = stringOrNull(commandResource.id) ?? stringOrNull(attempt.commandId);
|
|
const runnerJob = record(data.runnerJob);
|
|
const runnerJobId = stringOrNull(runnerJob.id) ?? stringOrNull(attempt.runnerJobId);
|
|
const payload = {
|
|
kind: "TaskRetry",
|
|
taskId,
|
|
dryRun: options.dryRun,
|
|
mutation: data.mutation === true,
|
|
idempotentReplay: data.idempotentReplay === true,
|
|
task: pickCompact(task, ["id", "state", "queue", "lane", "payloadHash", "updatedAt"]),
|
|
attempt: normalizedAttempt,
|
|
previousAttempt: normalizedPreviousAttempt,
|
|
allowedTransition,
|
|
run: compactQueueRetryIdentity(run, ["id", "status", "terminalStatus", "createdAt", "updatedAt"]),
|
|
command: compactQueueRetryIdentity(commandResource, ["id", "type", "state", "terminalStatus", "createdAt", "updatedAt"]),
|
|
runnerJob: compactQueueRetryIdentity(runnerJob, ["id", "state", "phase", "terminalStatus", "attemptId", "createdAt", "updatedAt"]),
|
|
};
|
|
if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output, raw.ok !== false);
|
|
const retryIndex = attempt.retryIndex ?? allowedTransition.retryIndex;
|
|
const attemptId = stringOrNull(attempt.attemptId);
|
|
const previousAttemptId = stringOrNull(previousAttempt.attemptId) ?? stringOrNull(allowedTransition.previousAttemptId);
|
|
const lines = [
|
|
`${options.dryRun ? "Task retry validated" : "Task retry submitted"}: task/${taskId}`,
|
|
`OK: ${String(raw.ok !== false)}`,
|
|
`DryRun: ${String(options.dryRun)}`,
|
|
`Mutation: ${String(data.mutation === true)}`,
|
|
`IdempotentReplay: ${String(data.idempotentReplay === true)}`,
|
|
`Transition: ${displayValue(allowedTransition.from)} -> ${displayValue(allowedTransition.to)} retryIndex=${displayValue(retryIndex)}`,
|
|
`Attempt: ${attemptId === null ? "planned" : `attempt/${shortId(attemptId)}`} state=${displayValue(attempt.state ?? allowedTransition.to)}`,
|
|
`PreviousAttempt: ${previousAttemptId === null ? "-" : `attempt/${shortId(previousAttemptId)}`}`,
|
|
];
|
|
if (runId !== null) lines.push(`Run: run/${shortId(runId)}`);
|
|
if (commandId !== null) lines.push(`Command: command/${shortId(commandId)}`);
|
|
if (runnerJobId !== null) lines.push(`RunnerJob: runnerjob/${shortId(runnerJobId)}`);
|
|
const next = [
|
|
`bun scripts/cli.ts agentrun get attempts --task ${taskId}`,
|
|
`bun scripts/cli.ts agentrun describe task/${taskId}`,
|
|
runId === null ? null : `bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 100`,
|
|
runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun describe command/${commandId} --run ${runId}`,
|
|
].filter((value): value is string => value !== null);
|
|
lines.push("", "Next:", ...next.map((value) => ` ${value}`));
|
|
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
|
|
}
|
|
|
|
function compactQueueRetryAttempt(value: Record<string, unknown>): Record<string, unknown> | null {
|
|
if (Object.keys(value).length === 0) return null;
|
|
const attempt = pickCompact(value, ["attemptId", "taskId", "retryIndex", "state", "idempotencyKey", "reason", "payloadHash", "previousAttemptId", "runId", "commandId", "runnerJobId", "sessionId", "sessionPath", "failureKind", "failureMessage", "createdAt", "updatedAt", "activatedAt", "terminalAt"]);
|
|
attempt.reason = boundedQueueAttemptText(attempt.reason, 240);
|
|
attempt.failureMessage = boundedQueueAttemptText(attempt.failureMessage, 320);
|
|
attempt.sessionPath = boundedQueueAttemptText(attempt.sessionPath, 240);
|
|
return attempt;
|
|
}
|
|
|
|
function compactQueueRetryIdentity(value: Record<string, unknown>, keys: string[]): Record<string, unknown> | null {
|
|
return Object.keys(value).length === 0 ? null : pickCompact(value, keys);
|
|
}
|
|
|
|
function nextQueueAttemptPageCommand(command: string, cursor: string, limit: number): string {
|
|
const base = command
|
|
.replace(/\s+--cursor(?:=|\s+)\S+/gu, "")
|
|
.replace(/\s+--limit(?:=|\s+)\S+/gu, "")
|
|
.trim();
|
|
return `bun scripts/cli.ts ${base} --cursor ${cursor} --limit ${limit}`;
|
|
}
|
|
|
|
export function renderDescribe(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, ref: AgentRunResourceRef, text: string): RenderedCliResult {
|
|
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
|
|
if (options.output === "json" || options.output === "yaml") {
|
|
const resource = record(innerData(raw));
|
|
const visibleResource = ref.kind === "runnerjob" && !options.full ? compactRunnerJobDescriptionPayload(resource) : resource;
|
|
return renderMachine(command, { kind: ref.kind, name: ref.name, resource: visibleResource }, options.output, raw.ok !== false);
|
|
}
|
|
return renderedCliResult(raw.ok !== false, command, text);
|
|
}
|
|
|
|
export function compactRunnerJobDescriptionPayload(value: Record<string, unknown>): Record<string, unknown> {
|
|
const observation = record(value.runtimeObservation);
|
|
const runner = record(observation.runner);
|
|
const target = record(observation.target);
|
|
const runId = value.runId ?? runner.runId ?? null;
|
|
const commandId = value.commandId ?? runner.commandId ?? null;
|
|
const runnerJobId = value.id ?? runner.runnerJobId ?? null;
|
|
const targetArgs = typeof target.node === "string" && typeof target.lane === "string"
|
|
? ` --node ${target.node} --lane ${target.lane}`
|
|
: "";
|
|
return {
|
|
id: runnerJobId,
|
|
runId,
|
|
commandId,
|
|
attemptId: value.attemptId ?? null,
|
|
runnerId: value.runnerId ?? runner.runnerId ?? null,
|
|
namespace: value.namespace ?? runner.namespace ?? target.namespace ?? null,
|
|
jobName: value.jobName ?? runner.name ?? null,
|
|
state: value.state ?? null,
|
|
managerPhase: value.managerPhase ?? value.phase ?? null,
|
|
terminalStatus: value.terminalStatus ?? null,
|
|
failureKind: value.failureKind ?? null,
|
|
image: value.image ?? null,
|
|
createdAt: value.createdAt ?? null,
|
|
startedAt: value.startedAt ?? null,
|
|
finishedAt: value.finishedAt ?? null,
|
|
runtimeObservation: {
|
|
ok: observation.ok ?? false,
|
|
reason: observation.reason ?? null,
|
|
observedAt: observation.observedAt ?? null,
|
|
target,
|
|
managerFacts: observation.managerFacts ?? null,
|
|
kubernetesFacts: observation.kubernetesFacts ?? null,
|
|
runner: Object.keys(runner).length === 0 ? null : {
|
|
name: runner.name ?? null,
|
|
runId: runner.runId ?? null,
|
|
commandId: runner.commandId ?? null,
|
|
runnerJobId: runner.runnerJobId ?? null,
|
|
runnerId: runner.runnerId ?? null,
|
|
createdAt: runner.createdAt ?? null,
|
|
lastActiveAt: runner.lastActiveAt ?? null,
|
|
lastActiveAgeMs: runner.lastActiveAgeMs ?? null,
|
|
runStatus: runner.runStatus ?? null,
|
|
runClaimedBy: runner.runClaimedBy ?? null,
|
|
commandState: runner.commandState ?? null,
|
|
heartbeatAt: runner.heartbeatAt ?? null,
|
|
heartbeatAgeMs: runner.heartbeatAgeMs ?? null,
|
|
state: runner.state ?? null,
|
|
jobStatus: runner.jobStatus ?? null,
|
|
podPhases: Array.isArray(runner.podPhases) ? runner.podPhases.slice(0, 4) : [],
|
|
waitingReasons: Array.isArray(runner.waitingReasons) ? runner.waitingReasons.slice(0, 8) : [],
|
|
podObservations: Array.isArray(runner.podObservations) ? runner.podObservations.slice(0, 2) : [],
|
|
identityComplete: runner.identityComplete === true,
|
|
managerFactsComplete: runner.managerFactsComplete === true,
|
|
podFactsComplete: runner.podFactsComplete === true,
|
|
cleanupAuthority: runner.cleanupAuthority ?? null,
|
|
cleanupDecisionComputed: runner.cleanupDecisionComputed === true,
|
|
safeCleanupCandidate: typeof runner.safeCleanupCandidate === "boolean" ? runner.safeCleanupCandidate : null,
|
|
protectedActive: typeof runner.protectedActive === "boolean" ? runner.protectedActive : null,
|
|
protectedReason: runner.protectedReason ?? null,
|
|
candidateKind: runner.candidateKind ?? null,
|
|
classification: runner.classification ?? null,
|
|
valuesPrinted: false,
|
|
},
|
|
mutation: false,
|
|
valuesPrinted: false,
|
|
},
|
|
next: {
|
|
events: typeof runId === "string" ? `bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 100${targetArgs}` : null,
|
|
command: typeof runId === "string" && typeof commandId === "string" ? `bun scripts/cli.ts agentrun describe command/${commandId} --run ${runId}${targetArgs}` : null,
|
|
full: typeof runId === "string" && typeof runnerJobId === "string" ? `bun scripts/cli.ts agentrun describe runnerjob/${runnerJobId} --run ${runId}${targetArgs} --full -o json` : null,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export interface EventLikePageDisclosure {
|
|
requestedLimit: number;
|
|
effectiveLimit: number;
|
|
fetchedCount: number;
|
|
hasMore: boolean;
|
|
nextAfterSeq: number | string | null;
|
|
detailCommand: string;
|
|
}
|
|
|
|
export function renderEventLike(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, kind: string, items: Record<string, unknown>[], sourceName: string, pageDisclosure: EventLikePageDisclosure | null = null): RenderedCliResult {
|
|
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
|
|
const data = record(innerData(raw));
|
|
const nextAfterSeq = pageDisclosure?.nextAfterSeq ?? data.nextAfterSeq ?? null;
|
|
const payload = {
|
|
kind: `${kind}List`,
|
|
source: sourceName,
|
|
nextAfterSeq,
|
|
count: items.length,
|
|
...(pageDisclosure === null ? {} : {
|
|
requestedLimit: pageDisclosure.requestedLimit,
|
|
effectiveLimit: pageDisclosure.effectiveLimit,
|
|
fetchedCount: pageDisclosure.fetchedCount,
|
|
hasMore: pageDisclosure.hasMore,
|
|
disclosure: {
|
|
output: "omitted; metadata only",
|
|
identity: "item.identity; detail --detail-seq item.seq",
|
|
detail: pageDisclosure.detailCommand,
|
|
full: "--full",
|
|
raw: "--raw",
|
|
},
|
|
}),
|
|
items,
|
|
};
|
|
if (options.output === "json" && pageDisclosure !== null) {
|
|
return renderedCliResult(raw.ok !== false, command, `${JSON.stringify(payload)}\n`, "application/json");
|
|
}
|
|
if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output, raw.ok !== false);
|
|
const lines = pageDisclosure === null
|
|
? [
|
|
`${kind}s for ${sourceName}`,
|
|
renderTable(["SEQ", "TYPE", "STATUS", "COMMAND", "SUMMARY"], items.map((item) => [
|
|
String(item.seq ?? "-"),
|
|
String(item.type ?? "-"),
|
|
String(item.status ?? item.phase ?? "-"),
|
|
shortId(String(item.commandId ?? "-")),
|
|
truncateOneLine(String(item.summary ?? item.text ?? "") || "-", 96),
|
|
])),
|
|
]
|
|
: [
|
|
`${kind}s for ${sourceName}`,
|
|
renderTable(["SEQ", "TYPE", "STATUS", "COMMAND", "TOOL / DETAIL", "EXIT", "DURATION", "OUTPUT"], items.map((item) => [
|
|
String(item.seq ?? "-"),
|
|
String(item.type ?? "-"),
|
|
String(item.status ?? item.phase ?? "-"),
|
|
shortId(String(item.commandId ?? "-")),
|
|
eventHumanDetail(item),
|
|
displayValue(item.exitCode),
|
|
eventDurationCell(item.durationMs),
|
|
eventOutputCell(item),
|
|
])),
|
|
];
|
|
if (pageDisclosure !== null) lines.push(`Detail: ${pageDisclosure.detailCommand}`);
|
|
if (nextAfterSeq !== undefined && nextAfterSeq !== null) lines.push(`Next: bun scripts/cli.ts ${nextPagedResourceCommand(command, String(nextAfterSeq), options.limit)}`);
|
|
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
|
|
}
|
|
|
|
function eventHumanDetail(item: Record<string, unknown>): string {
|
|
const tool = stringOrNull(item.tool);
|
|
const command = stringOrNull(item.command);
|
|
const summary = stringOrNull(item.summary);
|
|
const detail = command === null ? summary ?? tool ?? "-" : tool === null ? command : `${tool} ${command}`;
|
|
return truncateOneLine(detail, 72);
|
|
}
|
|
|
|
function eventDurationCell(value: unknown): string {
|
|
return typeof value === "number" && Number.isFinite(value) ? `${value}ms` : "-";
|
|
}
|
|
|
|
function eventOutputCell(item: Record<string, unknown>): string {
|
|
const bytes = typeof item.outputBytes === "number" && Number.isFinite(item.outputBytes) ? `${item.outputBytes}B` : null;
|
|
const fingerprint = stringOrNull(item.outputSha256);
|
|
const shortFingerprint = fingerprint === null ? null : fingerprint.length <= 24 ? fingerprint : `${fingerprint.slice(0, 21)}...`;
|
|
const truncated = item.outputTruncated === true ? "truncated" : null;
|
|
const omitted = item.outputOmitted === true ? "omitted" : null;
|
|
return [bytes, shortFingerprint, truncated, omitted].filter((value): value is string => value !== null).join(" ") || "-";
|
|
}
|
|
|
|
export function renderResultSummary(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, ref: AgentRunResourceRef): RenderedCliResult {
|
|
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
|
|
const data = record(innerData(raw));
|
|
if (options.output === "json" || options.output === "yaml") return renderMachine(command, { kind: "Result", ref, result: data }, options.output, raw.ok !== false);
|
|
const executionOk = typeof data.executionOk === "boolean"
|
|
? data.executionOk
|
|
: typeof data.ok === "boolean"
|
|
? data.ok
|
|
: raw.ok !== false;
|
|
const lines = [
|
|
`Result: ${ref.kind}/${shortId(ref.name)}`,
|
|
`State: ${displayValue(data.state ?? data.status ?? data.terminalStatus ?? "-")}`,
|
|
`OK: ${String(executionOk)}`,
|
|
];
|
|
const providerTerminalFailure = typeof data.providerTerminalFailure === "boolean" ? data.providerTerminalFailure : null;
|
|
const authority = stringOrNull(data.finalResponseAuthority);
|
|
const fallback = typeof data.finalResponseFallback === "boolean" ? data.finalResponseFallback : null;
|
|
const needsContinuation = typeof data.needsContinuation === "boolean" ? data.needsContinuation : null;
|
|
if (providerTerminalFailure !== null) lines.push(`ProviderTerminalFailure: ${String(providerTerminalFailure)}`);
|
|
if (authority !== null) lines.push(`Authority: ${authority}`);
|
|
if (fallback !== null) lines.push(`Fallback: ${String(fallback)}`);
|
|
if (needsContinuation !== null) lines.push(`NeedsContinuation: ${String(needsContinuation)}`);
|
|
const finalRecord = record(data.finalResponse);
|
|
const final = stringOrNull(data.finalResponse)
|
|
?? stringOrNull(finalRecord.content)
|
|
?? stringOrNull(finalRecord.text)
|
|
?? stringOrNull(finalRecord.message)
|
|
?? stringOrNull(data.output)
|
|
?? stringOrNull(data.summary)
|
|
?? stringOrNull(data.result);
|
|
if (final !== null) lines.push("", truncateMultiline(final, options.fullText ? 8000 : 1600));
|
|
else {
|
|
const failure = renderFailureLines(data);
|
|
lines.push("", ...(failure.length > 0 ? failure : ["No final response is available yet."]));
|
|
}
|
|
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
|
|
}
|
|
|
|
export function renderMutationSummary(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, headline: string, overrideNextLines?: string[]): RenderedCliResult {
|
|
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
|
|
if (options.output === "json" || options.output === "yaml") return renderMachine(command, raw, options.output, raw.ok !== false);
|
|
const data = record(innerData(raw));
|
|
const id = stringOrNull(data.id) ?? stringOrNull(data.taskId) ?? stringOrNull(data.sessionId);
|
|
const lines = [
|
|
headline,
|
|
`OK: ${String(raw.ok !== false)}`,
|
|
];
|
|
if (id !== null) lines.push(`Name: ${id}`);
|
|
const decision = stringOrNull(data.decision);
|
|
const internalCommandType = stringOrNull(data.internalCommandType);
|
|
const dryRun = data.dryRun !== undefined ? data.dryRun : raw.dryRun;
|
|
const mutation = data.mutation !== undefined ? data.mutation : raw.mutation;
|
|
if (dryRun !== undefined) lines.push(`DryRun: ${String(dryRun)}`);
|
|
if (mutation !== undefined) lines.push(`Mutation: ${String(mutation)}`);
|
|
if (decision !== null) lines.push(`Decision: ${decision}`);
|
|
if (internalCommandType !== null) lines.push(`InternalCommandType: ${internalCommandType}`);
|
|
lines.push(...renderCancelLifecycleMutationLines(record(data.cancelLifecycle ?? raw.cancelLifecycle)));
|
|
const next = record(raw.next ?? data.next);
|
|
const nextLines = (overrideNextLines ?? Object.values(next).map(String)).filter((line) => line.length > 0).slice(0, 5);
|
|
if (nextLines.length > 0) lines.push("", "Next:", ...nextLines.map((line) => ` ${line}`));
|
|
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
|
|
}
|
|
|
|
export function renderCancelLifecycleMutationLines(lifecycle: Record<string, unknown>): string[] {
|
|
if (Object.keys(lifecycle).length === 0) return [];
|
|
const authority = record(lifecycle.authority);
|
|
const runnerAbort = record(lifecycle.runnerAbort);
|
|
const fencing = record(lifecycle.fencing);
|
|
const expectedStages = Array.isArray(lifecycle.expectedStages) ? lifecycle.expectedStages.map(String).filter((value) => value.length > 0) : [];
|
|
const node = stringOrNull(authority.node);
|
|
const lane = stringOrNull(authority.lane);
|
|
const target = node !== null && lane !== null ? `${node}/${lane}` : "-";
|
|
const lines = ["", "CancelLifecycle:"];
|
|
lines.push(` Authority: ${displayValue(authority.transport)} policy=${displayValue(authority.policySource)} lane=${target}`);
|
|
const namespace = stringOrNull(authority.namespace);
|
|
const deployment = stringOrNull(authority.managerDeployment);
|
|
if (namespace !== null || deployment !== null) lines.push(` Runtime: ns=${displayValue(namespace)} manager=${displayValue(deployment)}`);
|
|
const cascadeScope = stringOrNull(lifecycle.cascadeScope);
|
|
if (cascadeScope !== null) lines.push(` Cascade: ${cascadeScope}`);
|
|
if (Object.keys(runnerAbort).length > 0) {
|
|
lines.push(` RunnerAbort: mode=${displayValue(runnerAbort.deliveryMode)} gracefulMs=${displayValue(runnerAbort.gracefulAbortMs)} killMs=${displayValue(runnerAbort.killEscalationMs)}`);
|
|
}
|
|
if (Object.keys(fencing).length > 0) {
|
|
lines.push(` Fencing: cancelEpoch=${displayValue(fencing.cancelEpoch)} staleHeartbeatMs=${displayValue(fencing.staleHeartbeatFencingMs)} lateWrite=${displayValue(fencing.lateWriteFencing)}`);
|
|
}
|
|
if (expectedStages.length > 0) lines.push(` Stages: ${expectedStages.join(", ")}`);
|
|
const terminalAuthority = stringOrNull(lifecycle.terminalAuthority);
|
|
if (terminalAuthority !== null) lines.push(` Terminal: ${terminalAuthority}`);
|
|
return lines;
|
|
}
|
|
|
|
export function rerunWithoutDryRun(command: string): string {
|
|
return `bun scripts/cli.ts ${command.replace(/\s+--dry-run\b/gu, "").trim()}`;
|
|
}
|
|
|
|
export function renderMachine(command: string, value: unknown, mode: "json" | "yaml", ok = true): RenderedCliResult {
|
|
const text = mode === "json" ? `${JSON.stringify(value, null, 2)}\n` : `${Bun.YAML.stringify(value)}\n`;
|
|
return renderedCliResult(ok, command, text, mode === "json" ? "application/json" : "application/yaml");
|
|
}
|
|
|
|
export function parseResourceKind(raw: string | undefined): AgentRunResourceKind | null {
|
|
if (raw === undefined) return null;
|
|
const value = raw.toLowerCase().replace(/s$/u, "");
|
|
if (value === "task" || value === "qt" || value === "queue" || value === "queuetask") return "task";
|
|
if (value === "attempt") return "attempt";
|
|
if (value === "run") return "run";
|
|
if (value === "command" || value === "cmd") return "command";
|
|
if (value === "runnerjob" || value === "runner-job" || value === "rjob" || value === "job") return "runnerjob";
|
|
if (value === "session" || value === "ses") return "session";
|
|
if (value === "aipodspec" || value === "aipod-spec" || value === "aipod-specs" || value === "aps") return "aipodspec";
|
|
return null;
|
|
}
|
|
|
|
export function parseResourceRef(action: string | undefined, args: string[], defaultKind: AgentRunResourceKind | null = null): AgentRunResourceRef {
|
|
const raw = action ?? "";
|
|
if (raw.includes("/")) {
|
|
const [kindRaw, ...nameParts] = raw.split("/");
|
|
const kind = parseResourceKind(kindRaw);
|
|
const name = nameParts.join("/");
|
|
if (kind === null || name.length === 0) throw new Error(`invalid resource reference: ${raw}`);
|
|
return { kind, name };
|
|
}
|
|
const kind = parseResourceKind(raw);
|
|
if (kind !== null) {
|
|
const name = args.find((arg) => !arg.startsWith("-") && arg !== raw);
|
|
if (name === undefined) throw new Error(`${kind} reference requires a name`);
|
|
return { kind, name };
|
|
}
|
|
if (defaultKind !== null && raw.length > 0) return { kind: defaultKind, name: raw };
|
|
throw new Error("resource reference required, for example task/<id> or session/<id>");
|
|
}
|
|
|
|
export function stripLeadingResource(args: string[], resourceName: string): string[] {
|
|
let skipped = false;
|
|
const result: string[] = [];
|
|
for (const arg of args) {
|
|
if (!skipped && (arg === resourceName || arg.endsWith(`/${resourceName}`) || parseResourceKind(arg) !== null)) {
|
|
skipped = true;
|
|
continue;
|
|
}
|
|
result.push(arg);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function requiredContext(context: string, flag: string): never {
|
|
throw new Error(`${context} requires ${flag}`);
|
|
}
|
|
|
|
export function innerData(raw: Record<string, unknown>): unknown {
|
|
const data = raw.data;
|
|
if (typeof data === "object" && data !== null && "data" in data && Object.keys(raw).length <= 3) return record(data).data;
|
|
return data ?? raw;
|
|
}
|
|
|
|
export function normalizeTaskItems(data: unknown, options: AgentRunResourceOptions): Record<string, unknown>[] {
|
|
const items = arrayRecords(record(data).items ?? data);
|
|
return items
|
|
.filter((item) => options.queue === null || String(item.queue ?? "") === options.queue)
|
|
.filter((item) => options.state === null || options.state.split(",").includes(String(item.state ?? "")))
|
|
.filter((item) => !options.unread || item.unread === true || item.attention === true)
|
|
.map((item) => {
|
|
const attempt = record(item.latestAttempt);
|
|
return {
|
|
kind: "task",
|
|
name: item.id,
|
|
state: item.state,
|
|
queue: item.queue,
|
|
lane: item.lane,
|
|
profile: item.backendProfile,
|
|
run: attempt.runId,
|
|
command: attempt.commandId,
|
|
runnerjob: attempt.runnerJobId,
|
|
session: attempt.sessionId ?? stringOrNull(record(item.sessionRef).sessionId),
|
|
age: relativeAge(stringOrNull(item.updatedAt) ?? stringOrNull(item.createdAt)),
|
|
title: item.title,
|
|
updatedAt: item.updatedAt,
|
|
attention: item.unread === true || item.attention === true ? "unread" : "",
|
|
};
|
|
});
|
|
}
|
|
|
|
export function normalizeQueueAttemptItems(data: unknown): Record<string, unknown>[] {
|
|
const items = arrayRecords(record(data).items ?? data);
|
|
return items.map((item) => ({
|
|
kind: "attempt",
|
|
name: item.attemptId,
|
|
attemptId: item.attemptId,
|
|
taskId: item.taskId,
|
|
retryIndex: item.retryIndex,
|
|
state: item.state,
|
|
idempotencyKey: item.idempotencyKey,
|
|
reason: boundedQueueAttemptText(item.reason, 240),
|
|
payloadHash: item.payloadHash,
|
|
previousAttemptId: item.previousAttemptId,
|
|
runId: item.runId,
|
|
commandId: item.commandId,
|
|
runnerJobId: item.runnerJobId,
|
|
sessionId: item.sessionId,
|
|
sessionPath: boundedQueueAttemptText(item.sessionPath, 240),
|
|
failureKind: item.failureKind,
|
|
failureMessage: boundedQueueAttemptText(item.failureMessage, 320),
|
|
createdAt: item.createdAt,
|
|
updatedAt: item.updatedAt,
|
|
activatedAt: item.activatedAt,
|
|
terminalAt: item.terminalAt,
|
|
age: relativeAge(stringOrNull(item.updatedAt) ?? stringOrNull(item.createdAt)),
|
|
}));
|
|
}
|
|
|
|
function boundedQueueAttemptText(value: unknown, maxChars: number): string | null {
|
|
const text = stringOrNull(value);
|
|
return text === null ? null : truncateOneLine(text, maxChars);
|
|
}
|
|
|
|
function queueAttemptFailureSummary(item: Record<string, unknown>): string {
|
|
const kind = stringOrNull(item.failureKind);
|
|
const message = stringOrNull(item.failureMessage);
|
|
if (kind === null && message === null) return "-";
|
|
return truncateOneLine(kind === null ? message ?? "-" : message === null ? kind : `${kind}: ${message}`, 72);
|
|
}
|
|
|
|
export function taskListState(options: AgentRunResourceOptions): string {
|
|
const requested = options.state?.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
return requested?.[0] ?? "running";
|
|
}
|
|
|
|
export function normalizeSessionItems(data: unknown): Record<string, unknown>[] {
|
|
const items = arrayRecords(record(data).items ?? data);
|
|
return items.map((item) => ({
|
|
kind: "session",
|
|
name: item.id ?? item.sessionId ?? item.name,
|
|
state: item.state ?? item.status ?? item.phase ?? "-",
|
|
queue: item.queue ?? "",
|
|
lane: item.lane ?? "",
|
|
profile: item.backendProfile ?? item.profile ?? "",
|
|
run: item.runId ?? "",
|
|
command: item.commandId ?? "",
|
|
runnerjob: item.runnerJobId ?? "",
|
|
session: item.id ?? item.sessionId ?? item.name,
|
|
age: relativeAge(stringOrNull(item.updatedAt) ?? stringOrNull(item.createdAt)),
|
|
title: item.title ?? "",
|
|
}));
|
|
}
|
|
|
|
export function normalizeAipodSpecItems(data: unknown): Record<string, unknown>[] {
|
|
const items = arrayRecords(record(data).items ?? data);
|
|
return items.map((item) => ({
|
|
kind: "aipodspec",
|
|
name: item.name ?? item.id ?? item.metadata?.toString(),
|
|
state: item.state ?? "available",
|
|
queue: "",
|
|
lane: item.lane ?? "",
|
|
profile: item.backendProfile ?? item.profile ?? "",
|
|
title: item.description ?? item.title ?? "",
|
|
}));
|
|
}
|
|
|
|
export function normalizeAttemptResources(task: Record<string, unknown>, kind: "run" | "command" | "runnerjob"): Record<string, unknown>[] {
|
|
const attempt = record(task.latestAttempt);
|
|
const key = kind === "run" ? "runId" : kind === "command" ? "commandId" : "runnerJobId";
|
|
const name = stringOrNull(attempt[key]);
|
|
if (name === null) return [];
|
|
return [{
|
|
kind,
|
|
name,
|
|
state: attempt.state ?? task.state,
|
|
queue: task.queue,
|
|
lane: task.lane,
|
|
run: attempt.runId,
|
|
command: attempt.commandId,
|
|
runnerjob: attempt.runnerJobId,
|
|
session: attempt.sessionId,
|
|
title: task.title,
|
|
}];
|
|
}
|
|
|
|
export function normalizeSingleCommand(data: unknown): Record<string, unknown>[] {
|
|
const item = record(data);
|
|
return [{
|
|
kind: "command",
|
|
name: item.id ?? item.commandId ?? "",
|
|
state: item.state ?? item.status ?? "",
|
|
run: item.runId ?? "",
|
|
command: item.id ?? item.commandId ?? "",
|
|
session: item.sessionId ?? "",
|
|
title: item.summary ?? item.commandType ?? "",
|
|
}];
|
|
}
|
|
|
|
export function normalizeRunnerJobItems(data: unknown): Record<string, unknown>[] {
|
|
const items = arrayRecords(record(data).items ?? data);
|
|
return items.map((item) => ({
|
|
kind: "runnerjob",
|
|
name: item.id ?? item.runnerJobId ?? item.name ?? item.jobName,
|
|
state: item.phase ?? item.state ?? item.status ?? "",
|
|
run: item.runId ?? "",
|
|
command: item.commandId ?? "",
|
|
runnerjob: item.id ?? item.runnerJobId ?? item.name ?? item.jobName,
|
|
session: item.sessionId ?? "",
|
|
title: item.image ?? item.namespace ?? "",
|
|
}));
|
|
}
|
|
|
|
const EVENT_SUMMARY_CHARS = 160;
|
|
const EVENT_COMMAND_CHARS = 160;
|
|
|
|
export function normalizeEventItems(data: unknown, runId: string | null = null): Record<string, unknown>[] {
|
|
return arrayRecords(record(data).items ?? data).map((item) => {
|
|
const payload = record(item.payload);
|
|
const seq = finiteNumberOrNull(item.seq);
|
|
const eventId = boundedEventIdentity(item.id ?? item.eventId);
|
|
const tool = agentRunEventTool(payload);
|
|
const eventCommand = eventCommandFirstLine(payload);
|
|
const exitCode = finiteNumberOrNull(payload.exitCode);
|
|
const durationMs = finiteNumberOrNull(payload.durationMs);
|
|
const output = agentRunEventOutputMetadata(String(item.type ?? ""), payload);
|
|
const identity = eventId === null
|
|
? runId === null || seq === null ? null : `run/${runId}/seq/${seq}`
|
|
: `event/${eventId}`;
|
|
return {
|
|
seq: seq ?? item.seq,
|
|
type: boundedEventScalar(item.type, 64),
|
|
status: agentRunEventStatus(item, payload),
|
|
...(item.phase === undefined && payload.phase === undefined ? {} : { phase: boundedEventScalar(item.phase ?? payload.phase, 80) }),
|
|
commandId: agentRunEventCommandId(item, payload),
|
|
...(identity === null ? {} : { identity }),
|
|
...(tool === null ? {} : { tool }),
|
|
...(eventCommand === null ? {} : { command: eventCommand }),
|
|
...(exitCode === null ? {} : { exitCode }),
|
|
...(durationMs === null ? {} : { durationMs }),
|
|
...(output.bytes === null ? {} : { outputBytes: output.bytes }),
|
|
...(output.sha256 === null ? {} : { outputSha256: output.sha256 }),
|
|
...(output.truncated === null ? {} : { outputTruncated: output.truncated }),
|
|
...(output.omitted ? { outputOmitted: true } : {}),
|
|
summary: agentRunEventSummary(item, payload),
|
|
};
|
|
});
|
|
}
|
|
|
|
export function normalizeLogItems(data: unknown): Record<string, unknown>[] {
|
|
return arrayRecords(record(data).items ?? data).map((item) => {
|
|
const payload = record(item.payload);
|
|
return {
|
|
seq: item.seq,
|
|
type: item.type ?? item.role ?? "output",
|
|
status: agentRunEventStatus(item, payload),
|
|
commandId: agentRunEventCommandId(item, payload),
|
|
summary: agentRunLogSummary(item, payload),
|
|
};
|
|
});
|
|
}
|
|
|
|
function agentRunLogSummary(item: Record<string, unknown>, payload: Record<string, unknown>): string {
|
|
const type = stringOrNull(item.type);
|
|
const parts: string[] = [];
|
|
const push = (value: unknown): void => {
|
|
const text = eventText(value);
|
|
if (text !== null && !parts.includes(text)) parts.push(text);
|
|
};
|
|
|
|
if (type === "error") {
|
|
push(payload.failureKind);
|
|
const error = record(payload.error);
|
|
push(error.message);
|
|
push(error.additionalDetails);
|
|
if (payload.willRetry === true) push("willRetry=true");
|
|
}
|
|
|
|
if (type === "backend_status") {
|
|
push(payload.phase);
|
|
push(formatCountField("tools", record(payload.tools).count));
|
|
push(formatCountField("bundles", record(payload.bundles).count));
|
|
push(formatCountField("promptRefs", record(payload.promptRefs).count));
|
|
push(payload.jobName);
|
|
push(payload.namespace);
|
|
}
|
|
|
|
if (type === "tool_call") {
|
|
push(payload.summary);
|
|
push(payload.toolName);
|
|
push(payload.command);
|
|
const exitCode = payload.exitCode;
|
|
if (exitCode !== undefined && exitCode !== null) push(`exit=${String(exitCode)}`);
|
|
push(payload.outputSummary);
|
|
}
|
|
|
|
if (type === "command_output") push(commandOutputSummary(payload));
|
|
if (type === "assistant_message") {
|
|
push(payload.summary);
|
|
push(payload.text);
|
|
}
|
|
|
|
push(item.summary);
|
|
push(item.outputSummary);
|
|
push(item.text);
|
|
if (type !== "command_output") {
|
|
push(payload.summary);
|
|
push(payload.outputSummary);
|
|
push(payload.text);
|
|
}
|
|
push(payload.message);
|
|
push(payload.reason);
|
|
return parts.join("; ");
|
|
}
|
|
|
|
export function agentRunEventCommandId(item: Record<string, unknown>, payload: Record<string, unknown>): string {
|
|
return boundedEventIdentity(stringOrNull(item.commandId)
|
|
?? stringOrNull(payload.commandId)
|
|
?? stringOrNull(record(payload.summary).commandId)
|
|
?? "-") ?? "-";
|
|
}
|
|
|
|
export function agentRunEventStatus(item: Record<string, unknown>, payload: Record<string, unknown>): string {
|
|
return boundedEventScalar(stringOrNull(item.status)
|
|
?? stringOrNull(item.phase)
|
|
?? stringOrNull(payload.status)
|
|
?? stringOrNull(payload.phase)
|
|
?? stringOrNull(payload.failureKind)
|
|
?? (item.type === "error" ? "error" : ""), 80);
|
|
}
|
|
|
|
export function agentRunEventSummary(item: Record<string, unknown>, payload: Record<string, unknown>): string {
|
|
const type = stringOrNull(item.type);
|
|
if (type === "tool_call") return toolCallEventSummary(item, payload);
|
|
if (type === "command_output") return commandOutputEventSummary(payload);
|
|
const parts: string[] = [];
|
|
const push = (value: unknown): void => {
|
|
const text = eventText(value);
|
|
const bounded = text === null ? null : truncateOneLine(text, EVENT_SUMMARY_CHARS);
|
|
if (bounded !== null && !parts.includes(bounded)) parts.push(bounded);
|
|
};
|
|
|
|
if (type === "error") {
|
|
push(payload.failureKind);
|
|
const error = record(payload.error);
|
|
push(error.message);
|
|
push(error.additionalDetails);
|
|
if (payload.willRetry === true) push("willRetry=true");
|
|
}
|
|
|
|
if (type === "backend_status") {
|
|
push(payload.phase);
|
|
push(formatCountField("tools", record(payload.tools).count));
|
|
push(formatCountField("bundles", record(payload.bundles).count));
|
|
push(formatCountField("promptRefs", record(payload.promptRefs).count));
|
|
push(payload.jobName);
|
|
push(payload.namespace);
|
|
}
|
|
|
|
if (type === "assistant_message") {
|
|
push(payload.summary);
|
|
push(payload.text);
|
|
}
|
|
|
|
push(item.summary);
|
|
push(item.outputSummary);
|
|
push(item.text);
|
|
if (type !== "command_output") {
|
|
push(payload.summary);
|
|
push(payload.outputSummary);
|
|
push(payload.text);
|
|
}
|
|
push(payload.message);
|
|
push(payload.reason);
|
|
|
|
return truncateOneLine(parts.join("; "), EVENT_SUMMARY_CHARS);
|
|
}
|
|
|
|
function toolCallEventSummary(item: Record<string, unknown>, payload: Record<string, unknown>): string {
|
|
const tool = agentRunEventTool(payload);
|
|
const status = agentRunEventStatus(item, payload);
|
|
const parts = [tool, status.length === 0 ? null : status]
|
|
.filter((value): value is string => value !== null);
|
|
return truncateOneLine(parts.join("; "), EVENT_SUMMARY_CHARS);
|
|
}
|
|
|
|
function commandOutputEventSummary(payload: Record<string, unknown>): string {
|
|
const stream = boundedEventScalar(payload.stream, 40);
|
|
const parts = [stream.length === 0 ? "command output" : stream]
|
|
.filter((value): value is string => value !== null);
|
|
return truncateOneLine(parts.join("; "), EVENT_SUMMARY_CHARS);
|
|
}
|
|
|
|
function agentRunEventTool(payload: Record<string, unknown>): string | null {
|
|
const value = stringOrNull(payload.toolName)
|
|
?? stringOrNull(payload.type)
|
|
?? stringOrNull(payload.name)
|
|
?? stringOrNull(payload.method);
|
|
return value === null ? null : truncateOneLine(value, 80);
|
|
}
|
|
|
|
function eventCommandFirstLine(payload: Record<string, unknown>): string | null {
|
|
const value = stringOrNull(payload.command) ?? stringOrNull(payload.commandLine);
|
|
if (value === null) return null;
|
|
const firstLine = value.split(/\r?\n/u)[0] ?? "";
|
|
const bounded = truncateOneLine(firstLine, EVENT_COMMAND_CHARS);
|
|
return bounded.length === 0 ? null : bounded;
|
|
}
|
|
|
|
function agentRunEventOutputMetadata(type: string, payload: Record<string, unknown>): { bytes: number | null; sha256: string | null; truncated: boolean | null; omitted: boolean } {
|
|
const summary = record(payload.summary);
|
|
const material = eventOutputMaterial(type, payload, summary);
|
|
const lifecycle = [stringOrNull(payload.status), stringOrNull(payload.method), stringOrNull(payload.phase)].filter((value): value is string => value !== null).join(" ");
|
|
const terminalToolOutputFacts = type === "tool_call"
|
|
&& /(?:completed|failed|succeeded)/iu.test(lifecycle)
|
|
&& firstFiniteNumber(payload.outputBytes, payload.textBytes, summary.outputBytes, summary.textBytes) !== null;
|
|
const outputPresent = material !== null || type === "command_output" || terminalToolOutputFacts;
|
|
const bytes = outputPresent
|
|
? firstFiniteNumber(payload.outputBytes, payload.textBytes, summary.outputBytes, summary.textBytes)
|
|
?? (material === null ? null : Buffer.byteLength(material, "utf8"))
|
|
: null;
|
|
const providedHash = outputPresent ? firstEventFingerprint(payload.outputSha256, payload.outputHash, summary.outputSha256, summary.outputHash) : null;
|
|
const sha256 = providedHash ?? (material === null ? null : sha256Fingerprint(material));
|
|
const truncationFacts = [payload.outputTruncated, payload.textTruncated, summary.outputTruncated, summary.textTruncated]
|
|
.filter((value): value is boolean => typeof value === "boolean");
|
|
return {
|
|
bytes,
|
|
sha256,
|
|
truncated: outputPresent && truncationFacts.length > 0 ? truncationFacts.some(Boolean) : null,
|
|
omitted: outputPresent,
|
|
};
|
|
}
|
|
|
|
function eventOutputMaterial(type: string, payload: Record<string, unknown>, summary: Record<string, unknown>): string | null {
|
|
const nestedSummary = stringOrNull(summary.text);
|
|
const nestedSummaryContainsOutput = nestedSummary !== null
|
|
&& /(?:^|\s)output=/u.test(nestedSummary);
|
|
const values = type === "command_output"
|
|
? [payload.text, payload.output, payload.stdout, payload.stderr, payload.outputSummary, summary.text]
|
|
: [payload.output, payload.outputSummary, payload.stdout, payload.stderr, payload.stdoutSummary, payload.stderrSummary, nestedSummaryContainsOutput ? nestedSummary : null];
|
|
for (const value of values) {
|
|
const text = stringOrNull(value);
|
|
if (text !== null) return text;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function firstFiniteNumber(...values: unknown[]): number | null {
|
|
for (const value of values) {
|
|
const number = finiteNumberOrNull(value);
|
|
if (number !== null) return number;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function finiteNumberOrNull(value: unknown): number | null {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
}
|
|
|
|
function firstEventFingerprint(...values: unknown[]): string | null {
|
|
for (const value of values) {
|
|
const text = stringOrNull(value);
|
|
if (text === null) continue;
|
|
if (/^sha256:[a-f0-9]{64}$/iu.test(text)) return text.toLowerCase();
|
|
if (/^[a-f0-9]{64}$/iu.test(text)) return `sha256:${text.toLowerCase()}`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function boundedEventIdentity(value: unknown): string | null {
|
|
const text = stringOrNull(value);
|
|
return text === null ? null : truncateOneLine(text, 240);
|
|
}
|
|
|
|
function boundedEventScalar(value: unknown, maxChars: number): string {
|
|
const text = stringOrNull(value);
|
|
return text === null ? "" : truncateOneLine(text, maxChars);
|
|
}
|
|
|
|
export function commandOutputSummary(payload: Record<string, unknown>): string | null {
|
|
const raw = commandOutputText(payload);
|
|
if (raw === null) return null;
|
|
const structured = parseJsonRecordFromText(raw);
|
|
if (structured !== null) {
|
|
const summary = summarizeStructuredCliOutput(structured);
|
|
if (summary !== null) return summary;
|
|
}
|
|
return summarizePartialJsonCommandOutput(raw) ?? summarizeTextCommandOutput(raw);
|
|
}
|
|
|
|
export function commandOutputText(payload: Record<string, unknown>): string | null {
|
|
const summary = record(payload.summary);
|
|
const summaryText = stringOrNull(summary.text);
|
|
const payloadText = stringOrNull(payload.text);
|
|
if (summary.textTruncated === true && payloadText !== null) return payloadText;
|
|
return summaryText
|
|
?? payloadText
|
|
?? stringOrNull(payload.outputSummary)
|
|
?? eventText(payload.summary);
|
|
}
|
|
|
|
export function parseJsonRecordFromText(raw: string): Record<string, unknown> | null {
|
|
const trimmed = raw.trim();
|
|
const direct = tryParseJsonRecord(trimmed);
|
|
if (direct !== null) return direct;
|
|
|
|
const firstBrace = trimmed.indexOf("{");
|
|
const lastBrace = trimmed.lastIndexOf("}");
|
|
if (firstBrace >= 0 && lastBrace > firstBrace) {
|
|
const sliced = tryParseJsonRecord(trimmed.slice(firstBrace, lastBrace + 1));
|
|
if (sliced !== null) return sliced;
|
|
}
|
|
|
|
const line = trimmed
|
|
.split(/\r?\n/u)
|
|
.map((item) => item.trim())
|
|
.reverse()
|
|
.find((item) => item.startsWith("{") && item.endsWith("}"));
|
|
return line === undefined ? null : tryParseJsonRecord(line);
|
|
}
|
|
|
|
export function tryParseJsonRecord(raw: string): Record<string, unknown> | null {
|
|
try {
|
|
const parsed = JSON.parse(raw) as unknown;
|
|
return isRecord(parsed) ? parsed : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function summarizeStructuredCliOutput(value: Record<string, unknown>): string | null {
|
|
const parts: string[] = [];
|
|
const action = firstPathText(value, ["action", "operation", "command", "kind"]);
|
|
const status = firstPathText(value, ["status", "state", "phase", "body.status"]);
|
|
const failure = firstPathText(value, ["failureKind", "error.failureKind", "body.failureKind"]);
|
|
const message = firstPathText(value, ["error.message", "error.additionalDetails", "message", "reason", "body.message", "body.error", "result.message"]);
|
|
const primary = [
|
|
action,
|
|
status,
|
|
labeledSummaryValue("http", firstPathText(value, ["httpStatus", "request.httpStatus"])),
|
|
].filter((item): item is string => item !== null);
|
|
if (primary.length > 0) pushSummaryPart(parts, primary.join(" "));
|
|
|
|
pushLabeledSummaryPart(parts, "failure", failure);
|
|
if (failure !== null || isFailureLikeStatus(status)) pushLabeledSummaryPart(parts, "message", message);
|
|
pushLabeledSummaryPart(parts, "trace", firstPathText(value, ["traceId", "lastTraceId", "trace.traceId", "trace.id", "body.traceId", "body.trace.id", "providerTrace.traceId", "runnerTrace.traceId"]), true);
|
|
pushLabeledSummaryPart(parts, "session", firstPathText(value, ["sessionId", "session.sessionId", "body.sessionId", "workspace.selectedAgentSessionId", "workspace.selectedConversation.sessionId"]), true);
|
|
pushLabeledSummaryPart(parts, "conversation", firstPathText(value, ["conversationId", "session.conversationId", "body.conversationId", "workspace.selectedConversationId", "workspace.selectedConversation.conversationId"]), true);
|
|
pushLabeledSummaryPart(parts, "thread", firstPathText(value, ["threadId", "session.threadId", "body.threadId", "workspace.selectedConversation.threadId"]), true);
|
|
pushLabeledSummaryPart(parts, "provider", firstPathText(value, ["providerProfile", "backendProfile", "profile", "session.providerProfile", "workspace.providerProfile"]));
|
|
pushLabeledSummaryPart(parts, "route", commandOutputRouteSummary(value));
|
|
pushLabeledSummaryPart(parts, "pipeline", firstPathText(value, ["pipelineRun", "pipelineRunName", "pipelineRun.name", "pipeline.runName", "pipeline.name"]), true);
|
|
pushLabeledSummaryPart(parts, "run", firstPathText(value, ["runId", "agentRun.runId"]), true);
|
|
pushLabeledSummaryPart(parts, "cmd", firstPathText(value, ["commandId", "agentRun.commandId", "providerTrace.commandId", "runnerTrace.commandId"]), true);
|
|
pushLabeledSummaryPart(parts, "job", firstPathText(value, ["jobName", "agentRun.jobName", "runnerJobId"]), true);
|
|
pushLabeledSummaryPart(parts, "lane", firstPathText(value, ["runtimeEndpoint.lane", "lane"]));
|
|
pushLabeledSummaryPart(parts, "ns", firstPathText(value, ["runtimeEndpoint.namespace", "namespace"]));
|
|
pushLabeledSummaryPart(parts, "actor", firstPathText(value, ["actor.username", "body.actor.username", "actor.displayName", "body.actor.displayName"]));
|
|
pushLabeledSummaryPart(parts, "auth", firstPathText(value, ["authMethod", "auth.authMethod", "body.authMethod"]));
|
|
pushLabeledSummaryPart(parts, "reply", firstPathText(value, ["reply.content", "result.reply.content", "body.reply.content", "answer", "content", "body.content"]));
|
|
if (failure === null && !isFailureLikeStatus(status)) pushLabeledSummaryPart(parts, "message", message);
|
|
|
|
const itemsCount = arrayRecords(value.items).length;
|
|
if (itemsCount > 0) pushSummaryPart(parts, `items=${itemsCount}`);
|
|
const eventsCount = arrayRecords(value.events).length;
|
|
if (eventsCount > 0) pushSummaryPart(parts, `events=${eventsCount}`);
|
|
if (value.sessionUsable === false) pushSummaryPart(parts, "sessionUsable=false");
|
|
if (record(value.summary).textTruncated === true || value.textTruncated === true || value.outputTruncated === true) pushSummaryPart(parts, "truncated=true");
|
|
|
|
return parts.length > 0 ? parts.slice(0, 10).join("; ") : null;
|
|
}
|
|
|
|
export function commandOutputRouteSummary(value: Record<string, unknown>): string | null {
|
|
const route = record(value.route);
|
|
const request = record(value.request);
|
|
const method = stringOrNull(route.method) ?? stringOrNull(request.method);
|
|
const path = stringOrNull(route.path) ?? stringOrNull(request.path);
|
|
if (method !== null && path !== null) return `${method} ${path}`;
|
|
return stringOrNull(request.url) ?? stringOrNull(value.baseUrl);
|
|
}
|
|
|
|
export function firstPathText(value: Record<string, unknown>, paths: string[]): string | null {
|
|
for (const path of paths) {
|
|
const text = summaryScalar(pathValue(value, path));
|
|
if (text !== null) return text;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function pathValue(value: Record<string, unknown>, path: string): unknown {
|
|
let current: unknown = value;
|
|
for (const key of path.split(".")) {
|
|
if (!isRecord(current)) return undefined;
|
|
current = current[key];
|
|
}
|
|
return current;
|
|
}
|
|
|
|
export function summaryScalar(value: unknown): string | null {
|
|
if (typeof value === "string") {
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? trimmed : null;
|
|
}
|
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
if (typeof value === "boolean") return String(value);
|
|
return null;
|
|
}
|
|
|
|
export function isFailureLikeStatus(value: string | null): boolean {
|
|
return value !== null && /(?:fail|error|timeout|cancel|interrupt|denied|rejected)/iu.test(value);
|
|
}
|
|
|
|
export function pushLabeledSummaryPart(parts: string[], label: string, value: string | null, shorten = false): void {
|
|
const text = labeledSummaryValue(label, value, shorten);
|
|
if (text !== null) pushSummaryPart(parts, text);
|
|
}
|
|
|
|
export function labeledSummaryValue(label: string, value: string | null, shorten = false): string | null {
|
|
if (value === null) return null;
|
|
const rendered = shorten ? shortId(value) : truncateOneLine(value, 80);
|
|
return `${label}=${rendered}`;
|
|
}
|
|
|
|
export function pushSummaryPart(parts: string[], value: string): void {
|
|
const text = truncateOneLine(value, 160);
|
|
if (text.length > 0 && !parts.includes(text)) parts.push(text);
|
|
}
|
|
|
|
export function summarizeTextCommandOutput(raw: string): string | null {
|
|
const lines = raw
|
|
.split(/\r?\n/u)
|
|
.map((line) => line.trim())
|
|
.filter((line) => line.length > 0 && !isLowSignalCommandOutputLine(line));
|
|
const signal = lines.find((line) => /(?:trace|session|conversation|thread|status|failure|error|route|http|pipeline|provider|commit|succeeded|failed|created)/iu.test(line))
|
|
?? lines[0]
|
|
?? null;
|
|
if (signal === null) return null;
|
|
return truncateOneLine(cleanCommandOutputLine(signal), 220);
|
|
}
|
|
|
|
export function summarizePartialJsonCommandOutput(raw: string): string | null {
|
|
const fields = new Map<string, string>();
|
|
for (const line of raw.split(/\r?\n/u)) {
|
|
const field = jsonLineField(line);
|
|
if (field === null || isLowSignalJsonField(field.key)) continue;
|
|
if (!fields.has(field.key) && field.value.length > 0) fields.set(field.key, field.value);
|
|
}
|
|
|
|
const parts: string[] = [];
|
|
const action = fields.get("action") ?? fields.get("operation") ?? fields.get("command") ?? fields.get("kind") ?? null;
|
|
const status = fields.get("status") ?? fields.get("state") ?? fields.get("phase") ?? null;
|
|
const http = fields.get("httpStatus") ?? null;
|
|
const failure = fields.get("failureKind") ?? null;
|
|
const message = fields.get("message") ?? fields.get("reason") ?? fields.get("error") ?? null;
|
|
const primary = [action, status, labeledSummaryValue("http", http)].filter((item): item is string => item !== null);
|
|
if (primary.length > 0) pushSummaryPart(parts, primary.join(" "));
|
|
|
|
pushLabeledSummaryPart(parts, "failure", failure);
|
|
if (failure !== null || isFailureLikeStatus(status)) pushLabeledSummaryPart(parts, "message", message);
|
|
pushLabeledSummaryPart(parts, "trace", fields.get("traceId") ?? fields.get("lastTraceId") ?? null, true);
|
|
pushLabeledSummaryPart(parts, "session", fields.get("sessionId") ?? fields.get("selectedAgentSessionId") ?? null, true);
|
|
pushLabeledSummaryPart(parts, "conversation", fields.get("conversationId") ?? fields.get("selectedConversationId") ?? null, true);
|
|
pushLabeledSummaryPart(parts, "thread", fields.get("threadId") ?? null, true);
|
|
pushLabeledSummaryPart(parts, "provider", fields.get("providerProfile") ?? fields.get("backendProfile") ?? fields.get("profile") ?? null);
|
|
pushLabeledSummaryPart(parts, "route", partialRouteSummary(fields));
|
|
pushLabeledSummaryPart(parts, "pipeline", fields.get("pipelineRun") ?? fields.get("pipelineRunName") ?? null, true);
|
|
pushLabeledSummaryPart(parts, "run", fields.get("runId") ?? null, true);
|
|
pushLabeledSummaryPart(parts, "cmd", fields.get("commandId") ?? null, true);
|
|
pushLabeledSummaryPart(parts, "job", fields.get("jobName") ?? fields.get("runnerJobId") ?? null, true);
|
|
pushLabeledSummaryPart(parts, "lane", fields.get("lane") ?? null);
|
|
pushLabeledSummaryPart(parts, "ns", fields.get("namespace") ?? null);
|
|
pushLabeledSummaryPart(parts, "actor", fields.get("username") ?? fields.get("displayName") ?? null);
|
|
pushLabeledSummaryPart(parts, "auth", fields.get("authMethod") ?? null);
|
|
if (failure === null && !isFailureLikeStatus(status)) pushLabeledSummaryPart(parts, "message", message);
|
|
|
|
return parts.length > 0 ? parts.slice(0, 10).join("; ") : null;
|
|
}
|
|
|
|
export function partialRouteSummary(fields: Map<string, string>): string | null {
|
|
const method = fields.get("method") ?? null;
|
|
const path = fields.get("path") ?? null;
|
|
if (method !== null && path !== null) return `${method} ${path}`;
|
|
return fields.get("url") ?? fields.get("baseUrl") ?? null;
|
|
}
|
|
|
|
export function jsonLineField(line: string): { key: string; value: string } | null {
|
|
const match = line.trim().replace(/,$/u, "").match(/^"([^"]+)":\s*(.+)$/u);
|
|
if (match === null) return null;
|
|
return { key: match[1], value: cleanJsonScalar(match[2]) };
|
|
}
|
|
|
|
export function cleanJsonScalar(raw: string): string {
|
|
const trimmed = raw.trim();
|
|
if (trimmed === "null" || trimmed === "{" || trimmed === "[" || trimmed === "}" || trimmed === "]") return "";
|
|
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
try {
|
|
const parsed = JSON.parse(trimmed) as unknown;
|
|
return typeof parsed === "string" ? parsed.trim() : String(parsed);
|
|
} catch {
|
|
return trimmed.slice(1, -1).trim();
|
|
}
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
export function cleanCommandOutputLine(line: string): string {
|
|
const trimmed = line.replace(/,$/u, "").trim();
|
|
const jsonField = trimmed.match(/^"([^"]+)":\s*(.+)$/u);
|
|
if (jsonField === null) return trimmed;
|
|
const key = jsonField[1];
|
|
const value = jsonField[2].replace(/^"|"$/gu, "").trim();
|
|
return `${key}=${value}`;
|
|
}
|
|
|
|
export function isLowSignalCommandOutputLine(line: string): boolean {
|
|
const trimmed = line.replace(/,$/u, "").trim();
|
|
if (trimmed === "{" || trimmed === "}" || trimmed === "}," || trimmed === "];" || trimmed === "[") return true;
|
|
const field = jsonLineField(trimmed);
|
|
return field !== null && isLowSignalJsonField(field.key);
|
|
}
|
|
|
|
export function isLowSignalJsonField(key: string): boolean {
|
|
return key === "generatedAt" || key === "cli" || key === "version" || key === "valuesRedacted" || key === "secretMaterialStored";
|
|
}
|
|
|
|
export function eventText(value: unknown): string | null {
|
|
const direct = stringOrNull(value);
|
|
if (direct !== null && direct.trim().length > 0) return direct.trim();
|
|
const nested = record(value);
|
|
return stringOrNull(nested.text)
|
|
?? stringOrNull(nested.message)
|
|
?? stringOrNull(nested.reason)
|
|
?? stringOrNull(nested.summary);
|
|
}
|
|
|
|
export function formatCountField(label: string, value: unknown): string | null {
|
|
if (typeof value === "number" && Number.isFinite(value)) return `${label}=${value}`;
|
|
if (typeof value === "string" && value.length > 0) return `${label}=${value}`;
|
|
return null;
|
|
}
|
|
|
|
export function renderResourceTable(items: Record<string, unknown>[], wide: boolean): string {
|
|
const headers = wide
|
|
? ["NAME", "STATE", "QUEUE", "LANE", "PROFILE", "RUN", "CMD", "RJOB", "SESSION", "AGE", "TITLE"]
|
|
: ["NAME", "STATE", "QUEUE", "LANE", "RUN", "CMD", "RJOB", "SESSION", "AGE", "ATTENTION"];
|
|
const rows = items.map((item) => {
|
|
if (wide) {
|
|
return [
|
|
resourceName(item),
|
|
displayValue(item.state),
|
|
displayValue(item.queue),
|
|
displayValue(item.lane),
|
|
displayValue(item.profile),
|
|
shortId(stringOrDash(item.run)),
|
|
shortId(stringOrDash(item.command)),
|
|
shortId(stringOrDash(item.runnerjob)),
|
|
shortId(stringOrDash(item.session)),
|
|
displayValue(item.age),
|
|
truncateOneLine(displayValue(item.title), 48),
|
|
];
|
|
}
|
|
return [
|
|
resourceName(item),
|
|
displayValue(item.state),
|
|
displayValue(item.queue),
|
|
displayValue(item.lane),
|
|
shortId(stringOrDash(item.run)),
|
|
shortId(stringOrDash(item.command)),
|
|
shortId(stringOrDash(item.runnerjob)),
|
|
shortId(stringOrDash(item.session)),
|
|
displayValue(item.age),
|
|
displayValue(item.attention),
|
|
];
|
|
});
|
|
return renderTable(headers, rows);
|
|
}
|
|
|
|
export function renderTaskDescription(task: Record<string, unknown>, options: AgentRunResourceOptions): string {
|
|
const attempt = record(task.latestAttempt);
|
|
const taskId = displayValue(task.id);
|
|
const runId = stringOrNull(attempt.runId);
|
|
const commandId = stringOrNull(attempt.commandId);
|
|
const runnerJobId = stringOrNull(attempt.runnerJobId);
|
|
const sessionId = stringOrNull(attempt.sessionId) ?? stringOrNull(record(task.sessionRef).sessionId);
|
|
const lines = [
|
|
`Name: task/${taskId}`,
|
|
`State: ${displayValue(task.state)} Attention: ${displayValue(task.unread === true ? "unread" : "")}`,
|
|
`Queue: ${displayValue(task.queue)}`,
|
|
`Lane: ${displayValue(task.lane)} Profile: ${displayValue(task.backendProfile)} Provider: ${displayValue(task.providerId)}`,
|
|
`Title: ${displayValue(task.title)}`,
|
|
"",
|
|
"Latest Attempt:",
|
|
` Attempt: ${displayValue(attempt.attemptId)} State: ${displayValue(attempt.state)}`,
|
|
` Run: ${runId === null ? "-" : `run/${runId}`}`,
|
|
` Command: ${commandId === null ? "-" : `command/${commandId}`}`,
|
|
` Runner: ${runnerJobId === null ? "-" : `runnerjob/${runnerJobId}`}`,
|
|
` Session: ${sessionId === null ? "-" : `session/${sessionId}`}`,
|
|
"",
|
|
"Conditions:",
|
|
` Terminal: ${displayValue(task.state)}`,
|
|
` Failure: ${displayValue(task.failureKind ?? task.degradedReason ?? "none")}`,
|
|
" Values: redacted",
|
|
"",
|
|
"Next:",
|
|
];
|
|
const next = [
|
|
`bun scripts/cli.ts agentrun describe task/${taskId} --input -o json`,
|
|
runId === null ? null : `bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 100`,
|
|
sessionId === null ? null : `bun scripts/cli.ts agentrun logs session/${sessionId} --tail 100`,
|
|
runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`,
|
|
`bun scripts/cli.ts agentrun ack task/${taskId}`,
|
|
options.full ? null : `bun scripts/cli.ts agentrun describe task/${taskId} --full`,
|
|
].filter((item): item is string => item !== null).slice(0, 6);
|
|
lines.push(...next.map((item) => ` ${item}`));
|
|
return lines.join("\n");
|
|
}
|
|
|
|
export function compactTaskDescriptionPayload(ref: AgentRunResourceRef, task: Record<string, unknown>): Record<string, unknown> {
|
|
const attempt = record(task.latestAttempt);
|
|
const taskId = stringOrNull(task.id) ?? ref.name;
|
|
const runId = stringOrNull(attempt.runId);
|
|
const commandId = stringOrNull(attempt.commandId);
|
|
const runnerJobId = stringOrNull(attempt.runnerJobId);
|
|
const sessionId = stringOrNull(attempt.sessionId) ?? stringOrNull(record(task.sessionRef).sessionId);
|
|
return {
|
|
kind: ref.kind,
|
|
name: ref.name,
|
|
summary: {
|
|
id: taskId,
|
|
state: task.state ?? null,
|
|
queue: task.queue ?? null,
|
|
lane: task.lane ?? null,
|
|
backendProfile: task.backendProfile ?? null,
|
|
providerId: task.providerId ?? null,
|
|
title: task.title ?? null,
|
|
unread: task.unread === true,
|
|
failureKind: task.failureKind ?? task.degradedReason ?? null,
|
|
},
|
|
latestAttempt: {
|
|
attemptId: attempt.attemptId ?? null,
|
|
state: attempt.state ?? null,
|
|
runId,
|
|
commandId,
|
|
runnerJobId,
|
|
sessionId,
|
|
},
|
|
next: {
|
|
events: runId === null ? null : `bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 100`,
|
|
logs: sessionId === null ? null : `bun scripts/cli.ts agentrun logs session/${sessionId} --tail 100`,
|
|
result: runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`,
|
|
ack: `bun scripts/cli.ts agentrun ack task/${taskId}`,
|
|
input: `bun scripts/cli.ts agentrun describe task/${taskId} --input -o json`,
|
|
full: `bun scripts/cli.ts agentrun describe task/${taskId} --full -o json`,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function compactAipodSpecDescriptionPayload(ref: AgentRunResourceRef, data: unknown): Record<string, unknown> {
|
|
const root = record(data);
|
|
const describedItem = record(root.item);
|
|
const item = Object.keys(describedItem).length > 0 ? describedItem : root;
|
|
const manifest = record(root.spec);
|
|
const manifestMetadata = record(manifest.metadata);
|
|
const manifestSpec = record(manifest.spec);
|
|
const manifestAipodMetadata = record(manifestSpec.metadata);
|
|
const model = firstNonEmptyRecord(item.model, manifestSpec.model);
|
|
const workspaceRef = compactAipodSpecWorkspaceRef(firstNonEmptyRecord(item.workspaceRef, manifestSpec.workspaceRef));
|
|
const resourceBundleRef = compactAipodSpecResourceBundleRef(item.resourceBundleRef, manifestSpec.resourceBundleRef);
|
|
const secretScope = record(record(manifestSpec.executionPolicy).secretScope);
|
|
const providerCredentialRefs = compactAipodSpecCredentialRefs(item.providerCredentials, secretScope.providerCredentials, "provider");
|
|
const toolCredentialRefs = compactAipodSpecCredentialRefs(item.toolCredentials, secretScope.toolCredentials, "tool");
|
|
const imageRef = firstNonEmptyRecord(item.imageRef, manifestSpec.imageRef);
|
|
return {
|
|
kind: "AipodSpec",
|
|
name: ref.name,
|
|
state: boundedAipodSpecText(item.state ?? item.status ?? "available", 80),
|
|
identity: {
|
|
declaredName: boundedAipodSpecText(item.name ?? manifestMetadata.name ?? ref.name, 160),
|
|
displayName: boundedAipodSpecText(item.displayName ?? manifestMetadata.displayName, 200),
|
|
specHash: boundedAipodSpecText(item.specHash, 160),
|
|
valuesPrinted: false,
|
|
},
|
|
runtime: {
|
|
queue: boundedAipodSpecText(item.queue ?? manifestSpec.queue, 120),
|
|
lane: boundedAipodSpecText(item.lane ?? manifestSpec.lane, 120),
|
|
backendProfile: boundedAipodSpecText(item.backendProfile ?? item.profile ?? manifestSpec.backendProfile, 160),
|
|
providerId: boundedAipodSpecText(item.providerId ?? manifestSpec.providerId, 160),
|
|
model: {
|
|
name: boundedAipodSpecText(model.model, 200),
|
|
reasoningEffort: boundedAipodSpecText(model.reasoningEffort, 80),
|
|
valuesPrinted: false,
|
|
},
|
|
valuesPrinted: false,
|
|
},
|
|
workspaceRef,
|
|
resourceBundleRef,
|
|
credentialRefs: {
|
|
totalCount: providerCredentialRefs.count + toolCredentialRefs.count,
|
|
provider: providerCredentialRefs,
|
|
tool: toolCredentialRefs,
|
|
valuesPrinted: false,
|
|
},
|
|
provenance: {
|
|
source: boundedAipodSpecText(item.source ?? manifestAipodMetadata.source, 320),
|
|
apiVersion: boundedAipodSpecText(manifest.apiVersion, 120),
|
|
sourceKind: boundedAipodSpecText(manifest.kind, 120),
|
|
imageSourceIdentity: boundedAipodSpecText(imageRef.sourceIdentity, 320),
|
|
createdAt: boundedAipodSpecText(item.createdAt, 120),
|
|
updatedAt: boundedAipodSpecText(item.updatedAt, 120),
|
|
valuesPrinted: false,
|
|
},
|
|
redaction: {
|
|
credentialValues: "omitted",
|
|
secretRefsOnly: true,
|
|
valuesPrinted: false,
|
|
},
|
|
next: {
|
|
full: `bun scripts/cli.ts agentrun describe aipodspec/${ref.name} --full -o json`,
|
|
raw: `bun scripts/cli.ts agentrun describe aipodspec/${ref.name} --raw`,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function renderAipodSpecDescription(payload: Record<string, unknown>): string {
|
|
const identity = record(payload.identity);
|
|
const runtime = record(payload.runtime);
|
|
const model = record(runtime.model);
|
|
const workspaceRef = record(payload.workspaceRef);
|
|
const resourceBundleRef = record(payload.resourceBundleRef);
|
|
const sourceIdentity = record(resourceBundleRef.sourceIdentity);
|
|
const credentialRefs = record(payload.credentialRefs);
|
|
const providerCredentialRefs = record(credentialRefs.provider);
|
|
const toolCredentialRefs = record(credentialRefs.tool);
|
|
const provenance = record(payload.provenance);
|
|
const next = record(payload.next);
|
|
const lines = [
|
|
`Name: aipodspec/${displayValue(payload.name)}`,
|
|
`State: ${displayValue(payload.state)}`,
|
|
"",
|
|
"Identity:",
|
|
` Declared: ${displayValue(identity.declaredName)}`,
|
|
` Display: ${displayValue(identity.displayName)}`,
|
|
` Hash: ${displayValue(identity.specHash)}`,
|
|
"",
|
|
"Runtime:",
|
|
` Queue: ${displayValue(runtime.queue)}`,
|
|
` Lane: ${displayValue(runtime.lane)}`,
|
|
` Profile: ${displayValue(runtime.backendProfile)}`,
|
|
` Provider: ${displayValue(runtime.providerId)}`,
|
|
` Model: ${displayValue(model.name)} reasoning=${displayValue(model.reasoningEffort)}`,
|
|
"",
|
|
"Workspace:",
|
|
` Kind: ${displayValue(workspaceRef.kind)} path=${displayValue(workspaceRef.path)} branch=${displayValue(workspaceRef.branch)}`,
|
|
"",
|
|
"Primary resource bundle:",
|
|
` Source: kind=${displayValue(sourceIdentity.kind)} repo=${displayValue(sourceIdentity.repoUrl)} ref=${displayValue(sourceIdentity.ref)} commit=${displayValue(sourceIdentity.commitId)}`,
|
|
` References: bundles=${displayValue(resourceBundleRef.bundleCount)} requiredSkills=${displayValue(resourceBundleRef.requiredSkillCount)} promptRefs=${displayValue(resourceBundleRef.promptRefCount)}`,
|
|
` Credentials: provider=${displayValue(providerCredentialRefs.count)} tool=${displayValue(toolCredentialRefs.count)} total=${displayValue(credentialRefs.totalCount)}`,
|
|
];
|
|
const credentialLines = [
|
|
...renderAipodSpecCredentialRefLines(arrayRecords(providerCredentialRefs.items), "provider"),
|
|
...renderAipodSpecCredentialRefLines(arrayRecords(toolCredentialRefs.items), "tool"),
|
|
];
|
|
if (credentialLines.length > 0) lines.push("", "SecretRefs:", ...credentialLines.map((line) => ` ${line}`));
|
|
lines.push(
|
|
"",
|
|
"Provenance:",
|
|
` Source: ${displayValue(provenance.source)}`,
|
|
` Manifest: ${displayValue(provenance.apiVersion)} ${displayValue(provenance.sourceKind)}`,
|
|
` Image source: ${displayValue(provenance.imageSourceIdentity)}`,
|
|
` Updated: ${displayValue(provenance.updatedAt)}`,
|
|
" Values printed: false",
|
|
"",
|
|
"Next:",
|
|
` ${displayValue(next.full)}`,
|
|
` ${displayValue(next.raw)}`,
|
|
);
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function compactAipodSpecWorkspaceRef(value: Record<string, unknown>): Record<string, unknown> | null {
|
|
if (Object.keys(value).length === 0) return null;
|
|
return {
|
|
kind: boundedAipodSpecText(value.kind, 80),
|
|
path: boundedAipodSpecText(value.path, 320),
|
|
branch: boundedAipodSpecText(value.branch, 160),
|
|
repo: boundedAipodSpecRepoUrl(value.repo ?? value.repoUrl),
|
|
commitId: boundedAipodSpecText(value.commitId, 160),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function compactAipodSpecResourceBundleRef(primaryValue: unknown, fallbackValue: unknown): Record<string, unknown> | null {
|
|
const value = firstNonEmptyRecord(primaryValue, fallbackValue);
|
|
if (Object.keys(value).length === 0) return null;
|
|
return {
|
|
sourceIdentity: {
|
|
kind: boundedAipodSpecText(value.kind, 80),
|
|
repoUrl: boundedAipodSpecRepoUrl(value.repoUrl),
|
|
ref: boundedAipodSpecText(value.ref, 160),
|
|
commitId: boundedAipodSpecText(value.commitId, 160),
|
|
valuesPrinted: false,
|
|
},
|
|
bundleCount: aipodSpecReferenceCount(value.bundles),
|
|
requiredSkillCount: aipodSpecReferenceCount(value.requiredSkills),
|
|
promptRefCount: aipodSpecReferenceCount(value.promptRefs),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function compactAipodSpecCredentialRefs(primaryValue: unknown, fallbackValue: unknown, kind: "provider" | "tool"): { count: number; items: Record<string, unknown>[]; omittedCount: number; valuesPrinted: false } {
|
|
const primary = record(primaryValue);
|
|
const primaryItems = arrayRecords(primary.items ?? primaryValue);
|
|
const fallbackItems = arrayRecords(fallbackValue);
|
|
const sourceItems = primaryItems.length > 0 ? primaryItems : fallbackItems;
|
|
const count = Object.keys(primary).length > 0 ? aipodSpecReferenceCount(primaryValue) : sourceItems.length;
|
|
const items = sourceItems.slice(0, 6).map((item) => {
|
|
const nestedSecretRef = record(item.secretRef);
|
|
const projection = record(item.projection);
|
|
return {
|
|
...(kind === "provider"
|
|
? { profile: boundedAipodSpecText(item.profile, 160) }
|
|
: {
|
|
tool: boundedAipodSpecText(item.tool, 160),
|
|
purpose: boundedAipodSpecText(item.purpose, 200),
|
|
}),
|
|
secretRef: {
|
|
namespace: boundedAipodSpecText(item.namespace ?? nestedSecretRef.namespace, 160),
|
|
name: boundedAipodSpecText(item.name ?? nestedSecretRef.name, 200),
|
|
keys: boundedAipodSpecStrings(item.keys ?? nestedSecretRef.keys, 6, 80),
|
|
valuesPrinted: false,
|
|
},
|
|
...(kind === "tool"
|
|
? {
|
|
projection: {
|
|
kind: boundedAipodSpecText(projection.kind, 80),
|
|
envName: boundedAipodSpecText(projection.envName, 160),
|
|
secretKey: boundedAipodSpecText(projection.secretKey, 160),
|
|
mountPath: boundedAipodSpecText(projection.mountPath, 320),
|
|
valuesPrinted: false,
|
|
},
|
|
}
|
|
: {}),
|
|
valuesPrinted: false,
|
|
};
|
|
});
|
|
return {
|
|
count,
|
|
items,
|
|
omittedCount: Math.max(0, count - items.length),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function renderAipodSpecCredentialRefLines(items: Record<string, unknown>[], kind: "provider" | "tool"): string[] {
|
|
return items.map((item) => {
|
|
const secretRef = record(item.secretRef);
|
|
const namespace = stringOrNull(secretRef.namespace);
|
|
const name = displayValue(secretRef.name);
|
|
const reference = namespace === null ? name : `${namespace}/${name}`;
|
|
const keys = Array.isArray(secretRef.keys) ? secretRef.keys.map(String).join(",") : "-";
|
|
const identity = kind === "provider"
|
|
? `profile=${displayValue(item.profile)}`
|
|
: `tool=${displayValue(item.tool)} purpose=${displayValue(item.purpose)}`;
|
|
return `${kind} ${identity} secret=${reference} keys=${keys}`;
|
|
});
|
|
}
|
|
|
|
function firstNonEmptyRecord(...values: unknown[]): Record<string, unknown> {
|
|
for (const value of values) {
|
|
const candidate = record(value);
|
|
if (Object.keys(candidate).length > 0) return candidate;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
function aipodSpecReferenceCount(value: unknown): number {
|
|
if (Array.isArray(value)) return value.length;
|
|
const summary = record(value);
|
|
const count = summary.count;
|
|
if (typeof count === "number" && Number.isFinite(count) && count >= 0) return Math.floor(count);
|
|
return arrayRecords(summary.items).length;
|
|
}
|
|
|
|
function boundedAipodSpecStrings(value: unknown, limit: number, maxChars: number): string[] {
|
|
if (!Array.isArray(value)) return [];
|
|
return value
|
|
.slice(0, limit)
|
|
.map((item) => boundedAipodSpecText(item, maxChars))
|
|
.filter((item): item is string => item !== null);
|
|
}
|
|
|
|
function boundedAipodSpecText(value: unknown, maxChars: number): string | null {
|
|
const text = stringOrNull(value);
|
|
return text === null ? null : truncateOneLine(text, maxChars);
|
|
}
|
|
|
|
function boundedAipodSpecRepoUrl(value: unknown): string | null {
|
|
const sanitized = taskInputRepoUrl(value);
|
|
return typeof sanitized === "string" ? truncateOneLine(sanitized, 320) : null;
|
|
}
|
|
|
|
export function taskInputDescriptionPayload(ref: AgentRunResourceRef, task: Record<string, unknown>): Record<string, unknown> {
|
|
const metadata = taskInputMetadata(task.metadata);
|
|
const aipodImageRef = taskInputImageRef(metadata.aipodImageRef);
|
|
if (aipodImageRef !== null) metadata.aipodImageRef = aipodImageRef;
|
|
const spec = pickCompact(task, [
|
|
"tenantId",
|
|
"projectId",
|
|
"queue",
|
|
"lane",
|
|
"title",
|
|
"priority",
|
|
"backendProfile",
|
|
"providerId",
|
|
"payload",
|
|
"references",
|
|
"idempotencyKey",
|
|
]);
|
|
spec.workspaceRef = taskInputWorkspaceRef(task.workspaceRef);
|
|
spec.sessionRef = taskInputSessionRef(task.sessionRef);
|
|
spec.executionPolicy = taskInputExecutionPolicy(task.executionPolicy);
|
|
spec.resourceBundleRef = taskInputResourceBundleRef(task.resourceBundleRef);
|
|
spec.metadata = metadata;
|
|
return {
|
|
kind: "TaskInput",
|
|
name: ref.name,
|
|
spec,
|
|
aipodSpec: {
|
|
name: metadata.aipod ?? null,
|
|
specHash: metadata.aipodSpecHash ?? null,
|
|
imageRef: aipodImageRef,
|
|
valuesPrinted: false,
|
|
},
|
|
provenance: {
|
|
sourceTaskId: stringOrNull(task.id) ?? ref.name,
|
|
source: metadata.source ?? null,
|
|
payloadHash: task.payloadHash ?? null,
|
|
valuesPrinted: false,
|
|
},
|
|
redaction: {
|
|
credentialValues: "omitted",
|
|
secretRefsOnly: true,
|
|
valuesPrinted: false,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function taskInputExecutionPolicy(value: unknown): Record<string, unknown> | null {
|
|
if (!isRecord(value)) return null;
|
|
const policy = pickCompact(value, ["sandbox", "approval", "timeoutMs", "network"]);
|
|
const secretScope = record(value.secretScope);
|
|
policy.secretScope = {
|
|
allowCredentialEcho: false,
|
|
providerCredentials: arrayRecords(secretScope.providerCredentials).map((credential) => ({
|
|
...pickCompact(credential, ["profile"]),
|
|
secretRef: taskInputSecretRef(credential.secretRef),
|
|
valuesPrinted: false,
|
|
})),
|
|
toolCredentials: arrayRecords(secretScope.toolCredentials).map((credential) => ({
|
|
...pickCompact(credential, ["tool", "purpose"]),
|
|
secretRef: taskInputSecretRef(credential.secretRef),
|
|
projection: pickCompact(record(credential.projection), ["kind", "envName", "secretKey", "mountPath"]),
|
|
valuesPrinted: false,
|
|
})),
|
|
valuesPrinted: false,
|
|
};
|
|
return policy;
|
|
}
|
|
|
|
function taskInputSecretRef(value: unknown): Record<string, unknown> | null {
|
|
if (!isRecord(value)) return null;
|
|
return {
|
|
...pickCompact(value, ["namespace", "name", "keys", "mountPath"]),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function taskInputResourceBundleRef(value: unknown): Record<string, unknown> | null {
|
|
if (!isRecord(value)) return null;
|
|
const result = pickCompact(value, ["kind", "commitId", "ref", "submodules", "lfs"]);
|
|
result.repoUrl = taskInputRepoUrl(value.repoUrl);
|
|
result.bundles = arrayRecords(value.bundles).map((bundle) => ({
|
|
...pickCompact(bundle, ["name", "commitId", "ref", "subpath", "targetPath"]),
|
|
...(bundle.repoUrl === undefined ? {} : { repoUrl: taskInputRepoUrl(bundle.repoUrl) }),
|
|
}));
|
|
if (Array.isArray(value.promptRefs)) {
|
|
result.promptRefs = arrayRecords(value.promptRefs).map((item) => pickCompact(item, ["name", "path", "inject", "required"]));
|
|
}
|
|
if (Array.isArray(value.requiredSkills)) {
|
|
result.requiredSkills = arrayRecords(value.requiredSkills).map((item) => pickCompact(item, ["name"]));
|
|
}
|
|
if (value.credentialRef !== undefined) result.credentialRef = taskInputSecretRef(value.credentialRef);
|
|
result.valuesPrinted = false;
|
|
return result;
|
|
}
|
|
|
|
function taskInputWorkspaceRef(value: unknown): Record<string, unknown> | null {
|
|
if (!isRecord(value)) return null;
|
|
const result = pickCompact(value, ["kind", "path", "branch"]);
|
|
if (value.repo !== undefined) result.repo = taskInputRepoUrl(value.repo);
|
|
return result;
|
|
}
|
|
|
|
function taskInputSessionRef(value: unknown): Record<string, unknown> | null {
|
|
if (!isRecord(value)) return null;
|
|
return pickCompact(value, ["sessionId", "conversationId", "threadId", "expiresAt"]);
|
|
}
|
|
|
|
function taskInputMetadata(value: unknown): Record<string, unknown> {
|
|
if (!isRecord(value)) return {};
|
|
return pickCompact(value, ["aipod", "source", "aipodSpecHash", "aipodImageRef"]);
|
|
}
|
|
|
|
function taskInputImageRef(value: unknown): Record<string, unknown> | null {
|
|
if (!isRecord(value)) return null;
|
|
const result = pickCompact(value, ["kind", "commitId", "dockerfilePath", "sourceIdentity"]);
|
|
if (value.repoUrl !== undefined) result.repoUrl = taskInputRepoUrl(value.repoUrl);
|
|
result.valuesPrinted = false;
|
|
return result;
|
|
}
|
|
|
|
function taskInputRepoUrl(value: unknown): unknown {
|
|
if (typeof value !== "string") return value ?? null;
|
|
if (!value.includes("://")) return value;
|
|
try {
|
|
const parsed = new URL(value);
|
|
parsed.username = "";
|
|
parsed.password = "";
|
|
return parsed.toString();
|
|
} catch {
|
|
return "<invalid-or-redacted-url>";
|
|
}
|
|
}
|
|
|
|
export function unwrapTaskDetail(data: Record<string, unknown>): Record<string, unknown> {
|
|
const task = record(data.task);
|
|
if (Object.keys(task).length > 0) return task;
|
|
return data;
|
|
}
|
|
|
|
export function renderGenericDescription(ref: AgentRunResourceRef, data: unknown): string {
|
|
const value = record(data);
|
|
const lines = [
|
|
`Name: ${ref.kind}/${ref.name}`,
|
|
`State: ${displayValue(value.state ?? value.status ?? value.phase ?? value.executionState ?? value.terminalStatus ?? "-")}`,
|
|
];
|
|
const identity = renderResourceIdentityLines(ref, value);
|
|
if (identity.length > 0) lines.push("", ...identity);
|
|
const runnerObservation = renderRunnerJobObservationLines(ref, value);
|
|
if (runnerObservation.length > 0) lines.push("", "Runtime observation:", ...runnerObservation.map((line) => ` ${line}`));
|
|
const supervisor = renderSupervisorLines(ref, value);
|
|
if (supervisor.length > 0) lines.push("", "Supervisor:", ...supervisor.map((line) => ` ${line}`));
|
|
const failure = renderFailureLines(value);
|
|
if (failure.length > 0) lines.push("", ...failure);
|
|
const next = renderResourceNextLines(ref, value);
|
|
if (next.length > 0) lines.push("", "Next:", ...next.map((line) => ` ${line}`));
|
|
if (identity.length === 0 && failure.length === 0 && next.length === 0) {
|
|
lines.push(
|
|
"",
|
|
"Summary:",
|
|
JSON.stringify(pickCompact(value, ["id", "name", "runId", "commandId", "runnerJobId", "sessionId", "state", "status", "phase", "reason", "failureKind", "valuesPrinted"]), null, 2),
|
|
);
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
|
|
export function renderRunnerJobObservationLines(ref: AgentRunResourceRef, value: Record<string, unknown>): string[] {
|
|
if (ref.kind !== "runnerjob") return [];
|
|
const observation = record(value.runtimeObservation);
|
|
const runner = record(observation.runner);
|
|
if (Object.keys(observation).length === 0) return [];
|
|
if (Object.keys(runner).length === 0) {
|
|
return [
|
|
`Available: false`,
|
|
`Reason: ${displayValue(observation.reason ?? "runtime-observation-unavailable")}`,
|
|
];
|
|
}
|
|
const podPhases = Array.isArray(runner.podPhases) ? runner.podPhases.map(String).join(",") : "-";
|
|
const waitingReasons = Array.isArray(runner.waitingReasons) ? runner.waitingReasons.map(String).join(",") : "-";
|
|
return [
|
|
`Manager phase: ${displayValue(value.managerPhase ?? "-")}`,
|
|
`Pod phase: ${podPhases || "-"}`,
|
|
`Waiting: ${waitingReasons || "-"}`,
|
|
`AgeMs: ${displayValue(runner.lastActiveAgeMs ?? "-")}`,
|
|
`Run: ${displayValue(runner.runStatus ?? "-")} claimedBy=${displayValue(runner.runClaimedBy ?? "-")}`,
|
|
`Command: ${displayValue(runner.commandState ?? "-")}`,
|
|
`Heartbeat: ${displayValue(runner.heartbeatAt ?? "-")} ageMs=${displayValue(runner.heartbeatAgeMs ?? "-")}`,
|
|
`Classification: ${displayValue(runner.classification ?? "-")} protected=${displayValue(runner.protectedActive ?? "-")}`,
|
|
];
|
|
}
|
|
|
|
export function renderResourceIdentityLines(ref: AgentRunResourceRef, value: Record<string, unknown>): string[] {
|
|
const supervisor = record(value.supervisor);
|
|
const fields: [string, unknown][] = [
|
|
["Run", firstKnownString(value.runId, value.activeRunId, value.lastRunId, supervisor.runId, supervisor.activeRunId, supervisor.lastRunId)],
|
|
["Command", firstKnownString(value.commandId, value.activeCommandId, value.lastCommandId, supervisor.commandId, supervisor.activeCommandId, supervisor.lastCommandId)],
|
|
["RunnerJob", value.runnerJobId ?? (ref.kind === "runnerjob" ? value.id : null)],
|
|
["Session", firstKnownString(value.sessionId, supervisor.sessionId)],
|
|
["Namespace", value.namespace],
|
|
["Job", value.jobName],
|
|
["Image", value.image],
|
|
["LogPath", value.logPath],
|
|
["Started", value.startedAt],
|
|
["Finished", value.finishedAt],
|
|
];
|
|
if (ref.kind !== "runnerjob") {
|
|
return fields
|
|
.filter(([label]) => label === "Run" || label === "Command" || label === "RunnerJob" || label === "Session")
|
|
.map(([label, item]) => formatResourceField(label, item))
|
|
.filter((line): line is string => line !== null);
|
|
}
|
|
return fields.map(([label, item]) => formatResourceField(label, item)).filter((line): line is string => line !== null);
|
|
}
|
|
|
|
export function firstKnownString(...values: unknown[]): string | null {
|
|
for (const value of values) {
|
|
const text = stringOrNull(value);
|
|
if (text !== null && text.length > 0) return text;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function formatResourceField(label: string, value: unknown): string | null {
|
|
const text = stringOrNull(value);
|
|
if (text === null || text.length === 0) return null;
|
|
return `${label}: ${text}`;
|
|
}
|
|
|
|
export function renderSupervisorLines(ref: AgentRunResourceRef, value: Record<string, unknown>): string[] {
|
|
if (ref.kind !== "session" && ref.kind !== "task") return [];
|
|
const supervisor = record(value.supervisor);
|
|
const liveness = record(value.liveness);
|
|
const source = Object.keys(supervisor).length > 0 ? supervisor : liveness;
|
|
if (Object.keys(source).length === 0) return [];
|
|
const timeoutBudget = record(source.timeoutBudget);
|
|
const terminalClassification = record(source.terminalClassification);
|
|
const runId = firstKnownString(source.activeRunId, source.runId, source.lastRunId, value.activeRunId, value.runId, value.lastRunId);
|
|
const commandId = firstKnownString(source.activeCommandId, source.commandId, source.lastCommandId, value.activeCommandId, value.commandId, value.lastCommandId);
|
|
const lines: string[] = [];
|
|
const executionState = firstKnownString(source.executionState, value.executionState);
|
|
if (executionState !== null) lines.push(`Execution: ${executionState}`);
|
|
if (typeof source.active === "boolean") lines.push(`Active: ${String(source.active)}`);
|
|
if (runId !== null) lines.push(`Run: run/${runId}`);
|
|
if (commandId !== null) lines.push(`Command: command/${commandId}`);
|
|
const phase = firstKnownString(source.phase, liveness.phase);
|
|
const lastEventAgeMs = typeof source.lastEventAgeMs === "number" ? source.lastEventAgeMs : typeof liveness.lastEventAgeMs === "number" ? liveness.lastEventAgeMs : null;
|
|
if (phase !== null || lastEventAgeMs !== null) lines.push(`Liveness: ${displayValue(phase)}${lastEventAgeMs === null ? "" : ` ageMs=${String(lastEventAgeMs)}`}`);
|
|
const timeoutKind = firstKnownString(timeoutBudget.timeoutKind);
|
|
const timeoutState = firstKnownString(timeoutBudget.state);
|
|
const remainingMs = typeof timeoutBudget.remainingMs === "number" ? timeoutBudget.remainingMs : null;
|
|
if (timeoutKind !== null || timeoutState !== null || remainingMs !== null) lines.push(`Timeout: ${displayValue(timeoutKind)} / ${displayValue(timeoutState)}${remainingMs === null ? "" : ` remainingMs=${String(remainingMs)}`}`);
|
|
const category = firstKnownString(terminalClassification.category);
|
|
const reason = firstKnownString(terminalClassification.reason);
|
|
if (category !== null || reason !== null) lines.push(`Classification: ${displayValue(category)}${reason === null ? "" : `; ${reason}`}`);
|
|
return lines;
|
|
}
|