pi-task-dispatcher 3.1.2 → 3.2.1

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";
@@ -103,7 +104,6 @@ interface WorkerAction {
103
104
 
104
105
  /** Keep one replaceable presentation record for each assistant response or child tool lifecycle. */
105
106
  interface WorkerPresentationEntry {
106
- sequence: number;
107
107
  id: string;
108
108
  kind: "assistant" | "tool";
109
109
  label: string;
@@ -214,6 +214,55 @@ interface GateBoundaryMessage {
214
214
  toolResults: number;
215
215
  }
216
216
 
217
+ type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
218
+
219
+ interface ToolNamespaceManifest {
220
+ name: string;
221
+ description?: string;
222
+ instructions?: string;
223
+ }
224
+
225
+ interface ToolAnnotationsManifest {
226
+ readOnlyHint?: boolean;
227
+ destructiveHint?: boolean;
228
+ idempotentHint?: boolean;
229
+ openWorldHint?: boolean;
230
+ }
231
+
232
+ /** A JSON-safe snapshot of one exact manager capability granted to a worker. */
233
+ interface DelegatedToolManifest {
234
+ name: string;
235
+ label: string;
236
+ description: string;
237
+ parameters: { [key: string]: JsonValue };
238
+ outputSchema?: JsonValue;
239
+ namespace?: ToolNamespaceManifest;
240
+ annotations?: ToolAnnotationsManifest;
241
+ promptGuidelines?: string[];
242
+ executionMode?: "sequential" | "parallel";
243
+ fingerprint: string;
244
+ }
245
+
246
+ /** The only model-facing fields that cross the private proxy IPC boundary. */
247
+ interface ProxyWireResult {
248
+ content: JsonValue[];
249
+ structuredContent?: JsonValue;
250
+ isError?: boolean;
251
+ terminate?: boolean;
252
+ }
253
+
254
+ interface ExecutorLease {
255
+ rootToolCallId: string;
256
+ ctx: ExtensionToolContext;
257
+ signal: AbortSignal | undefined;
258
+ }
259
+
260
+ interface ProxyRequestState {
261
+ lease: ExecutorLease;
262
+ controller: AbortController;
263
+ removeParentAbort: (() => void) | undefined;
264
+ }
265
+
217
266
  type EventUsage = Partial<UsageTotals> & { cost?: Partial<UsageTotals["cost"]> };
218
267
 
219
268
  const TEST_MODE = process.env.PI_TASK_DISPATCHER_TEST_MODE === "1";
@@ -226,10 +275,13 @@ const MODEL_TEXT_LIMIT = 5_000;
226
275
  const PRESENTATION_PREVIEW_ROWS = 5;
227
276
  const PRESENTATION_UPDATE_THROTTLE_MS = 100;
228
277
  const STDERR_LIMIT = 16_000;
278
+ const PROXY_PAYLOAD_LIMIT_BYTES = 16 * 1024 * 1024;
279
+ const PROXY_MANIFEST_LIMIT_BYTES = 8 * 1024 * 1024;
280
+ const PROXY_MAX_IN_FLIGHT_REQUESTS = 32;
281
+ const PROXY_MAX_REQUESTS_PER_JOB = 4_096;
282
+ const REVOKED_EXECUTOR_ROOT_LIMIT = 4_096;
229
283
  const THINKING_LEVELS = new Set<ThinkingLevel>(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
230
284
  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
285
 
234
286
  const DEFAULT_CONFIG: DispatcherConfig = {
235
287
  mechanic: {},
@@ -317,6 +369,81 @@ function isRecord(value: unknown): value is Record<string, unknown> {
317
369
  return typeof value === "object" && value !== null && !Array.isArray(value);
318
370
  }
319
371
 
372
+ /**
373
+ * Copy the JSON-visible surface of a schema or result without silently dropping unsupported values.
374
+ * TypeBox's own symbol markers are deliberately excluded: they are not model-facing JSON Schema.
375
+ */
376
+ function cloneJsonValue(value: unknown, label: string): JsonValue {
377
+ const seen = new WeakSet<object>();
378
+ const visit = (candidate: unknown, location: string): JsonValue => {
379
+ if (candidate === null || typeof candidate === "string" || typeof candidate === "boolean") return candidate;
380
+ if (typeof candidate === "number") {
381
+ if (!Number.isFinite(candidate) || Object.is(candidate, -0)) throw new Error(`${label} is not JSON-serializable at ${location}`);
382
+ return candidate;
383
+ }
384
+ if (Array.isArray(candidate)) {
385
+ if (seen.has(candidate)) throw new Error(`${label} contains a cycle at ${location}`);
386
+ seen.add(candidate);
387
+ const result = candidate.map((item, index) => visit(item, `${location}[${index}]`));
388
+ seen.delete(candidate);
389
+ return result;
390
+ }
391
+ if (!isRecord(candidate)) throw new Error(`${label} is not JSON-serializable at ${location}`);
392
+ if (seen.has(candidate)) throw new Error(`${label} contains a cycle at ${location}`);
393
+ const prototype = Object.getPrototypeOf(candidate);
394
+ if (prototype !== Object.prototype && prototype !== null) throw new Error(`${label} has an unsupported object at ${location}`);
395
+ const unsupportedSymbols = Object.getOwnPropertySymbols(candidate)
396
+ .filter((symbol) => Object.prototype.propertyIsEnumerable.call(candidate, symbol))
397
+ .filter((symbol) => !String(symbol).startsWith("Symbol(TypeBox."));
398
+ if (unsupportedSymbols.length > 0) throw new Error(`${label} has an unsupported symbol at ${location}`);
399
+ seen.add(candidate);
400
+ const result: { [key: string]: JsonValue } = {};
401
+ for (const key of Object.keys(candidate)) {
402
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
403
+ if (!descriptor || !("value" in descriptor)) throw new Error(`${label} has an accessor at ${location}.${key}`);
404
+ result[key] = visit(descriptor.value, `${location}.${key}`);
405
+ }
406
+ seen.delete(candidate);
407
+ return result;
408
+ };
409
+ return visit(value, "$" );
410
+ }
411
+
412
+ /** Produce a deterministic compatibility fingerprint without changing the model-facing field order. */
413
+ function stableJson(value: JsonValue): string {
414
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
415
+ if (Array.isArray(value)) return `[${value.map((item) => stableJson(item)).join(",")}]`;
416
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
417
+ }
418
+
419
+ /** Reject an otherwise JSON-safe proxy value before it can create an unbounded IPC message. */
420
+ function assertJsonByteLimit(value: JsonValue, label: string, maximum: number): void {
421
+ const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
422
+ if (bytes > maximum) throw new Error(`${label} exceeds the ${maximum}-byte proxy limit`);
423
+ }
424
+
425
+ /** Return a failure result that is safe to send over the proxy channel. */
426
+ function proxyErrorResult(message: string): ProxyWireResult {
427
+ return { content: [{ type: "text", text: message }], isError: true };
428
+ }
429
+
430
+ /** Strip parent-only details and usage before a nested manager result reaches the child session. */
431
+ function serializeProxyResult(value: unknown, forceError = false): ProxyWireResult {
432
+ if (!isRecord(value) || !Array.isArray(value.content)) return proxyErrorResult("Manager tool returned an invalid result.");
433
+ try {
434
+ const result: ProxyWireResult = { content: cloneJsonValue(value.content, "manager tool content") as JsonValue[] };
435
+ if (Object.prototype.hasOwnProperty.call(value, "structuredContent")) {
436
+ result.structuredContent = cloneJsonValue(value.structuredContent, "manager tool structured content");
437
+ }
438
+ if (value.isError === true || forceError) result.isError = true;
439
+ if (value.terminate === true) result.terminate = true;
440
+ assertJsonByteLimit(result as unknown as JsonValue, "manager tool result", PROXY_PAYLOAD_LIMIT_BYTES);
441
+ return result;
442
+ } catch (error) {
443
+ return proxyErrorResult(`Manager tool result could not be serialized: ${error instanceof Error ? error.message : String(error)}`);
444
+ }
445
+ }
446
+
320
447
  /** Create a fresh usage accumulator for one worker job. */
321
448
  function emptyUsage(): UsageTotals {
322
449
  return {
@@ -536,6 +663,132 @@ export default function codemodeOnlyExtension(pi: ExtensionAPI): void {
536
663
  `;
537
664
  }
538
665
 
666
+ /** Build a child-local proxy for exactly the manager tools granted to this job. */
667
+ function proxyExtensionSource(manifests: readonly DelegatedToolManifest[], nonce: string): string {
668
+ const tools = manifests
669
+ .filter((manifest) => manifest.name !== "codemode")
670
+ .map(({ fingerprint: _fingerprint, ...manifest }) => manifest);
671
+ const configuration = JSON.stringify({ nonce, tools });
672
+ return `/** Route selected worker tool calls through the parent manager's current ExtensionToolContext. */
673
+ import { Type } from "typebox";
674
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
675
+
676
+ type WireResult = { content: unknown[]; structuredContent?: unknown; isError?: boolean; terminate?: boolean };
677
+ type ProxyTool = {
678
+ name: string;
679
+ label: string;
680
+ description: string;
681
+ parameters: Record<string, unknown>;
682
+ outputSchema?: unknown;
683
+ namespace?: { name: string; description?: string; instructions?: string };
684
+ annotations?: { readOnlyHint?: boolean; destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean };
685
+ promptGuidelines?: string[];
686
+ executionMode?: "sequential" | "parallel";
687
+ };
688
+ const configuration = ${configuration} as { nonce: string; tools: ProxyTool[] };
689
+ const payloadLimitBytes = ${PROXY_PAYLOAD_LIMIT_BYTES};
690
+ const maximumInFlightRequests = ${PROXY_MAX_IN_FLIGHT_REQUESTS};
691
+ const textEncoder = new TextEncoder();
692
+ const pending = new Map<string, { resolve: (result: any) => void; onUpdate: ((result: any) => void) | undefined; signal: AbortSignal | undefined; onAbort: (() => void) | undefined }>();
693
+ let requestSequence = 0;
694
+
695
+ function failure(message: string): any {
696
+ return { content: [{ type: "text", text: message }], details: undefined, isError: true };
697
+ }
698
+
699
+ function resultFromWire(value: unknown): any {
700
+ if (!value || typeof value !== "object" || Array.isArray(value)) return failure("Dispatcher proxy received an invalid manager result.");
701
+ try {
702
+ const encoded = JSON.stringify(value);
703
+ if (encoded === undefined || textEncoder.encode(encoded).byteLength > payloadLimitBytes) return failure("Dispatcher proxy received an oversized manager result.");
704
+ } catch {
705
+ return failure("Dispatcher proxy received a non-serializable manager result.");
706
+ }
707
+ const wire = value as WireResult;
708
+ if (!Array.isArray(wire.content)) return failure("Dispatcher proxy received a manager result without content.");
709
+ const result: any = { content: wire.content, details: undefined };
710
+ if (Object.prototype.hasOwnProperty.call(wire, "structuredContent")) result.structuredContent = wire.structuredContent;
711
+ if (wire.isError === true) result.isError = true;
712
+ if (wire.terminate === true) result.terminate = true;
713
+ return result;
714
+ }
715
+
716
+ function send(message: Record<string, unknown>): void {
717
+ if (typeof process.send !== "function") throw new Error("Task dispatcher proxy IPC is unavailable.");
718
+ process.send(message);
719
+ }
720
+
721
+ function finish(requestId: string, result: any): void {
722
+ const request = pending.get(requestId);
723
+ if (!request) return;
724
+ pending.delete(requestId);
725
+ if (request.signal && request.onAbort) request.signal.removeEventListener("abort", request.onAbort);
726
+ request.resolve(result);
727
+ }
728
+
729
+ process.on("message", (incoming: unknown) => {
730
+ if (!incoming || typeof incoming !== "object" || Array.isArray(incoming)) return;
731
+ const message = incoming as Record<string, unknown>;
732
+ if (message.nonce !== configuration.nonce || typeof message.requestId !== "string") return;
733
+ if (message.type === "task_dispatcher_proxy_update") {
734
+ const request = pending.get(message.requestId);
735
+ if (!request) return;
736
+ try { request.onUpdate?.(resultFromWire(message.result)); } catch { /* A renderer must not affect proxy control flow. */ }
737
+ return;
738
+ }
739
+ if (message.type === "task_dispatcher_proxy_result") finish(message.requestId, resultFromWire(message.result));
740
+ });
741
+ process.on("disconnect", () => {
742
+ for (const requestId of [...pending.keys()]) finish(requestId, failure("Task dispatcher parent IPC disconnected."));
743
+ });
744
+
745
+ export default function taskDispatcherProxy(pi: ExtensionAPI): void {
746
+ for (const manifest of configuration.tools) {
747
+ pi.registerTool({
748
+ name: manifest.name,
749
+ label: manifest.label,
750
+ description: manifest.description,
751
+ parameters: Type.Unsafe(manifest.parameters) as any,
752
+ ...(manifest.outputSchema === undefined ? {} : { outputSchema: Type.Unsafe(manifest.outputSchema) as any }),
753
+ ...(manifest.namespace === undefined ? {} : { namespace: manifest.namespace }),
754
+ ...(manifest.annotations === undefined ? {} : { annotations: manifest.annotations }),
755
+ ...(manifest.promptGuidelines === undefined ? {} : { promptGuidelines: manifest.promptGuidelines }),
756
+ ...(manifest.executionMode === undefined ? {} : { executionMode: manifest.executionMode }),
757
+ exposure: "direct",
758
+ defaultActive: true,
759
+ async execute(_toolCallId, args, signal, onUpdate) {
760
+ if (signal?.aborted) return failure("Worker proxy call was already aborted.");
761
+ if (pending.size >= maximumInFlightRequests) return failure("Worker proxy has too many in-flight manager requests.");
762
+ let serializedArgs: unknown;
763
+ try {
764
+ const encoded = JSON.stringify(args);
765
+ if (encoded === undefined) throw new Error("arguments are not JSON-serializable");
766
+ if (textEncoder.encode(encoded).byteLength > payloadLimitBytes) throw new Error("arguments exceed the " + payloadLimitBytes + "-byte proxy limit");
767
+ serializedArgs = JSON.parse(encoded);
768
+ } catch (error) {
769
+ return failure("Worker proxy arguments could not be serialized: " + (error instanceof Error ? error.message : String(error)));
770
+ }
771
+ const requestId = "proxy-" + (++requestSequence);
772
+ return await new Promise<any>((resolve) => {
773
+ const onAbort = () => {
774
+ try { send({ type: "task_dispatcher_proxy_cancel", nonce: configuration.nonce, requestId }); } catch { /* Parent teardown wins. */ }
775
+ finish(requestId, failure("Worker proxy call was aborted."));
776
+ };
777
+ pending.set(requestId, { resolve, onUpdate, signal, onAbort });
778
+ signal?.addEventListener("abort", onAbort, { once: true });
779
+ try {
780
+ send({ type: "task_dispatcher_proxy_request", nonce: configuration.nonce, requestId, toolName: manifest.name, args: serializedArgs });
781
+ } catch (error) {
782
+ finish(requestId, failure("Task dispatcher proxy request failed: " + (error instanceof Error ? error.message : String(error))));
783
+ }
784
+ });
785
+ },
786
+ });
787
+ }
788
+ }
789
+ `;
790
+ }
791
+
539
792
  /** Build the worker-only gate extension that stops the agent loop after persisted tool results. */
540
793
  function gateSource(): string {
541
794
  return `/** Pause this worker after each persisted tool batch so only the parent dispatcher can resume it. */
@@ -560,6 +813,33 @@ function isTerminal(status: JobStatus): boolean {
560
813
  }
561
814
 
562
815
  let nextJobIndex = 1;
816
+ const revokedExecutorRoots = new Set<string>();
817
+ let revokedExecutorGuardSaturated = false;
818
+
819
+ /** Bound stale-lease tracking and fail every otherwise-unattributed nested call after saturation. */
820
+ function revokeExecutorRoot(rootToolCallId: string): void {
821
+ if (revokedExecutorGuardSaturated) return;
822
+ if (revokedExecutorRoots.size >= REVOKED_EXECUTOR_ROOT_LIMIT) {
823
+ revokedExecutorRoots.clear();
824
+ revokedExecutorGuardSaturated = true;
825
+ return;
826
+ }
827
+ revokedExecutorRoots.add(rootToolCallId);
828
+ }
829
+
830
+ /** Reject a nested manager call attributable to a revoked lease. */
831
+ function revokedExecutorDecision(parentToolCallId: string | undefined, toolName: string): { block: boolean; reason: string } | undefined {
832
+ if (!parentToolCallId) return undefined;
833
+ if (revokedExecutorGuardSaturated) {
834
+ return { block: true, reason: `Nested manager tool ${toolName} was rejected because stale-lease tracking reached its fail-closed limit.` };
835
+ }
836
+ for (const rootToolCallId of revokedExecutorRoots) {
837
+ if (parentToolCallId === rootToolCallId || parentToolCallId.startsWith(`${rootToolCallId}/`)) {
838
+ return { block: true, reason: `Nested manager tool ${toolName} was rejected because its executor lease is no longer active.` };
839
+ }
840
+ }
841
+ return undefined;
842
+ }
563
843
 
564
844
  /** Supervise one reusable in-memory Pi worker and expose only boundary-sized state. */
565
845
  class WorkerJob {
@@ -567,6 +847,7 @@ class WorkerJob {
567
847
  readonly role: Role;
568
848
  readonly task: string;
569
849
  readonly tools: string[];
850
+ readonly manifests: readonly DelegatedToolManifest[];
570
851
  readonly model: string;
571
852
  readonly thinking: ThinkingLevel;
572
853
  readonly cwd: string;
@@ -576,6 +857,10 @@ class WorkerJob {
576
857
  readonly usage = emptyUsage();
577
858
  readonly actions: WorkerAction[] = [];
578
859
  readonly context: ContextMetrics;
860
+ private readonly pi: ExtensionAPI;
861
+ private readonly manifestByName: ReadonlyMap<string, DelegatedToolManifest>;
862
+ private readonly callableIdentityByName: ReadonlyMap<string, unknown>;
863
+ private readonly proxyNonce = randomBytes(24).toString("hex");
579
864
 
580
865
  status: JobStatus = "starting";
581
866
  sequence = 0;
@@ -587,12 +872,13 @@ class WorkerJob {
587
872
 
588
873
  private child: ChildProcess | undefined;
589
874
  private temporaryDirectory: string | undefined;
875
+ private temporaryRemoval: Promise<void> | undefined;
590
876
  private stdoutBuffer = "";
591
877
  private stderr = "";
592
878
  private requestSequence = 0;
593
879
  private actionSequence = 0;
594
880
  private completionSequence = 0;
595
- private presentationSequence = 0;
881
+ private assistantIdSequence = 0;
596
882
  private readonly presentationEntries: WorkerPresentationEntry[] = [];
597
883
  private readonly presentationById = new Map<string, WorkerPresentationEntry>();
598
884
  private readonly assistantTextBlocks = new Map<number, string>();
@@ -610,25 +896,34 @@ class WorkerJob {
610
896
  private progressUpdate: ReturnType<typeof setTimeout> | undefined;
611
897
  private pausedLease: ReturnType<typeof setTimeout> | undefined;
612
898
  private closePromise: Promise<void> | undefined;
899
+ private executorLease: ExecutorLease | undefined;
900
+ private readonly proxyRequests = new Map<string, ProxyRequestState>();
901
+ private readonly seenProxyRequests = new Set<string>();
613
902
 
614
903
  constructor(
615
904
  role: Role,
616
905
  task: string,
617
- tools: string[],
906
+ manifests: DelegatedToolManifest[],
618
907
  config: ResolvedWorkerRoleConfig,
619
908
  cwd: string,
620
909
  policy: DispatcherPolicy,
621
910
  budget: CalculatedBudget,
911
+ pi: ExtensionAPI,
912
+ callableTools: ExtensionToolContext["tools"],
622
913
  ) {
623
914
  this.id = `${role}-${nextJobIndex++}`;
624
915
  this.role = role;
625
916
  this.task = task;
626
- this.tools = tools;
917
+ this.manifests = manifests.map((manifest) => structuredClone(manifest));
918
+ this.tools = this.manifests.map((manifest) => manifest.name);
919
+ this.manifestByName = new Map(this.manifests.map((manifest) => [manifest.name, manifest]));
920
+ this.callableIdentityByName = new Map(callableTools.map((tool) => [tool.name, tool]));
627
921
  this.model = config.model;
628
922
  this.thinking = config.thinking;
629
923
  this.cwd = cwd;
630
924
  this.policy = policy;
631
925
  this.budget = budget;
926
+ this.pi = pi;
632
927
  this.context = {
633
928
  contextWindow: budget.contextWindow,
634
929
  peakPercent: 0,
@@ -640,12 +935,14 @@ class WorkerJob {
640
935
  }
641
936
 
642
937
  /** Start the isolated child, initialize compaction policy, and execute the first supervised stage. */
643
- async start(signal: AbortSignal | undefined, onProgress: (snapshot: WorkerSnapshot) => void): Promise<WorkerSnapshot> {
938
+ async start(rootToolCallId: string, signal: AbortSignal | undefined, onProgress: (snapshot: WorkerSnapshot) => void, ctx: ExtensionToolContext): Promise<WorkerSnapshot> {
644
939
  this.temporaryDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-task-dispatcher-"));
645
940
  const rolePromptPath = path.join(this.temporaryDirectory, `${this.role}.md`);
941
+ const proxyPath = path.join(this.temporaryDirectory, "tool-proxies.ts");
646
942
  const gatePath = path.join(this.temporaryDirectory, "supervision-gate.ts");
647
943
  const codemodeOnlyPath = path.join(this.temporaryDirectory, "codemode-only.ts");
648
944
  await fs.promises.writeFile(rolePromptPath, ROLE_PROMPTS[this.role], { encoding: "utf8", mode: 0o600 });
945
+ await fs.promises.writeFile(proxyPath, proxyExtensionSource(this.manifests, this.proxyNonce), { encoding: "utf8", mode: 0o600 });
649
946
  await fs.promises.writeFile(gatePath, gateSource(), { encoding: "utf8", mode: 0o600 });
650
947
  if (this.tools.includes("codemode")) {
651
948
  await fs.promises.writeFile(codemodeOnlyPath, codemodeOnlySource(), { encoding: "utf8", mode: 0o600 });
@@ -659,6 +956,7 @@ class WorkerJob {
659
956
  "--thinking", this.thinking,
660
957
  ...(this.tools.length > 0 ? ["--tools", this.tools.join(",")] : ["--no-tools"]),
661
958
  "--append-system-prompt", rolePromptPath,
959
+ "--extension", proxyPath,
662
960
  ...(this.tools.includes("codemode") ? ["--extension", codemodeOnlyPath] : []),
663
961
  "--extension", gatePath,
664
962
  ];
@@ -676,23 +974,32 @@ class WorkerJob {
676
974
  } finally {
677
975
  this.commitActiveElapsed();
678
976
  }
679
- return await this.runStages(`Delegated ${this.role} task:\n${this.task}`, 1, signal, onProgress);
977
+ return await this.runStages(`Delegated ${this.role} task:\n${this.task}`, 1, rootToolCallId, signal, onProgress, ctx);
680
978
  }
681
979
 
682
980
  /** Continue or revise work for a bounded number of action batches. */
683
981
  async continue(
684
982
  instructions: string | undefined,
685
983
  actionBatches: number,
984
+ rootToolCallId: string,
686
985
  signal: AbortSignal | undefined,
687
986
  onProgress: (snapshot: WorkerSnapshot) => void,
987
+ ctx: ExtensionToolContext,
688
988
  ): Promise<WorkerSnapshot> {
689
989
  if (this.status !== "paused") throw new Error(`Job ${this.id} is ${this.status}, not paused.`);
990
+ try {
991
+ this.revalidateManifest(ctx);
992
+ } catch (error) {
993
+ const reason = `Delegated tool manifest revalidation failed: ${error instanceof Error ? error.message : String(error)}`;
994
+ await this.stop("hard_stopped", reason);
995
+ throw new Error(reason);
996
+ }
690
997
  this.clearPausedLease();
691
998
  const message = instructions
692
999
  ? `The manager revised the delegated task with these controlling instructions:\n${instructions}\nContinue from completed work without repeating it.`
693
1000
  : "Continue the delegated task from the completed tool results. Do not repeat completed work.";
694
1001
  try {
695
- return await this.runStages(message, actionBatches, signal, onProgress);
1002
+ return await this.runStages(message, actionBatches, rootToolCallId, signal, onProgress, ctx);
696
1003
  } catch (error) {
697
1004
  if (this.status === "paused") this.schedulePausedLease();
698
1005
  throw error;
@@ -752,6 +1059,7 @@ class WorkerJob {
752
1059
  /** End the job from parent-observed state without asking the worker to cooperate. */
753
1060
  async stop(status: Extract<JobStatus, "checkpointed" | "taken_over" | "cancelled" | "hard_stopped">, reason: string): Promise<WorkerSnapshot> {
754
1061
  this.clearPausedLease();
1062
+ this.clearExecutorLease();
755
1063
  this.status = status;
756
1064
  this.phase = reason;
757
1065
  this.recommendation = status === "taken_over" ? "manager owns remaining work" : "closed";
@@ -810,6 +1118,7 @@ class WorkerJob {
810
1118
  async dispose(): Promise<void> {
811
1119
  if (this.closePromise) return await this.closePromise;
812
1120
  this.clearPausedLease();
1121
+ this.clearExecutorLease();
813
1122
  if (this.progressUpdate) clearTimeout(this.progressUpdate);
814
1123
  this.progressUpdate = undefined;
815
1124
  this.closePromise = (async () => {
@@ -820,7 +1129,13 @@ class WorkerJob {
820
1129
  new Promise<void>((resolve) => child.once("close", () => resolve())),
821
1130
  new Promise<void>((resolve) => setTimeout(resolve, 5_000)),
822
1131
  ]);
823
- if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
1132
+ if (child.exitCode === null && child.signalCode === null) {
1133
+ child.kill("SIGKILL");
1134
+ await Promise.race([
1135
+ new Promise<void>((resolve) => child.once("close", () => resolve())),
1136
+ new Promise<void>((resolve) => setTimeout(resolve, 1_000)),
1137
+ ]);
1138
+ }
824
1139
  }
825
1140
  await this.removeTemporaryDirectory();
826
1141
  })();
@@ -850,12 +1165,20 @@ class WorkerJob {
850
1165
  });
851
1166
  child.on("message", (message) => {
852
1167
  this.lastProgressAt = Date.now();
853
- if (!isRecord(message) || message.type !== "task_dispatcher_boundary") return;
854
- const boundary = message as unknown as GateBoundaryMessage;
855
- this.boundaryObserved = true;
856
- this.sequence++;
857
- this.phase = `completed tool batch at worker turn ${boundary.turnIndex}`;
858
- this.boundaryWaiter?.resolve();
1168
+ if (!isRecord(message)) return;
1169
+ if (message.type === "task_dispatcher_boundary") {
1170
+ const boundary = message as unknown as GateBoundaryMessage;
1171
+ this.boundaryObserved = true;
1172
+ this.sequence++;
1173
+ this.phase = `completed tool batch at worker turn ${boundary.turnIndex}`;
1174
+ this.boundaryWaiter?.resolve();
1175
+ return;
1176
+ }
1177
+ if (message.type === "task_dispatcher_proxy_request") {
1178
+ void this.handleProxyRequest(message);
1179
+ return;
1180
+ }
1181
+ if (message.type === "task_dispatcher_proxy_cancel") void this.handleProxyCancel(message);
859
1182
  });
860
1183
  child.on("error", (error) => {
861
1184
  this.diagnostic = error.message;
@@ -925,7 +1248,7 @@ class WorkerJob {
925
1248
  toolCallId: event.toolCallId,
926
1249
  toolName: event.toolName,
927
1250
  label: formatActivity(event.toolName, event.args),
928
- mutating: MUTATING_TOOLS.has(event.toolName),
1251
+ mutating: this.manifestByName.get(event.toolName)?.annotations?.readOnlyHint !== true,
929
1252
  status: "running",
930
1253
  startedAt: Date.now(),
931
1254
  parentToolCallId: event.parentToolCallId,
@@ -989,8 +1312,7 @@ class WorkerJob {
989
1312
  if (this.activeAssistantPresentation) return this.activeAssistantPresentation;
990
1313
  this.assistantTextBlocks.clear();
991
1314
  const entry: WorkerPresentationEntry = {
992
- sequence: ++this.presentationSequence,
993
- id: `assistant-${this.presentationSequence}`,
1315
+ id: `assistant-${++this.assistantIdSequence}`,
994
1316
  kind: "assistant",
995
1317
  label: "assistant",
996
1318
  status: "streaming",
@@ -1039,7 +1361,6 @@ class WorkerJob {
1039
1361
  if (existing) return existing;
1040
1362
  const action = this.activeActions.get(toolCallId);
1041
1363
  const entry: WorkerPresentationEntry = {
1042
- sequence: ++this.presentationSequence,
1043
1364
  id,
1044
1365
  kind: "tool",
1045
1366
  label: action?.label ?? formatActivity(toolName, args),
@@ -1078,10 +1399,14 @@ class WorkerJob {
1078
1399
  private async runStages(
1079
1400
  initialMessage: string,
1080
1401
  actionBatches: number,
1402
+ rootToolCallId: string,
1081
1403
  signal: AbortSignal | undefined,
1082
1404
  onProgress: (snapshot: WorkerSnapshot) => void,
1405
+ ctx: ExtensionToolContext,
1083
1406
  ): Promise<WorkerSnapshot> {
1084
- let message = initialMessage;
1407
+ const lease = this.openExecutorLease(rootToolCallId, ctx, signal);
1408
+ try {
1409
+ let message = initialMessage;
1085
1410
  for (let batch = 0; batch < actionBatches; batch++) {
1086
1411
  this.assertBudget();
1087
1412
  const actionBaseline = this.actionSequence;
@@ -1165,7 +1490,10 @@ class WorkerJob {
1165
1490
  this.schedulePausedLease();
1166
1491
  return this.snapshot();
1167
1492
  }
1168
- throw new Error("Unreachable supervised stage state.");
1493
+ throw new Error("Unreachable supervised stage state.");
1494
+ } finally {
1495
+ this.clearExecutorLease(lease);
1496
+ }
1169
1497
  }
1170
1498
 
1171
1499
  /** Wait for settlement while enforcing job deadline and no-progress policy externally. */
@@ -1229,6 +1557,189 @@ class WorkerJob {
1229
1557
  return "continue, revise, compact, checkpoint, cancel, or take over";
1230
1558
  }
1231
1559
 
1560
+ /** Rebuild the grant from this invocation's callable manager tools before a worker can resume. */
1561
+ private revalidateManifest(ctx: ExtensionToolContext): void {
1562
+ const current = buildDelegatedToolManifests(this.tools, this.pi, ctx);
1563
+ if (current.length !== this.manifests.length) throw new Error("the selected tool count changed");
1564
+ const currentIdentities = new Map(ctx.tools.map((tool) => [tool.name, tool]));
1565
+ for (let index = 0; index < current.length; index++) {
1566
+ const expected = this.manifests[index];
1567
+ const observed = current[index];
1568
+ if (expected.name !== observed.name || expected.fingerprint !== observed.fingerprint) {
1569
+ throw new Error(`selected tool ${expected.name} is no longer definition-compatible`);
1570
+ }
1571
+ if (expected.name !== "codemode" && this.callableIdentityByName.get(expected.name) !== currentIdentities.get(expected.name)) {
1572
+ throw new Error(`selected tool ${expected.name} no longer has the original callable identity`);
1573
+ }
1574
+ }
1575
+ }
1576
+
1577
+ /** Enforce selected capabilities during a lease and reject delayed calls from every revoked lease. */
1578
+ authorizeNestedToolCall(parentToolCallId: string | undefined, toolName: string): { block: boolean; reason: string } | undefined {
1579
+ if (!parentToolCallId) return undefined;
1580
+ const lease = this.executorLease;
1581
+ if (lease && (parentToolCallId === lease.rootToolCallId || parentToolCallId.startsWith(`${lease.rootToolCallId}/`))) {
1582
+ if (toolName === "codemode" || !this.manifestByName.has(toolName)) {
1583
+ return { block: true, reason: `Nested manager tool ${toolName} is outside this delegation's selected capability grant.` };
1584
+ }
1585
+ return undefined;
1586
+ }
1587
+ return revokedExecutorDecision(parentToolCallId, toolName);
1588
+ }
1589
+
1590
+ /** Grant parent execution only while a start or continuation call actively owns the worker. */
1591
+ private openExecutorLease(rootToolCallId: string, ctx: ExtensionToolContext, signal: AbortSignal | undefined): ExecutorLease {
1592
+ if (this.executorLease) throw new Error("A manager executor lease is already active for this worker.");
1593
+ const lease: ExecutorLease = { rootToolCallId, ctx, signal };
1594
+ this.executorLease = lease;
1595
+ return lease;
1596
+ }
1597
+
1598
+ /** Revoke parent execution before exposing an idle, paused, or terminal worker state. */
1599
+ private clearExecutorLease(expected?: ExecutorLease): void {
1600
+ if (expected && this.executorLease !== expected) return;
1601
+ const lease = this.executorLease;
1602
+ if (!lease) return;
1603
+ this.executorLease = undefined;
1604
+ revokeExecutorRoot(lease.rootToolCallId);
1605
+ for (const [requestId, request] of this.proxyRequests) {
1606
+ if (request.lease !== lease) continue;
1607
+ this.proxyRequests.delete(requestId);
1608
+ request.removeParentAbort?.();
1609
+ request.removeParentAbort = undefined;
1610
+ request.controller.abort();
1611
+ }
1612
+ }
1613
+
1614
+ /** Deliver a nonce-bound manager result or streaming update to the private child proxy. */
1615
+ private sendProxy(message: Record<string, unknown>): void {
1616
+ try {
1617
+ if (this.child?.connected) this.child.send(message);
1618
+ } catch {
1619
+ // Child teardown wins over a late nested-tool result.
1620
+ }
1621
+ }
1622
+
1623
+ /** Reject a malformed, stale, replayed, cross-job, or otherwise unauthorized proxy call. */
1624
+ private rejectProxyRequest(requestId: unknown, reason: string): void {
1625
+ if (typeof requestId !== "string" || requestId.length === 0 || requestId.length > 128) return;
1626
+ this.sendProxy({
1627
+ type: "task_dispatcher_proxy_result",
1628
+ nonce: this.proxyNonce,
1629
+ requestId,
1630
+ result: proxyErrorResult(reason),
1631
+ });
1632
+ }
1633
+
1634
+ /** Start one parent-side nested tool call while preserving manager hooks, validation, and cancellation. */
1635
+ private async handleProxyRequest(message: Record<string, unknown>): Promise<void> {
1636
+ const requestId = message.requestId;
1637
+ if (message.nonce !== this.proxyNonce) {
1638
+ this.rejectProxyRequest(requestId, "Worker proxy request was rejected because its job nonce is invalid.");
1639
+ return;
1640
+ }
1641
+ if (typeof requestId !== "string" || requestId.length === 0 || requestId.length > 128) return;
1642
+ if (this.seenProxyRequests.has(requestId)) {
1643
+ this.rejectProxyRequest(requestId, "Worker proxy request was rejected because its request id was replayed.");
1644
+ return;
1645
+ }
1646
+ if (this.seenProxyRequests.size >= PROXY_MAX_REQUESTS_PER_JOB) {
1647
+ this.rejectProxyRequest(requestId, "Worker proxy request was rejected because the per-job request limit was reached.");
1648
+ return;
1649
+ }
1650
+ this.seenProxyRequests.add(requestId);
1651
+ const lease = this.executorLease;
1652
+ if (!lease || this.status !== "running") {
1653
+ this.rejectProxyRequest(requestId, "Worker proxy request was rejected because no bounded manager executor lease is active.");
1654
+ return;
1655
+ }
1656
+ if (typeof message.toolName !== "string" || message.toolName === "codemode" || !this.manifestByName.has(message.toolName)) {
1657
+ this.rejectProxyRequest(requestId, "Worker proxy request was rejected because the requested tool is not in this job's selected capability grant.");
1658
+ return;
1659
+ }
1660
+ if (this.proxyRequests.size >= PROXY_MAX_IN_FLIGHT_REQUESTS) {
1661
+ this.rejectProxyRequest(requestId, "Worker proxy request was rejected because too many manager executions are in flight.");
1662
+ return;
1663
+ }
1664
+ try {
1665
+ this.revalidateManifest(lease.ctx);
1666
+ } catch (error) {
1667
+ const reason = `Delegated tool grant changed before ${message.toolName} execution: ${error instanceof Error ? error.message : String(error)}`;
1668
+ this.rejectProxyRequest(requestId, reason);
1669
+ void this.stop("hard_stopped", reason);
1670
+ return;
1671
+ }
1672
+ let args: JsonValue;
1673
+ try {
1674
+ args = cloneJsonValue(message.args, `worker proxy arguments for ${message.toolName}`);
1675
+ assertJsonByteLimit(args, `worker proxy arguments for ${message.toolName}`, PROXY_PAYLOAD_LIMIT_BYTES);
1676
+ } catch (error) {
1677
+ this.rejectProxyRequest(requestId, `Worker proxy request arguments are invalid: ${error instanceof Error ? error.message : String(error)}`);
1678
+ return;
1679
+ }
1680
+ const controller = new AbortController();
1681
+ const abortFromParent = () => controller.abort();
1682
+ if (lease.signal?.aborted) controller.abort();
1683
+ else lease.signal?.addEventListener("abort", abortFromParent, { once: true });
1684
+ const request: ProxyRequestState = {
1685
+ lease,
1686
+ controller,
1687
+ removeParentAbort: lease.signal ? () => lease.signal?.removeEventListener("abort", abortFromParent) : undefined,
1688
+ };
1689
+ this.proxyRequests.set(requestId, request);
1690
+ try {
1691
+ const outcome = await lease.ctx.executeTool(message.toolName as string, args, {
1692
+ signal: controller.signal,
1693
+ onUpdate: (partial) => {
1694
+ if (this.executorLease !== lease || this.proxyRequests.get(requestId) !== request) return;
1695
+ this.sendProxy({
1696
+ type: "task_dispatcher_proxy_update",
1697
+ nonce: this.proxyNonce,
1698
+ requestId,
1699
+ result: serializeProxyResult(partial),
1700
+ });
1701
+ },
1702
+ });
1703
+ if (this.executorLease === lease && this.proxyRequests.get(requestId) === request) {
1704
+ this.sendProxy({
1705
+ type: "task_dispatcher_proxy_result",
1706
+ nonce: this.proxyNonce,
1707
+ requestId,
1708
+ result: serializeProxyResult(outcome.result, outcome.isError),
1709
+ });
1710
+ }
1711
+ } catch (error) {
1712
+ if (this.executorLease === lease && this.proxyRequests.get(requestId) === request) {
1713
+ this.sendProxy({
1714
+ type: "task_dispatcher_proxy_result",
1715
+ nonce: this.proxyNonce,
1716
+ requestId,
1717
+ result: proxyErrorResult(`Manager execution of ${message.toolName as string} failed: ${error instanceof Error ? error.message : String(error)}`),
1718
+ });
1719
+ }
1720
+ } finally {
1721
+ request.removeParentAbort?.();
1722
+ request.removeParentAbort = undefined;
1723
+ if (this.proxyRequests.get(requestId) === request) this.proxyRequests.delete(requestId);
1724
+ }
1725
+ }
1726
+
1727
+ /** Propagate a child-side proxy cancellation to the current manager nested-tool signal. */
1728
+ private handleProxyCancel(message: Record<string, unknown>): void {
1729
+ const requestId = message.requestId;
1730
+ if (message.nonce !== this.proxyNonce) {
1731
+ this.rejectProxyRequest(requestId, "Worker proxy cancellation was rejected because its job nonce is invalid.");
1732
+ return;
1733
+ }
1734
+ if (typeof requestId !== "string" || requestId.length === 0 || requestId.length > 128) return;
1735
+ const request = this.proxyRequests.get(requestId);
1736
+ if (!request || request.lease !== this.executorLease || this.status !== "running") {
1737
+ this.rejectProxyRequest(requestId, "Worker proxy cancellation was rejected because the request is unknown or no longer active.");
1738
+ return;
1739
+ }
1740
+ request.controller.abort();
1741
+ }
1742
+
1232
1743
  /** Correlate one RPC request; an uncertain timeout closes the worker rather than permitting a racing command. */
1233
1744
  private async request(label: string, payload: Record<string, unknown>, timeoutMs: number): Promise<ParsedEvent> {
1234
1745
  const child = this.child;
@@ -1256,6 +1767,7 @@ class WorkerJob {
1256
1767
 
1257
1768
  /** Reject all consumers when the worker process can no longer produce state. */
1258
1769
  private failPending(error: Error): void {
1770
+ this.clearExecutorLease();
1259
1771
  for (const pending of this.pendingRequests.values()) pending.reject(error);
1260
1772
  this.pendingRequests.clear();
1261
1773
  this.settleWaiter?.reject(error);
@@ -1278,9 +1790,12 @@ class WorkerJob {
1278
1790
 
1279
1791
  /** Remove runtime-only worker instructions after process termination without touching workspace artifacts. */
1280
1792
  private async removeTemporaryDirectory(): Promise<void> {
1793
+ if (this.temporaryRemoval) return await this.temporaryRemoval;
1281
1794
  const directory = this.temporaryDirectory;
1282
1795
  this.temporaryDirectory = undefined;
1283
- if (directory) await fs.promises.rm(directory, { recursive: true, force: true });
1796
+ if (!directory) return;
1797
+ this.temporaryRemoval = fs.promises.rm(directory, { recursive: true, force: true });
1798
+ return await this.temporaryRemoval;
1284
1799
  }
1285
1800
 
1286
1801
  /** Coalesce token and partial-result bursts before rebuilding and serializing the complete snapshot. */
@@ -1329,7 +1844,7 @@ class WorkerJob {
1329
1844
  let activeJob: WorkerJob | undefined;
1330
1845
  let lastSnapshot: WorkerSnapshot | undefined;
1331
1846
  const latestRoleSnapshots = new Map<Role, WorkerSnapshot>();
1332
- const reportedUsage = new Map<string, UsageTotals>();
1847
+ let reportedUsage: { jobId: string; usage: UsageTotals } | undefined;
1333
1848
 
1334
1849
  /** Resolve role overrides against the active parent model without assuming provider availability. */
1335
1850
  function resolveWorkerConfig(config: WorkerRoleConfig, ctx: ExtensionToolContext): { role: ResolvedWorkerRoleConfig; contextWindow: number; maxTokens: number } {
@@ -1353,20 +1868,114 @@ function resolveWorkerConfig(config: WorkerRoleConfig, ctx: ExtensionToolContext
1353
1868
  };
1354
1869
  }
1355
1870
 
1356
- /** Restrict delegations to active, non-recursive isolated-worker tools selected by the manager. */
1357
- function validateDelegatedTools(requested: string[], pi: ExtensionAPI): string[] {
1358
- const tools = [...new Set(requested.map((tool) => tool.trim()).filter(Boolean))];
1871
+ /** Build exact child manifests from manager-callable tools plus the explicitly active child-local codemode. */
1872
+ function buildDelegatedToolManifests(requested: string[], pi: ExtensionAPI, ctx: ExtensionToolContext): DelegatedToolManifest[] {
1873
+ const malformed = requested.filter((name) => name.trim() !== name || name.length === 0);
1874
+ if (malformed.length > 0) throw new Error(`Delegated tool names must be exact nonempty names: ${malformed.join(", ")}`);
1875
+ const duplicates = requested.filter((name, index) => requested.indexOf(name) !== index);
1876
+ if (duplicates.length > 0) throw new Error(`Delegated tool names must not be repeated: ${[...new Set(duplicates)].join(", ")}`);
1359
1877
  const forbidden = new Set([...Object.values(ROLE_TOOL_NAMES), "delegate_control", "delegate_status"]);
1360
- const recursive = tools.filter((tool) => forbidden.has(tool));
1361
- if (recursive.length > 0) throw new Error(`Delegation tools cannot be delegated: ${recursive.join(", ")}`);
1878
+ const recursive = requested.filter((name) => forbidden.has(name));
1879
+ if (recursive.length > 0) throw new Error(`Delegation tools cannot be delegated recursively: ${recursive.join(", ")}`);
1880
+ if (typeof ctx.executeTool !== "function") throw new Error("The current manager tool context does not expose ctx.executeTool().");
1881
+ const callable = new Map(ctx.tools.map((tool) => [tool.name, tool]));
1882
+ const metadata = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
1362
1883
  const active = new Set(pi.getActiveTools());
1363
- const unavailable = tools.filter((tool) => !active.has(tool));
1364
- if (unavailable.length > 0) throw new Error(`Delegated tools are not active for the manager: ${unavailable.join(", ")}`);
1365
- const unsupportedTools = tools.filter((tool) => !ISOLATED_WORKER_TOOLS.has(tool));
1366
- if (unsupportedTools.length > 0) {
1367
- throw new Error(`Isolated workers do not support these tools: ${unsupportedTools.join(", ")}`);
1884
+ const unavailable = requested.filter((name) => name === "codemode"
1885
+ ? !active.has(name) || !metadata.has(name)
1886
+ : !callable.has(name));
1887
+ if (unavailable.length > 0) {
1888
+ const detail = unavailable.map((name) => {
1889
+ if (name === "codemode" && metadata.has(name)) return `${name} (registered but not active in the manager)`;
1890
+ return metadata.has(name)
1891
+ ? `${name} (registered but not callable through ctx.executeTool())`
1892
+ : `${name} (not registered or callable through ctx.executeTool())`;
1893
+ });
1894
+ throw new Error(`Requested delegated tools are unavailable: ${detail.join(", ")}`);
1368
1895
  }
1369
- return tools;
1896
+ const manifests = requested.map((name) => {
1897
+ const localCodemode = name === "codemode";
1898
+ const tool = callable.get(name);
1899
+ const info = metadata.get(name);
1900
+ if (!info) throw new Error(`Delegated tool ${name} does not expose registered public metadata.`);
1901
+ const label = localCodemode ? "Codemode" : tool?.label;
1902
+ const description = localCodemode ? info.description : tool?.description;
1903
+ if (typeof label !== "string" || typeof description !== "string") {
1904
+ throw new Error(`Delegated tool ${name} does not expose serializable label and description metadata.`);
1905
+ }
1906
+ const parameters = cloneJsonValue(localCodemode ? info.parameters : tool?.parameters, `${name} parameter schema`);
1907
+ if (!isRecord(parameters)) throw new Error(`Delegated tool ${name} parameter schema must be a JSON object.`);
1908
+ const outputSchema = localCodemode || tool?.outputSchema === undefined
1909
+ ? undefined
1910
+ : cloneJsonValue(tool.outputSchema, `${name} output schema`);
1911
+ const rawSourceInfo = info.sourceInfo;
1912
+ if (!isRecord(rawSourceInfo)
1913
+ || typeof rawSourceInfo.path !== "string"
1914
+ || typeof rawSourceInfo.source !== "string"
1915
+ || !new Set(["user", "project", "temporary"]).has(rawSourceInfo.scope as string)
1916
+ || !new Set(["package", "top-level"]).has(rawSourceInfo.origin as string)
1917
+ || (rawSourceInfo.baseDir !== undefined && typeof rawSourceInfo.baseDir !== "string")) {
1918
+ throw new Error(`Delegated tool ${name} has unsupported source metadata.`);
1919
+ }
1920
+ const sourceInfo = cloneJsonValue({
1921
+ path: rawSourceInfo.path,
1922
+ source: rawSourceInfo.source,
1923
+ scope: rawSourceInfo.scope,
1924
+ origin: rawSourceInfo.origin,
1925
+ ...(rawSourceInfo.baseDir === undefined ? {} : { baseDir: rawSourceInfo.baseDir }),
1926
+ }, `${name} source metadata`);
1927
+ let namespace: ToolNamespaceManifest | undefined;
1928
+ if (info?.namespace !== undefined) {
1929
+ if (!isRecord(info.namespace) || typeof info.namespace.name !== "string" || info.namespace.name.length === 0) {
1930
+ throw new Error(`Callable tool ${name} has unsupported namespace metadata.`);
1931
+ }
1932
+ if (info.namespace.description !== undefined && typeof info.namespace.description !== "string") throw new Error(`Callable tool ${name} has unsupported namespace description metadata.`);
1933
+ if (info.namespace.instructions !== undefined && typeof info.namespace.instructions !== "string") throw new Error(`Callable tool ${name} has unsupported namespace instructions metadata.`);
1934
+ namespace = {
1935
+ name: info.namespace.name,
1936
+ ...(info.namespace.description === undefined ? {} : { description: info.namespace.description }),
1937
+ ...(info.namespace.instructions === undefined ? {} : { instructions: info.namespace.instructions }),
1938
+ };
1939
+ }
1940
+ let annotations: ToolAnnotationsManifest | undefined;
1941
+ if (info?.annotations !== undefined) {
1942
+ if (!isRecord(info.annotations)) throw new Error(`Callable tool ${name} has unsupported annotations metadata.`);
1943
+ annotations = {};
1944
+ for (const key of ["readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"] as const) {
1945
+ const value = info.annotations[key];
1946
+ if (value !== undefined && typeof value !== "boolean") throw new Error(`Callable tool ${name} has unsupported ${key} metadata.`);
1947
+ if (value !== undefined) annotations[key] = value;
1948
+ }
1949
+ }
1950
+ let promptGuidelines: string[] | undefined;
1951
+ if (info?.promptGuidelines !== undefined) {
1952
+ if (!Array.isArray(info.promptGuidelines) || info.promptGuidelines.some((guideline) => typeof guideline !== "string")) {
1953
+ throw new Error(`Callable tool ${name} has unsupported prompt guideline metadata.`);
1954
+ }
1955
+ promptGuidelines = [...info.promptGuidelines];
1956
+ }
1957
+ const executionMode = localCodemode ? "sequential" : tool?.executionMode;
1958
+ if (executionMode !== undefined && executionMode !== "sequential" && executionMode !== "parallel") {
1959
+ throw new Error(`Delegated tool ${name} has unsupported execution-mode metadata.`);
1960
+ }
1961
+ const manifest: Omit<DelegatedToolManifest, "fingerprint"> = {
1962
+ name,
1963
+ label,
1964
+ description,
1965
+ parameters,
1966
+ ...(outputSchema === undefined ? {} : { outputSchema }),
1967
+ ...(namespace === undefined ? {} : { namespace }),
1968
+ ...(annotations === undefined ? {} : { annotations }),
1969
+ ...(promptGuidelines === undefined ? {} : { promptGuidelines }),
1970
+ ...(executionMode === undefined ? {} : { executionMode }),
1971
+ };
1972
+ return {
1973
+ ...manifest,
1974
+ fingerprint: stableJson(cloneJsonValue({ ...manifest, sourceInfo }, `${name} delegated manifest`)),
1975
+ };
1976
+ });
1977
+ assertJsonByteLimit(manifests as unknown as JsonValue, "delegated tool manifest", PROXY_MANIFEST_LIMIT_BYTES);
1978
+ return manifests;
1370
1979
  }
1371
1980
 
1372
1981
  /** Format context pressure against the worker model's own physical context window. */
@@ -1621,12 +2230,11 @@ function renderWorkerResult(
1621
2230
  /** Package a snapshot as bounded model content plus complete machine-readable details. */
1622
2231
  function snapshotResult(snapshot: WorkerSnapshot, isError = false): { content: Array<{ type: "text"; text: string }>; details: WorkerSnapshot; usage: UsageTotals; isError: boolean } {
1623
2232
  lastSnapshot = snapshot;
1624
- for (const jobId of reportedUsage.keys()) {
1625
- if (jobId !== snapshot.jobId) reportedUsage.delete(jobId);
1626
- }
1627
- const previous = reportedUsage.get(snapshot.jobId) ?? emptyUsage();
2233
+ const previous = reportedUsage && reportedUsage.jobId === snapshot.jobId
2234
+ ? reportedUsage.usage
2235
+ : emptyUsage();
1628
2236
  const usage = usageDifference(snapshot.usage, previous);
1629
- reportedUsage.set(snapshot.jobId, structuredClone(snapshot.usage));
2237
+ reportedUsage = { jobId: snapshot.jobId, usage: structuredClone(snapshot.usage) };
1630
2238
  return {
1631
2239
  content: [{ type: "text", text: formatSnapshot(snapshot) }],
1632
2240
  details: snapshot,
@@ -1673,7 +2281,7 @@ function registerDelegate(pi: ExtensionAPI, role: Role): void {
1673
2281
  renderResult(result, { expanded, isPartial }, theme, context) {
1674
2282
  return renderWorkerResult(result as any, expanded, isPartial, theme, context);
1675
2283
  },
1676
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
2284
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
1677
2285
  try {
1678
2286
  if (activeJob && !isTerminal(activeJob.status)) throw new Error(`${activeJob.id} is still ${activeJob.status}.`);
1679
2287
  if (activeJob) {
@@ -1681,16 +2289,16 @@ function registerDelegate(pi: ExtensionAPI, role: Role): void {
1681
2289
  activeJob = undefined;
1682
2290
  }
1683
2291
  const config = loadConfig();
1684
- const tools = validateDelegatedTools(params.tools, pi);
2292
+ const manifests = buildDelegatedToolManifests(params.tools, pi, ctx);
1685
2293
  const resolved = resolveWorkerConfig(config[role], ctx);
1686
2294
  const budget = calculateBudget(role, params.workload, resolved.contextWindow, resolved.maxTokens, config.policy);
1687
- const job = new WorkerJob(role, params.task, tools, resolved.role, ctx.cwd, config.policy, budget);
2295
+ const job = new WorkerJob(role, params.task, manifests, resolved.role, ctx.cwd, config.policy, budget, pi, ctx.tools);
1688
2296
  activeJob = job;
1689
2297
  const progress = (snapshot: WorkerSnapshot) => {
1690
2298
  updateWorkerStatus(ctx, snapshot);
1691
2299
  onUpdate?.({ content: [{ type: "text", text: formatSnapshot(snapshot) }], details: snapshot });
1692
2300
  };
1693
- const snapshot = await job.start(signal, progress);
2301
+ const snapshot = await job.start(toolCallId, signal, progress, ctx);
1694
2302
  updateWorkerStatus(ctx, snapshot);
1695
2303
  return snapshotResult(snapshot, snapshot.status === "failed" || snapshot.status === "hard_stopped");
1696
2304
  } catch (error) {
@@ -1734,7 +2342,7 @@ function registerControl(pi: ExtensionAPI): void {
1734
2342
  renderResult(result, { expanded, isPartial }, theme, context) {
1735
2343
  return renderWorkerResult(result as any, expanded, isPartial, theme, context);
1736
2344
  },
1737
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
2345
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
1738
2346
  try {
1739
2347
  const job = activeJob;
1740
2348
  if (!job || job.id !== params.jobId) throw new Error(`No live delegation matches ${params.jobId}.`);
@@ -1757,7 +2365,7 @@ function registerControl(pi: ExtensionAPI): void {
1757
2365
  updateWorkerStatus(ctx, state);
1758
2366
  onUpdate?.({ content: [{ type: "text", text: formatSnapshot(state) }], details: state });
1759
2367
  };
1760
- snapshot = await job.continue(params.action === "revise" ? params.instructions : undefined, params.actionBatches ?? 1, signal, progress);
2368
+ snapshot = await job.continue(params.action === "revise" ? params.instructions : undefined, params.actionBatches ?? 1, toolCallId, signal, progress, ctx);
1761
2369
  }
1762
2370
  snapshot.managerAction = params.action;
1763
2371
  updateWorkerStatus(ctx, snapshot);
@@ -1807,8 +2415,13 @@ export default function taskDispatcher(pi: ExtensionAPI): void {
1807
2415
  for (const role of ROLES) registerDelegate(pi, role);
1808
2416
  registerControl(pi);
1809
2417
  registerStatus(pi);
2418
+ pi.on("tool_call", (event) => activeJob
2419
+ ? activeJob.authorizeNestedToolCall(event.parentToolCallId, event.toolName)
2420
+ : revokedExecutorDecision(event.parentToolCallId, event.toolName));
1810
2421
  pi.on("session_shutdown", async () => {
1811
2422
  if (activeJob) await activeJob.dispose();
1812
2423
  activeJob = undefined;
2424
+ revokedExecutorRoots.clear();
2425
+ revokedExecutorGuardSaturated = false;
1813
2426
  });
1814
2427
  }