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/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, type AnalysisDepth } from "./depth.ts";
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. Auto uses Jev when available, otherwise the parent
12
- * routes in its normal turn; neither path needs a separate router agent.
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", initialDepth?: AnalysisDepth): string {
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, initialDepth),
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, initialDepth?: AnalysisDepth): string[] {
43
+ function modeInstructions(mode: ActiveUltracodeMode): string[] {
44
44
  if (mode === "auto") {
45
45
  return [
46
- ...(initialDepth
47
- ? [`Initial analysis depth: ${initialDepth} (Jev). Start with its evidence requirements; this is not a fixed mode. Do not repeat initial classification or spawn a router agent.`]
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 the workflow tool only when independent decomposition, targeted verification, isolation, or context scale adds value; use the parent agent directly for a bounded task.",
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 { selectJevEffort } from "../jev.ts";
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 that remains loadable on pre-0.80.8 Pi. */
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
- /** Synchronous extension facade used only for model selection and state replay. */
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 class WorkflowAgentRunner {
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 jevApiKey = process.env.TYPESAFE_API_KEY?.trim();
461
- if (jevApiKey && session.model) {
462
- const supportedEfforts = session.getAvailableThinkingLevels();
463
- // A single supported level leaves no decision to make.
464
- if (supportedEfforts.length > 1) {
465
- const effort = await selectJevEffort({
466
- apiKey: jevApiKey,
467
- task: prompt,
468
- model: session.model,
469
- supportedEfforts,
470
- signal: call.signal,
471
- });
472
- if (call.signal?.aborted) throw abortedError();
473
- if (effort !== undefined) session.setThinkingLevel(effort, { persist: false });
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
@@ -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"). Tests inject a runner, so
30
- // they never construct this class; production builds it via getRunner().
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
- /** Synchronous extension facade used for model selection. */
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 (tests). */
117
- runner?: { run: WorkflowAgentRunner["run"] };
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: { run: WorkflowAgentRunner["run"] } | undefined;
278
+ private runnerInstance: AgentRunner | undefined;
265
279
  private readonly agentTypes: Map<string, AgentTypeDef>;
266
- private readonly limiter: <R>(fn: () => Promise<R>) => Promise<R>;
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.limiter = createLimiter(concurrency, this.childController.signal);
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(): { run: WorkflowAgentRunner["run"] } {
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
- async agent(payload: {
580
- callPath?: unknown;
581
- prompt: unknown;
582
- options?: unknown;
583
- assignedPhase?: string;
584
- workflowPath?: string[];
585
- reservationIds?: string[];
586
- }): Promise<unknown> {
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 = this.limiter(async () => {
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 createLimiter(limit: number, signal: AbortSignal): <T>(fn: () => Promise<T>) => Promise<T> {
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 <T>(fn: () => Promise<T>): Promise<T> => {
1313
+ return async () => {
1262
1314
  await acquire();
1263
- try {
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
- pendingRpcCount++;
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
- async function handleRpc(message: any): Promise<void> {
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);