@bermudi/pi-delegate 0.1.0 → 0.1.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.
package/status.ts ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Background-activity visibility: footer status, settle warning, and
3
+ * session-replacement guards for live async tickets.
4
+ *
5
+ * Async tickets keep subagents running after the parent turn settles, but pi
6
+ * renders an idle session — nothing tells the human work is still in flight,
7
+ * and quitting silently kills it. This module owns the three signals that
8
+ * close that gap:
9
+ *
10
+ * 1. A persistent footer status (`ctx.ui.setStatus`) while any ticket is
11
+ * active — the only always-on indicator that background subagents exist.
12
+ * 2. A one-shot warning notification at the first `agent_settled` with each
13
+ * active ticket — the moment a user is most likely to assume everything
14
+ * is done and close the session.
15
+ * 3. A confirm guard on the session-replacement paths pi lets extensions
16
+ * cancel (`session_before_switch`, `session_before_fork`).
17
+ *
18
+ * Quit (Ctrl+C×2 / Ctrl+D / /quit) and /reload CANNOT be intercepted from an
19
+ * extension — `session_shutdown` is advisory, not cancellable. The footer
20
+ * status plus the exit/reload trace in extension.ts are the mitigations
21
+ * there.
22
+ */
23
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
24
+ import { ticketRegistry } from "./tickets.ts";
25
+ import type { AsyncTicket } from "./types.ts";
26
+
27
+ const STATUS_KEY = "delegate";
28
+
29
+ export interface ActiveTicketSummary {
30
+ /** Tickets in a non-terminal state (running or cancelling). */
31
+ tickets: AsyncTicket[];
32
+ /** Progress rows still executing or queued across active tickets. */
33
+ activeSubagents: number;
34
+ }
35
+
36
+ /** Snapshot the live background work from the ticket registry. */
37
+ export function activeTicketSummary(): ActiveTicketSummary {
38
+ const tickets: AsyncTicket[] = [];
39
+ let activeSubagents = 0;
40
+ for (const ticket of ticketRegistry.values()) {
41
+ if (ticket.status !== "running" && ticket.status !== "cancelling") continue;
42
+ tickets.push(ticket);
43
+ activeSubagents += ticket.progress.filter(
44
+ (p) => p.status === "running" || p.status === "pending",
45
+ ).length;
46
+ }
47
+ return { tickets, activeSubagents };
48
+ }
49
+
50
+ function plural(n: number, noun: string): string {
51
+ return `${n} ${noun}${n === 1 ? "" : "s"}`;
52
+ }
53
+
54
+ /** Footer status text, or undefined when nothing is active. */
55
+ export function buildStatusText(
56
+ summary: ActiveTicketSummary,
57
+ ): string | undefined {
58
+ const { tickets, activeSubagents } = summary;
59
+ if (tickets.length === 0) return undefined;
60
+ // Wind-down window: tasks have settled but the ticket has not flipped to a
61
+ // terminal status yet. "settling" is more honest than "0 subagents".
62
+ if (activeSubagents === 0) {
63
+ return tickets.length === 1
64
+ ? `⏳ ${tickets[0]!.id} settling…`
65
+ : `⏳ ${plural(tickets.length, "ticket")} settling…`;
66
+ }
67
+ return tickets.length === 1
68
+ ? `⏳ ${plural(activeSubagents, "subagent")} · ${tickets[0]!.id}`
69
+ : `⏳ ${plural(activeSubagents, "subagent")} · ${plural(tickets.length, "ticket")}`;
70
+ }
71
+
72
+ /** Most recent context with UI access. Refreshed by every sync call that
73
+ * receives a ctx, so event-driven updates can reach the footer without a
74
+ * direct ctx of their own. Single runtime per process — a replaced runtime
75
+ * refreshes this on its first delegate call or session event. */
76
+ let lastCtx: ExtensionContext | undefined;
77
+ /** Last text pushed to the footer — setStatus triggers a render, so dedupe. */
78
+ let lastStatusText: string | undefined;
79
+ /** Tickets already warned about at settle. Pruned to active tickets on sync. */
80
+ const settledWarnedTicketIds = new Set<string>();
81
+
82
+ /** Recompute the footer status from the registry and push it when changed.
83
+ * Called on every ticket lifecycle mutation (create, progress, complete,
84
+ * cancel); event-driven only — no timers, so the text never goes stale
85
+ * (counts are the only content). */
86
+ export function syncDelegateStatus(ctx?: ExtensionContext): void {
87
+ if (ctx) lastCtx = ctx;
88
+
89
+ const summary = activeTicketSummary();
90
+ const text = buildStatusText(summary);
91
+
92
+ if (settledWarnedTicketIds.size) {
93
+ const activeIds = new Set(summary.tickets.map((t) => t.id));
94
+ for (const id of settledWarnedTicketIds) {
95
+ if (!activeIds.has(id)) settledWarnedTicketIds.delete(id);
96
+ }
97
+ }
98
+
99
+ if (!lastCtx || text === lastStatusText) return;
100
+ try {
101
+ // The `ui` getter itself throws on a stale ctx — pi invalidates the old
102
+ // runtime on session replacement, and an unwinding ticket can race the
103
+ // teardown. A footer update must never crash the host.
104
+ lastCtx.ui.setStatus(STATUS_KEY, text);
105
+ lastStatusText = text;
106
+ } catch {
107
+ // Drop the stale ctx; the next live event re-caches a fresh one.
108
+ lastCtx = undefined;
109
+ lastStatusText = undefined;
110
+ }
111
+ }
112
+
113
+ /** Drop the cached ctx and warn-set on session shutdown — the runtime is
114
+ * about to be invalidated, and any post-teardown sync (e.g. an aborted
115
+ * ticket unwinding) must become a no-op instead of touching a stale ctx. */
116
+ export function clearDelegateStatusContext(): void {
117
+ lastCtx = undefined;
118
+ lastStatusText = undefined;
119
+ settledWarnedTicketIds.clear();
120
+ }
121
+
122
+ /** Warn once per ticket at the first agent_settled with that ticket active —
123
+ * the "looks idle but isn't" moment. The persistent footer status carries
124
+ * the information from then on, so later settles stay quiet. */
125
+ export function notifyActiveTicketsOnSettled(ctx: ExtensionContext): void {
126
+ lastCtx = ctx;
127
+ const summary = activeTicketSummary();
128
+ const fresh = summary.tickets.filter(
129
+ (t) => !settledWarnedTicketIds.has(t.id),
130
+ );
131
+ if (!fresh.length) return;
132
+
133
+ const subagents = fresh.reduce(
134
+ (n, t) =>
135
+ n +
136
+ t.progress.filter((p) => p.status === "running" || p.status === "pending")
137
+ .length,
138
+ 0,
139
+ );
140
+ const detail =
141
+ fresh.length === 1
142
+ ? `(ticket ${fresh[0]!.id})`
143
+ : `across ${plural(fresh.length, "ticket")} (${fresh.map((t) => t.id).join(", ")})`;
144
+ try {
145
+ ctx.ui.notify(
146
+ `⏳ ${plural(subagents, "background subagent")} still running ${detail} — quitting pi aborts them`,
147
+ "warning",
148
+ );
149
+ for (const t of fresh) settledWarnedTicketIds.add(t.id);
150
+ } catch {
151
+ // Stale or headless ctx: drop it so the next live event re-caches one.
152
+ lastCtx = undefined;
153
+ lastStatusText = undefined;
154
+ }
155
+ }
156
+
157
+ /** Block a session replacement (switch/fork) while background subagents are
158
+ * live, unless the human confirms. Returns `{ cancel: true }` to abort the
159
+ * replacement, undefined to let it proceed. Headless contexts (no dialog
160
+ * capability) are never blocked — automation must not deadlock on a
161
+ * confirm it cannot answer. */
162
+ export async function guardSessionReplacement(
163
+ ctx: ExtensionContext,
164
+ action: "switch" | "fork",
165
+ ): Promise<{ cancel: true } | undefined> {
166
+ lastCtx = ctx;
167
+ const summary = activeTicketSummary();
168
+ if (!summary.tickets.length || !ctx.hasUI) return undefined;
169
+
170
+ const ids = summary.tickets.map((t) => t.id).join(", ");
171
+ const verb =
172
+ action === "switch" ? "Switching sessions" : "Forking this session";
173
+ const proceed = await ctx.ui.confirm(
174
+ "Background subagents still running",
175
+ `${plural(summary.activeSubagents, "subagent")} (${ids}) still working. ` +
176
+ `${verb} aborts them — work already done is not rolled back. Continue anyway?`,
177
+ );
178
+ return proceed ? undefined : { cancel: true };
179
+ }
180
+
181
+ /** One-line description of live work for shutdown traces. */
182
+ export function describeActiveTickets(
183
+ summary: ActiveTicketSummary = activeTicketSummary(),
184
+ ): string {
185
+ const ids = summary.tickets.map((t) => t.id).join(", ");
186
+ const agents = [
187
+ ...new Set(
188
+ summary.tickets.flatMap((t) =>
189
+ t.progress
190
+ .filter((p) => p.status === "running" || p.status === "pending")
191
+ .map((p) => p.agent),
192
+ ),
193
+ ),
194
+ ];
195
+ const agentList = agents.length ? ` [${agents.join(", ")}]` : "";
196
+ return `${plural(summary.activeSubagents, "background subagent")}${agentList} (ticket${summary.tickets.length === 1 ? "" : "s"}: ${ids})`;
197
+ }
198
+
199
+ export function _resetDelegateStatusForTesting(): void {
200
+ lastCtx = undefined;
201
+ lastStatusText = undefined;
202
+ settledWarnedTicketIds.clear();
203
+ }
@@ -1,6 +1,10 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
3
- import { DEFAULT_TOOLS, VALID_THINKING } from "./constants.ts";
3
+ import {
4
+ DEFAULT_AGENT_NAME,
5
+ DEFAULT_TOOLS,
6
+ VALID_THINKING,
7
+ } from "./constants.ts";
4
8
  import { TOOL_FACTORIES, resolveToolGroups } from "./tools.ts";
5
9
  import { configFor } from "./pool.ts";
6
10
  import { isSessionBusy } from "./tickets.ts";
@@ -14,10 +18,62 @@ import type {
14
18
  AgentConfig,
15
19
  DelegateToolCtx,
16
20
  DelegateToolResult,
21
+ ParentAgentDefaults,
17
22
  ResolvedTask,
18
23
  TaskDef,
19
24
  } from "./types.ts";
20
25
 
26
+ const PROJECT_CONTEXT_START =
27
+ "\n\n<project_context>\n\nProject-specific instructions and guidelines:\n\n";
28
+ const PROJECT_CONTEXT_END = "\n</project_context>\n";
29
+
30
+ /**
31
+ * Parent `getSystemPrompt()` is the fully assembled prompt, including the
32
+ * parent's AGENTS.md files. A delegated session resolves resources for its own
33
+ * cwd, so carrying that section across would leak global instructions and
34
+ * duplicate project context. Preserve the parent's base prompt and everything
35
+ * outside Pi's structured context section; the child ResourceLoader appends its
36
+ * own (filtered) context afterward.
37
+ *
38
+ * The AGENTS.md/CLAUDE.md content is inserted verbatim inside
39
+ * `<project_instructions>...</project_instructions>` blocks. A file that
40
+ * itself contains `\n</project_context>\n` would otherwise terminate the
41
+ * scan early and leak the remainder of the parent context (P1). We therefore
42
+ * treat a candidate closing marker as valid only when it is *outside* any
43
+ * open `<project_instructions>` block — i.e. the first `PROJECT_CONTEXT_END`
44
+ * after `start` that is not nested. That matches the generated section's
45
+ * final closing marker while ignoring embedded fakes. A trailing
46
+ * `</project_context>` in post-context prompt text (skills/cwd) would also be
47
+ * outside, but such text is controlled by Pi and far less likely; picking the
48
+ * first outside marker preserves trailing prompt content instead of
49
+ * over-consuming it.
50
+ */
51
+ export function stripInheritedProjectContext(
52
+ prompt: string | undefined,
53
+ ): string | undefined {
54
+ if (!prompt) return prompt;
55
+ const start = prompt.indexOf(PROJECT_CONTEXT_START);
56
+ if (start < 0) return prompt;
57
+ let searchFrom = start + PROJECT_CONTEXT_START.length;
58
+ let end = -1;
59
+ while (true) {
60
+ const candidate = prompt.indexOf(PROJECT_CONTEXT_END, searchFrom);
61
+ if (candidate < 0) break;
62
+ const before = prompt.slice(start, candidate);
63
+ const openCount = (before.match(/<project_instructions/g) || []).length;
64
+ const closeCount = (before.match(/<\/project_instructions>/g) || []).length;
65
+ if (openCount > closeCount) {
66
+ // Inside a file block — embedded fake, skip it.
67
+ searchFrom = candidate + PROJECT_CONTEXT_END.length;
68
+ continue;
69
+ }
70
+ end = candidate;
71
+ break; // first valid outside block is the true closing
72
+ }
73
+ if (end < 0) return prompt;
74
+ return `${prompt.slice(0, start)}${prompt.slice(end + PROJECT_CONTEXT_END.length)}`;
75
+ }
76
+
21
77
  /** Build a tool result for an error/notice with no task progress. */
22
78
  function noticeResult(
23
79
  text: string,
@@ -67,10 +123,12 @@ export function validateTasks(
67
123
 
68
124
  const unknown: string[] = [];
69
125
  for (const t of tasks) {
70
- if (t.agent && !agents.has(t.agent)) unknown.push(t.agent);
126
+ if (t.agent && t.agent !== DEFAULT_AGENT_NAME && !agents.has(t.agent)) {
127
+ unknown.push(t.agent);
128
+ }
71
129
  }
72
130
  if (unknown.length) {
73
- const names = [...agents.keys()];
131
+ const names = [DEFAULT_AGENT_NAME, ...agents.keys()];
74
132
  return noticeResult(
75
133
  `Unknown agent(s): ${unknown.join(", ")}. Available: ${names.join(", ") || "(none)"}. Call delegate with an empty tasks array for help.`,
76
134
  tasks,
@@ -89,6 +147,7 @@ export function resolveTasks(
89
147
  tasks: TaskDef[],
90
148
  ctx: DelegateToolCtx,
91
149
  agents: Map<string, AgentConfig>,
150
+ parentDefaults: ParentAgentDefaults,
92
151
  ): ResolvedTask[] {
93
152
  // Build parent transcript lazily — only computed once if any task uses with-parent-transcript
94
153
  let parentTranscript: string | null = null;
@@ -107,22 +166,26 @@ export function resolveTasks(
107
166
  );
108
167
  }
109
168
 
110
- const parentSystemPrompt = ctx.getSystemPrompt?.();
169
+ const parentSystemPrompt = stripInheritedProjectContext(
170
+ ctx.getSystemPrompt?.(),
171
+ );
111
172
 
112
173
  return tasks.map((t, i) => {
113
- const agent = t.agent ? agents.get(t.agent) : undefined;
174
+ const isDefaultAgent = t.agent === DEFAULT_AGENT_NAME;
175
+ const agent = t.agent && !isDefaultAgent ? agents.get(t.agent) : undefined;
114
176
  const cwd = resolveCwd(t.cwd ?? ctx.cwd, ctx.cwd);
115
177
 
116
178
  // Load settings-based overrides for this agent
117
179
  const settings = loadDelegateSettings(cwd);
118
180
  const agentOverride =
119
- t.agent && settings?.agentOverrides?.[t.agent]
181
+ t.agent && !isDefaultAgent && settings?.agentOverrides?.[t.agent]
120
182
  ? settings.agentOverrides[t.agent]
121
183
  : undefined;
122
184
 
123
185
  // Build system prompt. Explicit task prompts and named agent prompts
124
- // win; ad-hoc subagents inherit the parent prompt when Pi exposes it,
125
- // then get explicit skills/AGENTS.md injection below.
186
+ // win; ad-hoc subagents inherit the parent's base prompt when Pi exposes
187
+ // it. The assembled parent project-context section was stripped above;
188
+ // the child ResourceLoader supplies context for this task's cwd.
126
189
  const pooledConfig = t.sessionId ? configFor(t.sessionId) : undefined;
127
190
 
128
191
  // Prompt is required for fresh tasks. ResumeFrom provides context already.
@@ -138,10 +201,11 @@ export function resolveTasks(
138
201
  }
139
202
 
140
203
  // System prompt resolution. AgentSession's resource loader owns
141
- // skills + AGENTS.md discovery (it appends them via _rebuildSystemPrompt),
142
- // so we resolve only the *base* prompt here: explicit task prompt → named
143
- // agent bodyparent session promptdefault. The resolved base is
144
- // passed as the loader's customPrompt (see buildDelegateSession).
204
+ // skills + project AGENTS.md discovery (it appends them via
205
+ // _rebuildSystemPrompt), so we resolve only the *base* prompt here:
206
+ // explicit task prompt named agent bodysanitized parent prompt
207
+ // default. The resolved base is passed as the loader's customPrompt (see
208
+ // buildDelegateSession).
145
209
  // Keep explicit intent separate: a bare `{ prompt, sessionId }` continues
146
210
  // the frozen prompt even if the parent prompt has since changed, while an
147
211
  // explicit task/profile prompt must not be silently ignored on reuse.
@@ -149,7 +213,9 @@ export function resolveTasks(
149
213
  ? t.systemPrompt
150
214
  : agent?.systemPrompt?.trim()
151
215
  ? agent.systemPrompt
152
- : undefined;
216
+ : isDefaultAgent
217
+ ? parentSystemPrompt
218
+ : undefined;
153
219
  const systemPrompt = buildSubagentSystemPrompt({
154
220
  taskSystemPrompt: t.systemPrompt,
155
221
  agentSystemPrompt: agent?.systemPrompt,
@@ -193,39 +259,44 @@ export function resolveTasks(
193
259
  if (t.action !== "close" && t.action !== "list") {
194
260
  // A pool hit always runs its frozen model, but an explicitly requested
195
261
  // task/profile model still has to be resolved so checkout can reject a
196
- // contradictory request rather than silently discarding it.
262
+ // contradictory request rather than silently discarding it. Naming the
263
+ // built-in `default` profile is also explicit: it requests the live
264
+ // parent model, so reuse fails clearly if the pool was frozen differently.
197
265
  if (pooledConfig) {
198
266
  const requestedModelSpec =
199
267
  t.model ??
200
- (t.agent ? (agentOverride?.model ?? agent?.model) : undefined);
268
+ (t.agent && !isDefaultAgent
269
+ ? (agentOverride?.model ?? agent?.model)
270
+ : undefined);
201
271
  if (requestedModelSpec) {
202
- requestedModel = resolveModelRequest(
272
+ const requested = resolveModelRequest(
203
273
  requestedModelSpec,
204
274
  ctx.modelRegistry,
205
275
  ctx.model,
206
- ).model;
276
+ );
277
+ requestedModel = requested.model;
278
+ modelSuffix = requested.strippedSuffix;
207
279
  if (!requestedModel) {
208
280
  throw new Error(
209
281
  `Task ${i}: requested model '${requestedModelSpec}' is not available. Check provider config or remove the model field to continue the pooled session.`,
210
282
  );
211
283
  }
284
+ } else if (isDefaultAgent) {
285
+ requestedModel = ctx.model;
212
286
  }
213
287
  model = pooledConfig.model;
214
288
  } else {
215
- // Resolve an explicit model spec (precedence: task > session > config >
216
- // frontmatter). resolveModelSpec returns undefined when none is set, so
217
- // we skip model re-resolution entirely passing the parent's composite id
218
- // (e.g. OpenRouter's "deepseek/deepseek-v4-flash") would split on "/"
219
- // and misroute to the upstream provider. Leaving resolvedModel
220
- // undefined also lets findAvailableAlternative run below: it returns
221
- // ctx.model as-is when it has auth, or swaps to an authenticated
222
- // same-id alternative when the parent's provider lost auth.
289
+ // The built-in `default` profile bypasses delegate.json and settings:
290
+ // absent a task override, it means this exact live parent Model object.
291
+ // Other tasks retain the normal task > config > frontmatter chain.
223
292
  const agentType = t.agent ?? "inline";
224
- const modelSpec = resolveModelSpec({
225
- taskModel: t.model ?? agentOverride?.model,
226
- agentType,
227
- frontmatterModel: agent?.model,
228
- });
293
+ const modelSpec = isDefaultAgent
294
+ ? t.model
295
+ : resolveModelSpec({
296
+ taskModel: t.model ?? agentOverride?.model,
297
+ agentType,
298
+ frontmatterModel: agent?.model,
299
+ });
229
300
  const resolvedRequest = modelSpec
230
301
  ? resolveModelRequest(modelSpec, ctx.modelRegistry, ctx.model)
231
302
  : undefined;
@@ -242,10 +313,11 @@ export function resolveTasks(
242
313
  );
243
314
  }
244
315
 
245
- model =
246
- resolvedModel ??
247
- findAvailableAlternative(ctx.model, ctx.modelRegistry) ??
248
- ctx.model;
316
+ model = isDefaultAgent
317
+ ? (resolvedModel ?? ctx.model)
318
+ : (resolvedModel ??
319
+ findAvailableAlternative(ctx.model, ctx.modelRegistry) ??
320
+ ctx.model);
249
321
  }
250
322
 
251
323
  if (!model) {
@@ -259,10 +331,14 @@ export function resolveTasks(
259
331
  // "continue with only sessionId" works without re-supplying tools.
260
332
  // Explicit overrides that don't match get rejected by acquireAgentSession.
261
333
  const isPoolHit = pooledConfig !== undefined;
334
+ const parentNativeTools = parentDefaults.tools.filter(
335
+ (name) => name in TOOL_FACTORIES,
336
+ );
262
337
  tools = resolveToolGroups(
263
338
  t.tools ??
264
339
  agentOverride?.tools ??
265
340
  agent?.tools ??
341
+ (isDefaultAgent ? parentNativeTools : undefined) ??
266
342
  (isPoolHit ? pooledConfig?.tools : undefined) ??
267
343
  DEFAULT_TOOLS,
268
344
  );
@@ -273,17 +349,23 @@ export function resolveTasks(
273
349
  );
274
350
  }
275
351
 
276
- // Resolve thinking. Precedence: task field agent override → agent
277
- // frontmatter frozen pooled config a Pi-style `:level` model suffix
278
- // (honored only as a last-resort default, so a model-emitted `claude:max`
279
- // runs at max when nothing else sets thinking). The suffix is lowest on
280
- // purpose: an agent author's `thinking: low` must beat `model: x:max`.
352
+ // Resolve thinking. Precedence for most agents: explicit `thinking` >
353
+ // agent override > frontmatter > frozen pooled config > model `:level`
354
+ // suffix (last resort). The built-in `default` agent intentionally
355
+ // inverts the last two steps: model suffix beats the parent's live
356
+ // thinking, which beats the frozen pooled value. This surfaces a clear
357
+ // `config mismatch` error on reuse when the parent thinking level has
358
+ // changed, rather than silently reusing a stale frozen value. The final
359
+ // pooled fallback is reachable only when parentDefaults.thinking is
360
+ // undefined (headless parent without a thinking level).
281
361
  const thinkingRaw =
282
362
  t.thinking ??
283
363
  agentOverride?.thinking ??
284
364
  agent?.thinking ??
285
- (isPoolHit ? pooledConfig?.thinking : undefined) ??
365
+ (isPoolHit && !isDefaultAgent ? pooledConfig?.thinking : undefined) ??
286
366
  modelSuffix ??
367
+ (isDefaultAgent ? parentDefaults.thinking : undefined) ??
368
+ (isPoolHit ? pooledConfig?.thinking : undefined) ??
287
369
  "off";
288
370
  thinking = VALID_THINKING.has(thinkingRaw)
289
371
  ? (thinkingRaw as ThinkingLevel)
@@ -307,10 +389,11 @@ export function resolveTasks(
307
389
  // Empty only for close/list actions (validated above) — downstream
308
390
  // display code treats "" and absent alike (`t.prompt || …`).
309
391
  prompt: prompt ?? "",
310
- // Display label for ad-hoc subagents (no named profile). NOTE: the
311
- // config-namespace key at resolveModelSpec stays "inline" that's a
312
- // delegate.json/settings contract, not a display string (friction #4).
313
- agentName: agent?.name ?? "ad-hoc",
392
+ // Keep the built-in selector visible in progress/results. Omitted-agent
393
+ // inline tasks retain the established `ad-hoc` label and config namespace.
394
+ agentName: isDefaultAgent
395
+ ? DEFAULT_AGENT_NAME
396
+ : (agent?.name ?? "ad-hoc"),
314
397
  warnings,
315
398
  reuseIntent: {
316
399
  model: requestedModel,
package/types.ts CHANGED
@@ -65,6 +65,14 @@ export interface AsyncTicket {
65
65
  waiters?: TicketWaiter[];
66
66
  }
67
67
 
68
+ /** Live parent settings captured when a delegate call starts. The built-in
69
+ * `default` profile mirrors these settings, limited to tools delegate can
70
+ * safely recreate without loading the parent's extensions. */
71
+ export interface ParentAgentDefaults {
72
+ thinking: ThinkingLevel;
73
+ tools: string[];
74
+ }
75
+
68
76
  export interface ReuseIntent {
69
77
  /** Explicit model requested by this call/profile; omitted means use frozen. */
70
78
  model?: Model<Api>;