feat: 为 gitbundle 装配 required skills (#138)

* Add gitbundle required skills validation

* fix: 限定 required skill blocked result 覆盖

---------

Co-authored-by: AgentRun Codex <agentrun@example.invalid>
Co-authored-by: Codex <codex@pikas.tech>
This commit is contained in:
Lyon
2026-06-10 10:36:26 +08:00
committed by GitHub
parent 2e95276db8
commit 74b83b43c2
12 changed files with 247 additions and 45 deletions
+63 -8
View File
@@ -1,5 +1,5 @@
import type { AgentRunStore } from "./store.js";
import type { CommandRecord, JsonRecord, JsonValue, RunEvent, RunRecord, RunnerJobRecord, TerminalStatus } from "../common/types.js";
import type { CommandRecord, FailureKind, JsonRecord, JsonValue, RunEvent, RunRecord, RunnerJobRecord, TerminalStatus } from "../common/types.js";
import { boundedTextSummary, outputBytesFromPayload, outputTruncatedFromPayload } from "../common/output.js";
const maxToolCallSummaryItems = 40;
@@ -35,12 +35,14 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman
const latestJob = jobs.at(-1) ?? null;
const commandTerminal = command ? terminalFromCommand(command) : null;
const terminalEventStatus = terminalFromEvents(scopedEvents);
const terminal = commandTerminal ?? terminalEventStatus ?? run.terminalStatus;
const terminalSource = commandTerminal ? "command-record" : terminalEventStatus ? "terminal_status-event" : run.terminalStatus ? "run-record" : "none";
const failureKind = resultFailureKind(run, command, scopedEvents, terminal);
const preliminaryTerminal = commandTerminal ?? terminalEventStatus ?? run.terminalStatus;
const failureKind = resultFailureKind(run, command, scopedEvents, preliminaryTerminal);
const terminal = resultTerminal(commandTerminal, terminalEventStatus, run.terminalStatus, failureKind);
const terminalSource = resultTerminalSource(commandTerminal, terminalEventStatus, run.terminalStatus, failureKind);
const failureMessage = resultFailureMessage(run, command, scopedEvents, terminal);
const failureDetails = resultFailureDetails(scopedEvents, terminal);
const reply = assistantReply(scopedEvents);
const blocker = terminal === "blocked" || terminal === "failed" ? { failureKind, message: failureMessage } : null;
const blocker = terminal === "blocked" || terminal === "failed" ? { failureKind, message: failureMessage, details: failureDetails } : null;
const liveness = livenessSnapshot(run, command, events, scopedEvents, terminal);
const steerDelivery = command?.type === "steer" ? steerDeliverySummary(events, command.id) : null;
return {
@@ -72,6 +74,7 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman
finalAssistantOutputTruncated: reply.outputTruncated,
failureKind,
failureMessage,
failureDetails,
blocker,
liveness,
...(steerDelivery ? { steerDelivery } : {}),
@@ -292,25 +295,64 @@ function terminalFromCommand(command: CommandRecord): TerminalStatus | null {
return null;
}
function resultTerminal(commandTerminal: TerminalStatus | null, terminalEventStatus: TerminalStatus | null, runTerminalStatus: TerminalStatus | null, failureKind: FailureKind | null): TerminalStatus | null {
if (commandTerminal === "failed" && terminalEventStatus === "blocked" && failureKind === "required-skill-unavailable") return "blocked";
return commandTerminal ?? terminalEventStatus ?? runTerminalStatus;
}
function resultTerminalSource(commandTerminal: TerminalStatus | null, terminalEventStatus: TerminalStatus | null, runTerminalStatus: TerminalStatus | null, failureKind: FailureKind | null): string {
if (commandTerminal === "failed" && terminalEventStatus === "blocked" && failureKind === "required-skill-unavailable") return "terminal_status-event";
if (commandTerminal) return "command-record";
if (terminalEventStatus) return "terminal_status-event";
if (runTerminalStatus) return "run-record";
return "none";
}
function eventsForCommand(events: RunEvent[], commandId: string): RunEvent[] {
const scoped = events.filter((event) => event.payload.commandId === commandId || typeof event.payload.commandId !== "string");
return scoped.length > 0 ? scoped : events;
}
function failureKindFromEvents(events: RunEvent[]): string | null {
function failureKindFromEvents(events: RunEvent[]): FailureKind | null {
for (const event of [...events].reverse()) {
const value = event.payload.failureKind;
if (typeof value === "string") return value;
if (isFailureKind(value)) return value;
}
return null;
}
function resultFailureKind(run: RunRecord, command: CommandRecord | null, events: RunEvent[], terminal: TerminalStatus | null): string | null {
function resultFailureKind(run: RunRecord, command: CommandRecord | null, events: RunEvent[], terminal: TerminalStatus | null): FailureKind | null {
if (terminal === "completed") return null;
if (command) return failureKindFromEvents(events);
return run.failureKind ?? failureKindFromEvents(events);
}
function isFailureKind(value: unknown): value is FailureKind {
return typeof value === "string" && [
"cancelled",
"tenant-policy-denied",
"secret-unavailable",
"prompt-unavailable",
"prompt-too-large",
"required-skill-unavailable",
"skill-unavailable",
"runner-lease-conflict",
"backend-failed",
"backend-timeout",
"backend-response-invalid",
"thread-resume-failed",
"provider-auth-failed",
"provider-invalid-tool-call",
"provider-compact-unsupported",
"provider-rate-limited",
"provider-unavailable",
"provider-stream-disconnected",
"provider-refused-retry-recovered",
"infra-failed",
"schema-invalid",
].includes(value);
}
function messageFromEvents(events: RunEvent[]): string | null {
for (const event of [...events].reverse()) {
const value = event.payload.message;
@@ -325,6 +367,18 @@ function resultFailureMessage(run: RunRecord, command: CommandRecord | null, eve
return run.failureMessage ?? messageFromEvents(events);
}
function detailsFromEvents(events: RunEvent[]): JsonRecord | null {
for (const event of [...events].reverse()) {
const value = event.payload.details;
if (typeof value === "object" && value !== null && !Array.isArray(value)) return value as JsonRecord;
}
return null;
}
function resultFailureDetails(events: RunEvent[], terminal: TerminalStatus | null): JsonRecord | null {
return terminal === "completed" ? null : detailsFromEvents(events);
}
function assistantReply(events: RunEvent[]): AssistantReplySummary {
const assistantEvents = events.filter((event) => event.type === "assistant_message");
const latestAuthoritative = [...assistantEvents].reverse().find((event) => (event.payload.replyAuthority === true || event.payload.final === true) && textPayload(event.payload).length > 0);
@@ -474,6 +528,7 @@ function resourceBundleSummary(run: RunRecord, events: RunEvent[]): JsonRecord |
valuesPrinted: false,
},
promptRefs: run.resourceBundleRef.promptRefs ? { count: run.resourceBundleRef.promptRefs.length, names: run.resourceBundleRef.promptRefs.map((item) => item.name), required: run.resourceBundleRef.promptRefs.filter((item) => item.required === true).map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], required: [], valuesPrinted: false },
requiredSkills: run.resourceBundleRef.requiredSkills ? { count: run.resourceBundleRef.requiredSkills.length, names: run.resourceBundleRef.requiredSkills.map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], valuesPrinted: false },
materialized: materialized as JsonValue,
};
}
+1
View File
@@ -761,6 +761,7 @@ export function summarizeResourceBundleRef(resourceBundleRef: RunRecord["resourc
valuesPrinted: false,
},
promptRefs: resourceBundleRef.promptRefs ? { count: resourceBundleRef.promptRefs.length, names: resourceBundleRef.promptRefs.map((item) => item.name), required: resourceBundleRef.promptRefs.filter((item) => item.required === true).map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], required: [], valuesPrinted: false },
requiredSkills: resourceBundleRef.requiredSkills ? { count: resourceBundleRef.requiredSkills.length, names: resourceBundleRef.requiredSkills.map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], valuesPrinted: false },
submodules: resourceBundleRef.submodules ?? false,
lfs: resourceBundleRef.lfs ?? false,
credentialRef: resourceBundleRef.credentialRef ? { name: resourceBundleRef.credentialRef.name, namespace: resourceBundleRef.credentialRef.namespace ?? null, keys: resourceBundleRef.credentialRef.keys ?? [], valuesPrinted: false } : null,