Merge pull request #1818 from pikasTech/fix/1817-agentrun-event-summary
fix: 收敛 AgentRun 事件摘要输出
This commit is contained in:
@@ -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<Record<string, unknown>> {
|
||||
const items: Array<Record<string, unknown>> = [];
|
||||
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<ReturnType<typeof runAgentRunCommand>>): 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<Record<string, unknown>>;
|
||||
};
|
||||
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<Record<string, unknown>> };
|
||||
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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -283,7 +283,7 @@ export function agentRunHelpText(args: string[]): string {
|
||||
].join("\n");
|
||||
}
|
||||
if (verb === "events") {
|
||||
return "Usage: bun scripts/cli.ts agentrun events run/<runId> [--after-seq N] [--limit 100] [-o json|yaml] [--raw]";
|
||||
return "Usage: bun scripts/cli.ts agentrun events run/<runId> [--after-seq N] [--limit 100] [-o json|yaml] [--full|--raw]";
|
||||
}
|
||||
if (verb === "logs") {
|
||||
return "Usage: bun scripts/cli.ts agentrun logs session/<sessionId> [--tail 100|--after-seq N] [--limit 100] [--full-text] [-o json|yaml] [--raw]";
|
||||
|
||||
+272
-38
@@ -351,25 +351,93 @@ export function compactRunnerJobDescriptionPayload(value: Record<string, unknown
|
||||
};
|
||||
}
|
||||
|
||||
export function renderEventLike(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, kind: string, items: Record<string, unknown>[], 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<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 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, 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));
|
||||
@@ -674,15 +742,37 @@ export function normalizeRunnerJobItems(data: unknown): Record<string, unknown>[
|
||||
}));
|
||||
}
|
||||
|
||||
export function normalizeEventItems(data: unknown): Record<string, unknown>[] {
|
||||
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: 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<string, unknown>[] {
|
||||
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<string, unknown>, payload: Record<string, unknown>): string {
|
||||
return stringOrNull(item.commandId)
|
||||
?? stringOrNull(payload.commandId)
|
||||
?? stringOrNull(record(payload.summary).commandId)
|
||||
?? "-";
|
||||
}
|
||||
|
||||
export function agentRunEventStatus(item: Record<string, unknown>, payload: Record<string, unknown>): 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<string, unknown>, payload: Record<string, unknown>): string {
|
||||
function agentRunLogSummary(item: Record<string, unknown>, payload: Record<string, unknown>): string {
|
||||
const type = stringOrNull(item.type);
|
||||
const parts: string[] = [];
|
||||
const push = (value: unknown): void => {
|
||||
@@ -751,8 +825,67 @@ export function agentRunEventSummary(item: Record<string, unknown>, 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<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") {
|
||||
@@ -771,7 +904,108 @@ export function agentRunEventSummary(item: Record<string, unknown>, payload: Rec
|
||||
push(payload.message);
|
||||
push(payload.reason);
|
||||
|
||||
return parts.join("; ");
|
||||
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 {
|
||||
|
||||
@@ -456,9 +456,33 @@ function runnerJobObservationIdentity(managerResource: Record<string, unknown>,
|
||||
export async function resourceEvents(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
|
||||
const ref = parseResourceRef(action, args, "run");
|
||||
const runId = ref.kind === "run" ? ref.name : options.runId ?? requiredContext("events", "--run <runId>");
|
||||
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 <item.seq-1> --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<RenderedCliResult> {
|
||||
|
||||
Reference in New Issue
Block a user