pi-subagents 0.11.4 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.11.6] - 2026-03-20
6
+
7
+ ### Added
8
+ - Added `delegate` builtin agent — a lightweight subagent with no model, output, or default reads. Inherits the parent session's model, making it the natural target for prompt-template delegated execution.
9
+
10
+ ## [0.11.5] - 2026-03-20
11
+
12
+ ### Added
13
+ - Added fork context preamble: tasks run with `context: "fork"` are now wrapped with a default preamble that anchors the subagent to its task, preventing it from continuing the parent conversation. The default is `DEFAULT_FORK_PREAMBLE` in `types.ts`. Internal/programmatic callers can use `wrapForkTask(task, false)` to disable it or pass a custom string (this is not exposed as a tool parameter).
14
+ - Added a prompt-template delegation bridge (`prompt-template-bridge.ts`) on the shared extension event bus. The subagent extension now listens for `prompt-template:subagent:request` and emits correlated `started`/`response`/`update` events, with cwd safety checks and race-safe cancellation handling.
15
+ - Added delegated progress streaming via `prompt-template:subagent:update`, mapped from subagent executor `onUpdate` progress payloads.
16
+
17
+ ### Changed
18
+ - Session lifecycle reset now preserves the latest extension context for event-bus delegated runs.
19
+ - `[fork]` badge is now shown only on the result row, not duplicated on both the tool-call and result rows.
20
+
5
21
  ## [0.11.4] - 2026-03-19
6
22
 
7
23
  ### Added
package/README.md CHANGED
@@ -34,7 +34,7 @@ Agents are markdown files with YAML frontmatter that define specialized subagent
34
34
 
35
35
  Use `agentScope` parameter to control discovery: `"user"`, `"project"`, or `"both"` (default; project takes priority).
36
36
 
37
- **Builtin agents:** The extension ships with ready-to-use agents — `scout`, `planner`, `worker`, `reviewer`, `context-builder`, and `researcher`. They load at lowest priority so any user or project agent with the same name overrides them. Builtin agents appear with a `[builtin]` badge in listings and cannot be modified through management actions (create a same-named user agent to override instead).
37
+ **Builtin agents:** The extension ships with ready-to-use agents — `scout`, `planner`, `worker`, `reviewer`, `context-builder`, `researcher`, and `delegate`. They load at lowest priority so any user or project agent with the same name overrides them. Builtin agents appear with a `[builtin]` badge in listings and cannot be modified through management actions (create a same-named user agent to override instead).
38
38
 
39
39
  > **Note:** The `researcher` agent uses `web_search`, `fetch_content`, and `get_search_content` tools which require the [pi-web-access](https://github.com/nicobailon/pi-web-access) extension. Install it with `pi install npm:pi-web-access`.
40
40
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: delegate
3
+ description: Lightweight subagent that inherits the parent model with no default reads
4
+ ---
5
+
6
+ You are a delegated agent. Execute the assigned task using your tools. Be direct and efficient.
package/index.ts CHANGED
@@ -27,6 +27,7 @@ import { createSubagentExecutor } from "./subagent-executor.js";
27
27
  import { createAsyncJobTracker } from "./async-job-tracker.js";
28
28
  import { createResultWatcher } from "./result-watcher.js";
29
29
  import { registerSlashCommands } from "./slash-commands.js";
30
+ import { registerPromptTemplateDelegationBridge } from "./prompt-template-bridge.js";
30
31
  import {
31
32
  type Details,
32
33
  type ExtensionConfig,
@@ -138,6 +139,27 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
138
139
  discoverAgents,
139
140
  });
140
141
 
142
+ const promptTemplateBridge = registerPromptTemplateDelegationBridge({
143
+ events: pi.events,
144
+ getContext: () => state.lastUiContext,
145
+ execute: async (requestId, request, signal, ctx, onUpdate) =>
146
+ executor.execute(
147
+ requestId,
148
+ {
149
+ agent: request.agent,
150
+ task: request.task,
151
+ context: request.context,
152
+ cwd: request.cwd,
153
+ model: request.model,
154
+ async: false,
155
+ clarify: false,
156
+ },
157
+ signal,
158
+ onUpdate,
159
+ ctx,
160
+ ),
161
+ });
162
+
141
163
  const tool: ToolDefinition<typeof SubagentParams, Details> = {
142
164
  name: "subagent",
143
165
  label: "Subagent",
@@ -184,21 +206,20 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
184
206
  }
185
207
  const isParallel = (args.tasks?.length ?? 0) > 0;
186
208
  const asyncLabel = args.async === true && !isParallel ? theme.fg("warning", " [async]") : "";
187
- const contextLabel = args.context === "fork" ? theme.fg("warning", " [fork]") : "";
188
209
  if (args.chain?.length)
189
210
  return new Text(
190
- `${theme.fg("toolTitle", theme.bold("subagent "))}chain (${args.chain.length})${asyncLabel}${contextLabel}`,
211
+ `${theme.fg("toolTitle", theme.bold("subagent "))}chain (${args.chain.length})${asyncLabel}`,
191
212
  0,
192
213
  0,
193
214
  );
194
215
  if (isParallel)
195
216
  return new Text(
196
- `${theme.fg("toolTitle", theme.bold("subagent "))}parallel (${args.tasks!.length})${contextLabel}`,
217
+ `${theme.fg("toolTitle", theme.bold("subagent "))}parallel (${args.tasks!.length})`,
197
218
  0,
198
219
  0,
199
220
  );
200
221
  return new Text(
201
- `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent || "?")}${asyncLabel}${contextLabel}`,
222
+ `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
202
223
  0,
203
224
  0,
204
225
  );
@@ -332,6 +353,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
332
353
  const resetSessionState = (ctx: ExtensionContext) => {
333
354
  state.baseCwd = ctx.cwd;
334
355
  state.currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
356
+ state.lastUiContext = ctx;
335
357
  cleanupSessionArtifacts(ctx);
336
358
  resetJobs(ctx);
337
359
  };
@@ -354,6 +376,8 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
354
376
  }
355
377
  state.cleanupTimers.clear();
356
378
  state.asyncJobs.clear();
379
+ promptTemplateBridge.cancelAll();
380
+ promptTemplateBridge.dispose();
357
381
  if (state.lastUiContext?.hasUI) {
358
382
  state.lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
359
383
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -0,0 +1,225 @@
1
+ export const PROMPT_TEMPLATE_SUBAGENT_REQUEST_EVENT = "prompt-template:subagent:request";
2
+ export const PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT = "prompt-template:subagent:started";
3
+ export const PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT = "prompt-template:subagent:response";
4
+ export const PROMPT_TEMPLATE_SUBAGENT_UPDATE_EVENT = "prompt-template:subagent:update";
5
+ export const PROMPT_TEMPLATE_SUBAGENT_CANCEL_EVENT = "prompt-template:subagent:cancel";
6
+
7
+ export interface PromptTemplateDelegationRequest {
8
+ requestId: string;
9
+ agent: string;
10
+ task: string;
11
+ context: "fresh" | "fork";
12
+ model: string;
13
+ cwd: string;
14
+ }
15
+
16
+ export interface PromptTemplateDelegationResponse extends PromptTemplateDelegationRequest {
17
+ messages: unknown[];
18
+ isError: boolean;
19
+ errorText?: string;
20
+ }
21
+
22
+ export interface PromptTemplateDelegationUpdate {
23
+ requestId: string;
24
+ currentTool?: string;
25
+ currentToolArgs?: string;
26
+ recentOutput?: string;
27
+ toolCount?: number;
28
+ durationMs?: number;
29
+ tokens?: number;
30
+ }
31
+
32
+ export interface PromptTemplateBridgeEvents {
33
+ on(event: string, handler: (data: unknown) => void): (() => void) | void;
34
+ emit(event: string, data: unknown): void;
35
+ }
36
+
37
+ interface PromptTemplateBridgeResult {
38
+ isError?: boolean;
39
+ content?: unknown;
40
+ details?: {
41
+ results?: Array<{
42
+ messages?: unknown[];
43
+ }>;
44
+ progress?: Array<{
45
+ currentTool?: string;
46
+ currentToolArgs?: string;
47
+ recentOutput?: string[];
48
+ toolCount?: number;
49
+ }>;
50
+ };
51
+ }
52
+
53
+ export interface PromptTemplateBridgeOptions<Ctx extends { cwd?: string }> {
54
+ events: PromptTemplateBridgeEvents;
55
+ getContext: () => Ctx | null;
56
+ execute: (
57
+ requestId: string,
58
+ request: PromptTemplateDelegationRequest,
59
+ signal: AbortSignal,
60
+ ctx: Ctx,
61
+ onUpdate: (result: PromptTemplateBridgeResult) => void,
62
+ ) => Promise<PromptTemplateBridgeResult>;
63
+ }
64
+
65
+ export function parsePromptTemplateRequest(data: unknown): PromptTemplateDelegationRequest | undefined {
66
+ if (!data || typeof data !== "object") return undefined;
67
+ const value = data as Partial<PromptTemplateDelegationRequest>;
68
+ if (!value.requestId || !value.agent || !value.task || !value.model || !value.cwd) return undefined;
69
+ if (value.context !== "fresh" && value.context !== "fork") return undefined;
70
+ return {
71
+ requestId: value.requestId,
72
+ agent: value.agent,
73
+ task: value.task,
74
+ context: value.context,
75
+ model: value.model,
76
+ cwd: value.cwd,
77
+ };
78
+ }
79
+
80
+ export function firstTextContent(content: unknown): string | undefined {
81
+ if (!Array.isArray(content)) return undefined;
82
+ for (const part of content) {
83
+ if (!part || typeof part !== "object") continue;
84
+ if ((part as { type?: string }).type !== "text") continue;
85
+ const text = (part as { text?: unknown }).text;
86
+ if (typeof text === "string" && text.trim()) return text.trim();
87
+ }
88
+ return undefined;
89
+ }
90
+
91
+ function toDelegationUpdate(requestId: string, update: PromptTemplateBridgeResult): PromptTemplateDelegationUpdate | undefined {
92
+ const progress = update.details?.progress?.[0];
93
+ if (!progress) return undefined;
94
+ const lastOutput = progress.recentOutput?.[progress.recentOutput.length - 1];
95
+ return {
96
+ requestId,
97
+ currentTool: progress.currentTool,
98
+ currentToolArgs: progress.currentToolArgs,
99
+ recentOutput: lastOutput && lastOutput !== "(running...)" ? lastOutput : undefined,
100
+ toolCount: progress.toolCount,
101
+ durationMs: (progress as { durationMs?: number }).durationMs,
102
+ tokens: (progress as { tokens?: number }).tokens,
103
+ };
104
+ }
105
+
106
+ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: string }>(
107
+ options: PromptTemplateBridgeOptions<Ctx>,
108
+ ): {
109
+ cancelAll: () => void;
110
+ dispose: () => void;
111
+ } {
112
+ const controllers = new Map<string, AbortController>();
113
+ const pendingCancels = new Set<string>();
114
+ const subscriptions: Array<() => void> = [];
115
+
116
+ const subscribe = (event: string, handler: (data: unknown) => void): void => {
117
+ const unsubscribe = options.events.on(event, handler);
118
+ if (typeof unsubscribe === "function") subscriptions.push(unsubscribe);
119
+ };
120
+
121
+ subscribe(PROMPT_TEMPLATE_SUBAGENT_CANCEL_EVENT, (data) => {
122
+ if (!data || typeof data !== "object") return;
123
+ const requestId = (data as { requestId?: unknown }).requestId;
124
+ if (typeof requestId !== "string") return;
125
+ const controller = controllers.get(requestId);
126
+ if (controller) {
127
+ controller.abort();
128
+ return;
129
+ }
130
+ pendingCancels.add(requestId);
131
+ });
132
+
133
+ subscribe(PROMPT_TEMPLATE_SUBAGENT_REQUEST_EVENT, async (data) => {
134
+ const request = parsePromptTemplateRequest(data);
135
+ if (!request) return;
136
+
137
+ const ctx = options.getContext();
138
+ if (!ctx) {
139
+ const response: PromptTemplateDelegationResponse = {
140
+ ...request,
141
+ messages: [],
142
+ isError: true,
143
+ errorText: "No active extension context for delegated subagent execution.",
144
+ };
145
+ options.events.emit(PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT, response);
146
+ return;
147
+ }
148
+
149
+ if (typeof ctx.cwd === "string" && ctx.cwd !== request.cwd) {
150
+ const response: PromptTemplateDelegationResponse = {
151
+ ...request,
152
+ messages: [],
153
+ isError: true,
154
+ errorText: `Delegated request cwd mismatch: active context is '${ctx.cwd}' but request asked for '${request.cwd}'. Retry from the target session/cwd.`,
155
+ };
156
+ options.events.emit(PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT, response);
157
+ return;
158
+ }
159
+
160
+ const controller = new AbortController();
161
+ controllers.set(request.requestId, controller);
162
+
163
+ if (pendingCancels.delete(request.requestId)) {
164
+ controller.abort();
165
+ const response: PromptTemplateDelegationResponse = {
166
+ ...request,
167
+ messages: [],
168
+ isError: true,
169
+ errorText: "Delegated prompt cancelled.",
170
+ };
171
+ options.events.emit(PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT, response);
172
+ controllers.delete(request.requestId);
173
+ return;
174
+ }
175
+
176
+ options.events.emit(PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT, { requestId: request.requestId });
177
+
178
+ try {
179
+ const result = await options.execute(
180
+ request.requestId,
181
+ request,
182
+ controller.signal,
183
+ ctx,
184
+ (update) => {
185
+ const payload = toDelegationUpdate(request.requestId, update);
186
+ if (!payload) return;
187
+ options.events.emit(PROMPT_TEMPLATE_SUBAGENT_UPDATE_EVENT, payload);
188
+ },
189
+ );
190
+ const messages = result.details?.results?.[0]?.messages ?? [];
191
+ const response: PromptTemplateDelegationResponse = {
192
+ ...request,
193
+ messages,
194
+ isError: result.isError === true,
195
+ errorText: result.isError ? firstTextContent(result.content) : undefined,
196
+ };
197
+ options.events.emit(PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT, response);
198
+ } catch (error) {
199
+ const response: PromptTemplateDelegationResponse = {
200
+ ...request,
201
+ messages: [],
202
+ isError: true,
203
+ errorText: error instanceof Error ? error.message : String(error),
204
+ };
205
+ options.events.emit(PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT, response);
206
+ } finally {
207
+ controllers.delete(request.requestId);
208
+ }
209
+ });
210
+
211
+ return {
212
+ cancelAll: () => {
213
+ for (const controller of controllers.values()) {
214
+ controller.abort();
215
+ }
216
+ controllers.clear();
217
+ pendingCancels.clear();
218
+ },
219
+ dispose: () => {
220
+ for (const unsubscribe of subscriptions) unsubscribe();
221
+ subscriptions.length = 0;
222
+ pendingCancels.clear();
223
+ },
224
+ };
225
+ }
@@ -37,6 +37,7 @@ import {
37
37
  MAX_CONCURRENCY,
38
38
  MAX_PARALLEL,
39
39
  checkSubagentDepth,
40
+ wrapForkTask,
40
41
  } from "./types.js";
41
42
 
42
43
  interface TaskParam {
@@ -223,6 +224,26 @@ function collectChainSessionFiles(
223
224
  return sessionFiles;
224
225
  }
225
226
 
227
+ function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["context"]): ChainStep[] {
228
+ if (context !== "fork") return chain;
229
+ return chain.map((step, stepIndex) => {
230
+ if (isParallelStep(step)) {
231
+ return {
232
+ ...step,
233
+ parallel: step.parallel.map((task) => ({
234
+ ...task,
235
+ task: wrapForkTask(task.task ?? "{previous}"),
236
+ })),
237
+ };
238
+ }
239
+ const sequential = step as SequentialStep;
240
+ return {
241
+ ...sequential,
242
+ task: wrapForkTask(sequential.task ?? (stepIndex === 0 ? "{task}" : "{previous}")),
243
+ };
244
+ });
245
+ }
246
+
226
247
  function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentToolResult<Details> | null {
227
248
  const {
228
249
  params,
@@ -252,8 +273,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
252
273
  if (hasChain && params.chain) {
253
274
  const normalized = normalizeSkillInput(params.skill);
254
275
  const chainSkills = normalized === false ? [] : (normalized ?? []);
276
+ const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
255
277
  return executeAsyncChain(id, {
256
- chain: params.chain as ChainStep[],
278
+ chain,
257
279
  agents,
258
280
  ctx: asyncCtx,
259
281
  cwd: params.cwd,
@@ -263,7 +285,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
263
285
  shareEnabled,
264
286
  sessionRoot,
265
287
  chainSkills,
266
- sessionFilesByFlatIndex: collectChainSessionFiles(params.chain as ChainStep[], sessionFileForIndex),
288
+ sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForIndex),
267
289
  });
268
290
  }
269
291
 
@@ -282,7 +304,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
282
304
  const skills = normalizedSkills === false ? [] : normalizedSkills;
283
305
  return executeAsyncSingle(id, {
284
306
  agent: params.agent!,
285
- task: params.task!,
307
+ task: params.context === "fork" ? wrapForkTask(params.task!) : params.task!,
286
308
  agentConfig: a,
287
309
  ctx: asyncCtx,
288
310
  cwd: params.cwd,
@@ -317,8 +339,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
317
339
  } = data;
318
340
  const normalized = normalizeSkillInput(params.skill);
319
341
  const chainSkills = normalized === false ? [] : (normalized ?? []);
342
+ const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
320
343
  const chainResult = await executeChain({
321
- chain: params.chain as ChainStep[],
344
+ chain,
322
345
  task: params.task,
323
346
  agents,
324
347
  ctx,
@@ -347,8 +370,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
347
370
  }
348
371
  const id = randomUUID();
349
372
  const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
373
+ const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
350
374
  return executeAsyncChain(id, {
351
- chain: chainResult.requestedAsync.chain,
375
+ chain: asyncChain,
352
376
  agents,
353
377
  ctx: asyncCtx,
354
378
  cwd: params.cwd,
@@ -358,7 +382,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
358
382
  shareEnabled,
359
383
  sessionRoot,
360
384
  chainSkills: chainResult.requestedAsync.chainSkills,
361
- sessionFilesByFlatIndex: collectChainSessionFiles(chainResult.requestedAsync.chain, sessionFileForIndex),
385
+ sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForIndex),
362
386
  });
363
387
  }
364
388
 
@@ -463,7 +487,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
463
487
  const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
464
488
  const parallelTasks = tasks.map((t, i) => ({
465
489
  agent: t.agent,
466
- task: taskTexts[i]!,
490
+ task: params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!,
467
491
  cwd: t.cwd,
468
492
  ...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
469
493
  ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
@@ -487,6 +511,11 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
487
511
  const behaviors = agentConfigs.map((c) => resolveStepBehavior(c, {}));
488
512
  const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
489
513
  const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
514
+ if (params.context === "fork") {
515
+ for (let i = 0; i < taskTexts.length; i++) {
516
+ taskTexts[i] = wrapForkTask(taskTexts[i]!);
517
+ }
518
+ }
490
519
  const results = await mapConcurrent(tasks, MAX_CONCURRENCY, async (t, i) => {
491
520
  const overrideSkills = skillOverrides[i];
492
521
  const effectiveSkills = overrideSkills === undefined ? behaviors[i]?.skills : overrideSkills;
@@ -641,7 +670,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
641
670
  const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
642
671
  return executeAsyncSingle(id, {
643
672
  agent: params.agent!,
644
- task,
673
+ task: params.context === "fork" ? wrapForkTask(task) : task,
645
674
  agentConfig,
646
675
  ctx: asyncCtx,
647
676
  cwd: params.cwd,
@@ -657,6 +686,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
657
686
  }
658
687
  }
659
688
 
689
+ if (params.context === "fork") {
690
+ task = wrapForkTask(task);
691
+ }
660
692
  const cleanTask = task;
661
693
  const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, params.cwd);
662
694
  task = injectSingleOutputInstruction(task, outputPath);
package/types.ts CHANGED
@@ -267,6 +267,19 @@ export const POLL_INTERVAL_MS = 250;
267
267
  export const MAX_WIDGET_JOBS = 4;
268
268
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
269
269
 
270
+ export const DEFAULT_FORK_PREAMBLE =
271
+ "You are a delegated subagent with access to the parent session's context for reference. " +
272
+ "Your sole job is to execute the task below. Do not continue or respond to the prior conversation " +
273
+ "— focus exclusively on completing this task using your tools.";
274
+
275
+ export function wrapForkTask(task: string, preamble?: string | false): string {
276
+ if (preamble === false) return task;
277
+ const effectivePreamble = preamble ?? DEFAULT_FORK_PREAMBLE;
278
+ const wrappedPrefix = `${effectivePreamble}\n\nTask:\n`;
279
+ if (task.startsWith(wrappedPrefix)) return task;
280
+ return `${wrappedPrefix}${task}`;
281
+ }
282
+
270
283
  // ============================================================================
271
284
  // Recursion Depth Guard
272
285
  // ============================================================================