pi-extensible-workflows 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/agent-execution.js +47 -7
- package/dist/src/ambient-workflow-evals.js +14 -33
- package/dist/src/doctor.js +3 -5
- package/dist/src/index.d.ts +7 -4
- package/dist/src/index.js +176 -117
- package/dist/src/persistence.d.ts +2 -0
- package/dist/src/persistence.js +27 -4
- package/dist/src/workflow-evals.d.ts +1 -0
- package/dist/src/workflow-evals.js +23 -25
- package/package.json +5 -2
- package/skills/pi-extensible-workflows/SKILL.md +6 -2
- package/src/agent-execution.ts +45 -7
- package/src/ambient-workflow-evals.ts +18 -28
- package/src/doctor.ts +2 -5
- package/src/index.ts +145 -79
- package/src/persistence.ts +23 -4
- package/src/workflow-evals.ts +13 -16
|
@@ -252,6 +252,7 @@ export interface CaptureCaseInput {
|
|
|
252
252
|
maxCost: number;
|
|
253
253
|
}
|
|
254
254
|
export declare function parseSemanticJudge(raw: string, criteria: readonly SemanticCriterion[]): CriterionResult[];
|
|
255
|
+
export declare function findSessionFile(directory: string, sessionId: string): string | undefined;
|
|
255
256
|
export declare function captureEvalCase(input: CaptureCaseInput): Promise<EvalCaseResult>;
|
|
256
257
|
export interface IsolatedProcessOptions {
|
|
257
258
|
childPath: string;
|
|
@@ -8,13 +8,12 @@ import { Value } from "typebox/value";
|
|
|
8
8
|
import { getAgentDir, parseFrontmatter, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { CAPTURE_ERROR_PREFIX, CAPTURE_IDENTITY, resolveWorkflowSkillPath } from "./eval-capture-extension.js";
|
|
10
10
|
export { resolveWorkflowSkillPath } from "./eval-capture-extension.js";
|
|
11
|
-
import { ERROR_CODES, inspectWorkflowScript, loadAgentDefinitions, runWorkflow, WorkflowError } from "./index.js";
|
|
11
|
+
import { ERROR_CODES, inspectWorkflowScript, isObject, loadAgentDefinitions, runWorkflow, WORKFLOW_CALL_KINDS, WorkflowError } from "./index.js";
|
|
12
12
|
const CASE_PROCESS_GRACE_MS = 1_000;
|
|
13
13
|
export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "bash", "workflow"]);
|
|
14
14
|
const EVAL_MODEL_TOKEN = "$EVAL_MODEL";
|
|
15
15
|
const semantic = (description) => [{ id: "intent", description }];
|
|
16
16
|
const JSON_RESULT_TYPES = ["null", "boolean", "number", "integer", "string", "array", "object"];
|
|
17
|
-
const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"];
|
|
18
17
|
const AGENT_OPTION_NAMES = ["role", "model", "thinking", "tools", "retries"];
|
|
19
18
|
const expectationKeys = ["firstSignificantAction", "firstTool", "firstBatchToolSequence", "parentToolSequence", "workflowCallCount", "requiredOperations", "forbiddenOperations", "requiredRoles", "minimumAgentCalls", "requireOutputSchema", "expectedResults", "agentPolicies", "requiredAgentOrder", "requiredAgentStructures", "requiredDataFlow"];
|
|
20
19
|
const caseKeys = ["id", "prompt", "timeoutMs", "maxCost", "expectations", "expectedWorkflowCalls", "semanticCriteria"];
|
|
@@ -234,7 +233,6 @@ catch (error) {
|
|
|
234
233
|
cases.push(candidate);
|
|
235
234
|
} return Object.freeze(cases); }
|
|
236
235
|
export const INITIAL_WORKFLOW_EVAL_CASES = loadWorkflowEvalCases();
|
|
237
|
-
function isObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
238
236
|
function isJson(value) { if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
239
237
|
return true; if (typeof value === "number")
|
|
240
238
|
return Number.isFinite(value); if (Array.isArray(value))
|
|
@@ -945,6 +943,27 @@ function seedEvalProject(cwd, home, model) {
|
|
|
945
943
|
writeFileSync(path, `${content.slice(0, frontmatterEnd).replace(/^model:.*$/m, `model: ${model}`)}${content.slice(frontmatterEnd)}`);
|
|
946
944
|
}
|
|
947
945
|
}
|
|
946
|
+
export function findSessionFile(directory, sessionId) {
|
|
947
|
+
if (!existsSync(directory))
|
|
948
|
+
return undefined;
|
|
949
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
950
|
+
const path = join(directory, entry.name);
|
|
951
|
+
if (entry.isDirectory()) {
|
|
952
|
+
const found = findSessionFile(path, sessionId);
|
|
953
|
+
if (found)
|
|
954
|
+
return found;
|
|
955
|
+
}
|
|
956
|
+
else if (entry.name.endsWith(".jsonl")) {
|
|
957
|
+
try {
|
|
958
|
+
const header = JSON.parse(readFileSync(path, "utf8").split("\n")[0] ?? "{}");
|
|
959
|
+
if (isObject(header) && header.id === sessionId)
|
|
960
|
+
return path;
|
|
961
|
+
}
|
|
962
|
+
catch { /* Ignore incomplete sessions. */ }
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return undefined;
|
|
966
|
+
}
|
|
948
967
|
async function findParentSession(cwd, sessionDir, sessionId) {
|
|
949
968
|
try {
|
|
950
969
|
const sessions = await SessionManager.list(cwd, sessionDir);
|
|
@@ -953,28 +972,7 @@ async function findParentSession(cwd, sessionDir, sessionId) {
|
|
|
953
972
|
return found.path;
|
|
954
973
|
}
|
|
955
974
|
catch { /* Fall through to the JSONL scan. */ }
|
|
956
|
-
|
|
957
|
-
if (!existsSync(directory))
|
|
958
|
-
return undefined;
|
|
959
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
960
|
-
const path = join(directory, entry.name);
|
|
961
|
-
if (entry.isDirectory()) {
|
|
962
|
-
const found = visit(path);
|
|
963
|
-
if (found)
|
|
964
|
-
return found;
|
|
965
|
-
}
|
|
966
|
-
else if (entry.name.endsWith(".jsonl")) {
|
|
967
|
-
try {
|
|
968
|
-
const header = JSON.parse(readFileSync(path, "utf8").split("\n")[0] ?? "{}");
|
|
969
|
-
if (isObject(header) && header.id === sessionId)
|
|
970
|
-
return path;
|
|
971
|
-
}
|
|
972
|
-
catch { /* Ignore incomplete files. */ }
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
return undefined;
|
|
976
|
-
};
|
|
977
|
-
return visit(sessionDir);
|
|
975
|
+
return findSessionFile(sessionDir, sessionId);
|
|
978
976
|
}
|
|
979
977
|
function emptyAccounting(cost = 0) { return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost, models: [] }; }
|
|
980
978
|
function emptyMetrics(requiredWorkflowCallCount = 1) { return { parentUsageThroughCandidate: null, parentOutputTokensThroughCandidate: null, nonWorkflowToolSequenceBeforeCandidate: [], nonWorkflowToolCallCountBeforeCandidate: 0, workflowCallCountBeforeCandidate: 0, invalidWorkflowCallCount: 0, productionValidationErrorCodes: [], candidateCallIndices: [], staticCandidates: [], semanticCriteria: [], anyValidCandidate: false, requiredWorkflowCallCount, surplusWorkflowCallCount: 0 }; }
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extensible-workflows",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Deterministic multi-agent workflow orchestration for Pi",
|
|
5
5
|
"homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
|
|
6
|
-
"repository": {
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/vekexasia/pi-extensible-workflows.git"
|
|
9
|
+
},
|
|
7
10
|
"type": "module",
|
|
8
11
|
"keywords": [
|
|
9
12
|
"pi-package",
|
|
@@ -92,7 +92,11 @@ const results = await parallel("implementation", {
|
|
|
92
92
|
});
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
Registered extension functions receive `withWorktree` in their context, so they may create a shared scope internally.
|
|
95
|
+
Registered extension functions receive `withWorktree` in their context, so they may create a shared scope internally. They can compose other registered functions without importing their source:
|
|
96
|
+
```ts
|
|
97
|
+
const report = await context.invoke("reviewRepository", { focus: "security" });
|
|
98
|
+
```
|
|
99
|
+
Their public inputs and outputs must remain JSON; callbacks cannot cross the extension-function boundary.
|
|
96
100
|
|
|
97
101
|
## Rules
|
|
98
102
|
|
|
@@ -104,7 +108,7 @@ Registered extension functions receive `withWorktree` in their context, so they
|
|
|
104
108
|
- Preserve item metadata in workflow code between pipeline stages instead of requiring agents to echo it through `outputSchema`.
|
|
105
109
|
- Repeated work uses a JavaScript loop; each direct `agent(...)` call receives deterministic call-site and occurrence identity.
|
|
106
110
|
- Runs default to background; set tool-call `foreground: true` when asked to wait.
|
|
107
|
-
- Add `budget` only when the run needs aggregate
|
|
111
|
+
- Add `budget` only when the run needs aggregate limits. The only valid dimension names are exactly `tokens`, `costUsd`, `durationMs`, and `agentLaunches` (never `cost`, `duration`, `launches`, or other shorthand). Each dimension is `{ soft?: number, hard?: number }`; `soft` must be less than `hard`.
|
|
108
112
|
- A `budget_exhausted` run is resumable through `workflow_resume`. Omitted patch values stay unchanged, explicit `null` removes a limit, and any relaxation requires an exact human-approved proposal through `workflow_respond`.
|
|
109
113
|
- `parallel()` and `pipeline()` return keyed bare values. Await results before use.
|
|
110
114
|
- Interpolate results with `prompt("...{value}", { value })`; placeholders in plain strings stay literal.
|
package/src/agent-execution.ts
CHANGED
|
@@ -192,6 +192,42 @@ export async function createNativeAgentSession(input: SessionInput): Promise<Nat
|
|
|
192
192
|
}
|
|
193
193
|
function changedOption(options: Readonly<Record<string, JsonValue>>, baseline: Readonly<Record<string, JsonValue>>, key: string): boolean { return JSON.stringify(options[key]) !== JSON.stringify(baseline[key]); }
|
|
194
194
|
function validThinking(value: unknown): value is ThinkingLevel { return typeof value === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value); }
|
|
195
|
+
function jsonValue(value: unknown, seen = new Set<object>()): value is JsonValue {
|
|
196
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") return true;
|
|
197
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
198
|
+
if (typeof value !== "object" || seen.has(value)) return false;
|
|
199
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
|
|
200
|
+
const keys = Reflect.ownKeys(value);
|
|
201
|
+
if (keys.some((key) => typeof key !== "string")) return false;
|
|
202
|
+
seen.add(value);
|
|
203
|
+
const valid = (Array.isArray(value) ? Array.from(value) : keys.map((key) => (value as Record<string, unknown>)[key as string])).every((item) => jsonValue(item, seen));
|
|
204
|
+
seen.delete(value);
|
|
205
|
+
return valid;
|
|
206
|
+
}
|
|
207
|
+
function jsonObject(value: unknown): value is Record<string, JsonValue> { return jsonValue(value) && typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
208
|
+
interface ChildAgentToolParams {
|
|
209
|
+
prompt: string;
|
|
210
|
+
label: string;
|
|
211
|
+
tools?: string[];
|
|
212
|
+
model?: string;
|
|
213
|
+
thinking?: ThinkingLevel;
|
|
214
|
+
role?: string;
|
|
215
|
+
outputSchema?: JsonSchema;
|
|
216
|
+
retries?: number;
|
|
217
|
+
timeoutMs?: number | null;
|
|
218
|
+
[key: string]: unknown;
|
|
219
|
+
}
|
|
220
|
+
function isChildAgentToolParams(value: unknown): value is ChildAgentToolParams & Record<string, JsonValue> {
|
|
221
|
+
if (!jsonObject(value) || typeof value.prompt !== "string" || typeof value.label !== "string") return false;
|
|
222
|
+
if (value.tools !== undefined && (!Array.isArray(value.tools) || value.tools.some((tool) => typeof tool !== "string"))) return false;
|
|
223
|
+
if (value.model !== undefined && typeof value.model !== "string") return false;
|
|
224
|
+
if (value.thinking !== undefined && !validThinking(value.thinking)) return false;
|
|
225
|
+
if (value.role !== undefined && typeof value.role !== "string") return false;
|
|
226
|
+
if (value.outputSchema !== undefined && !jsonObject(value.outputSchema)) return false;
|
|
227
|
+
if (value.retries !== undefined && (typeof value.retries !== "number" || !Number.isInteger(value.retries) || value.retries < 0)) return false;
|
|
228
|
+
if (value.timeoutMs !== undefined && (value.timeoutMs !== null && (typeof value.timeoutMs !== "number" || !Number.isInteger(value.timeoutMs) || value.timeoutMs < 1))) return false;
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
195
231
|
function fallbackSetupContext(root: AgentExecutionRoot, options: AgentExecutionOptions, signal: AbortSignal): { run: Readonly<WorkflowRunContext>; identity: Readonly<AgentIdentity> } {
|
|
196
232
|
const identity = options.agentIdentity ?? { structuralPath: [], callSite: options.label, occurrence: 1 };
|
|
197
233
|
const run = root.runContext ?? Object.freeze({ cwd: root.cwd, sessionId: "", runId: "", workflow: Object.freeze({ name: options.workflowName }), args: null, signal });
|
|
@@ -640,17 +676,19 @@ export class FairAgentScheduler {
|
|
|
640
676
|
if (nodes.every(({ restored }) => restored)) run.logical = 0;
|
|
641
677
|
}
|
|
642
678
|
|
|
643
|
-
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument, @typescript-eslint/restrict-template-expressions */
|
|
644
679
|
toolsFor(parentId: string, resolveTools?: (role: string | undefined, tools: readonly string[] | undefined, model: string | undefined, inheritedTools: readonly string[], thinking: ThinkingLevel | undefined) => readonly string[]): ToolDefinition[] {
|
|
645
680
|
const parent = this.#node(parentId);
|
|
646
681
|
if (!parent.options.tools.includes("agent")) return [];
|
|
647
682
|
const agentTool = {
|
|
648
683
|
name: "agent", label: "Child Agent", description: "Start a direct child agent",
|
|
649
684
|
parameters: Type.Object({ prompt: Type.String(), label: Type.String(), tools: Type.Optional(Type.Array(Type.String())), model: Type.Optional(Type.String()), thinking: Type.Optional(Type.String()), role: Type.Optional(Type.String()), outputSchema: Type.Optional(Type.Unsafe<JsonSchema>({})), retries: Type.Optional(Type.Integer({ minimum: 0 })), timeoutMs: Type.Optional(Type.Union([Type.Integer({ minimum: 1 }), Type.Null()])) }, { additionalProperties: true }),
|
|
650
|
-
execute: async (_id: string,
|
|
685
|
+
execute: async (_id: string, rawParams: unknown) => {
|
|
686
|
+
if (!isChildAgentToolParams(rawParams)) throw new WorkflowError("INVALID_METADATA", "Invalid child agent parameters");
|
|
687
|
+
const params = rawParams;
|
|
651
688
|
if (params.role !== undefined && (params.model !== undefined || params.thinking !== undefined || params.tools !== undefined)) throw new WorkflowError("INVALID_METADATA", "Role agents must not specify model, thinking, or tools");
|
|
652
689
|
const tools = (params.tools !== undefined || params.role !== undefined ? resolveTools?.(params.role, params.tools, params.model, parent.options.tools, params.thinking) : undefined) ?? params.tools ?? parent.options.tools;
|
|
653
|
-
const agentOptions =
|
|
690
|
+
const agentOptions = { ...params };
|
|
691
|
+
Reflect.deleteProperty(agentOptions, "prompt");
|
|
654
692
|
const options: ScheduledAgentOptions = { label: params.label, requestedLabel: params.label, cwd: parent.options.cwd, tools, agentOptions, ...(params.model ? { model: params.model } : {}), ...(params.thinking ? { thinking: params.thinking } : {}), ...(params.role ? { role: params.role } : {}), ...(params.outputSchema ? { schema: params.outputSchema } : {}), ...(params.retries === undefined ? {} : { retries: params.retries }), ...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }) };
|
|
655
693
|
const child = this.spawn(parent.runId, params.prompt, options, parentId);
|
|
656
694
|
return { content: [{ type: "text" as const, text: JSON.stringify({ id: child.id }) }], details: { id: child.id } };
|
|
@@ -694,14 +732,14 @@ export class FairAgentScheduler {
|
|
|
694
732
|
|
|
695
733
|
#inherit(parent: ScheduledNode | undefined, options: ScheduledAgentOptions): Readonly<ScheduledAgentOptions> {
|
|
696
734
|
if (!options.label.trim() || !options.cwd || !Array.isArray(options.tools)) throw new WorkflowError("INVALID_METADATA", "Agents require label, cwd, and tools");
|
|
697
|
-
|
|
735
|
+
const inheritedTools: readonly string[] = options.tools;
|
|
736
|
+
if (!parent) return Object.freeze({ ...options, tools: Object.freeze([...inheritedTools]), ...(options.agentOptions ? { agentOptions: structuredClone(options.agentOptions) } : {}), ...(options.agentIdentity ? { agentIdentity: Object.freeze({ ...options.agentIdentity, structuralPath: Object.freeze([...options.agentIdentity.structuralPath]) }) } : {}) });
|
|
698
737
|
if (options.cwd !== parent.options.cwd) throw new WorkflowError("UNKNOWN_TOOL", "Child cwd cannot differ from its parent");
|
|
699
|
-
const forbidden =
|
|
738
|
+
const forbidden = inheritedTools.find((tool: string) => !parent.options.tools.includes(tool));
|
|
700
739
|
if (forbidden) throw new WorkflowError("UNKNOWN_TOOL", `Child tool escalates parent boundary: ${forbidden}`);
|
|
701
740
|
const identity = options.agentIdentity ?? parent.options.agentIdentity;
|
|
702
|
-
return Object.freeze({ ...options, cwd: parent.options.cwd, tools: Object.freeze([...
|
|
741
|
+
return Object.freeze({ ...options, cwd: parent.options.cwd, tools: Object.freeze([...inheritedTools]), ...(options.agentOptions ? { agentOptions: structuredClone(options.agentOptions) } : {}), ...(parent.options.parentBreadcrumb && !options.parentBreadcrumb ? { parentBreadcrumb: parent.options.parentBreadcrumb } : {}), ...(identity ? { agentIdentity: Object.freeze({ ...identity, structuralPath: Object.freeze([...identity.structuralPath]) }) } : {}), ...(parent.options.worktreeOwner ? { worktreeOwner: parent.options.worktreeOwner } : {}) });
|
|
703
742
|
}
|
|
704
|
-
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument, @typescript-eslint/restrict-template-expressions */
|
|
705
743
|
|
|
706
744
|
#enqueue(runId: string, node: ScheduledNode | undefined, start: () => void): void { this.#runs.get(runId)?.queue.push({ ...(node ? { node } : {}), start }); this.#dispatch(); }
|
|
707
745
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { existsSync, mkdirSync, mkdtempSync,
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
6
|
import { join, relative } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { CAPTURE_IDENTITY } from "./eval-capture-extension.js";
|
|
9
|
-
import {
|
|
9
|
+
import { isObject } from "./index.js";
|
|
10
|
+
import { extractCapturedWorkflows, extractParentOracleFile, findSessionFile } from "./workflow-evals.js";
|
|
10
11
|
|
|
11
12
|
export const AMBIENT_OPT_IN = "PI_WORKFLOW_EVAL_AMBIENT";
|
|
12
13
|
export const AMBIENT_CAPTURE_NOTE = "Ambient Tier D uses the explicit capture extension. Pi 0.80.6 orders CLI extensions before discovered extensions and the first tool registration wins, so ambient tools and skills remain available while workflow execution stays capture-only.";
|
|
@@ -327,26 +328,13 @@ function ambientAgentDir(environment: NodeJS.ProcessEnv): string {
|
|
|
327
328
|
return environment.PI_CODING_AGENT_DIR ?? process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
328
329
|
}
|
|
329
330
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
function findSessionFile(directory: string, sessionId: string): string | undefined {
|
|
333
|
-
if (!existsSync(directory)) return undefined;
|
|
334
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
335
|
-
const path = join(directory, entry.name);
|
|
336
|
-
if (entry.isDirectory()) { const found = findSessionFile(path, sessionId); if (found) return found; }
|
|
337
|
-
else if (entry.name.endsWith(".jsonl")) {
|
|
338
|
-
try { const header = JSON.parse(readFileSync(path, "utf8").split("\n")[0] ?? "{}") as unknown; if (isObject(header) && header.id === sessionId) return path; } catch { /* Ignore incomplete sessions. */ }
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return undefined;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function emptyResult(candidate: AmbientEvalCase, repository: AmbientFixtureRepository, worktree: AmbientCaseWorktree, environment: NodeJS.ProcessEnv, error: string): AmbientCaseResult {
|
|
331
|
+
type AmbientCaseResultDraft = Omit<AmbientCaseResult, "manifest"> & { manifest: Omit<AmbientManifest, "gitStatusAfter"> };
|
|
332
|
+
function emptyResult(candidate: AmbientEvalCase, repository: AmbientFixtureRepository, worktree: AmbientCaseWorktree, environment: NodeJS.ProcessEnv, error: string): AmbientCaseResultDraft {
|
|
345
333
|
const accounting = emptyAccounting();
|
|
346
334
|
return {
|
|
347
335
|
id: candidate.id, status: "failed", workflows: [], accounting, accountingTrustworthy: false, diagnostics: [], errors: [error],
|
|
348
336
|
manifest: {
|
|
349
|
-
invocationMode: AMBIENT_INVOCATION_MODE, fixtureRoot: repository.fixtureRoot, worktreePath: worktree.path, ambientAgentDir: ambientAgentDir(environment), fixtureFileList: worktree.fixtureFiles, gitStatusBefore: worktree.gitStatusBefore,
|
|
337
|
+
invocationMode: AMBIENT_INVOCATION_MODE, fixtureRoot: repository.fixtureRoot, worktreePath: worktree.path, ambientAgentDir: ambientAgentDir(environment), fixtureFileList: worktree.fixtureFiles, gitStatusBefore: worktree.gitStatusBefore, parentToolSequence: [], skillReads: [], workflowCalls: [], workflowCallCount: 0, tokenCost: accounting,
|
|
350
338
|
cleanup: { processExited: false, processGroupTerminated: false, worktreeRemoved: false, fixtureRepoRemoved: false, tempRootRemoved: false, captureIdentityVerified: false, realWorkflowAgentsLaunched: 0 },
|
|
351
339
|
},
|
|
352
340
|
};
|
|
@@ -356,12 +344,13 @@ async function runAmbientCase(repository: AmbientFixtureRepository, candidate: A
|
|
|
356
344
|
const worktree = createAmbientCaseWorktree(repository, candidate.id);
|
|
357
345
|
const sessionId = randomUUID();
|
|
358
346
|
const sessionDir = join(repository.root, "sessions", candidate.id);
|
|
359
|
-
let result:
|
|
360
|
-
let
|
|
347
|
+
let result: AmbientCaseResultDraft | undefined;
|
|
348
|
+
let failure: string | undefined;
|
|
349
|
+
let gitStatusAfter: readonly string[];
|
|
361
350
|
try {
|
|
362
351
|
const pi = await runAmbientPiProcess({ worktree: worktree.path, sessionDir, sessionId, prompt: candidate.prompt, provider, model, ...(thinking ? { thinking } : {}), ...(piCommand ? { piCommand } : {}), timeoutMs: candidate.timeoutMs, maxCost: candidate.maxCost, environment });
|
|
363
352
|
const sessionFile = findSessionFile(sessionDir, sessionId);
|
|
364
|
-
if (!sessionFile)
|
|
353
|
+
if (!sessionFile) failure = "Ambient parent session was not written.";
|
|
365
354
|
else {
|
|
366
355
|
const oracle = extractParentOracleFile(sessionFile);
|
|
367
356
|
const workflows = extractCapturedWorkflows(oracle);
|
|
@@ -371,24 +360,25 @@ async function runAmbientCase(repository: AmbientFixtureRepository, candidate: A
|
|
|
371
360
|
result = {
|
|
372
361
|
id: candidate.id, status, workflows, accounting: oracle.usage, accountingTrustworthy: !pi.timedOut && !pi.budgetExceeded && pi.exitCode === 0, diagnostics: [pi.stderr].filter(Boolean), errors,
|
|
373
362
|
manifest: {
|
|
374
|
-
invocationMode: AMBIENT_INVOCATION_MODE, fixtureRoot: repository.fixtureRoot, worktreePath: worktree.path, ambientAgentDir: ambientAgentDir(environment), fixtureFileList: worktree.fixtureFiles, gitStatusBefore: worktree.gitStatusBefore,
|
|
363
|
+
invocationMode: AMBIENT_INVOCATION_MODE, fixtureRoot: repository.fixtureRoot, worktreePath: worktree.path, ambientAgentDir: ambientAgentDir(environment), fixtureFileList: worktree.fixtureFiles, gitStatusBefore: worktree.gitStatusBefore, parentToolSequence: oracle.parentToolSequence, skillReads: oracle.skillReads, workflowCalls: workflows, workflowCallCount: oracle.workflowCallCount, tokenCost: oracle.usage,
|
|
375
364
|
cleanup: { processExited: pi.exitCode !== null, processGroupTerminated: pi.processGroupTerminated, worktreeRemoved: false, fixtureRepoRemoved: false, tempRootRemoved: false, captureIdentityVerified, realWorkflowAgentsLaunched: 0 },
|
|
376
365
|
},
|
|
377
366
|
};
|
|
378
367
|
}
|
|
379
368
|
} catch (error) {
|
|
380
|
-
|
|
369
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
381
370
|
} finally {
|
|
382
371
|
try { gitStatusAfter = gitStatus(worktree.path); } catch { gitStatusAfter = ["<unavailable>"]; }
|
|
383
372
|
const worktreeRemoved = removeAmbientCaseWorktree(repository, worktree);
|
|
384
|
-
if (!result) result = emptyResult(candidate, repository, worktree, environment, "Ambient case produced no result.");
|
|
373
|
+
if (!result) result = emptyResult(candidate, repository, worktree, environment, failure ?? "Ambient case produced no result.");
|
|
385
374
|
const cleanup = { ...result.manifest.cleanup, worktreeRemoved };
|
|
386
|
-
result = { ...result, manifest: { ...result.manifest,
|
|
387
|
-
writeFileSync(join(artifactsDir, `${candidate.id}.json`), `${JSON.stringify(result, null, 2)}\n`, { mode: 0o600 });
|
|
375
|
+
result = { ...result, manifest: { ...result.manifest, cleanup } };
|
|
388
376
|
}
|
|
389
|
-
return result;
|
|
390
|
-
}
|
|
391
377
|
|
|
378
|
+
const finalized: AmbientCaseResult = { ...result, manifest: { ...result.manifest, gitStatusAfter } };
|
|
379
|
+
writeFileSync(join(artifactsDir, `${candidate.id}.json`), `${JSON.stringify(finalized, null, 2)}\n`, { mode: 0o600 });
|
|
380
|
+
return finalized;
|
|
381
|
+
}
|
|
392
382
|
export function assertAmbientOptIn(environment: NodeJS.ProcessEnv = process.env): void {
|
|
393
383
|
if (environment[AMBIENT_OPT_IN] !== "1") throw new Error(`Ambient Tier D evals are opt-in. Set ${AMBIENT_OPT_IN}=1 to run them.`);
|
|
394
384
|
}
|
package/src/doctor.ts
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
DEFAULT_SETTINGS,
|
|
15
15
|
loadSettings,
|
|
16
|
-
parseModelReference,
|
|
17
16
|
resolveAgentResourcePolicy,
|
|
18
17
|
resolveModelReference,
|
|
19
18
|
parseRoleMarkdown,
|
|
@@ -188,9 +187,6 @@ function inspectRole(path: string, activeTools: ReadonlySet<string>, knownModels
|
|
|
188
187
|
return definition;
|
|
189
188
|
}
|
|
190
189
|
|
|
191
|
-
class DoctorModelSet extends Set<string> {
|
|
192
|
-
override has(value: string): boolean { parseModelReference(value); return true; }
|
|
193
|
-
}
|
|
194
190
|
function matchResourcePolicy(policy: AgentResourcePolicy, pi: DoctorPiState): AgentResourcePolicy {
|
|
195
191
|
const extensions = new Set((pi.extensions ?? []).map(canonical));
|
|
196
192
|
const skills = new Set(pi.skills ?? []);
|
|
@@ -262,12 +258,13 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
|
|
|
262
258
|
let valid = true;
|
|
263
259
|
try {
|
|
264
260
|
const checked = preflight(workflow.script, {
|
|
265
|
-
models: new
|
|
261
|
+
models: new Set(pi.knownModels),
|
|
266
262
|
tools: activeTools,
|
|
267
263
|
agentTypes: new Set(definitions.keys()),
|
|
268
264
|
modelAliases: aliases,
|
|
269
265
|
knownModels,
|
|
270
266
|
settingsPath,
|
|
267
|
+
skipModelAvailability: true,
|
|
271
268
|
}, [], { name, description: workflow.description });
|
|
272
269
|
for (const model of checked.referenced.models) if (!knownModels.has(model) || !availableModels.has(model)) diagnostics.push(diagnostic("warning", "MODEL_UNAVAILABLE", `Model is valid-shaped but unavailable: ${model}`, name));
|
|
273
270
|
} catch (error) {
|