pi-ultracode 0.1.2 → 0.3.0

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/src/prompts.ts CHANGED
@@ -13,18 +13,13 @@ export const ULTRACODE_TAGLINE = "max thinking + dynamic workflow orchestration"
13
13
  * The standing system-prompt block injected on every turn while ultracode is on.
14
14
  * Mirrors the "Ultracode" section of the Workflow tool contract.
15
15
  */
16
- export function ultracodeSystemBlock(opts: { budgetTotal: number | null }): string {
17
- const budgetLine =
18
- opts.budgetTotal != null
19
- ? `\n- Token budget for this session: ~${formatTokens(opts.budgetTotal)} output tokens. Treat it as a hard ceiling — scale workflow depth (fleet size, verification rounds) to fit it, and stop spawning agents as it runs out.`
20
- : "";
21
-
16
+ export function ultracodeSystemBlock(): string {
22
17
  return [
23
18
  "<ultracode>",
24
19
  "Ultracode is ON. This opt-in is standing: author and run a workflow for every substantive task by default.",
25
20
  "",
26
- "The goal is the most exhaustive, correct answer you can produce token cost is not a constraint.",
27
- "For multi-phase work (understand → design → implement → review), run several workflows in sequence one per phase so you stay in the loop between them, reading each result before deciding the next phase.",
21
+ "The goal is a bounded, exhaustive-enough, correct answer: choose explicit fan-out and round limits before launching work.",
22
+ "For multi-phase work (understand → design → implement → review), prefer one bounded workflow and inspect the result before deciding whether another workflow is justified. Do not run consecutive workflows by default; continue only when new evidence changes the plan.",
28
23
  "",
29
24
  "Lean toward orchestrating with the workflow tool and adversarially verifying your findings, unless the work is trivial or already verified. Solo (no workflow) only on conversational turns or trivial mechanical edits.",
30
25
  "",
@@ -32,12 +27,11 @@ export function ultracodeSystemBlock(opts: { budgetTotal: number | null }): stri
32
27
  "- Adversarial verify: spawn N independent skeptics per finding, each prompted to REFUTE it; kill the finding if a majority refute. Stops plausible-but-wrong findings from surviving.",
33
28
  "- Perspective-diverse verify: when a finding can fail in more than one way, give each verifier a distinct lens (correctness, security, performance, does-it-reproduce) instead of N identical refuters.",
34
29
  "- Multi-modal sweep: parallel agents each searching a different way (by-container, by-content, by-entity, by-time); each is blind to what the others surface.",
35
- "- Loop-until-dry: for unknown-size discovery (bugs, edge cases), keep spawning finders until K consecutive rounds return nothing new.",
30
+ "- Loop-until-dry: for unknown-size discovery (bugs, edge cases), run at most 2 rounds by default (3 only when the user explicitly asks for comprehensive coverage); stop sooner when no fresh evidence appears.",
36
31
  "- Completeness critic: a final agent that asks \"what's missing — a modality not run, a claim unverified, a source unread?\" What it finds becomes the next round of work.",
37
32
  "- No silent caps: if a workflow bounds coverage (top-N, no-retry, sampling), log() what was dropped.",
38
33
  "",
39
- "Scale to the task: \"find any bugs\" → a few finders, single-vote verify; \"thoroughly audit\" / \"be comprehensive\" → larger finder pool, 3–5 vote adversarial pass, a synthesis stage." +
40
- budgetLine,
34
+ "Scale to the task: \"find any bugs\" → a few finders, single-vote verify; \"thoroughly audit\" / \"be comprehensive\" → a larger but explicit finder pool, 3–5 vote adversarial pass, and a synthesis stage within maxAgents.",
41
35
  "</ultracode>",
42
36
  ].join("\n");
43
37
  }
@@ -49,35 +43,31 @@ export const ULTRACODE_ACTIVE_REMINDER =
49
43
  /** Tool description for the `workflow` tool. */
50
44
  export const WORKFLOW_TOOL_DESCRIPTION = [
51
45
  "Execute a deterministic JavaScript workflow that orchestrates multiple subagents to be comprehensive (decompose and cover in parallel), confident (independent perspectives and adversarial checks before committing), or to take on scale one context can't hold (migrations, audits, broad sweeps).",
52
- "`script` is required raw JavaScript. It must begin with `export const meta = { name, description }` (a pure literal) and must call agent() at least once.",
53
- "Available globals: agent(prompt, opts), parallel(thunks), pipeline(items, ...stages), phase(title), log(message), workflow(nameOrRef, args), args, cwd, budget.",
46
+ "Each invocation must provide workflow source via `script`, `scriptPath`, or `name`. Inline `script` must be raw JavaScript (no Markdown fences) beginning with `export const meta = { name, description }` (a pure literal) and should call agent() at least once for useful orchestration.",
47
+ "Available globals: agent(prompt, opts), parallel(thunks, options?), pipeline(items, ...stages), phase(title), log(message), workflow(nameOrRef, args), args, cwd. The tool accepts maxAgents (default 128, max 1024) as a lifetime live-agent admission cap across resumes, not a token budget; cache replay is free.",
54
48
  ].join(" ");
55
49
 
56
50
  /** One-line snippet for the Available tools section. */
57
51
  export const WORKFLOW_PROMPT_SNIPPET =
58
- "Run a deterministic JS workflow that fans out subagents. Header: export const meta = { name: 'snake_case', description: '...' }. Globals: agent/parallel/pipeline/phase/log/workflow/args/budget.";
52
+ "Run a deterministic JS workflow that fans out subagents. Header: export const meta = { name: 'snake_case', description: '...' }. Globals: agent/parallel/pipeline/phase/log/workflow/args/cwd.";
59
53
 
60
54
  /** Guideline bullets appended to the Guidelines section when the tool is active. */
61
55
  export const WORKFLOW_GUIDELINES: string[] = [
62
56
  "While Ultracode is enabled, use the workflow tool to decompose-and-cover in parallel, gather independent perspectives that adversarially verify each other, or take on scale one context can't hold.",
63
- "For the workflow tool, pass one raw JavaScript string in the required `script` parameter; no Markdown fences, no prose around the script.",
57
+ "For the workflow tool, provide workflow source with `script`, `scriptPath`, or `name`. Inline `script` must be one raw JavaScript string: no Markdown fences, no prose around the script.",
64
58
  "For the workflow tool, the script's first statement must be `export const meta = { name: 'short_snake_case', description: 'non-empty human description' }`. meta must be a pure literal: no variables, function calls, spreads, or template interpolation. meta.phases is optional and should mirror your phase() titles.",
65
- "For the workflow tool, write plain JavaScript after the meta export. No TypeScript syntax, imports, require(), fs, network, Date.now(), Math.random(), or new Date() (they break determinism and resume). Stamp timestamps after the workflow returns; vary randomness by agent index.",
66
- "For the workflow tool, every workflow must call agent() at least once. Each agent() call should pass a short unique label (2-5 words) so live status and error reporting stay readable.",
59
+ "For the workflow tool, write plain JavaScript after the meta export. No TypeScript syntax, imports, require(), fs, network, Date/Intl/Temporal, Math.random(), binary memory constructors, WebAssembly, or dynamic method calls such as value[key](...) (they bypass deterministic checks or resource bounds). Stamp timestamps after the workflow returns; vary randomness by agent index.",
60
+ "For the workflow tool, a useful workflow should call agent() at least once. Each agent() call should pass a short unique label (2-5 words) so live status and error reporting stay readable.",
67
61
  "For the workflow tool, DEFAULT TO pipeline(items, ...stages): items flow through stages independently with no barrier, so item A can be in stage 3 while item B is still in stage 1. Only use parallel() (a barrier that awaits all thunks) when a later stage genuinely needs ALL prior results together (dedup/merge across the full set, early-exit on zero, cross-item comparison).",
68
- "For the workflow tool, parallel() takes functions, not promises: `await parallel(items.map(item => () => agent('...', { label: '...' })))`, never `await parallel(items.map(item => agent(...)))`. Results are returned in input order; a thunk that throws resolves to null, so .filter(Boolean) before using results.",
62
+ "For the workflow tool, parallel() takes functions, not promises: `await parallel(items.map(item => () => agent('...', { label: '...' })))`, never `await parallel(items.map(item => agent(...)))`. It reserves `options.reserveAgents ?? thunks.length` agent slots before any thunk starts; reserveAgents must cover every agent call in the panel because calls beyond it are fatal. Results are returned in input order; a normal thunk throw resolves to null, while policy/cancellation failures are fatal.",
69
63
  "For the workflow tool, pipeline(items, ...stages) passes each stage (previousValue, originalItem, index). A stage that throws drops that item to null and skips its remaining stages.",
70
- "For the workflow tool, if agent() needs machine-readable output pass a plain JSON Schema via opts.schema; agent() then returns the validated object. Use JSON Schema, not TypeScript or TypeBox constructors.",
64
+ "For the workflow tool, if agent() needs machine-readable output pass an inline JSON Schema via opts.schema; agent() then returns the validated object. Use only the bounded subset: types, object/array structure, enum/const, anyOf/allOf, length/numeric constraints, and annotations. Unknown keywords, $ref variants, oneOf, format, pattern, and patternProperties are rejected. Keep schemas under 256 KiB / 64 levels and every agent/workflow output under 2 MiB. Use JSON Schema, not TypeScript or TypeBox constructors.",
71
65
  "For the workflow tool, when agent() is called WITHOUT a schema, its return value is the subagent's final assistant text (the last text the subagent produced). With a schema it returns the validated structured_output object. Prefer a schema for machine-readable results; use the text form only for prose summaries.",
72
66
  "For the workflow tool, agent opts also accept: model (override the subagent model by pattern), agentType (use a custom subagent role/system-prompt), isolation:'worktree' (run the agent in an isolated git worktree — use ONLY when agents mutate files in parallel and would conflict), and phase (assign the agent to a progress group explicitly inside parallel()/pipeline()).",
73
- "For the workflow tool, use budget for dynamic depth: `while (budget.total && budget.remaining() > 50000) { ... }`. Guard the loop on budget.total with no budget set, remaining() is Infinity and the loop runs to the agent cap.",
74
- "For the workflow tool, workflow(nameOrRef, args) runs a saved workflow (by name) or a scriptPath inline as a sub-step, sharing this run's concurrency cap, agent counter, and token budget. Nesting is one level only.",
75
- "For the workflow tool, failed agent()/parallel()/pipeline() branches return null and log the failure (unless the whole run is aborted). Check for nulls before synthesizing conclusions, and prefer a final synthesis/assertion agent that returns a compact JSON-serializable verdict.",
67
+ "For the workflow tool, workflow(nameOrRef, args) runs a saved workflow (by name) or a scriptPath inline as a sub-step, sharing this run's concurrency, maxAgents, active reservations, and agent counter. Nesting is one level only. Child subagent sessions explicitly exclude the workflow tool and cannot start an independent recursive workflow chain.",
68
+ "For workflow resumeFromRunId, reuse the exact same script and args. Resume is immutable, successful calls replay by stable structural call path, and maxAgents may only stay the same or increase; changed work must start a new run.",
69
+ "For the workflow tool, failed agent()/parallel()/pipeline() branches return null and log the failure (unless the whole run is aborted or a policy limit is hit). Check for nulls before synthesizing conclusions, and prefer a final synthesis/assertion agent that returns a compact JSON-serializable verdict.",
70
+ "For the workflow tool, directly await or return every orchestration promise (agent/parallel/pipeline/workflow). Native .then/.catch/.finally chains and Promise.all/allSettled/race/any are rejected; use parallel() or pipeline() so call identity stays deterministic. Unobserved, pending, or native same-scope concurrent orchestration is fatal.",
71
+ "Workflow helpers that call orchestration must be directly declared functions/function variables or static methods on a declared object/stored class instance. Do not alias them, assign them later, forward through this.otherMethod(), or call them from temporary/awaited factory receivers; rewrite those forms as a direct declared helper so resume identity remains stable.",
76
72
  "For the workflow tool, do not assume subagents share the parent's repository context; include enough task context and relevant file paths in each agent prompt.",
77
73
  ];
78
-
79
- function formatTokens(n: number): string {
80
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
81
- if (n >= 1_000) return `${Math.round(n / 1_000)}k`;
82
- return String(n);
83
- }
@@ -0,0 +1,318 @@
1
+ export const DEFAULT_MAX_AGENTS = 128;
2
+ export const ABSOLUTE_MAX_AGENTS = 1024;
3
+ export const MAX_ACTIVE_WORKFLOWS_PER_SESSION = 4;
4
+
5
+ export const WORKFLOW_POLICY_ERROR_CODE = "WORKFLOW_POLICY_ERROR";
6
+ export const WORKFLOW_ABORT_ERROR_CODE = "WORKFLOW_ABORTED";
7
+ export const WORKFLOW_STALL_ERROR_CODE = "WORKFLOW_STALLED";
8
+ export const WORKFLOW_CLEANUP_TIMEOUT_ERROR_CODE = "WORKFLOW_CLEANUP_TIMEOUT";
9
+
10
+ export class WorkflowPolicyError extends Error {
11
+ readonly code = WORKFLOW_POLICY_ERROR_CODE;
12
+
13
+ constructor(message: string) {
14
+ super(message);
15
+ this.name = "WorkflowPolicyError";
16
+ }
17
+ }
18
+
19
+ export class WorkflowAbortError extends Error {
20
+ readonly code = WORKFLOW_ABORT_ERROR_CODE;
21
+
22
+ constructor(message = "workflow aborted") {
23
+ super(message);
24
+ this.name = "WorkflowAbortError";
25
+ }
26
+ }
27
+
28
+ export class WorkflowStallError extends Error {
29
+ readonly code = WORKFLOW_STALL_ERROR_CODE;
30
+
31
+ constructor(message: string) {
32
+ super(message);
33
+ this.name = "WorkflowStallError";
34
+ }
35
+ }
36
+
37
+ export class WorkflowCleanupTimeoutError extends Error {
38
+ readonly code = WORKFLOW_CLEANUP_TIMEOUT_ERROR_CODE;
39
+
40
+ constructor(timeoutMs: number) {
41
+ super(`workflow cleanup did not drain within ${timeoutMs}ms`);
42
+ this.name = "WorkflowCleanupTimeoutError";
43
+ }
44
+ }
45
+
46
+ export function isWorkflowPolicyError(error: unknown): error is WorkflowPolicyError {
47
+ return !!error && typeof error === "object" && (error as any).code === WORKFLOW_POLICY_ERROR_CODE;
48
+ }
49
+
50
+ export function isWorkflowAbortError(error: unknown): error is WorkflowAbortError {
51
+ return !!error && typeof error === "object" && (error as any).code === WORKFLOW_ABORT_ERROR_CODE;
52
+ }
53
+
54
+ export function isWorkflowStallError(error: unknown): error is WorkflowStallError {
55
+ return !!error && typeof error === "object" && (error as any).code === WORKFLOW_STALL_ERROR_CODE;
56
+ }
57
+
58
+ export function isWorkflowCleanupTimeoutError(error: unknown): error is WorkflowCleanupTimeoutError {
59
+ return !!error && typeof error === "object" && (error as any).code === WORKFLOW_CLEANUP_TIMEOUT_ERROR_CODE;
60
+ }
61
+
62
+ export function normalizeMaxAgents(value: unknown, name = "maxAgents"): number {
63
+ if (value === undefined || value === null) return DEFAULT_MAX_AGENTS;
64
+ if (typeof value !== "number" || !Number.isInteger(value)) {
65
+ throw new WorkflowPolicyError(`workflow ${name} must be an integer between 1 and ${ABSOLUTE_MAX_AGENTS}`);
66
+ }
67
+ if (value < 1 || value > ABSOLUTE_MAX_AGENTS) {
68
+ throw new WorkflowPolicyError(`workflow ${name} must be between 1 and ${ABSOLUTE_MAX_AGENTS} (got ${value})`);
69
+ }
70
+ return value;
71
+ }
72
+
73
+ export function normalizeReservationSize(value: unknown, fallback: number): number {
74
+ if (value === undefined || value === null) return fallback;
75
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > ABSOLUTE_MAX_AGENTS) {
76
+ throw new WorkflowPolicyError(`parallel reserveAgents must be an integer between 0 and ${ABSOLUTE_MAX_AGENTS}`);
77
+ }
78
+ return value;
79
+ }
80
+
81
+ interface SlotEntry {
82
+ /**
83
+ * Return path for an unused slot. The first live reservation in this stack gets
84
+ * the slot back; if none remain, the slot returns to root capacity. This lets a
85
+ * nested panel transfer a sibling's branch slot without permanently stealing it
86
+ * from its immediate parent, while still preserving the parent's own upstream
87
+ * return path when that parent is released later.
88
+ */
89
+ returnStack: string[];
90
+ }
91
+
92
+ interface Reservation {
93
+ id: string;
94
+ slots: SlotEntry[];
95
+ contingentSlots: number;
96
+ contingentParentIds: string[];
97
+ released: boolean;
98
+ }
99
+
100
+ export interface PanelReservation {
101
+ panelReservationId: string;
102
+ branchReservationIds: string[];
103
+ }
104
+
105
+ export class AgentAdmission {
106
+ readonly maxAgents: number;
107
+ private rootFree: number;
108
+ private consumed = 0;
109
+ private nextReservation = 0;
110
+ private readonly reservations = new Map<string, Reservation>();
111
+
112
+ constructor(maxAgents: number, usedAgents = 0) {
113
+ this.maxAgents = normalizeMaxAgents(maxAgents);
114
+ if (!Number.isInteger(usedAgents) || usedAgents < 0 || usedAgents > this.maxAgents) {
115
+ throw new WorkflowPolicyError(
116
+ `workflow usedAgents must be an integer between 0 and maxAgents=${this.maxAgents}`,
117
+ );
118
+ }
119
+ this.consumed = usedAgents;
120
+ this.rootFree = this.maxAgents - usedAgents;
121
+ }
122
+
123
+ get usedAgents(): number {
124
+ return this.consumed;
125
+ }
126
+
127
+ get availableAgents(): number {
128
+ return this.rootFree;
129
+ }
130
+
131
+ reservePanel(requestedSlots: number, parentReservationIds: string[] = []): string {
132
+ const slots = normalizeReservationSize(requestedSlots, requestedSlots);
133
+ const parents = this.uniqueReservations(parentReservationIds);
134
+ const parentFree = parents.reduce((sum, reservation) => sum + reservation.slots.length, 0);
135
+ const totalFree = parentFree + this.rootFree;
136
+ if (totalFree < slots) {
137
+ throw new WorkflowPolicyError(
138
+ `parallel() needs ${slots} agent slot(s), but only ${totalFree} remain under maxAgents=${this.maxAgents}`,
139
+ );
140
+ }
141
+
142
+ const entries: SlotEntry[] = [];
143
+ let remaining = slots;
144
+ for (const parent of parents) {
145
+ while (remaining > 0 && parent.slots.length > 0) {
146
+ const slot = parent.slots.shift()!;
147
+ entries.push({ returnStack: [parent.id, ...slot.returnStack] });
148
+ remaining--;
149
+ }
150
+ if (remaining === 0) break;
151
+ }
152
+ if (remaining > 0) {
153
+ this.rootFree -= remaining;
154
+ for (let i = 0; i < remaining; i++) entries.push({ returnStack: [] });
155
+ }
156
+
157
+ return this.createReservation(entries).id;
158
+ }
159
+
160
+ reservePanelWithBranchNeeds(
161
+ requestedSlots: number,
162
+ branchNeedsSlot: boolean[],
163
+ parentReservationIds: string[] = [],
164
+ contingentSlots = 0,
165
+ ): PanelReservation {
166
+ const branchCount = branchNeedsSlot.length;
167
+ const requiredBranches = branchNeedsSlot.filter(Boolean).length;
168
+ const slots = normalizeReservationSize(requestedSlots, requestedSlots);
169
+ if (branchCount > ABSOLUTE_MAX_AGENTS) {
170
+ throw new WorkflowPolicyError(`parallel branch count must be at most ${ABSOLUTE_MAX_AGENTS}`);
171
+ }
172
+ if (slots < requiredBranches) {
173
+ throw new WorkflowPolicyError(
174
+ `parallel replay needs at least ${requiredBranches} live branch slot(s); got ${slots}`,
175
+ );
176
+ }
177
+ const contingent = normalizeReservationSize(contingentSlots, contingentSlots);
178
+ const panelReservationId = this.reservePanel(slots, parentReservationIds);
179
+ const panelReservation = this.requireReservation(panelReservationId);
180
+ panelReservation.contingentSlots = contingent;
181
+ panelReservation.contingentParentIds = [...parentReservationIds];
182
+ const branchReservationIds: string[] = [];
183
+ try {
184
+ for (const needsSlot of branchNeedsSlot) {
185
+ branchReservationIds.push(this.reservePanel(needsSlot ? 1 : 0, [panelReservationId]));
186
+ }
187
+ return { panelReservationId, branchReservationIds };
188
+ } catch (error) {
189
+ for (const id of branchReservationIds) this.releasePanel(id);
190
+ this.releasePanel(panelReservationId);
191
+ throw error;
192
+ }
193
+ }
194
+
195
+ reservePanelWithBranches(
196
+ requestedSlots: number,
197
+ branchCount: number,
198
+ parentReservationIds: string[] = [],
199
+ ): PanelReservation {
200
+ const slots = normalizeReservationSize(requestedSlots, requestedSlots);
201
+ if (!Number.isInteger(branchCount) || branchCount < 0 || branchCount > ABSOLUTE_MAX_AGENTS) {
202
+ throw new WorkflowPolicyError(`parallel branch count must be an integer between 0 and ${ABSOLUTE_MAX_AGENTS}`);
203
+ }
204
+ if (slots < branchCount) {
205
+ throw new WorkflowPolicyError(
206
+ `parallel reserveAgents must be at least the number of thunks (${branchCount}); got ${slots}`,
207
+ );
208
+ }
209
+
210
+ const panelReservationId = this.reservePanel(slots, parentReservationIds);
211
+ const branchReservationIds: string[] = [];
212
+ try {
213
+ for (let i = 0; i < branchCount; i++) {
214
+ branchReservationIds.push(this.reservePanel(1, [panelReservationId]));
215
+ }
216
+ return { panelReservationId, branchReservationIds };
217
+ } catch (error) {
218
+ for (const id of branchReservationIds) this.releasePanel(id);
219
+ this.releasePanel(panelReservationId);
220
+ throw error;
221
+ }
222
+ }
223
+
224
+ releasePanel(id: string): void {
225
+ const reservation = this.reservations.get(id);
226
+ if (!reservation || reservation.released) return;
227
+ reservation.released = true;
228
+ for (const slot of reservation.slots) this.returnSlot(slot);
229
+ reservation.slots = [];
230
+ this.reservations.delete(id);
231
+ }
232
+
233
+ consumeAgent(reservationIds: string[] = []): number {
234
+ const reservations = this.uniqueReservations(reservationIds);
235
+ for (const reservation of reservations) {
236
+ if (reservation.slots.length > 0) {
237
+ reservation.slots.shift();
238
+ this.consumed++;
239
+ return this.consumed;
240
+ }
241
+ }
242
+ for (const reservation of reservations) {
243
+ if (reservation.contingentSlots > 0 && this.acquireContingentSlot(reservation)) {
244
+ reservation.contingentSlots--;
245
+ this.consumed++;
246
+ return this.consumed;
247
+ }
248
+ }
249
+ if (reservationIds.length > 0) {
250
+ throw new WorkflowPolicyError(
251
+ "parallel() reservation has no agent slots left; increase reserveAgents for this panel or lower fan-out",
252
+ );
253
+ }
254
+ if (this.rootFree < 1) {
255
+ throw new WorkflowPolicyError(`workflow exceeded maxAgents=${this.maxAgents} (no agent slots remain)`);
256
+ }
257
+ this.rootFree--;
258
+ this.consumed++;
259
+ return this.consumed;
260
+ }
261
+
262
+ private createReservation(slots: SlotEntry[]): Reservation {
263
+ const id = `res_${++this.nextReservation}`;
264
+ const reservation: Reservation = {
265
+ id,
266
+ slots,
267
+ contingentSlots: 0,
268
+ contingentParentIds: [],
269
+ released: false,
270
+ };
271
+ this.reservations.set(id, reservation);
272
+ return reservation;
273
+ }
274
+
275
+ private uniqueReservations(ids: string[]): Reservation[] {
276
+ const out: Reservation[] = [];
277
+ const seen = new Set<string>();
278
+ for (const id of ids) {
279
+ if (!id || seen.has(id)) continue;
280
+ seen.add(id);
281
+ out.push(this.requireReservation(id));
282
+ }
283
+ return out;
284
+ }
285
+
286
+ private requireReservation(id: string): Reservation {
287
+ const reservation = this.reservations.get(id);
288
+ if (!reservation || reservation.released) {
289
+ throw new WorkflowPolicyError(`unknown or expired parallel reservation: ${id}`);
290
+ }
291
+ return reservation;
292
+ }
293
+
294
+ private acquireContingentSlot(reservation: Reservation): boolean {
295
+ for (const parent of this.uniqueReservations(reservation.contingentParentIds)) {
296
+ if (parent.slots.length === 0) continue;
297
+ parent.slots.shift();
298
+ return true;
299
+ }
300
+ if (this.rootFree < 1) return false;
301
+ this.rootFree--;
302
+ return true;
303
+ }
304
+
305
+ private returnSlot(slot: SlotEntry): void {
306
+ const [targetId, ...rest] = slot.returnStack;
307
+ if (!targetId) {
308
+ this.rootFree++;
309
+ return;
310
+ }
311
+ const target = this.reservations.get(targetId);
312
+ if (target && !target.released) {
313
+ target.slots.push({ returnStack: rest });
314
+ return;
315
+ }
316
+ this.returnSlot({ returnStack: rest });
317
+ }
318
+ }
@@ -60,6 +60,8 @@ export interface ModelRegistryLike {
60
60
  getAll?(): ModelLike[];
61
61
  getRegisteredProviderIds?(): readonly string[];
62
62
  getRegisteredProviderConfig?(provider: string): unknown;
63
+ /** Pi 0.81+ complete provider registration, distinct from legacy config providers. */
64
+ getRegisteredNativeProvider?(provider: string): any | undefined;
63
65
  getProviderAuthStatus?(provider: string): {
64
66
  configured: boolean;
65
67
  source?: string;
@@ -71,6 +73,8 @@ export interface ModelRegistryLike {
71
73
  export interface ModelRuntimeLike {
72
74
  getModel?(provider: string, modelId: string): ModelLike | undefined;
73
75
  registerProvider?(provider: string, config: any): void;
76
+ /** Pi 0.81+ complete provider registration. */
77
+ registerNativeProvider?(provider: any): void;
74
78
  refresh?(options?: { allowNetwork?: boolean }): Promise<unknown>;
75
79
  setRuntimeApiKey?(provider: string, apiKey: string): Promise<void>;
76
80
  }
@@ -306,6 +310,7 @@ export class WorkflowAgentRunner {
306
310
  sessionManager: SessionManager.inMemory(cwd),
307
311
  settingsManager: SettingsManager.create(cwd, agentDir),
308
312
  customTools,
313
+ excludeTools: ["workflow"],
309
314
  ...(model ? { model: model as any } : {}),
310
315
  ...(level ? { thinkingLevel: level as any } : {}),
311
316
  ...(toolAllowlist ? { tools: toolAllowlist } : {}),
@@ -519,6 +524,13 @@ export class WorkflowAgentRunner {
519
524
  const registeredProviderIds = this.modelRegistry?.getRegisteredProviderIds?.() ?? [];
520
525
  let providersChanged = false;
521
526
  for (const provider of registeredProviderIds) {
527
+ const nativeProvider = this.modelRegistry?.getRegisteredNativeProvider?.(provider);
528
+ if (nativeProvider !== undefined && runtime.registerNativeProvider) {
529
+ runtime.registerNativeProvider(nativeProvider);
530
+ providersChanged = true;
531
+ continue;
532
+ }
533
+
522
534
  const config = this.modelRegistry?.getRegisteredProviderConfig?.(provider);
523
535
  if (config === undefined || !runtime.registerProvider) continue;
524
536
  runtime.registerProvider(provider, config);
@@ -31,6 +31,8 @@ export interface WorkflowAgentUsageSnapshot {
31
31
 
32
32
  export interface WorkflowAgentSnapshot {
33
33
  id: number;
34
+ /** Stable orchestration identity; display order continues to use id. */
35
+ callPath?: string;
34
36
  label: string;
35
37
  phase?: string;
36
38
  workflowPath?: string[];
@@ -84,16 +86,19 @@ export interface WorkflowSnapshot {
84
86
  spentTokens: number;
85
87
  newTokens: number;
86
88
  replayedTokens: number;
89
+ /** Effective logical agent-slot cap for this run; usage-only, not a token budget. */
90
+ maxAgents?: number;
91
+ /** Lifetime live-agent admissions across resume generations. */
92
+ agentsUsed?: number;
87
93
  /** Sanitized session-scoped workflow detail manifest for headless consumers. */
88
94
  detailsManifestPath?: string;
89
- budgetTotal: number | null;
90
95
  durationMs?: number;
91
96
  result?: unknown;
92
97
  status: "running" | "completed" | "aborted" | "failed";
93
98
  }
94
99
 
95
100
  export interface RenderOptions {
96
- maxAgents?: number;
101
+ maxAgentRows?: number;
97
102
  maxLogs?: number;
98
103
  showResultPreviews?: boolean;
99
104
  /** @deprecated No-op retained for source compatibility. */
@@ -102,7 +107,7 @@ export interface RenderOptions {
102
107
  now?: number;
103
108
  }
104
109
 
105
- export function createSnapshot(meta: WorkflowMeta, runId: string, budgetTotal: number | null): WorkflowSnapshot {
110
+ export function createSnapshot(meta: WorkflowMeta, runId: string): WorkflowSnapshot {
106
111
  return {
107
112
  runId,
108
113
  name: safeDisplayText(meta.name, 120) || "workflow",
@@ -119,7 +124,6 @@ export function createSnapshot(meta: WorkflowMeta, runId: string, budgetTotal: n
119
124
  spentTokens: 0,
120
125
  newTokens: 0,
121
126
  replayedTokens: 0,
122
- budgetTotal,
123
127
  status: "running",
124
128
  };
125
129
  }
@@ -134,7 +138,7 @@ export function recompute(snapshot: WorkflowSnapshot): WorkflowSnapshot {
134
138
  }
135
139
 
136
140
  export function renderWorkflowLines(snapshot: WorkflowSnapshot, options: RenderOptions = {}): string[] {
137
- const maxAgents = options.maxAgents ?? 6;
141
+ const maxAgentRows = options.maxAgentRows ?? 6;
138
142
  const maxLogs = options.maxLogs ?? 2;
139
143
  const showResultPreviews = options.showResultPreviews ?? false;
140
144
  const now = options.now ?? Date.now();
@@ -145,9 +149,6 @@ export function renderWorkflowLines(snapshot: WorkflowSnapshot, options: RenderO
145
149
  : snapshot.spentTokens
146
150
  ? ` · ${formatTokens(snapshot.spentTokens)} output token`
147
151
  : "";
148
- const budget = snapshot.budgetTotal
149
- ? ` · ${formatTokens(snapshot.spentTokens)}/${formatTokens(snapshot.budgetTotal)} out`
150
- : "";
151
152
  const stateParts = [
152
153
  snapshot.runningCount > 0 ? `${snapshot.runningCount} running` : undefined,
153
154
  snapshot.errorCount > 0 ? `${snapshot.errorCount} errors` : undefined,
@@ -155,7 +156,10 @@ export function renderWorkflowLines(snapshot: WorkflowSnapshot, options: RenderO
155
156
  ].filter(Boolean);
156
157
  const state = stateParts.length ? `, ${stateParts.join(", ")}` : "";
157
158
  const cached = snapshot.cachedCount ? ` · ${snapshot.cachedCount} cached` : "";
158
- const header = `◆ ${statusMark(snapshot.status)} ${shorten(snapshot.name, 60)} (${snapshot.doneCount}/${snapshot.agentCount} done${state})${cached}${tokens}${budget}`;
159
+ const agentSlots = snapshot.maxAgents
160
+ ? ` · agents ${snapshot.agentsUsed ?? snapshot.agentCount}/${snapshot.maxAgents}`
161
+ : "";
162
+ const header = `◆ ${statusMark(snapshot.status)} ${shorten(snapshot.name, 60)} (${snapshot.doneCount}/${snapshot.agentCount} done${state})${cached}${agentSlots}${tokens}`;
159
163
  const lines = [header];
160
164
 
161
165
  const phaseNames = unique([
@@ -186,19 +190,19 @@ export function renderWorkflowLines(snapshot: WorkflowSnapshot, options: RenderO
186
190
  lines.push(
187
191
  ` ${marker} ${shorten(phase, 60)} ${done}/${agents.length}${running ? ` · ${running} running` : ""}${errors ? ` · ${errors} errors` : ""}${cancelled ? ` · ${cancelled} cancelled` : ""}`,
188
192
  );
189
- for (const agent of agents.slice(-maxAgents)) {
193
+ for (const agent of agents.slice(-maxAgentRows)) {
190
194
  lines.push(...renderAgentLines(agent, { showResultPreviews, now }));
191
195
  }
192
- if (agents.length > maxAgents) lines.push(` … ${agents.length - maxAgents} earlier agents`);
196
+ if (agents.length > maxAgentRows) lines.push(` … ${agents.length - maxAgentRows} earlier agents`);
193
197
  }
194
198
 
195
199
  const unphased = snapshot.agents.filter((a) => !rendered.has(a));
196
200
  if (unphased.length) {
197
201
  lines.push(" (unphased)");
198
- for (const agent of unphased.slice(-maxAgents)) {
202
+ for (const agent of unphased.slice(-maxAgentRows)) {
199
203
  lines.push(...renderAgentLines(agent, { showResultPreviews, now }));
200
204
  }
201
- if (unphased.length > maxAgents) lines.push(` … ${unphased.length - maxAgents} earlier agents`);
205
+ if (unphased.length > maxAgentRows) lines.push(` … ${unphased.length - maxAgentRows} earlier agents`);
202
206
  }
203
207
 
204
208
  for (const log of snapshot.logs.slice(-maxLogs)) lines.push(` log: ${shorten(log, 100)}`);
Binary file