pi-task-dispatcher 3.1.0 → 3.2.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.
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { spawn, type ChildProcess } from "node:child_process";
6
+ import { randomBytes } from "node:crypto";
6
7
  import * as fs from "node:fs";
7
8
  import * as os from "node:os";
8
9
  import * as path from "node:path";
@@ -214,6 +215,56 @@ interface GateBoundaryMessage {
214
215
  toolResults: number;
215
216
  }
216
217
 
218
+ type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
219
+
220
+ interface ToolNamespaceManifest {
221
+ name: string;
222
+ description?: string;
223
+ instructions?: string;
224
+ }
225
+
226
+ interface ToolAnnotationsManifest {
227
+ readOnlyHint?: boolean;
228
+ destructiveHint?: boolean;
229
+ idempotentHint?: boolean;
230
+ openWorldHint?: boolean;
231
+ }
232
+
233
+ /** A JSON-safe snapshot of one exact manager capability granted to a worker. */
234
+ interface DelegatedToolManifest {
235
+ name: string;
236
+ label: string;
237
+ description: string;
238
+ parameters: { [key: string]: JsonValue };
239
+ outputSchema?: JsonValue;
240
+ namespace?: ToolNamespaceManifest;
241
+ annotations?: ToolAnnotationsManifest;
242
+ promptGuidelines?: string[];
243
+ executionMode?: "sequential" | "parallel";
244
+ fingerprint: string;
245
+ }
246
+
247
+ /** The only model-facing fields that cross the private proxy IPC boundary. */
248
+ interface ProxyWireResult {
249
+ content: JsonValue[];
250
+ structuredContent?: JsonValue;
251
+ isError?: boolean;
252
+ terminate?: boolean;
253
+ }
254
+
255
+ interface ExecutorLease {
256
+ generation: number;
257
+ rootToolCallId: string;
258
+ ctx: ExtensionToolContext;
259
+ signal: AbortSignal | undefined;
260
+ }
261
+
262
+ interface ProxyRequestState {
263
+ lease: ExecutorLease;
264
+ controller: AbortController;
265
+ removeParentAbort: (() => void) | undefined;
266
+ }
267
+
217
268
  type EventUsage = Partial<UsageTotals> & { cost?: Partial<UsageTotals["cost"]> };
218
269
 
219
270
  const TEST_MODE = process.env.PI_TASK_DISPATCHER_TEST_MODE === "1";
@@ -226,10 +277,13 @@ const MODEL_TEXT_LIMIT = 5_000;
226
277
  const PRESENTATION_PREVIEW_ROWS = 5;
227
278
  const PRESENTATION_UPDATE_THROTTLE_MS = 100;
228
279
  const STDERR_LIMIT = 16_000;
280
+ const PROXY_PAYLOAD_LIMIT_BYTES = 16 * 1024 * 1024;
281
+ const PROXY_MANIFEST_LIMIT_BYTES = 8 * 1024 * 1024;
282
+ const PROXY_MAX_IN_FLIGHT_REQUESTS = 32;
283
+ const PROXY_MAX_REQUESTS_PER_JOB = 4_096;
284
+ const REVOKED_EXECUTOR_ROOT_LIMIT = 4_096;
229
285
  const THINKING_LEVELS = new Set<ThinkingLevel>(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
230
286
  const ROLES: Role[] = ["mechanic", "assistant", "engineer", "designer"];
231
- const MUTATING_TOOLS = new Set(["edit", "write", "bash", "powershell", "codemode"]);
232
- const ISOLATED_WORKER_TOOLS = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls", "codemode"]);
233
287
 
234
288
  const DEFAULT_CONFIG: DispatcherConfig = {
235
289
  mechanic: {},
@@ -283,9 +337,12 @@ const WorkloadSchema = Type.Object({
283
337
  expectedLongRunningSeconds: Type.Integer({ minimum: 0, maximum: 3_600 }),
284
338
  });
285
339
 
340
+ /** Tell managers when a codemode delegation grant is warranted without granting it automatically. */
341
+ const CODEMODE_SELECTION_GUIDANCE = "When codemode is active and delegated work materially benefits from JavaScript orchestration, batched/parallel/filtering of selected tools, or classifier/image models, include codemode in the delegated allowlist rather than reserving it for the manager; also include each direct tool the codemode script must call; otherwise omit it.";
342
+
286
343
  const DelegateParams = Type.Object({
287
344
  task: Type.String({ minLength: 1, maxLength: 12_000 }),
288
- tools: Type.Array(Type.String({ minLength: 1 }), { maxItems: 32 }),
345
+ tools: Type.Array(Type.String({ minLength: 1 }), { maxItems: 32, description: CODEMODE_SELECTION_GUIDANCE }),
289
346
  workload: WorkloadSchema,
290
347
  });
291
348
 
@@ -314,6 +371,81 @@ function isRecord(value: unknown): value is Record<string, unknown> {
314
371
  return typeof value === "object" && value !== null && !Array.isArray(value);
315
372
  }
316
373
 
374
+ /**
375
+ * Copy the JSON-visible surface of a schema or result without silently dropping unsupported values.
376
+ * TypeBox's own symbol markers are deliberately excluded: they are not model-facing JSON Schema.
377
+ */
378
+ function cloneJsonValue(value: unknown, label: string): JsonValue {
379
+ const seen = new WeakSet<object>();
380
+ const visit = (candidate: unknown, location: string): JsonValue => {
381
+ if (candidate === null || typeof candidate === "string" || typeof candidate === "boolean") return candidate;
382
+ if (typeof candidate === "number") {
383
+ if (!Number.isFinite(candidate) || Object.is(candidate, -0)) throw new Error(`${label} is not JSON-serializable at ${location}`);
384
+ return candidate;
385
+ }
386
+ if (Array.isArray(candidate)) {
387
+ if (seen.has(candidate)) throw new Error(`${label} contains a cycle at ${location}`);
388
+ seen.add(candidate);
389
+ const result = candidate.map((item, index) => visit(item, `${location}[${index}]`));
390
+ seen.delete(candidate);
391
+ return result;
392
+ }
393
+ if (!isRecord(candidate)) throw new Error(`${label} is not JSON-serializable at ${location}`);
394
+ if (seen.has(candidate)) throw new Error(`${label} contains a cycle at ${location}`);
395
+ const prototype = Object.getPrototypeOf(candidate);
396
+ if (prototype !== Object.prototype && prototype !== null) throw new Error(`${label} has an unsupported object at ${location}`);
397
+ const unsupportedSymbols = Object.getOwnPropertySymbols(candidate)
398
+ .filter((symbol) => Object.prototype.propertyIsEnumerable.call(candidate, symbol))
399
+ .filter((symbol) => !String(symbol).startsWith("Symbol(TypeBox."));
400
+ if (unsupportedSymbols.length > 0) throw new Error(`${label} has an unsupported symbol at ${location}`);
401
+ seen.add(candidate);
402
+ const result: { [key: string]: JsonValue } = {};
403
+ for (const key of Object.keys(candidate)) {
404
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
405
+ if (!descriptor || !("value" in descriptor)) throw new Error(`${label} has an accessor at ${location}.${key}`);
406
+ result[key] = visit(descriptor.value, `${location}.${key}`);
407
+ }
408
+ seen.delete(candidate);
409
+ return result;
410
+ };
411
+ return visit(value, "$" );
412
+ }
413
+
414
+ /** Produce a deterministic compatibility fingerprint without changing the model-facing field order. */
415
+ function stableJson(value: JsonValue): string {
416
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
417
+ if (Array.isArray(value)) return `[${value.map((item) => stableJson(item)).join(",")}]`;
418
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
419
+ }
420
+
421
+ /** Reject an otherwise JSON-safe proxy value before it can create an unbounded IPC message. */
422
+ function assertJsonByteLimit(value: JsonValue, label: string, maximum: number): void {
423
+ const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
424
+ if (bytes > maximum) throw new Error(`${label} exceeds the ${maximum}-byte proxy limit`);
425
+ }
426
+
427
+ /** Return a failure result that is safe to send over the proxy channel. */
428
+ function proxyErrorResult(message: string): ProxyWireResult {
429
+ return { content: [{ type: "text", text: message }], isError: true };
430
+ }
431
+
432
+ /** Strip parent-only details and usage before a nested manager result reaches the child session. */
433
+ function serializeProxyResult(value: unknown, forceError = false): ProxyWireResult {
434
+ if (!isRecord(value) || !Array.isArray(value.content)) return proxyErrorResult("Manager tool returned an invalid result.");
435
+ try {
436
+ const result: ProxyWireResult = { content: cloneJsonValue(value.content, "manager tool content") as JsonValue[] };
437
+ if (Object.prototype.hasOwnProperty.call(value, "structuredContent")) {
438
+ result.structuredContent = cloneJsonValue(value.structuredContent, "manager tool structured content");
439
+ }
440
+ if (value.isError === true || forceError) result.isError = true;
441
+ if (value.terminate === true) result.terminate = true;
442
+ assertJsonByteLimit(result as unknown as JsonValue, "manager tool result", PROXY_PAYLOAD_LIMIT_BYTES);
443
+ return result;
444
+ } catch (error) {
445
+ return proxyErrorResult(`Manager tool result could not be serialized: ${error instanceof Error ? error.message : String(error)}`);
446
+ }
447
+ }
448
+
317
449
  /** Create a fresh usage accumulator for one worker job. */
318
450
  function emptyUsage(): UsageTotals {
319
451
  return {
@@ -533,6 +665,132 @@ export default function codemodeOnlyExtension(pi: ExtensionAPI): void {
533
665
  `;
534
666
  }
535
667
 
668
+ /** Build a child-local proxy for exactly the manager tools granted to this job. */
669
+ function proxyExtensionSource(manifests: readonly DelegatedToolManifest[], nonce: string): string {
670
+ const tools = manifests
671
+ .filter((manifest) => manifest.name !== "codemode")
672
+ .map(({ fingerprint: _fingerprint, ...manifest }) => manifest);
673
+ const configuration = JSON.stringify({ nonce, tools });
674
+ return `/** Route selected worker tool calls through the parent manager's current ExtensionToolContext. */
675
+ import { Type } from "typebox";
676
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
677
+
678
+ type WireResult = { content: unknown[]; structuredContent?: unknown; isError?: boolean; terminate?: boolean };
679
+ type ProxyTool = {
680
+ name: string;
681
+ label: string;
682
+ description: string;
683
+ parameters: Record<string, unknown>;
684
+ outputSchema?: unknown;
685
+ namespace?: { name: string; description?: string; instructions?: string };
686
+ annotations?: { readOnlyHint?: boolean; destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean };
687
+ promptGuidelines?: string[];
688
+ executionMode?: "sequential" | "parallel";
689
+ };
690
+ const configuration = ${configuration} as { nonce: string; tools: ProxyTool[] };
691
+ const payloadLimitBytes = ${PROXY_PAYLOAD_LIMIT_BYTES};
692
+ const maximumInFlightRequests = ${PROXY_MAX_IN_FLIGHT_REQUESTS};
693
+ const textEncoder = new TextEncoder();
694
+ const pending = new Map<string, { resolve: (result: any) => void; onUpdate: ((result: any) => void) | undefined; signal: AbortSignal | undefined; onAbort: (() => void) | undefined }>();
695
+ let requestSequence = 0;
696
+
697
+ function failure(message: string): any {
698
+ return { content: [{ type: "text", text: message }], details: undefined, isError: true };
699
+ }
700
+
701
+ function resultFromWire(value: unknown): any {
702
+ if (!value || typeof value !== "object" || Array.isArray(value)) return failure("Dispatcher proxy received an invalid manager result.");
703
+ try {
704
+ const encoded = JSON.stringify(value);
705
+ if (encoded === undefined || textEncoder.encode(encoded).byteLength > payloadLimitBytes) return failure("Dispatcher proxy received an oversized manager result.");
706
+ } catch {
707
+ return failure("Dispatcher proxy received a non-serializable manager result.");
708
+ }
709
+ const wire = value as WireResult;
710
+ if (!Array.isArray(wire.content)) return failure("Dispatcher proxy received a manager result without content.");
711
+ const result: any = { content: wire.content, details: undefined };
712
+ if (Object.prototype.hasOwnProperty.call(wire, "structuredContent")) result.structuredContent = wire.structuredContent;
713
+ if (wire.isError === true) result.isError = true;
714
+ if (wire.terminate === true) result.terminate = true;
715
+ return result;
716
+ }
717
+
718
+ function send(message: Record<string, unknown>): void {
719
+ if (typeof process.send !== "function") throw new Error("Task dispatcher proxy IPC is unavailable.");
720
+ process.send(message);
721
+ }
722
+
723
+ function finish(requestId: string, result: any): void {
724
+ const request = pending.get(requestId);
725
+ if (!request) return;
726
+ pending.delete(requestId);
727
+ if (request.signal && request.onAbort) request.signal.removeEventListener("abort", request.onAbort);
728
+ request.resolve(result);
729
+ }
730
+
731
+ process.on("message", (incoming: unknown) => {
732
+ if (!incoming || typeof incoming !== "object" || Array.isArray(incoming)) return;
733
+ const message = incoming as Record<string, unknown>;
734
+ if (message.nonce !== configuration.nonce || typeof message.requestId !== "string") return;
735
+ if (message.type === "task_dispatcher_proxy_update") {
736
+ const request = pending.get(message.requestId);
737
+ if (!request) return;
738
+ try { request.onUpdate?.(resultFromWire(message.result)); } catch { /* A renderer must not affect proxy control flow. */ }
739
+ return;
740
+ }
741
+ if (message.type === "task_dispatcher_proxy_result") finish(message.requestId, resultFromWire(message.result));
742
+ });
743
+ process.on("disconnect", () => {
744
+ for (const requestId of [...pending.keys()]) finish(requestId, failure("Task dispatcher parent IPC disconnected."));
745
+ });
746
+
747
+ export default function taskDispatcherProxy(pi: ExtensionAPI): void {
748
+ for (const manifest of configuration.tools) {
749
+ pi.registerTool({
750
+ name: manifest.name,
751
+ label: manifest.label,
752
+ description: manifest.description,
753
+ parameters: Type.Unsafe(manifest.parameters) as any,
754
+ ...(manifest.outputSchema === undefined ? {} : { outputSchema: Type.Unsafe(manifest.outputSchema) as any }),
755
+ ...(manifest.namespace === undefined ? {} : { namespace: manifest.namespace }),
756
+ ...(manifest.annotations === undefined ? {} : { annotations: manifest.annotations }),
757
+ ...(manifest.promptGuidelines === undefined ? {} : { promptGuidelines: manifest.promptGuidelines }),
758
+ ...(manifest.executionMode === undefined ? {} : { executionMode: manifest.executionMode }),
759
+ exposure: "direct",
760
+ defaultActive: true,
761
+ async execute(_toolCallId, args, signal, onUpdate) {
762
+ if (signal?.aborted) return failure("Worker proxy call was already aborted.");
763
+ if (pending.size >= maximumInFlightRequests) return failure("Worker proxy has too many in-flight manager requests.");
764
+ let serializedArgs: unknown;
765
+ try {
766
+ const encoded = JSON.stringify(args);
767
+ if (encoded === undefined) throw new Error("arguments are not JSON-serializable");
768
+ if (textEncoder.encode(encoded).byteLength > payloadLimitBytes) throw new Error("arguments exceed the " + payloadLimitBytes + "-byte proxy limit");
769
+ serializedArgs = JSON.parse(encoded);
770
+ } catch (error) {
771
+ return failure("Worker proxy arguments could not be serialized: " + (error instanceof Error ? error.message : String(error)));
772
+ }
773
+ const requestId = "proxy-" + (++requestSequence);
774
+ return await new Promise<any>((resolve) => {
775
+ const onAbort = () => {
776
+ try { send({ type: "task_dispatcher_proxy_cancel", nonce: configuration.nonce, requestId }); } catch { /* Parent teardown wins. */ }
777
+ finish(requestId, failure("Worker proxy call was aborted."));
778
+ };
779
+ pending.set(requestId, { resolve, onUpdate, signal, onAbort });
780
+ signal?.addEventListener("abort", onAbort, { once: true });
781
+ try {
782
+ send({ type: "task_dispatcher_proxy_request", nonce: configuration.nonce, requestId, toolName: manifest.name, args: serializedArgs });
783
+ } catch (error) {
784
+ finish(requestId, failure("Task dispatcher proxy request failed: " + (error instanceof Error ? error.message : String(error))));
785
+ }
786
+ });
787
+ },
788
+ });
789
+ }
790
+ }
791
+ `;
792
+ }
793
+
536
794
  /** Build the worker-only gate extension that stops the agent loop after persisted tool results. */
537
795
  function gateSource(): string {
538
796
  return `/** Pause this worker after each persisted tool batch so only the parent dispatcher can resume it. */
@@ -557,6 +815,33 @@ function isTerminal(status: JobStatus): boolean {
557
815
  }
558
816
 
559
817
  let nextJobIndex = 1;
818
+ const revokedExecutorRoots = new Set<string>();
819
+ let revokedExecutorGuardSaturated = false;
820
+
821
+ /** Bound stale-lease tracking and fail every otherwise-unattributed nested call after saturation. */
822
+ function revokeExecutorRoot(rootToolCallId: string): void {
823
+ if (revokedExecutorGuardSaturated) return;
824
+ if (revokedExecutorRoots.size >= REVOKED_EXECUTOR_ROOT_LIMIT) {
825
+ revokedExecutorRoots.clear();
826
+ revokedExecutorGuardSaturated = true;
827
+ return;
828
+ }
829
+ revokedExecutorRoots.add(rootToolCallId);
830
+ }
831
+
832
+ /** Reject a nested manager call attributable to a revoked lease. */
833
+ function revokedExecutorDecision(parentToolCallId: string | undefined, toolName: string): { block: boolean; reason: string } | undefined {
834
+ if (!parentToolCallId) return undefined;
835
+ if (revokedExecutorGuardSaturated) {
836
+ return { block: true, reason: `Nested manager tool ${toolName} was rejected because stale-lease tracking reached its fail-closed limit.` };
837
+ }
838
+ for (const rootToolCallId of revokedExecutorRoots) {
839
+ if (parentToolCallId === rootToolCallId || parentToolCallId.startsWith(`${rootToolCallId}/`)) {
840
+ return { block: true, reason: `Nested manager tool ${toolName} was rejected because its executor lease is no longer active.` };
841
+ }
842
+ }
843
+ return undefined;
844
+ }
560
845
 
561
846
  /** Supervise one reusable in-memory Pi worker and expose only boundary-sized state. */
562
847
  class WorkerJob {
@@ -564,6 +849,7 @@ class WorkerJob {
564
849
  readonly role: Role;
565
850
  readonly task: string;
566
851
  readonly tools: string[];
852
+ readonly manifests: readonly DelegatedToolManifest[];
567
853
  readonly model: string;
568
854
  readonly thinking: ThinkingLevel;
569
855
  readonly cwd: string;
@@ -573,6 +859,10 @@ class WorkerJob {
573
859
  readonly usage = emptyUsage();
574
860
  readonly actions: WorkerAction[] = [];
575
861
  readonly context: ContextMetrics;
862
+ private readonly pi: ExtensionAPI;
863
+ private readonly manifestByName: ReadonlyMap<string, DelegatedToolManifest>;
864
+ private readonly callableIdentityByName: ReadonlyMap<string, unknown>;
865
+ private readonly proxyNonce = randomBytes(24).toString("hex");
576
866
 
577
867
  status: JobStatus = "starting";
578
868
  sequence = 0;
@@ -584,6 +874,7 @@ class WorkerJob {
584
874
 
585
875
  private child: ChildProcess | undefined;
586
876
  private temporaryDirectory: string | undefined;
877
+ private temporaryRemoval: Promise<void> | undefined;
587
878
  private stdoutBuffer = "";
588
879
  private stderr = "";
589
880
  private requestSequence = 0;
@@ -607,25 +898,35 @@ class WorkerJob {
607
898
  private progressUpdate: ReturnType<typeof setTimeout> | undefined;
608
899
  private pausedLease: ReturnType<typeof setTimeout> | undefined;
609
900
  private closePromise: Promise<void> | undefined;
901
+ private executorLease: ExecutorLease | undefined;
902
+ private executorLeaseSequence = 0;
903
+ private readonly proxyRequests = new Map<string, ProxyRequestState>();
904
+ private readonly seenProxyRequests = new Set<string>();
610
905
 
611
906
  constructor(
612
907
  role: Role,
613
908
  task: string,
614
- tools: string[],
909
+ manifests: DelegatedToolManifest[],
615
910
  config: ResolvedWorkerRoleConfig,
616
911
  cwd: string,
617
912
  policy: DispatcherPolicy,
618
913
  budget: CalculatedBudget,
914
+ pi: ExtensionAPI,
915
+ callableTools: ExtensionToolContext["tools"],
619
916
  ) {
620
917
  this.id = `${role}-${nextJobIndex++}`;
621
918
  this.role = role;
622
919
  this.task = task;
623
- this.tools = tools;
920
+ this.manifests = manifests.map((manifest) => structuredClone(manifest));
921
+ this.tools = this.manifests.map((manifest) => manifest.name);
922
+ this.manifestByName = new Map(this.manifests.map((manifest) => [manifest.name, manifest]));
923
+ this.callableIdentityByName = new Map(callableTools.map((tool) => [tool.name, tool]));
624
924
  this.model = config.model;
625
925
  this.thinking = config.thinking;
626
926
  this.cwd = cwd;
627
927
  this.policy = policy;
628
928
  this.budget = budget;
929
+ this.pi = pi;
629
930
  this.context = {
630
931
  contextWindow: budget.contextWindow,
631
932
  peakPercent: 0,
@@ -637,12 +938,14 @@ class WorkerJob {
637
938
  }
638
939
 
639
940
  /** Start the isolated child, initialize compaction policy, and execute the first supervised stage. */
640
- async start(signal: AbortSignal | undefined, onProgress: (snapshot: WorkerSnapshot) => void): Promise<WorkerSnapshot> {
941
+ async start(rootToolCallId: string, signal: AbortSignal | undefined, onProgress: (snapshot: WorkerSnapshot) => void, ctx: ExtensionToolContext): Promise<WorkerSnapshot> {
641
942
  this.temporaryDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-task-dispatcher-"));
642
943
  const rolePromptPath = path.join(this.temporaryDirectory, `${this.role}.md`);
944
+ const proxyPath = path.join(this.temporaryDirectory, "tool-proxies.ts");
643
945
  const gatePath = path.join(this.temporaryDirectory, "supervision-gate.ts");
644
946
  const codemodeOnlyPath = path.join(this.temporaryDirectory, "codemode-only.ts");
645
947
  await fs.promises.writeFile(rolePromptPath, ROLE_PROMPTS[this.role], { encoding: "utf8", mode: 0o600 });
948
+ await fs.promises.writeFile(proxyPath, proxyExtensionSource(this.manifests, this.proxyNonce), { encoding: "utf8", mode: 0o600 });
646
949
  await fs.promises.writeFile(gatePath, gateSource(), { encoding: "utf8", mode: 0o600 });
647
950
  if (this.tools.includes("codemode")) {
648
951
  await fs.promises.writeFile(codemodeOnlyPath, codemodeOnlySource(), { encoding: "utf8", mode: 0o600 });
@@ -656,6 +959,7 @@ class WorkerJob {
656
959
  "--thinking", this.thinking,
657
960
  ...(this.tools.length > 0 ? ["--tools", this.tools.join(",")] : ["--no-tools"]),
658
961
  "--append-system-prompt", rolePromptPath,
962
+ "--extension", proxyPath,
659
963
  ...(this.tools.includes("codemode") ? ["--extension", codemodeOnlyPath] : []),
660
964
  "--extension", gatePath,
661
965
  ];
@@ -673,23 +977,32 @@ class WorkerJob {
673
977
  } finally {
674
978
  this.commitActiveElapsed();
675
979
  }
676
- return await this.runStages(`Delegated ${this.role} task:\n${this.task}`, 1, signal, onProgress);
980
+ return await this.runStages(`Delegated ${this.role} task:\n${this.task}`, 1, rootToolCallId, signal, onProgress, ctx);
677
981
  }
678
982
 
679
983
  /** Continue or revise work for a bounded number of action batches. */
680
984
  async continue(
681
985
  instructions: string | undefined,
682
986
  actionBatches: number,
987
+ rootToolCallId: string,
683
988
  signal: AbortSignal | undefined,
684
989
  onProgress: (snapshot: WorkerSnapshot) => void,
990
+ ctx: ExtensionToolContext,
685
991
  ): Promise<WorkerSnapshot> {
686
992
  if (this.status !== "paused") throw new Error(`Job ${this.id} is ${this.status}, not paused.`);
993
+ try {
994
+ this.revalidateManifest(ctx);
995
+ } catch (error) {
996
+ const reason = `Delegated tool manifest revalidation failed: ${error instanceof Error ? error.message : String(error)}`;
997
+ await this.stop("hard_stopped", reason);
998
+ throw new Error(reason);
999
+ }
687
1000
  this.clearPausedLease();
688
1001
  const message = instructions
689
1002
  ? `The manager revised the delegated task with these controlling instructions:\n${instructions}\nContinue from completed work without repeating it.`
690
1003
  : "Continue the delegated task from the completed tool results. Do not repeat completed work.";
691
1004
  try {
692
- return await this.runStages(message, actionBatches, signal, onProgress);
1005
+ return await this.runStages(message, actionBatches, rootToolCallId, signal, onProgress, ctx);
693
1006
  } catch (error) {
694
1007
  if (this.status === "paused") this.schedulePausedLease();
695
1008
  throw error;
@@ -749,6 +1062,7 @@ class WorkerJob {
749
1062
  /** End the job from parent-observed state without asking the worker to cooperate. */
750
1063
  async stop(status: Extract<JobStatus, "checkpointed" | "taken_over" | "cancelled" | "hard_stopped">, reason: string): Promise<WorkerSnapshot> {
751
1064
  this.clearPausedLease();
1065
+ this.clearExecutorLease();
752
1066
  this.status = status;
753
1067
  this.phase = reason;
754
1068
  this.recommendation = status === "taken_over" ? "manager owns remaining work" : "closed";
@@ -807,6 +1121,7 @@ class WorkerJob {
807
1121
  async dispose(): Promise<void> {
808
1122
  if (this.closePromise) return await this.closePromise;
809
1123
  this.clearPausedLease();
1124
+ this.clearExecutorLease();
810
1125
  if (this.progressUpdate) clearTimeout(this.progressUpdate);
811
1126
  this.progressUpdate = undefined;
812
1127
  this.closePromise = (async () => {
@@ -817,7 +1132,13 @@ class WorkerJob {
817
1132
  new Promise<void>((resolve) => child.once("close", () => resolve())),
818
1133
  new Promise<void>((resolve) => setTimeout(resolve, 5_000)),
819
1134
  ]);
820
- if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
1135
+ if (child.exitCode === null && child.signalCode === null) {
1136
+ child.kill("SIGKILL");
1137
+ await Promise.race([
1138
+ new Promise<void>((resolve) => child.once("close", () => resolve())),
1139
+ new Promise<void>((resolve) => setTimeout(resolve, 1_000)),
1140
+ ]);
1141
+ }
821
1142
  }
822
1143
  await this.removeTemporaryDirectory();
823
1144
  })();
@@ -847,12 +1168,20 @@ class WorkerJob {
847
1168
  });
848
1169
  child.on("message", (message) => {
849
1170
  this.lastProgressAt = Date.now();
850
- if (!isRecord(message) || message.type !== "task_dispatcher_boundary") return;
851
- const boundary = message as unknown as GateBoundaryMessage;
852
- this.boundaryObserved = true;
853
- this.sequence++;
854
- this.phase = `completed tool batch at worker turn ${boundary.turnIndex}`;
855
- this.boundaryWaiter?.resolve();
1171
+ if (!isRecord(message)) return;
1172
+ if (message.type === "task_dispatcher_boundary") {
1173
+ const boundary = message as unknown as GateBoundaryMessage;
1174
+ this.boundaryObserved = true;
1175
+ this.sequence++;
1176
+ this.phase = `completed tool batch at worker turn ${boundary.turnIndex}`;
1177
+ this.boundaryWaiter?.resolve();
1178
+ return;
1179
+ }
1180
+ if (message.type === "task_dispatcher_proxy_request") {
1181
+ void this.handleProxyRequest(message);
1182
+ return;
1183
+ }
1184
+ if (message.type === "task_dispatcher_proxy_cancel") void this.handleProxyCancel(message);
856
1185
  });
857
1186
  child.on("error", (error) => {
858
1187
  this.diagnostic = error.message;
@@ -922,7 +1251,7 @@ class WorkerJob {
922
1251
  toolCallId: event.toolCallId,
923
1252
  toolName: event.toolName,
924
1253
  label: formatActivity(event.toolName, event.args),
925
- mutating: MUTATING_TOOLS.has(event.toolName),
1254
+ mutating: this.manifestByName.get(event.toolName)?.annotations?.readOnlyHint !== true,
926
1255
  status: "running",
927
1256
  startedAt: Date.now(),
928
1257
  parentToolCallId: event.parentToolCallId,
@@ -1075,10 +1404,14 @@ class WorkerJob {
1075
1404
  private async runStages(
1076
1405
  initialMessage: string,
1077
1406
  actionBatches: number,
1407
+ rootToolCallId: string,
1078
1408
  signal: AbortSignal | undefined,
1079
1409
  onProgress: (snapshot: WorkerSnapshot) => void,
1410
+ ctx: ExtensionToolContext,
1080
1411
  ): Promise<WorkerSnapshot> {
1081
- let message = initialMessage;
1412
+ const lease = this.openExecutorLease(rootToolCallId, ctx, signal);
1413
+ try {
1414
+ let message = initialMessage;
1082
1415
  for (let batch = 0; batch < actionBatches; batch++) {
1083
1416
  this.assertBudget();
1084
1417
  const actionBaseline = this.actionSequence;
@@ -1162,7 +1495,10 @@ class WorkerJob {
1162
1495
  this.schedulePausedLease();
1163
1496
  return this.snapshot();
1164
1497
  }
1165
- throw new Error("Unreachable supervised stage state.");
1498
+ throw new Error("Unreachable supervised stage state.");
1499
+ } finally {
1500
+ this.clearExecutorLease(lease);
1501
+ }
1166
1502
  }
1167
1503
 
1168
1504
  /** Wait for settlement while enforcing job deadline and no-progress policy externally. */
@@ -1226,6 +1562,191 @@ class WorkerJob {
1226
1562
  return "continue, revise, compact, checkpoint, cancel, or take over";
1227
1563
  }
1228
1564
 
1565
+ /** Rebuild the grant from this invocation's callable manager tools before a worker can resume. */
1566
+ private revalidateManifest(ctx: ExtensionToolContext): void {
1567
+ const current = buildDelegatedToolManifests(this.tools, this.pi, ctx);
1568
+ if (current.length !== this.manifests.length) throw new Error("the selected tool count changed");
1569
+ const currentIdentities = new Map(ctx.tools.map((tool) => [tool.name, tool]));
1570
+ for (let index = 0; index < current.length; index++) {
1571
+ const expected = this.manifests[index];
1572
+ const observed = current[index];
1573
+ if (expected.name !== observed.name || expected.fingerprint !== observed.fingerprint) {
1574
+ throw new Error(`selected tool ${expected.name} is no longer definition-compatible`);
1575
+ }
1576
+ if (expected.name !== "codemode" && this.callableIdentityByName.get(expected.name) !== currentIdentities.get(expected.name)) {
1577
+ throw new Error(`selected tool ${expected.name} no longer has the original callable identity`);
1578
+ }
1579
+ }
1580
+ }
1581
+
1582
+ /** Enforce selected capabilities during a lease and reject delayed calls from every revoked lease. */
1583
+ authorizeNestedToolCall(_toolCallId: string, parentToolCallId: string | undefined, toolName: string): { block: boolean; reason: string } | undefined {
1584
+ if (!parentToolCallId) return undefined;
1585
+ const lease = this.executorLease;
1586
+ if (lease && (parentToolCallId === lease.rootToolCallId || parentToolCallId.startsWith(`${lease.rootToolCallId}/`))) {
1587
+ if (toolName === "codemode" || !this.manifestByName.has(toolName)) {
1588
+ return { block: true, reason: `Nested manager tool ${toolName} is outside this delegation's selected capability grant.` };
1589
+ }
1590
+ return undefined;
1591
+ }
1592
+ return revokedExecutorDecision(parentToolCallId, toolName);
1593
+ }
1594
+
1595
+ /** Grant parent execution only while a start or continuation call actively owns the worker. */
1596
+ private openExecutorLease(rootToolCallId: string, ctx: ExtensionToolContext, signal: AbortSignal | undefined): ExecutorLease {
1597
+ if (this.executorLease) throw new Error("A manager executor lease is already active for this worker.");
1598
+ const lease: ExecutorLease = { generation: ++this.executorLeaseSequence, rootToolCallId, ctx, signal };
1599
+ this.executorLease = lease;
1600
+ return lease;
1601
+ }
1602
+
1603
+ /** Revoke parent execution before exposing an idle, paused, or terminal worker state. */
1604
+ private clearExecutorLease(expected?: ExecutorLease): void {
1605
+ if (expected && this.executorLease !== expected) return;
1606
+ const lease = this.executorLease;
1607
+ if (!lease) return;
1608
+ this.executorLease = undefined;
1609
+ revokeExecutorRoot(lease.rootToolCallId);
1610
+ for (const [requestId, request] of this.proxyRequests) {
1611
+ if (request.lease !== lease) continue;
1612
+ this.proxyRequests.delete(requestId);
1613
+ request.removeParentAbort?.();
1614
+ request.removeParentAbort = undefined;
1615
+ request.controller.abort();
1616
+ }
1617
+ }
1618
+
1619
+ /** Deliver a nonce-bound manager result or streaming update to the private child proxy. */
1620
+ private sendProxy(message: Record<string, unknown>): void {
1621
+ try {
1622
+ if (this.child?.connected) this.child.send(message);
1623
+ } catch {
1624
+ // Child teardown wins over a late nested-tool result.
1625
+ }
1626
+ }
1627
+
1628
+ /** Reject a malformed, stale, replayed, cross-job, or otherwise unauthorized proxy call. */
1629
+ private rejectProxyRequest(requestId: unknown, reason: string): void {
1630
+ if (typeof requestId !== "string" || requestId.length === 0 || requestId.length > 128) return;
1631
+ this.sendProxy({
1632
+ type: "task_dispatcher_proxy_result",
1633
+ nonce: this.proxyNonce,
1634
+ requestId,
1635
+ result: proxyErrorResult(reason),
1636
+ });
1637
+ }
1638
+
1639
+ /** Start one parent-side nested tool call while preserving manager hooks, validation, and cancellation. */
1640
+ private async handleProxyRequest(message: Record<string, unknown>): Promise<void> {
1641
+ const requestId = message.requestId;
1642
+ if (message.nonce !== this.proxyNonce) {
1643
+ this.rejectProxyRequest(requestId, "Worker proxy request was rejected because its job nonce is invalid.");
1644
+ return;
1645
+ }
1646
+ if (typeof requestId !== "string" || requestId.length === 0 || requestId.length > 128) return;
1647
+ if (this.seenProxyRequests.has(requestId)) {
1648
+ this.rejectProxyRequest(requestId, "Worker proxy request was rejected because its request id was replayed.");
1649
+ return;
1650
+ }
1651
+ if (this.seenProxyRequests.size >= PROXY_MAX_REQUESTS_PER_JOB) {
1652
+ this.rejectProxyRequest(requestId, "Worker proxy request was rejected because the per-job request limit was reached.");
1653
+ return;
1654
+ }
1655
+ this.seenProxyRequests.add(requestId);
1656
+ const lease = this.executorLease;
1657
+ if (!lease || this.status !== "running") {
1658
+ this.rejectProxyRequest(requestId, "Worker proxy request was rejected because no bounded manager executor lease is active.");
1659
+ return;
1660
+ }
1661
+ if (typeof message.toolName !== "string" || message.toolName === "codemode" || !this.manifestByName.has(message.toolName)) {
1662
+ this.rejectProxyRequest(requestId, "Worker proxy request was rejected because the requested tool is not in this job's selected capability grant.");
1663
+ return;
1664
+ }
1665
+ if (this.proxyRequests.size >= PROXY_MAX_IN_FLIGHT_REQUESTS) {
1666
+ this.rejectProxyRequest(requestId, "Worker proxy request was rejected because too many manager executions are in flight.");
1667
+ return;
1668
+ }
1669
+ try {
1670
+ this.revalidateManifest(lease.ctx);
1671
+ } catch (error) {
1672
+ const reason = `Delegated tool grant changed before ${message.toolName} execution: ${error instanceof Error ? error.message : String(error)}`;
1673
+ this.rejectProxyRequest(requestId, reason);
1674
+ void this.stop("hard_stopped", reason);
1675
+ return;
1676
+ }
1677
+ let args: JsonValue;
1678
+ try {
1679
+ args = cloneJsonValue(message.args, `worker proxy arguments for ${message.toolName}`);
1680
+ assertJsonByteLimit(args, `worker proxy arguments for ${message.toolName}`, PROXY_PAYLOAD_LIMIT_BYTES);
1681
+ } catch (error) {
1682
+ this.rejectProxyRequest(requestId, `Worker proxy request arguments are invalid: ${error instanceof Error ? error.message : String(error)}`);
1683
+ return;
1684
+ }
1685
+ const controller = new AbortController();
1686
+ const abortFromParent = () => controller.abort();
1687
+ if (lease.signal?.aborted) controller.abort();
1688
+ else lease.signal?.addEventListener("abort", abortFromParent, { once: true });
1689
+ const request: ProxyRequestState = {
1690
+ lease,
1691
+ controller,
1692
+ removeParentAbort: lease.signal ? () => lease.signal?.removeEventListener("abort", abortFromParent) : undefined,
1693
+ };
1694
+ this.proxyRequests.set(requestId, request);
1695
+ void (async () => {
1696
+ try {
1697
+ const outcome = await lease.ctx.executeTool(message.toolName as string, args, {
1698
+ signal: controller.signal,
1699
+ onUpdate: (partial) => {
1700
+ if (this.executorLease !== lease || this.proxyRequests.get(requestId) !== request) return;
1701
+ this.sendProxy({
1702
+ type: "task_dispatcher_proxy_update",
1703
+ nonce: this.proxyNonce,
1704
+ requestId,
1705
+ result: serializeProxyResult(partial),
1706
+ });
1707
+ },
1708
+ });
1709
+ if (this.executorLease === lease && this.proxyRequests.get(requestId) === request) {
1710
+ this.sendProxy({
1711
+ type: "task_dispatcher_proxy_result",
1712
+ nonce: this.proxyNonce,
1713
+ requestId,
1714
+ result: serializeProxyResult(outcome.result, outcome.isError),
1715
+ });
1716
+ }
1717
+ } catch (error) {
1718
+ if (this.executorLease === lease && this.proxyRequests.get(requestId) === request) {
1719
+ this.sendProxy({
1720
+ type: "task_dispatcher_proxy_result",
1721
+ nonce: this.proxyNonce,
1722
+ requestId,
1723
+ result: proxyErrorResult(`Manager execution of ${message.toolName as string} failed: ${error instanceof Error ? error.message : String(error)}`),
1724
+ });
1725
+ }
1726
+ } finally {
1727
+ request.removeParentAbort?.();
1728
+ request.removeParentAbort = undefined;
1729
+ if (this.proxyRequests.get(requestId) === request) this.proxyRequests.delete(requestId);
1730
+ }
1731
+ })();
1732
+ }
1733
+
1734
+ /** Propagate a child-side proxy cancellation to the current manager nested-tool signal. */
1735
+ private handleProxyCancel(message: Record<string, unknown>): void {
1736
+ const requestId = message.requestId;
1737
+ if (message.nonce !== this.proxyNonce) {
1738
+ this.rejectProxyRequest(requestId, "Worker proxy cancellation was rejected because its job nonce is invalid.");
1739
+ return;
1740
+ }
1741
+ if (typeof requestId !== "string" || requestId.length === 0 || requestId.length > 128) return;
1742
+ const request = this.proxyRequests.get(requestId);
1743
+ if (!request || request.lease !== this.executorLease || this.status !== "running") {
1744
+ this.rejectProxyRequest(requestId, "Worker proxy cancellation was rejected because the request is unknown or no longer active.");
1745
+ return;
1746
+ }
1747
+ request.controller.abort();
1748
+ }
1749
+
1229
1750
  /** Correlate one RPC request; an uncertain timeout closes the worker rather than permitting a racing command. */
1230
1751
  private async request(label: string, payload: Record<string, unknown>, timeoutMs: number): Promise<ParsedEvent> {
1231
1752
  const child = this.child;
@@ -1253,6 +1774,7 @@ class WorkerJob {
1253
1774
 
1254
1775
  /** Reject all consumers when the worker process can no longer produce state. */
1255
1776
  private failPending(error: Error): void {
1777
+ this.clearExecutorLease();
1256
1778
  for (const pending of this.pendingRequests.values()) pending.reject(error);
1257
1779
  this.pendingRequests.clear();
1258
1780
  this.settleWaiter?.reject(error);
@@ -1275,9 +1797,12 @@ class WorkerJob {
1275
1797
 
1276
1798
  /** Remove runtime-only worker instructions after process termination without touching workspace artifacts. */
1277
1799
  private async removeTemporaryDirectory(): Promise<void> {
1800
+ if (this.temporaryRemoval) return await this.temporaryRemoval;
1278
1801
  const directory = this.temporaryDirectory;
1279
1802
  this.temporaryDirectory = undefined;
1280
- if (directory) await fs.promises.rm(directory, { recursive: true, force: true });
1803
+ if (!directory) return;
1804
+ this.temporaryRemoval = fs.promises.rm(directory, { recursive: true, force: true });
1805
+ return await this.temporaryRemoval;
1281
1806
  }
1282
1807
 
1283
1808
  /** Coalesce token and partial-result bursts before rebuilding and serializing the complete snapshot. */
@@ -1350,20 +1875,123 @@ function resolveWorkerConfig(config: WorkerRoleConfig, ctx: ExtensionToolContext
1350
1875
  };
1351
1876
  }
1352
1877
 
1353
- /** Restrict delegations to active, non-recursive isolated-worker tools selected by the manager. */
1354
- function validateDelegatedTools(requested: string[], pi: ExtensionAPI): string[] {
1355
- const tools = [...new Set(requested.map((tool) => tool.trim()).filter(Boolean))];
1878
+ /** Build exact child manifests from manager-callable tools plus the explicitly active child-local codemode. */
1879
+ function buildDelegatedToolManifests(requested: string[], pi: ExtensionAPI, ctx: ExtensionToolContext): DelegatedToolManifest[] {
1880
+ const malformed = requested.filter((name) => name.trim() !== name || name.length === 0);
1881
+ if (malformed.length > 0) throw new Error(`Delegated tool names must be exact nonempty names: ${malformed.join(", ")}`);
1882
+ const duplicates = requested.filter((name, index) => requested.indexOf(name) !== index);
1883
+ if (duplicates.length > 0) throw new Error(`Delegated tool names must not be repeated: ${[...new Set(duplicates)].join(", ")}`);
1356
1884
  const forbidden = new Set([...Object.values(ROLE_TOOL_NAMES), "delegate_control", "delegate_status"]);
1357
- const recursive = tools.filter((tool) => forbidden.has(tool));
1358
- if (recursive.length > 0) throw new Error(`Delegation tools cannot be delegated: ${recursive.join(", ")}`);
1885
+ const recursive = requested.filter((name) => forbidden.has(name));
1886
+ if (recursive.length > 0) throw new Error(`Delegation tools cannot be delegated recursively: ${recursive.join(", ")}`);
1887
+ if (typeof ctx.executeTool !== "function") throw new Error("The current manager tool context does not expose ctx.executeTool().");
1888
+ const callable = new Map(ctx.tools.map((tool) => [tool.name, tool]));
1889
+ const metadata = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
1359
1890
  const active = new Set(pi.getActiveTools());
1360
- const unavailable = tools.filter((tool) => !active.has(tool));
1361
- if (unavailable.length > 0) throw new Error(`Delegated tools are not active for the manager: ${unavailable.join(", ")}`);
1362
- const unsupportedTools = tools.filter((tool) => !ISOLATED_WORKER_TOOLS.has(tool));
1363
- if (unsupportedTools.length > 0) {
1364
- throw new Error(`Isolated workers do not support these tools: ${unsupportedTools.join(", ")}`);
1891
+ const unavailable = requested.filter((name) => name === "codemode"
1892
+ ? !active.has(name) || !metadata.has(name)
1893
+ : !callable.has(name));
1894
+ if (unavailable.length > 0) {
1895
+ const detail = unavailable.map((name) => {
1896
+ if (name === "codemode" && metadata.has(name)) return `${name} (registered but not active in the manager)`;
1897
+ return metadata.has(name)
1898
+ ? `${name} (registered but not callable through ctx.executeTool())`
1899
+ : `${name} (not registered or callable through ctx.executeTool())`;
1900
+ });
1901
+ throw new Error(`Requested delegated tools are unavailable: ${detail.join(", ")}`);
1365
1902
  }
1366
- return tools;
1903
+ const manifests = requested.map((name) => {
1904
+ const localCodemode = name === "codemode";
1905
+ const tool = callable.get(name);
1906
+ const info = metadata.get(name);
1907
+ if (!info) throw new Error(`Delegated tool ${name} does not expose registered public metadata.`);
1908
+ const label = localCodemode ? "Codemode" : tool?.label;
1909
+ const description = localCodemode ? info.description : tool?.description;
1910
+ if (typeof label !== "string" || typeof description !== "string") {
1911
+ throw new Error(`Delegated tool ${name} does not expose serializable label and description metadata.`);
1912
+ }
1913
+ const parameters = cloneJsonValue(localCodemode ? info.parameters : tool?.parameters, `${name} parameter schema`);
1914
+ if (!isRecord(parameters)) throw new Error(`Delegated tool ${name} parameter schema must be a JSON object.`);
1915
+ const outputSchema = localCodemode || tool?.outputSchema === undefined
1916
+ ? undefined
1917
+ : cloneJsonValue(tool.outputSchema, `${name} output schema`);
1918
+ const rawSourceInfo = info.sourceInfo;
1919
+ if (!isRecord(rawSourceInfo)
1920
+ || typeof rawSourceInfo.path !== "string"
1921
+ || typeof rawSourceInfo.source !== "string"
1922
+ || !new Set(["user", "project", "temporary"]).has(rawSourceInfo.scope as string)
1923
+ || !new Set(["package", "top-level"]).has(rawSourceInfo.origin as string)
1924
+ || (rawSourceInfo.baseDir !== undefined && typeof rawSourceInfo.baseDir !== "string")) {
1925
+ throw new Error(`Delegated tool ${name} has unsupported source metadata.`);
1926
+ }
1927
+ const sourceInfo = cloneJsonValue({
1928
+ path: rawSourceInfo.path,
1929
+ source: rawSourceInfo.source,
1930
+ scope: rawSourceInfo.scope,
1931
+ origin: rawSourceInfo.origin,
1932
+ ...(rawSourceInfo.baseDir === undefined ? {} : { baseDir: rawSourceInfo.baseDir }),
1933
+ }, `${name} source metadata`);
1934
+ let namespace: ToolNamespaceManifest | undefined;
1935
+ if (info?.namespace !== undefined) {
1936
+ if (!isRecord(info.namespace) || typeof info.namespace.name !== "string" || info.namespace.name.length === 0) {
1937
+ throw new Error(`Callable tool ${name} has unsupported namespace metadata.`);
1938
+ }
1939
+ if (info.namespace.description !== undefined && typeof info.namespace.description !== "string") throw new Error(`Callable tool ${name} has unsupported namespace description metadata.`);
1940
+ if (info.namespace.instructions !== undefined && typeof info.namespace.instructions !== "string") throw new Error(`Callable tool ${name} has unsupported namespace instructions metadata.`);
1941
+ namespace = {
1942
+ name: info.namespace.name,
1943
+ ...(info.namespace.description === undefined ? {} : { description: info.namespace.description }),
1944
+ ...(info.namespace.instructions === undefined ? {} : { instructions: info.namespace.instructions }),
1945
+ };
1946
+ }
1947
+ let annotations: ToolAnnotationsManifest | undefined;
1948
+ if (info?.annotations !== undefined) {
1949
+ if (!isRecord(info.annotations)) throw new Error(`Callable tool ${name} has unsupported annotations metadata.`);
1950
+ annotations = {};
1951
+ for (const key of ["readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"] as const) {
1952
+ const value = info.annotations[key];
1953
+ if (value !== undefined && typeof value !== "boolean") throw new Error(`Callable tool ${name} has unsupported ${key} metadata.`);
1954
+ if (value !== undefined) annotations[key] = value;
1955
+ }
1956
+ }
1957
+ let promptGuidelines: string[] | undefined;
1958
+ if (info?.promptGuidelines !== undefined) {
1959
+ if (!Array.isArray(info.promptGuidelines) || info.promptGuidelines.some((guideline) => typeof guideline !== "string")) {
1960
+ throw new Error(`Callable tool ${name} has unsupported prompt guideline metadata.`);
1961
+ }
1962
+ promptGuidelines = [...info.promptGuidelines];
1963
+ }
1964
+ const executionMode = localCodemode ? "sequential" : tool?.executionMode;
1965
+ if (executionMode !== undefined && executionMode !== "sequential" && executionMode !== "parallel") {
1966
+ throw new Error(`Delegated tool ${name} has unsupported execution-mode metadata.`);
1967
+ }
1968
+ const comparable = cloneJsonValue({
1969
+ name,
1970
+ label,
1971
+ description,
1972
+ parameters,
1973
+ ...(outputSchema === undefined ? {} : { outputSchema }),
1974
+ ...(namespace === undefined ? {} : { namespace }),
1975
+ ...(annotations === undefined ? {} : { annotations }),
1976
+ ...(promptGuidelines === undefined ? {} : { promptGuidelines }),
1977
+ ...(executionMode === undefined ? {} : { executionMode }),
1978
+ sourceInfo,
1979
+ }, `${name} delegated manifest`);
1980
+ return {
1981
+ name,
1982
+ label,
1983
+ description,
1984
+ parameters,
1985
+ ...(outputSchema === undefined ? {} : { outputSchema }),
1986
+ ...(namespace === undefined ? {} : { namespace }),
1987
+ ...(annotations === undefined ? {} : { annotations }),
1988
+ ...(promptGuidelines === undefined ? {} : { promptGuidelines }),
1989
+ ...(executionMode === undefined ? {} : { executionMode }),
1990
+ fingerprint: stableJson(comparable),
1991
+ };
1992
+ });
1993
+ assertJsonByteLimit(manifests as unknown as JsonValue, "delegated tool manifest", PROXY_MANIFEST_LIMIT_BYTES);
1994
+ return manifests;
1367
1995
  }
1368
1996
 
1369
1997
  /** Format context pressure against the worker model's own physical context window. */
@@ -1644,6 +2272,7 @@ function registerDelegate(pi: ExtensionAPI, role: Role): void {
1644
2272
  ...(role === "assistant"
1645
2273
  ? ["When no other worker role applies, use delegate_assistant for the user-requested work instead of performing it directly. Retain delegation decisions, worker supervision, result integration, and the final response."]
1646
2274
  : []),
2275
+ CODEMODE_SELECTION_GUIDANCE,
1647
2276
  "Supply semantic workload units rather than raw turn or timeout values.",
1648
2277
  "Inspect every paused state and choose continue, revise, recalculate, compact, checkpoint, cancel, or take over.",
1649
2278
  "Worker reports remain evidence to review, not proof of correctness.",
@@ -1669,7 +2298,7 @@ function registerDelegate(pi: ExtensionAPI, role: Role): void {
1669
2298
  renderResult(result, { expanded, isPartial }, theme, context) {
1670
2299
  return renderWorkerResult(result as any, expanded, isPartial, theme, context);
1671
2300
  },
1672
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
2301
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
1673
2302
  try {
1674
2303
  if (activeJob && !isTerminal(activeJob.status)) throw new Error(`${activeJob.id} is still ${activeJob.status}.`);
1675
2304
  if (activeJob) {
@@ -1677,16 +2306,16 @@ function registerDelegate(pi: ExtensionAPI, role: Role): void {
1677
2306
  activeJob = undefined;
1678
2307
  }
1679
2308
  const config = loadConfig();
1680
- const tools = validateDelegatedTools(params.tools, pi);
2309
+ const manifests = buildDelegatedToolManifests(params.tools, pi, ctx);
1681
2310
  const resolved = resolveWorkerConfig(config[role], ctx);
1682
2311
  const budget = calculateBudget(role, params.workload, resolved.contextWindow, resolved.maxTokens, config.policy);
1683
- const job = new WorkerJob(role, params.task, tools, resolved.role, ctx.cwd, config.policy, budget);
2312
+ const job = new WorkerJob(role, params.task, manifests, resolved.role, ctx.cwd, config.policy, budget, pi, ctx.tools);
1684
2313
  activeJob = job;
1685
2314
  const progress = (snapshot: WorkerSnapshot) => {
1686
2315
  updateWorkerStatus(ctx, snapshot);
1687
2316
  onUpdate?.({ content: [{ type: "text", text: formatSnapshot(snapshot) }], details: snapshot });
1688
2317
  };
1689
- const snapshot = await job.start(signal, progress);
2318
+ const snapshot = await job.start(toolCallId, signal, progress, ctx);
1690
2319
  updateWorkerStatus(ctx, snapshot);
1691
2320
  return snapshotResult(snapshot, snapshot.status === "failed" || snapshot.status === "hard_stopped");
1692
2321
  } catch (error) {
@@ -1730,7 +2359,7 @@ function registerControl(pi: ExtensionAPI): void {
1730
2359
  renderResult(result, { expanded, isPartial }, theme, context) {
1731
2360
  return renderWorkerResult(result as any, expanded, isPartial, theme, context);
1732
2361
  },
1733
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
2362
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
1734
2363
  try {
1735
2364
  const job = activeJob;
1736
2365
  if (!job || job.id !== params.jobId) throw new Error(`No live delegation matches ${params.jobId}.`);
@@ -1753,7 +2382,7 @@ function registerControl(pi: ExtensionAPI): void {
1753
2382
  updateWorkerStatus(ctx, state);
1754
2383
  onUpdate?.({ content: [{ type: "text", text: formatSnapshot(state) }], details: state });
1755
2384
  };
1756
- snapshot = await job.continue(params.action === "revise" ? params.instructions : undefined, params.actionBatches ?? 1, signal, progress);
2385
+ snapshot = await job.continue(params.action === "revise" ? params.instructions : undefined, params.actionBatches ?? 1, toolCallId, signal, progress, ctx);
1757
2386
  }
1758
2387
  snapshot.managerAction = params.action;
1759
2388
  updateWorkerStatus(ctx, snapshot);
@@ -1803,8 +2432,13 @@ export default function taskDispatcher(pi: ExtensionAPI): void {
1803
2432
  for (const role of ROLES) registerDelegate(pi, role);
1804
2433
  registerControl(pi);
1805
2434
  registerStatus(pi);
2435
+ pi.on("tool_call", (event) => activeJob
2436
+ ? activeJob.authorizeNestedToolCall(event.toolCallId, event.parentToolCallId, event.toolName)
2437
+ : revokedExecutorDecision(event.parentToolCallId, event.toolName));
1806
2438
  pi.on("session_shutdown", async () => {
1807
2439
  if (activeJob) await activeJob.dispose();
1808
2440
  activeJob = undefined;
2441
+ revokedExecutorRoots.clear();
2442
+ revokedExecutorGuardSaturated = false;
1809
2443
  });
1810
2444
  }