feat: 补齐 HWLAB 基线 AgentRun 执行元语
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { AgentRunError } from "./errors.js";
|
||||
import type { EventType, JsonRecord, RunEvent, TerminalStatus } from "./types.js";
|
||||
import { boundedTextSummary, commandOutputPayload } from "./output.js";
|
||||
import { redactJson } from "./redaction.js";
|
||||
|
||||
export const eventTypes = ["backend_status", "assistant_message", "tool_call", "command_output", "diff", "error", "terminal_status"] as const satisfies readonly EventType[];
|
||||
export const terminalStatuses = ["completed", "failed", "blocked", "cancelled"] as const satisfies readonly TerminalStatus[];
|
||||
|
||||
const eventTypeSet = new Set<string>(eventTypes);
|
||||
const terminalStatusSet = new Set<string>(terminalStatuses);
|
||||
|
||||
export function isEventType(value: unknown): value is EventType {
|
||||
return typeof value === "string" && eventTypeSet.has(value);
|
||||
}
|
||||
|
||||
export function isTerminalStatus(value: unknown): value is TerminalStatus {
|
||||
return typeof value === "string" && terminalStatusSet.has(value);
|
||||
}
|
||||
|
||||
export function requireEventType(value: unknown): EventType {
|
||||
if (isEventType(value)) return value;
|
||||
throw new AgentRunError("schema-invalid", `event.type ${String(value)} is not supported`, { httpStatus: 400, details: { allowedEventTypes: [...eventTypes] } });
|
||||
}
|
||||
|
||||
export function normalizeRunEventPayload(type: EventType, payload: JsonRecord): JsonRecord {
|
||||
if (type === "terminal_status") return normalizeTerminalStatusPayload(payload);
|
||||
if (type === "command_output") return normalizeCommandOutputPayload(payload);
|
||||
if (type === "assistant_message") return normalizeTextPayload(payload);
|
||||
if (type === "tool_call") return normalizeToolCallPayload(payload);
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function eventContractSummary(events: RunEvent[]): JsonRecord {
|
||||
const issues: JsonRecord[] = [];
|
||||
let terminalStatusCount = 0;
|
||||
for (let index = 0; index < events.length; index += 1) {
|
||||
const event = events[index];
|
||||
if (!eventTypeSet.has(event.type)) issues.push({ code: "event-type-invalid", seq: event.seq, type: event.type });
|
||||
if (event.seq !== index + 1) issues.push({ code: "seq-not-contiguous", expectedSeq: index + 1, actualSeq: event.seq });
|
||||
if (event.type === "terminal_status") {
|
||||
terminalStatusCount += 1;
|
||||
if (!isTerminalStatus(event.payload.terminalStatus)) issues.push({ code: "terminal-status-invalid", seq: event.seq, terminalStatus: String(event.payload.terminalStatus ?? "") });
|
||||
}
|
||||
}
|
||||
if (terminalStatusCount > 1) issues.push({ code: "terminal-status-duplicated", terminalStatusCount });
|
||||
return {
|
||||
ok: issues.length === 0,
|
||||
eventCount: events.length,
|
||||
lastSeq: events.at(-1)?.seq ?? 0,
|
||||
terminalStatusCount,
|
||||
issues,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTerminalStatusPayload(payload: JsonRecord): JsonRecord {
|
||||
if (!isTerminalStatus(payload.terminalStatus)) {
|
||||
throw new AgentRunError("schema-invalid", "terminal_status event requires terminalStatus completed|failed|blocked|cancelled", { httpStatus: 400, details: { allowedTerminalStatuses: [...terminalStatuses] } });
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function normalizeCommandOutputPayload(payload: JsonRecord): JsonRecord {
|
||||
const { text: _text, delta: _delta, content: _content, summary: _summary, ...rest } = payload;
|
||||
const value = typeof payload.text === "string" ? payload.text : typeof payload.delta === "string" ? payload.delta : typeof payload.content === "string" ? payload.content : "";
|
||||
const stream = typeof payload.stream === "string" ? payload.stream : "stdout";
|
||||
return { ...rest, ...commandOutputPayload(stream, value) };
|
||||
}
|
||||
|
||||
function normalizeTextPayload(payload: JsonRecord): JsonRecord {
|
||||
const { text: _text, delta: _delta, content: _content, summary: _summary, ...rest } = payload;
|
||||
const value = typeof payload.text === "string" ? payload.text : typeof payload.delta === "string" ? payload.delta : typeof payload.content === "string" ? payload.content : "";
|
||||
const summary = boundedTextSummary(value);
|
||||
return { ...rest, text: summary.text, summary, textBytes: summary.textBytes, textTruncated: summary.textTruncated };
|
||||
}
|
||||
|
||||
function normalizeToolCallPayload(payload: JsonRecord): JsonRecord {
|
||||
const redacted = redactJson(payload);
|
||||
const json = JSON.stringify(redacted);
|
||||
const summary = boundedTextSummary(json);
|
||||
if (summary.outputTruncated !== true) return { ...redacted, summary, outputBytes: summary.outputBytes, outputTruncated: false };
|
||||
return {
|
||||
method: typeof payload.method === "string" ? payload.method : null,
|
||||
itemPreview: summary.text,
|
||||
summary,
|
||||
outputBytes: summary.outputBytes,
|
||||
outputTruncated: true,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user