diff --git a/scripts/src/agentrun-events.test.ts b/scripts/src/agentrun-events.test.ts new file mode 100644 index 00000000..b1c5b555 --- /dev/null +++ b/scripts/src/agentrun-events.test.ts @@ -0,0 +1,199 @@ +import { createHash } from "node:crypto"; +import { rmSync } from "node:fs"; +import { describe, expect, test } from "bun:test"; + +import { runAgentRunCommand } from "./agentrun"; + +const FIXTURE_RUN_ID = "run_event_summary_fixture"; +const HIDDEN_OUTPUT_MARKER = "HIDDEN_TOOL_OUTPUT_MUST_NOT_REACH_DEFAULT_RENDER"; +const HIDDEN_STDERR_MARKER = "HIDDEN_STDERR_MUST_NOT_REACH_DEFAULT_RENDER"; +const HIDDEN_SECOND_COMMAND_LINE = "HIDDEN_SECOND_COMMAND_LINE"; +const LARGE_TOOL_OUTPUT = `${HIDDEN_OUTPUT_MARKER}\n${"skill body and command output\n".repeat(2_400)}`; +const LARGE_OUTPUT_BYTES = Buffer.byteLength(LARGE_TOOL_OUTPUT, "utf8"); +const LARGE_OUTPUT_SHA256 = `sha256:${createHash("sha256").update(LARGE_TOOL_OUTPUT).digest("hex")}`; + +const agentRunClientYaml = [ + "version: 1", + "kind: AgentRunConfig", + "metadata:", + " name: agentrun", + "manager:", + " baseUrl: __BASE_URL__", + " timeoutMs: 15000", + "auth:", + " env: HWLAB_API_KEY", + " file: /tmp/hwlab-api-key.env", + " header: Authorization", + " scheme: Bearer", + "client:", + " role: render-only", + " transport: direct-http", + " sessionPolicy:", + " tenantId: unidesk", + " projectId: test", + " providerId: NC01", + " backendProfile: codex", + " workspaceRef:", + " kind: opaque", + " executionPolicy:", + " sandbox: workspace-write", + " approval: never", + " timeoutMs: 900000", + " network: enabled", + " secretScope:", + " allowCredentialEcho: false", +].join("\n"); + +function fixtureEvents(): Array> { + const items: Array> = []; + for (let seq = 22; seq <= 47; seq += 1) { + if (seq <= 41) { + items.push({ + id: `evt_${seq}`, + seq, + type: "tool_call", + payload: { + commandId: "cmd_fixture", + itemId: `tool_${seq}`, + toolName: "commandExecution", + type: "commandExecution", + method: "item/completed", + status: "completed", + command: `find . -maxdepth 3 -name package.json\n${HIDDEN_SECOND_COMMAND_LINE}`, + exitCode: 0, + durationMs: 17, + outputSummary: LARGE_TOOL_OUTPUT, + stderr: HIDDEN_STDERR_MARKER, + outputBytes: LARGE_OUTPUT_BYTES, + outputTruncated: true, + summary: { + text: `commandExecution completed output=${LARGE_TOOL_OUTPUT}`, + textBytes: LARGE_OUTPUT_BYTES, + textTruncated: true, + }, + }, + }); + continue; + } + items.push({ + id: `evt_${seq}`, + seq, + type: "backend_status", + payload: { commandId: "cmd_fixture", phase: `phase-${seq}`, message: "bounded status" }, + }); + } + return items; +} + +function renderedText(value: Awaited>): string { + if (!("renderedText" in value) || typeof value.renderedText !== "string") throw new Error("expected rendered AgentRun CLI output"); + return value.renderedText; +} + +describe("AgentRun events render-only progressive disclosure", () => { + test("default human/json/yaml stay bounded while full/raw retain the exact event page", async () => { + const events = fixtureEvents(); + const observedLimits: number[] = []; + const server = Bun.serve({ + port: 0, + fetch(request) { + const url = new URL(request.url); + expect(request.headers.get("authorization")).toBe("Bearer fixture-secret"); + expect(url.pathname).toBe(`/api/v1/runs/${FIXTURE_RUN_ID}/events`); + const afterSeq = Number(url.searchParams.get("afterSeq") ?? "0"); + const limit = Number(url.searchParams.get("limit") ?? "100"); + observedLimits.push(limit); + return Response.json({ + ok: true, + data: { + items: events.filter((item) => Number(item.seq) > afterSeq).slice(0, limit), + }, + }); + }, + }); + const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG; + const previousKey = process.env.HWLAB_API_KEY; + const configPath = `/tmp/unidesk-agentrun-events-${process.pid}-${Date.now()}.yaml`; + process.env.AGENTRUN_CLIENT_CONFIG = configPath; + process.env.HWLAB_API_KEY = "fixture-secret"; + await Bun.write(configPath, agentRunClientYaml.replace("__BASE_URL__", server.url.href.replace(/\/$/u, ""))); + try { + const args = ["events", `run/${FIXTURE_RUN_ID}`, "--after-seq", "21", "--limit", "100"]; + const jsonResult = await runAgentRunCommand(null, [...args, "-o", "json"]); + const jsonText = renderedText(jsonResult); + const json = JSON.parse(jsonText) as { + kind: string; + count: number; + requestedLimit: number; + effectiveLimit: number; + fetchedCount: number; + hasMore: boolean; + nextAfterSeq: number; + items: Array>; + }; + expect(json.kind).toBe("EventList"); + expect(json.count).toBe(20); + expect(json.requestedLimit).toBe(100); + expect(json.effectiveLimit).toBe(20); + expect(json.fetchedCount).toBe(21); + expect(json.hasMore).toBe(true); + expect(json.nextAfterSeq).toBe(41); + expect(Buffer.byteLength(jsonText, "utf8")).toBeLessThan(10_240); + expect(jsonText).not.toContain(HIDDEN_OUTPUT_MARKER); + expect(jsonText).not.toContain(HIDDEN_STDERR_MARKER); + expect(jsonText).not.toContain(HIDDEN_SECOND_COMMAND_LINE); + + const completed = json.items.find((item) => item.seq === 24); + expect(completed).toMatchObject({ + identity: "event/evt_24", + type: "tool_call", + status: "completed", + commandId: "cmd_fixture", + tool: "commandExecution", + command: "find . -maxdepth 3 -name package.json", + exitCode: 0, + durationMs: 17, + outputBytes: LARGE_OUTPUT_BYTES, + outputSha256: LARGE_OUTPUT_SHA256, + outputTruncated: true, + outputOmitted: true, + }); + + const humanText = renderedText(await runAgentRunCommand(null, args)); + expect(Buffer.byteLength(humanText, "utf8")).toBeLessThan(10_240); + expect(humanText).toContain("commandExecution find . -maxdepth 3 -name package.json"); + expect(humanText).toContain(`${LARGE_OUTPUT_BYTES}B`); + expect(humanText).toContain("truncated"); + expect(humanText).toContain("Detail: bun scripts/cli.ts agentrun events"); + expect(humanText).not.toContain(HIDDEN_OUTPUT_MARKER); + expect(humanText).not.toContain(HIDDEN_STDERR_MARKER); + expect(humanText).not.toContain(HIDDEN_SECOND_COMMAND_LINE); + + const yamlText = renderedText(await runAgentRunCommand(null, [...args, "-o", "yaml"])); + const yaml = Bun.YAML.parse(yamlText) as { kind?: string; items?: Array> }; + expect(yaml.kind).toBe("EventList"); + expect(yaml.items?.find((item) => item.seq === 24)?.outputSha256).toBe(LARGE_OUTPUT_SHA256); + expect(Buffer.byteLength(yamlText, "utf8")).toBeLessThan(10_240); + expect(yamlText).not.toContain(HIDDEN_OUTPUT_MARKER); + + for (const disclosure of ["--full", "--raw"]) { + const fullText = renderedText(await runAgentRunCommand(null, [...args, disclosure])); + const full = JSON.parse(fullText) as { data?: { items?: Array<{ payload?: { outputSummary?: string; stderr?: string } }> } }; + expect(full.data?.items?.length).toBe(26); + expect(full.data?.items?.[2]?.payload?.outputSummary).toBe(LARGE_TOOL_OUTPUT); + expect(full.data?.items?.[2]?.payload?.stderr).toBe(HIDDEN_STDERR_MARKER); + expect(fullText).toContain(HIDDEN_OUTPUT_MARKER); + expect(fullText).toContain(HIDDEN_SECOND_COMMAND_LINE); + } + + expect(observedLimits).toEqual([21, 21, 21, 100, 100]); + } finally { + server.stop(true); + if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG; + else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig; + if (previousKey === undefined) delete process.env.HWLAB_API_KEY; + else process.env.HWLAB_API_KEY = previousKey; + rmSync(configPath, { force: true }); + } + }); +}); diff --git a/scripts/src/agentrun/entry.ts b/scripts/src/agentrun/entry.ts index 21a2851a..fa3b5bb1 100644 --- a/scripts/src/agentrun/entry.ts +++ b/scripts/src/agentrun/entry.ts @@ -283,7 +283,7 @@ export function agentRunHelpText(args: string[]): string { ].join("\n"); } if (verb === "events") { - return "Usage: bun scripts/cli.ts agentrun events run/ [--after-seq N] [--limit 100] [-o json|yaml] [--raw]"; + return "Usage: bun scripts/cli.ts agentrun events run/ [--after-seq N] [--limit 100] [-o json|yaml] [--full|--raw]"; } if (verb === "logs") { return "Usage: bun scripts/cli.ts agentrun logs session/ [--tail 100|--after-seq N] [--limit 100] [--full-text] [-o json|yaml] [--raw]"; diff --git a/scripts/src/agentrun/render.ts b/scripts/src/agentrun/render.ts index 79f83014..abd50be8 100644 --- a/scripts/src/agentrun/render.ts +++ b/scripts/src/agentrun/render.ts @@ -351,25 +351,93 @@ export function compactRunnerJobDescriptionPayload(value: Record, options: AgentRunResourceOptions, kind: string, items: Record[], sourceName: string): RenderedCliResult { +export interface EventLikePageDisclosure { + requestedLimit: number; + effectiveLimit: number; + fetchedCount: number; + hasMore: boolean; + nextAfterSeq: number | string | null; + detailCommand: string; +} + +export function renderEventLike(command: string, raw: Record, options: AgentRunResourceOptions, kind: string, items: Record[], sourceName: string, pageDisclosure: EventLikePageDisclosure | null = null): RenderedCliResult { if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false); const data = record(innerData(raw)); - const payload = { kind: `${kind}List`, source: sourceName, nextAfterSeq: data.nextAfterSeq ?? null, count: items.length, items }; + 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 afterSeq=item.seq-1", + 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 = [ - `${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), - ])), - ]; - if (data.nextAfterSeq !== undefined && data.nextAfterSeq !== null) lines.push(`Next: bun scripts/cli.ts ${nextPagedResourceCommand(command, String(data.nextAfterSeq), options.limit)}`); + 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 { + 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 { + 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, options: AgentRunResourceOptions, ref: AgentRunResourceRef): RenderedCliResult { if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false); const data = record(innerData(raw)); @@ -674,15 +742,37 @@ export function normalizeRunnerJobItems(data: unknown): Record[ })); } -export function normalizeEventItems(data: unknown): Record[] { +const EVENT_SUMMARY_CHARS = 160; +const EVENT_COMMAND_CHARS = 160; + +export function normalizeEventItems(data: unknown, runId: string | null = null): Record[] { 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: item.seq, - type: item.type, + seq: seq ?? item.seq, + type: boundedEventScalar(item.type, 64), status: agentRunEventStatus(item, payload), - phase: item.phase ?? payload.phase, + ...(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), }; }); @@ -696,28 +786,12 @@ export function normalizeLogItems(data: unknown): Record[] { type: item.type ?? item.role ?? "output", status: agentRunEventStatus(item, payload), commandId: agentRunEventCommandId(item, payload), - summary: agentRunEventSummary(item, payload), + summary: agentRunLogSummary(item, payload), }; }); } -export function agentRunEventCommandId(item: Record, payload: Record): string { - return stringOrNull(item.commandId) - ?? stringOrNull(payload.commandId) - ?? stringOrNull(record(payload.summary).commandId) - ?? "-"; -} - -export function agentRunEventStatus(item: Record, payload: Record): string { - return stringOrNull(item.status) - ?? stringOrNull(item.phase) - ?? stringOrNull(payload.status) - ?? stringOrNull(payload.phase) - ?? stringOrNull(payload.failureKind) - ?? (item.type === "error" ? "error" : ""); -} - -export function agentRunEventSummary(item: Record, payload: Record): string { +function agentRunLogSummary(item: Record, payload: Record): string { const type = stringOrNull(item.type); const parts: string[] = []; const push = (value: unknown): void => { @@ -751,8 +825,67 @@ export function agentRunEventSummary(item: Record, payload: Rec push(payload.outputSummary); } - if (type === "command_output") { - push(commandOutputSummary(payload)); + 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, payload: Record): string { + return boundedEventIdentity(stringOrNull(item.commandId) + ?? stringOrNull(payload.commandId) + ?? stringOrNull(record(payload.summary).commandId) + ?? "-") ?? "-"; +} + +export function agentRunEventStatus(item: Record, payload: Record): 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, payload: Record): 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") { @@ -771,7 +904,108 @@ export function agentRunEventSummary(item: Record, payload: Rec push(payload.message); push(payload.reason); - return parts.join("; "); + return truncateOneLine(parts.join("; "), EVENT_SUMMARY_CHARS); +} + +function toolCallEventSummary(item: Record, payload: Record): 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 { + 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 | 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 | 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): { 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, summary: Record): 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 | null { diff --git a/scripts/src/agentrun/resource-actions.ts b/scripts/src/agentrun/resource-actions.ts index facefa15..10e67d4b 100644 --- a/scripts/src/agentrun/resource-actions.ts +++ b/scripts/src/agentrun/resource-actions.ts @@ -456,9 +456,33 @@ function runnerJobObservationIdentity(managerResource: Record, export async function resourceEvents(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise { const ref = parseResourceRef(action, args, "run"); const runId = ref.kind === "run" ? ref.name : options.runId ?? requiredContext("events", "--run "); - const eventArgs = ["events", runId, "--after-seq", String(options.afterSeq ?? 0), "--limit", String(options.limit), "--tail-summary"]; + const requestedLimit = Math.max(1, options.limit); + const effectiveLimit = options.full || options.raw ? requestedLimit : Math.min(requestedLimit, 20); + const requestLimit = options.full || options.raw ? effectiveLimit : Math.min(500, effectiveLimit + 1); + const eventArgs = ["events", runId, "--after-seq", String(options.afterSeq ?? 0), "--limit", String(requestLimit)]; const result = await runAgentRunRestCommand(config, "runs", eventArgs); - return renderEventLike(command, result, options, "Event", normalizeEventItems(innerData(result)), runId); + if (options.raw) return renderMachine(command, result, "json", result.ok !== false); + if (options.full) return renderMachine(command, result, options.output === "yaml" ? "yaml" : "json", result.ok !== false); + + const fetchedItems = arrayRecords(record(innerData(result)).items ?? innerData(result)); + const visibleItems = fetchedItems.slice(0, effectiveLimit); + const hasMore = fetchedItems.length > visibleItems.length; + const lastVisibleSeq = visibleItems.length === 0 ? null : nonNegativeIntegerOrNull(visibleItems.at(-1)?.seq); + const responseNextAfterSeq = nonNegativeIntegerOrNull(record(innerData(result)).nextAfterSeq); + const nextAfterSeq = hasMore ? lastVisibleSeq : responseNextAfterSeq; + const targetArgs = [ + options.node === null ? null : `--node ${options.node}`, + options.lane === null ? null : `--lane ${options.lane}`, + ].filter((value): value is string => value !== null).join(" "); + const detailCommand = `bun scripts/cli.ts agentrun events run/${runId} --after-seq --limit 1 --full -o json${targetArgs.length === 0 ? "" : ` ${targetArgs}`}`; + return renderEventLike(command, result, options, "Event", normalizeEventItems({ items: visibleItems }, runId), runId, { + requestedLimit, + effectiveLimit, + fetchedCount: fetchedItems.length, + hasMore, + nextAfterSeq, + detailCommand, + }); } export async function resourceLogs(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise {