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/README.md +47 -42
- package/examples/workflows/loop-until-dry-bugs.workflow.js +16 -6
- package/extensions/ultracode.ts +0 -1
- package/package.json +7 -7
- package/src/commands.ts +19 -23
- package/src/index.ts +1 -1
- package/src/mode.ts +6 -40
- package/src/prompts.ts +18 -28
- package/src/workflow/admission.ts +318 -0
- package/src/workflow/agent-runner.ts +12 -0
- package/src/workflow/display.ts +17 -13
- package/src/workflow/journal.ts +0 -0
- package/src/workflow/json-schema.ts +39 -17
- package/src/workflow/leases.ts +60 -0
- package/src/workflow/parser.ts +809 -5
- package/src/workflow/registry.ts +39 -22
- package/src/workflow/run-artifacts.ts +231 -0
- package/src/workflow/run-details.ts +106 -28
- package/src/workflow/runtime.ts +640 -303
- package/src/workflow/script-executor.ts +317 -0
- package/src/workflow/script-worker.mjs +678 -0
- package/src/workflow/structured-output.ts +2 -0
- package/src/workflow/tool.ts +207 -74
- package/src/workflow/value-limits.ts +363 -0
- package/src/workflow/worktree.ts +5 -3
- package/types/workflow.d.ts +60 -22
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(
|
|
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
|
|
27
|
-
"For multi-phase work (understand → design → implement → review),
|
|
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),
|
|
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`
|
|
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/
|
|
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,
|
|
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
|
|
66
|
-
"For the workflow tool,
|
|
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
|
|
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
|
|
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,
|
|
74
|
-
"For
|
|
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);
|
package/src/workflow/display.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(-
|
|
193
|
+
for (const agent of agents.slice(-maxAgentRows)) {
|
|
190
194
|
lines.push(...renderAgentLines(agent, { showResultPreviews, now }));
|
|
191
195
|
}
|
|
192
|
-
if (agents.length >
|
|
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(-
|
|
202
|
+
for (const agent of unphased.slice(-maxAgentRows)) {
|
|
199
203
|
lines.push(...renderAgentLines(agent, { showResultPreviews, now }));
|
|
200
204
|
}
|
|
201
|
-
if (unphased.length >
|
|
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)}`);
|
package/src/workflow/journal.ts
CHANGED
|
Binary file
|