pi-ultracode 0.9.0 → 0.10.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 +18 -17
- package/README.zh-CN.md +18 -17
- package/extensions/ultracode.ts +2 -12
- package/package.json +9 -10
- package/src/commands.ts +1 -1
- package/src/depth.ts +2 -2
- package/src/effort-policy.ts +1 -1
- package/src/jev.ts +97 -64
- package/src/mode.ts +3 -39
- package/src/prompts.ts +10 -11
- package/src/workflow/agent-runner.ts +48 -22
- package/src/workflow/runtime.ts +77 -29
- package/src/workflow/script-executor.ts +23 -9
- package/src/workflow/script-worker.mjs +43 -7
- package/src/workflow/tool.ts +10 -3
- package/types/workflow.d.ts +6 -0
package/src/prompts.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Prompt text for Ultracode's adaptive analysis-depth modes. */
|
|
2
2
|
|
|
3
|
-
import { ANALYSIS_DEPTHS, DEPTH_CRITERIA, DEPTH_SELECTION_RULES, type ActiveUltracodeMode
|
|
3
|
+
import { ANALYSIS_DEPTHS, DEPTH_CRITERIA, DEPTH_SELECTION_RULES, type ActiveUltracodeMode } from "./depth.ts";
|
|
4
4
|
import { WORKFLOW_EFFORT_GUIDELINES } from "./effort-policy.ts";
|
|
5
5
|
|
|
6
6
|
/** One-line description shown by `/ultracode status` and the footer. */
|
|
@@ -8,10 +8,10 @@ export const ULTRACODE_TAGLINE = "semantic-depth workflow orchestration";
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The standing system-prompt section body (Pi adds the XML wrapper). Injected
|
|
11
|
-
* on every active turn.
|
|
12
|
-
*
|
|
11
|
+
* on every active turn. In auto, the parent routes using its full context
|
|
12
|
+
* in its normal turn without a separate router agent.
|
|
13
13
|
*/
|
|
14
|
-
export function ultracodeSystemBlock(mode: ActiveUltracodeMode = "deep"
|
|
14
|
+
export function ultracodeSystemBlock(mode: ActiveUltracodeMode = "deep"): string {
|
|
15
15
|
return [
|
|
16
16
|
`Configured mode: ${mode}.`,
|
|
17
17
|
"Analysis depth is a semantic quality decision, never a wall-clock decision. Do not use elapsed time, deadlines, or duration limits to choose, lower, or stop analysis depth.",
|
|
@@ -22,7 +22,7 @@ export function ultracodeSystemBlock(mode: ActiveUltracodeMode = "deep", initial
|
|
|
22
22
|
"Depth applicability and required verification:",
|
|
23
23
|
...ANALYSIS_DEPTHS.map((depth) => `- ${depth}: ${DEPTH_CRITERIA[depth]}`),
|
|
24
24
|
"",
|
|
25
|
-
...modeInstructions(mode
|
|
25
|
+
...modeInstructions(mode),
|
|
26
26
|
"",
|
|
27
27
|
"Evidence-driven escalation and stopping:",
|
|
28
28
|
"- Escalate only for material correctness risk, missing direct evidence, conflicting findings, or an unresolved question that can change the answer.",
|
|
@@ -40,13 +40,11 @@ export function ultracodeSystemBlock(mode: ActiveUltracodeMode = "deep", initial
|
|
|
40
40
|
].join("\n");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function modeInstructions(mode: ActiveUltracodeMode
|
|
43
|
+
function modeInstructions(mode: ActiveUltracodeMode): string[] {
|
|
44
44
|
if (mode === "auto") {
|
|
45
45
|
return [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
: ["Before acting, silently route this task to focused, standard, or deep using the criteria above. Do not spawn a router agent or make an extra model request for classification."]),
|
|
49
|
-
"Begin at the shallowest sufficient depth and escalate only when evidence triggers an escalation condition below. Relevant conversation or repository evidence unavailable to the initial selector can justify immediate escalation.",
|
|
46
|
+
"Before acting, silently route this task to focused, standard, or deep using the criteria above and relevant conversation and repository context. Do not spawn a router agent or make an extra model request for classification.",
|
|
47
|
+
"Begin at the shallowest sufficient depth and escalate only when evidence triggers an escalation condition below.",
|
|
50
48
|
];
|
|
51
49
|
}
|
|
52
50
|
return [
|
|
@@ -80,13 +78,14 @@ export const WORKFLOW_PROMPT_SNIPPET =
|
|
|
80
78
|
|
|
81
79
|
/** Guideline bullets appended to the Guidelines section when the tool is active. */
|
|
82
80
|
export const WORKFLOW_GUIDELINES: string[] = [
|
|
83
|
-
"Use
|
|
81
|
+
"Work directly by default. Use workflow only for substantive independent subtasks, a specific claim needing independent verification, necessary isolation, or material context pressure—not merely multiple steps or files. Before invoking it, state the concrete benefit and use the smallest workflow that delivers it.",
|
|
84
82
|
"For an auto-depth workflow, log `analysis-depth: <level> — <reason>` before launching agents, `analysis-escalation: ...` when evidence requires more depth, and `analysis-stop: ...` when evidence is sufficient. Elapsed time must never determine depth.",
|
|
85
83
|
"For workflow verification, run skeptics only for high-risk, conflicting, low-confidence, or weakly evidenced claims. Do not automatically attach a skeptic to every branch.",
|
|
86
84
|
"For workflow synthesis, prefer structured results plus deterministic or parent-session merging. Start a synthesis/adjudication agent only for a material unresolved conflict, and normally give pure synthesis lower effort and no broad source-search mandate.",
|
|
87
85
|
...WORKFLOW_EFFORT_GUIDELINES,
|
|
88
86
|
"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.",
|
|
89
87
|
"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.",
|
|
88
|
+
"Provide real overall goals, acceptance checks, and known dependencies in the workflow description or optional literal meta fields when useful; never invent missing metadata. Jev uses existing meta/args and scheduler structure as shared background, not the raw script. Keep each agent prompt complete, including relevant upstream results: effort is judged for that task, not raised uniformly for the workflow.",
|
|
90
89
|
"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.",
|
|
91
90
|
"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.",
|
|
92
91
|
"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).",
|
|
@@ -14,6 +14,7 @@ import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
|
|
|
14
14
|
import {
|
|
15
15
|
createAgentSession,
|
|
16
16
|
createCodingTools,
|
|
17
|
+
createCodemodeExtension,
|
|
17
18
|
DefaultResourceLoader,
|
|
18
19
|
getAgentDir,
|
|
19
20
|
SessionManager,
|
|
@@ -43,7 +44,7 @@ import {
|
|
|
43
44
|
type ThinkingLevel,
|
|
44
45
|
} from "../thinking.ts";
|
|
45
46
|
import type { AgentTypeDef } from "./agent-types.ts";
|
|
46
|
-
import {
|
|
47
|
+
import { selectJevEfforts, type JevRuntime, type JevSelection } from "../jev.ts";
|
|
47
48
|
|
|
48
49
|
export type { ThinkingLevel } from "../thinking.ts";
|
|
49
50
|
|
|
@@ -77,12 +78,16 @@ export async function createWorkflowChildResourceLoader(
|
|
|
77
78
|
agentDir: options.agentDir,
|
|
78
79
|
settingsManager,
|
|
79
80
|
noExtensions: true,
|
|
81
|
+
extensionFactories: [createCodemodeExtension({ mode: "on", models: false })],
|
|
80
82
|
skillsOverride: ({ skills, diagnostics }) => ({
|
|
81
83
|
skills: skills.filter((skill) => !PARENT_ONLY_CHILD_SKILLS.has(skill.name)),
|
|
82
84
|
diagnostics,
|
|
83
85
|
}),
|
|
84
86
|
});
|
|
85
87
|
await loader.reload();
|
|
88
|
+
settingsManager.applyOverrides({
|
|
89
|
+
defaultTools: [...(settingsManager.getDefaultTools() ?? []), "+codemode"],
|
|
90
|
+
});
|
|
86
91
|
return loader;
|
|
87
92
|
}
|
|
88
93
|
|
|
@@ -97,7 +102,7 @@ export interface ModelLike {
|
|
|
97
102
|
thinkingLevelMap?: Partial<Record<ThinkingLevel, string | null>>;
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
export interface ModelRegistryLike {
|
|
105
|
+
export interface ModelRegistryLike extends Partial<JevRuntime> {
|
|
101
106
|
getAvailable(): ModelLike[];
|
|
102
107
|
/** Legacy public projection retained for consumers of this structural type. */
|
|
103
108
|
getAll?(): ModelLike[];
|
|
@@ -112,8 +117,8 @@ export interface ModelRegistryLike {
|
|
|
112
117
|
getApiKeyForProvider?(provider: string): Promise<string | undefined>;
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
/** Structural ModelRuntime seam
|
|
116
|
-
export interface ModelRuntimeLike {
|
|
120
|
+
/** Structural ModelRuntime seam for SDK hosts and test doubles. */
|
|
121
|
+
export interface ModelRuntimeLike extends Partial<JevRuntime> {
|
|
117
122
|
getModel?(provider: string, modelId: string): ModelLike | undefined;
|
|
118
123
|
registerProvider?(provider: string, config: any): void;
|
|
119
124
|
/** Pi 0.81+ complete provider registration. */
|
|
@@ -231,7 +236,7 @@ export interface WorkflowAgentRunnerOptions {
|
|
|
231
236
|
cwd: string;
|
|
232
237
|
/** Parent session's immutable project-trust decision. */
|
|
233
238
|
projectTrusted?: boolean;
|
|
234
|
-
/**
|
|
239
|
+
/** Host registry for model selection, state replay and native classification/auth. */
|
|
235
240
|
modelRegistry?: ModelRegistryLike;
|
|
236
241
|
/** Canonical runtime to share across child sessions when supplied by an SDK host. */
|
|
237
242
|
modelRuntime?: ModelRuntimeLike;
|
|
@@ -280,13 +285,30 @@ export interface AgentRunCall {
|
|
|
280
285
|
agentTypeDef?: AgentTypeDef;
|
|
281
286
|
/** Override cwd (worktree). */
|
|
282
287
|
cwd?: string;
|
|
288
|
+
/** Scheduling hooks supplied only to runners declaring supportsSchedulingHooks. */
|
|
289
|
+
selectEffort?: (task?: JevSelection) => Promise<ThinkingLevel | undefined>;
|
|
290
|
+
/** Await after selectEffort and before execution; repeated calls share one permit. */
|
|
291
|
+
acquireExecution?: () => Promise<void>;
|
|
283
292
|
/** Safe compact activity stream used by the inline workflow status. */
|
|
284
293
|
onActivity?: (event: AgentActivityInput) => void;
|
|
285
294
|
/** Private detailed stream used by the task transcript store. */
|
|
286
295
|
onTelemetry?: (event: AgentTelemetryEvent) => void;
|
|
287
296
|
}
|
|
288
297
|
|
|
289
|
-
export
|
|
298
|
+
export interface AgentRunner {
|
|
299
|
+
/**
|
|
300
|
+
* Opt in to preparation outside the execution limit. Before executing, run()
|
|
301
|
+
* must await selectEffort exactly once (undefined skips classification), then
|
|
302
|
+
* await acquireExecution. Forwarding wrappers must declare this capability
|
|
303
|
+
* and forward both hooks. A runner without it receives no scheduling hooks;
|
|
304
|
+
* the runtime admits its entire run() and excludes it from classification.
|
|
305
|
+
*/
|
|
306
|
+
readonly supportsSchedulingHooks?: boolean;
|
|
307
|
+
run(call: AgentRunCall): Promise<AgentRunResult>;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export class WorkflowAgentRunner implements AgentRunner {
|
|
311
|
+
readonly supportsSchedulingHooks = true;
|
|
290
312
|
private readonly baseCwd: string;
|
|
291
313
|
private readonly projectTrusted: boolean;
|
|
292
314
|
private readonly modelRegistry?: ModelRegistryLike;
|
|
@@ -457,22 +479,24 @@ export class WorkflowAgentRunner {
|
|
|
457
479
|
}
|
|
458
480
|
|
|
459
481
|
const prompt = this.buildPrompt(call, Boolean(call.schema));
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
482
|
+
const supportedEfforts = session.model ? session.getAvailableThinkingLevels() : [];
|
|
483
|
+
// Classifier-only runtime keys belong to the host, not the child runtime's
|
|
484
|
+
// chat-provider replay. An explicit SDK runtime remains authoritative.
|
|
485
|
+
const classifierRuntime = this.providedModelRuntime ?? this.modelRegistry ?? modelRuntime;
|
|
486
|
+
const selectionTask: JevSelection | undefined = session.model && supportedEfforts.length > 1
|
|
487
|
+
? { runtime: classifierRuntime, task: prompt, model: session.model, supportedEfforts }
|
|
488
|
+
: undefined;
|
|
489
|
+
const effort = call.selectEffort
|
|
490
|
+
? await call.selectEffort(selectionTask)
|
|
491
|
+
: selectionTask
|
|
492
|
+
? (await selectJevEfforts({ ...selectionTask, tasks: [selectionTask], signal: call.signal }))[0]
|
|
493
|
+
: undefined;
|
|
494
|
+
if (call.signal?.aborted) throw abortedError();
|
|
495
|
+
if (effort !== undefined) session.setThinkingLevel(effort, { persist: false });
|
|
496
|
+
// Idle session preparation and classification never hold an execution slot.
|
|
497
|
+
// The runtime retains the permit through result publication/worktree delivery.
|
|
498
|
+
await call.acquireExecution?.();
|
|
499
|
+
if (call.signal?.aborted) throw abortedError();
|
|
476
500
|
actualEffort = session.thinkingLevel;
|
|
477
501
|
safeEmitTelemetry(call.onTelemetry, {
|
|
478
502
|
kind: "model_resolved",
|
|
@@ -497,6 +521,8 @@ export class WorkflowAgentRunner {
|
|
|
497
521
|
});
|
|
498
522
|
}
|
|
499
523
|
|
|
524
|
+
// Observers can cancel synchronously; do not enter Pi's auth preflight afterward.
|
|
525
|
+
if (call.signal?.aborted) throw abortedError();
|
|
500
526
|
await session.prompt(prompt, {
|
|
501
527
|
// Pi invokes this after async input/before_agent_start preflight and
|
|
502
528
|
// immediately before _runAgentPrompt(). Throwing here closes the window
|
package/src/workflow/runtime.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import * as os from "node:os";
|
|
14
14
|
import * as path from "node:path";
|
|
15
|
+
import type { JsonObject } from "@earendil-works/pi-ai";
|
|
16
|
+
import { createJevBatch } from "../jev.ts";
|
|
15
17
|
import { parseWorkflowScript, type WorkflowMeta } from "./parser.ts";
|
|
16
18
|
import { executeWorkflowScript, type ScriptExecutorHost } from "./script-executor.ts";
|
|
17
19
|
import {
|
|
@@ -26,12 +28,13 @@ import {
|
|
|
26
28
|
type PanelReservation,
|
|
27
29
|
} from "./admission.ts";
|
|
28
30
|
// Static import: a dynamic import() of this module misbehaves under Pi's jiti
|
|
29
|
-
// loader ("WorkflowAgentRunner is not a constructor").
|
|
30
|
-
//
|
|
31
|
+
// loader ("WorkflowAgentRunner is not a constructor"). getRunner() constructs the
|
|
32
|
+
// default runner unless the host injects one.
|
|
31
33
|
import { resolveModelSelection, WorkflowAgentRunner } from "./agent-runner.ts";
|
|
32
34
|
import type {
|
|
33
35
|
AgentActivityInput,
|
|
34
36
|
AgentRunResult,
|
|
37
|
+
AgentRunner,
|
|
35
38
|
AgentTelemetryEvent,
|
|
36
39
|
AgentUsage,
|
|
37
40
|
ModelLike,
|
|
@@ -107,14 +110,14 @@ export interface WorkflowRunOptions {
|
|
|
107
110
|
hostCallLimit?: number;
|
|
108
111
|
/** Internal test seam for the script Worker's V8 old-generation heap cap. */
|
|
109
112
|
workerMemoryLimitMb?: number;
|
|
110
|
-
/**
|
|
113
|
+
/** Host registry used for model selection and native classification/auth. */
|
|
111
114
|
modelRegistry?: ModelRegistryLike;
|
|
112
115
|
/** Canonical model/auth runtime shared across child sessions. */
|
|
113
116
|
modelRuntime?: ModelRuntimeLike;
|
|
114
117
|
model?: ModelLike;
|
|
115
118
|
thinkingLevel?: ThinkingLevel;
|
|
116
|
-
/** Inject a runner
|
|
117
|
-
runner?:
|
|
119
|
+
/** Inject a runner; scheduling-aware wrappers must declare supportsSchedulingHooks. */
|
|
120
|
+
runner?: AgentRunner;
|
|
118
121
|
journal?: RunJournal;
|
|
119
122
|
/** Loads a saved workflow body by name; defaults to disk discovery. */
|
|
120
123
|
loadSavedWorkflow?: (name: string) => { meta: WorkflowMeta; body: string };
|
|
@@ -169,6 +172,17 @@ interface ActivePanelTrace {
|
|
|
169
172
|
calls: Array<{ callPath: string; status: "pending" | "success" | "failed" }>;
|
|
170
173
|
}
|
|
171
174
|
|
|
175
|
+
interface AgentPayload {
|
|
176
|
+
callPath?: unknown;
|
|
177
|
+
prompt: unknown;
|
|
178
|
+
options?: unknown;
|
|
179
|
+
assignedPhase?: string;
|
|
180
|
+
workflowPath?: string[];
|
|
181
|
+
workflowContext?: JsonObject;
|
|
182
|
+
scheduling?: JsonObject[];
|
|
183
|
+
reservationIds?: string[];
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
interface RuntimeState {
|
|
173
187
|
currentPhase?: string;
|
|
174
188
|
logs: string[];
|
|
@@ -208,7 +222,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
208
222
|
const started = Date.now();
|
|
209
223
|
const maxAgents = normalizeMaxAgents(options.maxAgents ?? options.journal?.effectiveMaxAgents);
|
|
210
224
|
const { meta, body } = parseWorkflowScript(rawScript);
|
|
211
|
-
const runtime = new Runtime(effectiveOptions, maxAgents);
|
|
225
|
+
const runtime = new Runtime(effectiveOptions, maxAgents, meta);
|
|
212
226
|
const onOuterAbort = () => runtime.abort();
|
|
213
227
|
options.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
214
228
|
if (options.signal?.aborted) runtime.abort();
|
|
@@ -261,9 +275,10 @@ class Runtime implements ScriptExecutorHost {
|
|
|
261
275
|
readonly state: RuntimeState;
|
|
262
276
|
private readonly options: WorkflowRunOptions;
|
|
263
277
|
private readonly cwd: string;
|
|
264
|
-
private runnerInstance:
|
|
278
|
+
private runnerInstance: AgentRunner | undefined;
|
|
265
279
|
private readonly agentTypes: Map<string, AgentTypeDef>;
|
|
266
|
-
private readonly
|
|
280
|
+
private readonly acquireExecution: () => Promise<() => void>;
|
|
281
|
+
private readonly workflowContext: JsonObject;
|
|
267
282
|
private readonly pending = new Set<Promise<unknown>>();
|
|
268
283
|
private readonly childController = new AbortController();
|
|
269
284
|
private readonly scriptController = new AbortController();
|
|
@@ -275,7 +290,8 @@ class Runtime implements ScriptExecutorHost {
|
|
|
275
290
|
private cleanupDeadline?: number;
|
|
276
291
|
policyError: WorkflowPolicyError | undefined;
|
|
277
292
|
|
|
278
|
-
constructor(options: WorkflowRunOptions, maxAgents: number) {
|
|
293
|
+
constructor(options: WorkflowRunOptions, maxAgents: number, meta: WorkflowMeta) {
|
|
294
|
+
this.workflowContext = JSON.parse(JSON.stringify({ meta, args: options.args }));
|
|
279
295
|
this.options = options;
|
|
280
296
|
this.cwd = options.cwd ?? process.cwd();
|
|
281
297
|
this.runnerInstance = options.runner;
|
|
@@ -294,7 +310,7 @@ class Runtime implements ScriptExecutorHost {
|
|
|
294
310
|
const cores = (globalThis as any).navigator?.hardwareConcurrency ?? os.cpus().length ?? 8;
|
|
295
311
|
const defaultConcurrency = Math.max(1, Math.min(Math.max(1, cores - 2), MAX_CONCURRENCY));
|
|
296
312
|
const concurrency = normalizeConcurrency(options.concurrency, defaultConcurrency);
|
|
297
|
-
this.
|
|
313
|
+
this.acquireExecution = createExecutionSemaphore(concurrency, this.childController.signal);
|
|
298
314
|
}
|
|
299
315
|
|
|
300
316
|
get agentsUsed(): number {
|
|
@@ -333,7 +349,7 @@ class Runtime implements ScriptExecutorHost {
|
|
|
333
349
|
}
|
|
334
350
|
|
|
335
351
|
/** Lazily construct the default in-memory runner (skipped when a runner is injected). */
|
|
336
|
-
private getRunner():
|
|
352
|
+
private getRunner(): AgentRunner {
|
|
337
353
|
if (!this.runnerInstance) {
|
|
338
354
|
this.runnerInstance = new WorkflowAgentRunner({
|
|
339
355
|
cwd: this.cwd,
|
|
@@ -353,6 +369,7 @@ class Runtime implements ScriptExecutorHost {
|
|
|
353
369
|
cwd: this.cwd,
|
|
354
370
|
args,
|
|
355
371
|
name,
|
|
372
|
+
meta: this.workflowContext.meta,
|
|
356
373
|
signal: this.scriptController.signal,
|
|
357
374
|
stallTimeoutMs: this.options.stallTimeoutMs,
|
|
358
375
|
hostCallLimit: this.options.hostCallLimit,
|
|
@@ -576,14 +593,17 @@ class Runtime implements ScriptExecutorHost {
|
|
|
576
593
|
}
|
|
577
594
|
}
|
|
578
595
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
596
|
+
agent(payload: AgentPayload): Promise<unknown> {
|
|
597
|
+
return this.agentBatch([payload])[0];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
agentBatch(payloads: AgentPayload[]): Promise<unknown>[] {
|
|
601
|
+
const members = createJevBatch(payloads.length, this.workflowContext, this.childController.signal);
|
|
602
|
+
return payloads.map((payload, index) =>
|
|
603
|
+
this.runAgent(payload, members[index]).finally(() => members[index].skip()));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private async runAgent(payload: AgentPayload, member: ReturnType<typeof createJevBatch>[number]): Promise<unknown> {
|
|
587
607
|
this.throwIfAborted();
|
|
588
608
|
const workerCallPath = requireString(payload.callPath, "agent callPath");
|
|
589
609
|
if (this.seenCallPaths.has(workerCallPath)) {
|
|
@@ -702,7 +722,8 @@ class Runtime implements ScriptExecutorHost {
|
|
|
702
722
|
}
|
|
703
723
|
|
|
704
724
|
this.beginPanelAgent(callPath);
|
|
705
|
-
const run =
|
|
725
|
+
const run = (async () => {
|
|
726
|
+
let releaseExecution: (() => void) | undefined;
|
|
706
727
|
this.notifyObserver(() => this.options.onAgentStart?.({
|
|
707
728
|
id,
|
|
708
729
|
callPath,
|
|
@@ -723,11 +744,22 @@ class Runtime implements ScriptExecutorHost {
|
|
|
723
744
|
let observedUsage: AgentUsage | undefined;
|
|
724
745
|
try {
|
|
725
746
|
this.throwIfAborted();
|
|
747
|
+
const runner = this.getRunner();
|
|
748
|
+
let executionPermit: Promise<() => void> | undefined;
|
|
749
|
+
const acquireExecution = async () => {
|
|
750
|
+
// Forwarding runners can re-enter while acquisition is pending or complete.
|
|
751
|
+
releaseExecution = await (executionPermit ??= this.acquireExecution());
|
|
752
|
+
this.throwIfAborted();
|
|
753
|
+
};
|
|
754
|
+
// Only declared scheduling-aware runners prepare/classify before admission.
|
|
755
|
+
if (!runner.supportsSchedulingHooks) {
|
|
756
|
+
member.skip();
|
|
757
|
+
await acquireExecution();
|
|
758
|
+
}
|
|
726
759
|
if (opts.isolation === "worktree") {
|
|
727
760
|
worktree = this.createIsolatedWorktree(id);
|
|
728
761
|
}
|
|
729
762
|
|
|
730
|
-
const runner = this.getRunner();
|
|
731
763
|
const onActivity: ((e: AgentActivityInput) => void) | undefined = this.options.onAgentActivity
|
|
732
764
|
? (e: AgentActivityInput) =>
|
|
733
765
|
this.options.onAgentActivity!({ id, callPath, label, phase: assignedPhase, workflowPath, ...e })
|
|
@@ -742,6 +774,25 @@ class Runtime implements ScriptExecutorHost {
|
|
|
742
774
|
label,
|
|
743
775
|
schema: opts.schema,
|
|
744
776
|
signal: this.childController.signal,
|
|
777
|
+
...(runner.supportsSchedulingHooks ? {
|
|
778
|
+
acquireExecution,
|
|
779
|
+
selectEffort: (task) => {
|
|
780
|
+
if (!task) {
|
|
781
|
+
member.skip();
|
|
782
|
+
return Promise.resolve(undefined);
|
|
783
|
+
}
|
|
784
|
+
return member.select({
|
|
785
|
+
...task,
|
|
786
|
+
context: {
|
|
787
|
+
callPath,
|
|
788
|
+
workflowPath,
|
|
789
|
+
scheduling: payload.scheduling ?? [],
|
|
790
|
+
...(assignedPhase ? { phase: assignedPhase } : {}),
|
|
791
|
+
...(payload.workflowContext ? { workflow: payload.workflowContext } : {}),
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
},
|
|
795
|
+
} : {}),
|
|
745
796
|
instructions: buildInstructions(assignedPhase, opts),
|
|
746
797
|
modelPattern: opts.model,
|
|
747
798
|
agentTypeDef,
|
|
@@ -858,6 +909,7 @@ class Runtime implements ScriptExecutorHost {
|
|
|
858
909
|
}));
|
|
859
910
|
return null;
|
|
860
911
|
} finally {
|
|
912
|
+
releaseExecution?.();
|
|
861
913
|
if (worktree && worktreeSettled && !keepWorktree) {
|
|
862
914
|
try {
|
|
863
915
|
removeWorktree(worktree);
|
|
@@ -866,7 +918,7 @@ class Runtime implements ScriptExecutorHost {
|
|
|
866
918
|
}
|
|
867
919
|
}
|
|
868
920
|
}
|
|
869
|
-
});
|
|
921
|
+
})();
|
|
870
922
|
this.track(run);
|
|
871
923
|
return await run;
|
|
872
924
|
}
|
|
@@ -1224,7 +1276,7 @@ async function raceWithCleanupTimeout(
|
|
|
1224
1276
|
}
|
|
1225
1277
|
}
|
|
1226
1278
|
|
|
1227
|
-
function
|
|
1279
|
+
function createExecutionSemaphore(limit: number, signal: AbortSignal): () => Promise<() => void> {
|
|
1228
1280
|
let active = 0;
|
|
1229
1281
|
const queue: Array<{ resolve: () => void; reject: (error: Error) => void }> = [];
|
|
1230
1282
|
|
|
@@ -1258,13 +1310,9 @@ function createLimiter(limit: number, signal: AbortSignal): <T>(fn: () => Promis
|
|
|
1258
1310
|
waiter.resolve();
|
|
1259
1311
|
};
|
|
1260
1312
|
|
|
1261
|
-
return async
|
|
1313
|
+
return async () => {
|
|
1262
1314
|
await acquire();
|
|
1263
|
-
|
|
1264
|
-
return await fn();
|
|
1265
|
-
} finally {
|
|
1266
|
-
release();
|
|
1267
|
-
}
|
|
1315
|
+
return release;
|
|
1268
1316
|
};
|
|
1269
1317
|
}
|
|
1270
1318
|
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
|
|
15
15
|
export interface ScriptExecutorHost {
|
|
16
16
|
agent(payload: any): Promise<unknown>;
|
|
17
|
+
/** Sealed ready calls; promises settle independently, without a result barrier. */
|
|
18
|
+
agentBatch?(payloads: any[]): Promise<unknown>[];
|
|
17
19
|
/** Cancel child work after a fatal script policy without terminating branch finally blocks. */
|
|
18
20
|
abortChildren?(error: Error): void;
|
|
19
21
|
reservePanel(payload: {
|
|
@@ -43,6 +45,7 @@ export interface ScriptExecutorOptions {
|
|
|
43
45
|
cwd: string;
|
|
44
46
|
args?: unknown;
|
|
45
47
|
name: string;
|
|
48
|
+
meta?: unknown;
|
|
46
49
|
signal: AbortSignal;
|
|
47
50
|
checkpointLimit?: number;
|
|
48
51
|
/** Internal test seam: how long the worker may go without a heartbeat. Not a public workflow option. */
|
|
@@ -70,6 +73,7 @@ export async function executeWorkflowScript(
|
|
|
70
73
|
args: options.args,
|
|
71
74
|
cwd: options.cwd,
|
|
72
75
|
name: options.name,
|
|
76
|
+
meta: options.meta,
|
|
73
77
|
checkpointLimit: options.checkpointLimit ?? DEFAULT_WORKFLOW_CHECKPOINT_LIMIT,
|
|
74
78
|
hostCallLimit: normalizeHostCallLimit(options.hostCallLimit),
|
|
75
79
|
},
|
|
@@ -185,14 +189,14 @@ export async function executeWorkflowScript(
|
|
|
185
189
|
}
|
|
186
190
|
return;
|
|
187
191
|
}
|
|
192
|
+
if (message.type === "agentBatch") {
|
|
193
|
+
const calls = message.calls as Array<{ payload: unknown }>;
|
|
194
|
+
const results = host.agentBatch?.(calls.map((call) => call.payload));
|
|
195
|
+
for (const [index, call] of calls.entries()) dispatchRpc(call, results?.[index]);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
188
198
|
if (message.type === "rpc") {
|
|
189
|
-
|
|
190
|
-
lastProgress = Date.now();
|
|
191
|
-
void handleRpc(message).finally(() => {
|
|
192
|
-
pendingRpcCount = Math.max(0, pendingRpcCount - 1);
|
|
193
|
-
lastProgress = Date.now();
|
|
194
|
-
maybeSettleFatal();
|
|
195
|
-
});
|
|
199
|
+
dispatchRpc(message);
|
|
196
200
|
return;
|
|
197
201
|
}
|
|
198
202
|
if (message.type === "result") {
|
|
@@ -238,13 +242,23 @@ export async function executeWorkflowScript(
|
|
|
238
242
|
}
|
|
239
243
|
});
|
|
240
244
|
|
|
241
|
-
|
|
245
|
+
function dispatchRpc(message: any, agentResult?: Promise<unknown>): void {
|
|
246
|
+
pendingRpcCount++;
|
|
247
|
+
lastProgress = Date.now();
|
|
248
|
+
void handleRpc(message, agentResult).finally(() => {
|
|
249
|
+
pendingRpcCount = Math.max(0, pendingRpcCount - 1);
|
|
250
|
+
lastProgress = Date.now();
|
|
251
|
+
maybeSettleFatal();
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function handleRpc(message: any, agentResult?: Promise<unknown>): Promise<void> {
|
|
242
256
|
const { id, op, payload } = message;
|
|
243
257
|
try {
|
|
244
258
|
let value: unknown;
|
|
245
259
|
switch (op) {
|
|
246
260
|
case "agent":
|
|
247
|
-
value = await host.agent(payload);
|
|
261
|
+
value = await (agentResult ?? host.agent(payload));
|
|
248
262
|
break;
|
|
249
263
|
case "reservePanel":
|
|
250
264
|
value = await host.reservePanel(payload);
|