@herbertgao/pi-subagents 0.15.5 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -48,6 +48,7 @@ import {
48
48
  getDefaultMaxTurns,
49
49
  getGraceTurns,
50
50
  normalizeMaxTurns,
51
+ resolveEffectiveMaxTurns,
51
52
  SUBAGENT_TOOL_NAMES,
52
53
  setDefaultMaxTurns,
53
54
  setGraceTurns,
@@ -72,6 +73,7 @@ import { type RpcHandle, registerRpcHandlers } from "./cross-extension-rpc.js"
72
73
  import { loadCustomAgents } from "./custom-agents.js"
73
74
  import { GroupJoinManager } from "./group-join.js"
74
75
  import {
76
+ isolationParam,
75
77
  resolveAgentInvocationConfig,
76
78
  resolveJoinMode,
77
79
  } from "./invocation-config.js"
@@ -120,6 +122,7 @@ import {
120
122
  buildInvocationTags,
121
123
  describeActivity,
122
124
  fgPreservingNestedStyles,
125
+ formatCost,
123
126
  formatDuration,
124
127
  formatMs,
125
128
  formatTokens,
@@ -134,11 +137,17 @@ import { FleetList, type FleetUICtx } from "./ui/fleet-list.js"
134
137
  import { showSchedulesMenu } from "./ui/schedule-menu.js"
135
138
  import { selectItem } from "./ui/select-item.js"
136
139
  import {
137
- addUsage,
140
+ getLifetimeCost,
138
141
  getLifetimeTotal,
139
142
  getSessionContextPercent,
140
143
  type LifetimeUsage,
144
+ PendingUsagePool,
145
+ toReportedUsage,
141
146
  } from "./usage.js"
147
+ import {
148
+ isWorktreeIsolationEnabled,
149
+ setWorktreeIsolationEnabled,
150
+ } from "./worktree.js"
142
151
 
143
152
  // ---- Shared helpers ----
144
153
 
@@ -186,7 +195,6 @@ function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
186
195
  maxTurns,
187
196
  responseText: "",
188
197
  session: undefined,
189
- lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
190
198
  }
191
199
 
192
200
  const callbacks = {
@@ -218,12 +226,7 @@ function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
218
226
  onSessionCreated: (session: any) => {
219
227
  state.session = session
220
228
  },
221
- onAssistantUsage: (usage: {
222
- input: number
223
- output: number
224
- cacheWrite: number
225
- }) => {
226
- addUsage(state.lifetimeUsage, usage)
229
+ onAssistantUsage: (_usage: LifetimeUsage) => {
227
230
  onStreamUpdate?.()
228
231
  },
229
232
  }
@@ -273,6 +276,7 @@ function escapeXml(s: string): string {
273
276
  function formatTaskNotification(
274
277
  record: AgentRecord,
275
278
  resultMaxLen: number,
279
+ showCost = false,
276
280
  ): string {
277
281
  const status = getStatusLabel(record.status, record.error)
278
282
  const durationMs = record.completedAt
@@ -287,6 +291,11 @@ function formatTaskNotification(
287
291
  const compactXml = record.compactionCount
288
292
  ? `<compactions>${record.compactionCount}</compactions>`
289
293
  : ""
294
+ const cost = showCost ? getLifetimeCost(record.lifetimeUsage) : 0
295
+ const costXml =
296
+ cost > 0
297
+ ? `<estimated_cost_usd>${cost.toFixed(4)}</estimated_cost_usd>`
298
+ : ""
290
299
 
291
300
  const resultPreview = record.result
292
301
  ? record.result.length > resultMaxLen
@@ -307,7 +316,7 @@ function formatTaskNotification(
307
316
  `<status>${escapeXml(status)}</status>`,
308
317
  `<summary>Agent "${escapeXml(record.description)}" ${record.status}${getStatusNote(record.status)}</summary>`,
309
318
  `<result>${escapeXml(resultPreview)}</result>`,
310
- `<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}<duration_ms>${durationMs}</duration_ms></usage>`,
319
+ `<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}${costXml}<duration_ms>${durationMs}</duration_ms></usage>`,
311
320
  `</task-notification>`,
312
321
  ]
313
322
  .filter(Boolean)
@@ -337,6 +346,7 @@ function buildDetails(
337
346
  ...base,
338
347
  toolUses: record.toolUses,
339
348
  tokens: formatLifetimeTokens(record),
349
+ cost: getLifetimeCost(record.lifetimeUsage),
340
350
  turnCount: activity?.turnCount,
341
351
  maxTurns: activity?.maxTurns,
342
352
  durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
@@ -363,6 +373,7 @@ function buildNotificationDetails(
363
373
  turnCount: activity?.turnCount ?? 0,
364
374
  maxTurns: activity?.maxTurns,
365
375
  totalTokens,
376
+ totalCost: getLifetimeCost(record.lifetimeUsage),
366
377
  durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
367
378
  outputFile: record.outputFile,
368
379
  error: record.error,
@@ -445,6 +456,10 @@ export default function (pi: ExtensionAPI) {
445
456
  if (d.toolUses > 0)
446
457
  parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`)
447
458
  if (d.totalTokens > 0) parts.push(formatTokens(d.totalTokens))
459
+ if (showCost) {
460
+ const costText = formatCost(d.totalCost ?? 0)
461
+ if (costText) parts.push(costText)
462
+ }
448
463
  if (d.durationMs > 0) parts.push(formatMs(d.durationMs))
449
464
  if (parts.length) {
450
465
  line +=
@@ -472,7 +487,22 @@ export default function (pi: ExtensionAPI) {
472
487
  }
473
488
 
474
489
  const all = [d, ...(d.others ?? [])]
475
- return new Text(all.map(renderOne).join("\n"), 0, 0)
490
+ const rendered = all.map(renderOne)
491
+ if (showCost && all.length > 1) {
492
+ const total = formatCost(
493
+ all.reduce((sum, item) => sum + (item.totalCost ?? 0), 0),
494
+ )
495
+ if (total) {
496
+ const tokens = all.reduce((sum, item) => sum + item.totalTokens, 0)
497
+ rendered.unshift(
498
+ theme.fg(
499
+ "dim",
500
+ `${all.length} agents · ${formatTokens(tokens)} · ${total}`,
501
+ ),
502
+ )
503
+ }
504
+ }
505
+ return new Text(rendered.join("\n"), 0, 0)
476
506
  },
477
507
  )
478
508
 
@@ -492,6 +522,26 @@ export default function (pi: ExtensionAPI) {
492
522
  // ---- Agent activity tracking + widget ----
493
523
  const agentActivity = new Map<string, AgentActivity>()
494
524
 
525
+ // ---- Usage reporting ----
526
+ let reportUsage = false
527
+ function isReportUsageEnabled(): boolean {
528
+ return reportUsage
529
+ }
530
+ const pendingUsage = new PendingUsagePool()
531
+ function setReportUsage(enabled: boolean): void {
532
+ reportUsage = enabled
533
+ if (!enabled) pendingUsage.drain()
534
+ }
535
+ let showCost = false
536
+ function isShowCostEnabled(): boolean {
537
+ return showCost
538
+ }
539
+ function setShowCost(enabled: boolean): void {
540
+ showCost = enabled
541
+ widget.update()
542
+ fleet.update()
543
+ }
544
+
495
545
  // ---- Cancellable pending notifications ----
496
546
  // Holds notifications briefly so get_subagent_result can cancel them
497
547
  // before they reach pi.sendMessage (fire-and-forget).
@@ -528,7 +578,7 @@ export default function (pi: ExtensionAPI) {
528
578
  function emitIndividualNudge(record: AgentRecord) {
529
579
  if (record.resultConsumed) return // re-check at send time
530
580
 
531
- const notification = formatTaskNotification(record, 500)
581
+ const notification = formatTaskNotification(record, 500, showCost)
532
582
  const footer = record.outputFile
533
583
  ? `\nFull transcript available at: ${record.outputFile}`
534
584
  : ""
@@ -574,7 +624,7 @@ export default function (pi: ExtensionAPI) {
574
624
  }
575
625
 
576
626
  const notifications = unconsumed
577
- .map((r) => formatTaskNotification(r, 300))
627
+ .map((r) => formatTaskNotification(r, 300, showCost))
578
628
  .join("\n\n")
579
629
  const label = partial
580
630
  ? `${unconsumed.length} agent(s) finished (partial — others still running)`
@@ -618,6 +668,7 @@ export default function (pi: ExtensionAPI) {
618
668
  const total = getLifetimeTotal(u)
619
669
  const tokens =
620
670
  total > 0 ? { input: u.input, output: u.output, total } : undefined
671
+ const usage = toReportedUsage(u)
621
672
  return {
622
673
  id: record.id,
623
674
  type: record.type,
@@ -628,6 +679,7 @@ export default function (pi: ExtensionAPI) {
628
679
  toolUses: record.toolUses,
629
680
  durationMs,
630
681
  tokens,
682
+ usage,
631
683
  }
632
684
  }
633
685
 
@@ -716,6 +768,9 @@ export default function (pi: ExtensionAPI) {
716
768
  compactionCount: record.compactionCount,
717
769
  })
718
770
  },
771
+ (_record, usage) => {
772
+ if (reportUsage) pendingUsage.add(usage)
773
+ },
719
774
  )
720
775
 
721
776
  // Expose manager via Symbol.for() global registry for cross-package access.
@@ -753,7 +808,15 @@ export default function (pi: ExtensionAPI) {
753
808
  reloadCustomAgents()
754
809
  const dispatch = resolveSpawnType(type)
755
810
  if (!dispatch.ok) throw new Error(dispatch.message)
756
- return manager.spawn(piRef, ctxRef, dispatch.type, prompt, safeOptions)
811
+ const { state, callbacks } = createActivityTracker(
812
+ resolveEffectiveMaxTurns(dispatch.type, safeOptions.maxTurns),
813
+ )
814
+ const id = manager.spawn(piRef, ctxRef, dispatch.type, prompt, {
815
+ ...safeOptions,
816
+ ...callbacks,
817
+ })
818
+ agentActivity.set(id, state)
819
+ return id
757
820
  }
758
821
  const registryEntry = {
759
822
  waitForAll: () => manager.waitForAll(),
@@ -863,7 +926,7 @@ export default function (pi: ExtensionAPI) {
863
926
  for (const timer of pendingNudges.values()) clearTimeout(timer)
864
927
  pendingNudges.clear()
865
928
  fleet.dispose()
866
- manager.dispose()
929
+ await manager.dispose()
867
930
  })
868
931
 
869
932
  // Live widget: show running agents above editor.
@@ -875,14 +938,19 @@ export default function (pi: ExtensionAPI) {
875
938
  function getWidgetMode(): WidgetMode {
876
939
  return widgetMode
877
940
  }
878
- const widget = new AgentWidget(manager, agentActivity, getWidgetMode)
941
+ const widget = new AgentWidget(
942
+ manager,
943
+ agentActivity,
944
+ getWidgetMode,
945
+ isShowCostEnabled,
946
+ )
879
947
  function setWidgetMode(m: WidgetMode): void {
880
948
  widgetMode = m
881
949
  widget.update()
882
950
  }
883
951
 
884
952
  // Claude Code-style FleetView: navigable list of main + subagents below the editor.
885
- const fleet = new FleetList(manager, agentActivity)
953
+ const fleet = new FleetList(manager, agentActivity, isShowCostEnabled)
886
954
  let fleetViewEnabled = true
887
955
  function isFleetViewEnabled(): boolean {
888
956
  return fleetViewEnabled
@@ -906,6 +974,14 @@ export default function (pi: ExtensionAPI) {
906
974
  defaultJoinMode = mode
907
975
  }
908
976
 
977
+ let backgroundByDefault = true
978
+ function getBackgroundByDefault(): boolean {
979
+ return backgroundByDefault
980
+ }
981
+ function setBackgroundByDefault(enabled: boolean): void {
982
+ backgroundByDefault = enabled
983
+ }
984
+
909
985
  // Master switch for the schedule subagent feature. Defaults to enabled.
910
986
  // Read once at extension init (before tool registration) so the Agent tool's
911
987
  // param schema reflects the persisted setting. Runtime toggles via /agents
@@ -1044,6 +1120,7 @@ export default function (pi: ExtensionAPI) {
1044
1120
  setDefaultMaxTurns,
1045
1121
  setGraceTurns,
1046
1122
  setDefaultJoinMode,
1123
+ setBackgroundByDefault,
1047
1124
  setSchedulingEnabled,
1048
1125
  setScopeModels: setScopeModelsEnabled,
1049
1126
  setStrictAgentFiles: (enabled) => {
@@ -1054,8 +1131,11 @@ export default function (pi: ExtensionAPI) {
1054
1131
  setFleetView: setFleetViewEnabled,
1055
1132
  setWidgetMode: setWidgetMode,
1056
1133
  setOutputTranscript: setOutputTranscriptDefault,
1134
+ setWorktreeIsolation: setWorktreeIsolationEnabled,
1057
1135
  setMaxSubagentDepth: setMaxSubagentDepth,
1058
1136
  setFallbackSubagent: setFallbackSubagent,
1137
+ setReportUsage,
1138
+ setShowCost,
1059
1139
  },
1060
1140
  (event, payload) => pi.events.emit(event, payload),
1061
1141
  )
@@ -1084,6 +1164,13 @@ export default function (pi: ExtensionAPI) {
1084
1164
  ? `\n- Use \`schedule\` only when the user explicitly asked for scheduled / recurring / delayed execution (e.g. "every Monday", "in an hour"). Don't auto-schedule from vague intent like "monitor X" — run once now or ask.`
1085
1165
  : ""
1086
1166
 
1167
+ const isolationGuideline = isWorktreeIsolationEnabled()
1168
+ ? `\n- Use isolation: "worktree" to give the agent its own git worktree; leave it unset, or pass "off", for none. A worktree cannot see uncommitted or staged changes in the main checkout.`
1169
+ : ""
1170
+ const isolationCompactGuideline = isWorktreeIsolationEnabled()
1171
+ ? `\n- isolation: "worktree" gives the agent its own git worktree; "off" leaves it in the current checkout.`
1172
+ : ""
1173
+
1087
1174
  // Compact Agent tool description (#91, `toolDescriptionMode: "compact"`) —
1088
1175
  // the same load-bearing facts as the full version at ~75% fewer tokens, for
1089
1176
  // small/local models. Per-option details live in the param descriptions.
@@ -1094,10 +1181,10 @@ Custom agents: .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.
1094
1181
 
1095
1182
  Notes:
1096
1183
  - description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
1097
- - Parallel work: one message, multiple Agent calls, run_in_background: true on each. You are notified when background agents finish never poll or sleep.
1184
+ - Parallel work: one message, multiple Agent calls — they run concurrently.
1185
+ - Subagents run in the background by default; you'll be notified when one completes. Pass run_in_background: false only when your next action depends on its result.
1098
1186
  - The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
1099
- - resume continues a previous agent by ID; steer_subagent messages a running one.
1100
- - isolation: "worktree" runs the agent in an isolated git worktree; changes land on a branch.`
1187
+ - resume continues a previous agent by ID; steer_subagent messages a running one.${isolationCompactGuideline}`
1101
1188
 
1102
1189
  const fullAgentToolDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
1103
1190
 
@@ -1115,19 +1202,19 @@ If the target is already known, use a direct tool — \`read\` for a known path,
1115
1202
  ## Usage notes
1116
1203
 
1117
1204
  - Always include a short (3-5 word) description summarizing what the agent will do (shown in UI).
1118
- - When you launch multiple agents for independent work, send them in a single message with multiple tool uses, with run_in_background: true on each, so they run concurrently. If the user specifies that they want agents run "in parallel", you MUST send a single message with multiple tool calls. Foreground calls run sequentially — only one executes at a time.
1205
+ - When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.
1119
1206
  - When the agent is done, it returns a single message back to you. The result is not visible to the user — to show the user, send a text message with a concise summary.
1120
1207
  - Trust but verify: an agent's summary describes what it intended to do, not necessarily what it did. When an agent writes or edits code, check the actual changes before reporting work as done.
1121
- - Use run_in_background for work you don't need immediately. You will be notified when it completes — do NOT poll or sleep waiting for it. Continue with other work or respond to the user instead.
1122
- - Foreground vs background: use foreground (default) when you need the agent's results before you can proceed. Use background when you have genuinely independent work to do in parallel.
1208
+ - Agents run in the background by default. You will be notified when one completes — do NOT poll or sleep waiting for it.
1209
+ - Pass \`run_in_background: false\` only when your very next action depends on the result and nothing else could usefully happen while it runs.
1210
+ - Never fabricate or predict a pending agent's results; if asked before completion, say it is still running.
1123
1211
  - Use resume with an agent ID to continue a previous agent's work. A new (non-resume) Agent call starts a fresh agent with no memory of prior runs, so the prompt must be self-contained.
1124
1212
  - Use steer_subagent to send mid-run messages to a running background agent.
1125
1213
  - Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
1126
1214
  - If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
1127
1215
  - Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
1128
1216
  - Use thinking to control extended thinking level.
1129
- - Use inherit_context if the agent needs the parent conversation history.
1130
- - Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications). The worktree is automatically cleaned up if the agent makes no changes; otherwise the path and branch are returned in the result.${scheduleGuideline}
1217
+ - Use inherit_context if the agent needs the parent conversation history.${isolationGuideline}${scheduleGuideline}
1131
1218
 
1132
1219
  ## Writing the prompt
1133
1220
 
@@ -1151,6 +1238,7 @@ Terse command-style prompts produce shallow, generic work.
1151
1238
  typeList: buildTypeListText,
1152
1239
  compactTypeList: buildCompactTypeListText,
1153
1240
  agentDir: getAgentDir,
1241
+ isolationGuideline: () => isolationGuideline,
1154
1242
  scheduleGuideline: () => scheduleGuideline,
1155
1243
  }
1156
1244
  // Replacement callback (not a string) — agent descriptions may contain `$&` etc.
@@ -1195,7 +1283,19 @@ Terse command-style prompts produce shallow, generic work.
1195
1283
  return fullAgentToolDescription
1196
1284
  })()
1197
1285
 
1198
- pi.registerTool(
1286
+ function registerToolReportingUsage(tool: any): void {
1287
+ pi.registerTool({
1288
+ ...tool,
1289
+ execute: async (toolCallId: string | undefined, ...args: any[]) => {
1290
+ const result = await tool.execute(toolCallId, ...args)
1291
+ if (!reportUsage || !toolCallId) return result
1292
+ const usage = pendingUsage.drain()
1293
+ return usage ? { ...result, usage } : result
1294
+ },
1295
+ })
1296
+ }
1297
+
1298
+ registerToolReportingUsage(
1199
1299
  defineTool({
1200
1300
  name: SUBAGENT_TOOL_NAMES.AGENT,
1201
1301
  label: "Agent",
@@ -1240,13 +1340,13 @@ Terse command-style prompts produce shallow, generic work.
1240
1340
  run_in_background: Type.Optional(
1241
1341
  Type.Boolean({
1242
1342
  description:
1243
- "Set to true to run in background. Returns agent ID immediately. You will be notified on completion.",
1343
+ "Defaults to true: returns the agent ID immediately and notifies on completion. Set false to block and return the full output inline.",
1244
1344
  }),
1245
1345
  ),
1246
1346
  resume: Type.Optional(
1247
1347
  Type.String({
1248
1348
  description:
1249
- "Optional agent ID to resume from. Continues from previous context. Combine with run_in_background to resume detached and be notified on completion. An agent can only be resumed once its current run has finished — use steer_subagent to reach one mid-run.",
1349
+ "Optional agent ID to resume from. Resumes detached by default; pass run_in_background: false to block and get the result inline. An agent can only be resumed after its current run finishes.",
1250
1350
  }),
1251
1351
  ),
1252
1352
  isolated: Type.Optional(
@@ -1261,12 +1361,7 @@ Terse command-style prompts produce shallow, generic work.
1261
1361
  "If true, fork parent conversation into the agent. Default: false (fresh context).",
1262
1362
  }),
1263
1363
  ),
1264
- isolation: Type.Optional(
1265
- Type.Literal("worktree", {
1266
- description:
1267
- 'Set to "worktree" to run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
1268
- }),
1269
- ),
1364
+ ...isolationParam(isWorktreeIsolationEnabled()),
1270
1365
  ...scheduleParam,
1271
1366
  }),
1272
1367
 
@@ -1325,6 +1420,10 @@ Terse command-style prompts produce shallow, generic work.
1325
1420
  if (d.toolUses > 0)
1326
1421
  parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`)
1327
1422
  if (d.tokens) parts.push(d.tokens)
1423
+ if (showCost) {
1424
+ const costText = formatCost(d.cost ?? 0)
1425
+ if (costText) parts.push(costText)
1426
+ }
1328
1427
  return parts
1329
1428
  .map((p) => fgPreservingNestedStyles(theme, "dim", p))
1330
1429
  .join(" " + theme.fg("dim", "·") + " ")
@@ -1460,6 +1559,10 @@ Terse command-style prompts produce shallow, generic work.
1460
1559
  const resolvedConfig = resolveAgentInvocationConfig(
1461
1560
  customConfig,
1462
1561
  params,
1562
+ {
1563
+ worktreeAllowed: isWorktreeIsolationEnabled(),
1564
+ defaultRunInBackground: getBackgroundByDefault(),
1565
+ },
1463
1566
  )
1464
1567
 
1465
1568
  // Resolve model from agent config first; tool-call params only fill gaps.
@@ -1890,7 +1993,10 @@ Terse command-style prompts produce shallow, generic work.
1890
1993
  const details: AgentDetails = {
1891
1994
  ...detailBase,
1892
1995
  toolUses: fgState.toolUses,
1893
- tokens: formatLifetimeTokens(fgState),
1996
+ tokens: fgId ? formatLifetimeTokens(manager.getRecord(fgId)!) : "",
1997
+ cost: fgId
1998
+ ? getLifetimeCost(manager.getRecord(fgId)?.lifetimeUsage)
1999
+ : 0,
1894
2000
  turnCount: fgState.turnCount,
1895
2001
  maxTurns: fgState.maxTurns,
1896
2002
  durationMs: Date.now() - startedAt,
@@ -1990,7 +2096,7 @@ Terse command-style prompts produce shallow, generic work.
1990
2096
  }
1991
2097
 
1992
2098
  // Get final token count
1993
- const tokenText = formatLifetimeTokens(fgState)
2099
+ const tokenText = formatLifetimeTokens(record)
1994
2100
 
1995
2101
  const details = buildDetails(detailBase, record, fgState, {
1996
2102
  tokens: tokenText,
@@ -2007,6 +2113,10 @@ Terse command-style prompts produce shallow, generic work.
2007
2113
  const durationMs = (record.completedAt ?? Date.now()) - record.startedAt
2008
2114
  const statsParts = [`${record.toolUses} tool uses`]
2009
2115
  if (tokenText) statsParts.push(tokenText)
2116
+ if (showCost) {
2117
+ const costText = formatCost(getLifetimeCost(record.lifetimeUsage))
2118
+ if (costText) statsParts.push(costText)
2119
+ }
2010
2120
  return textResult(
2011
2121
  `${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getForegroundOutcomeNote(record.status)}.\n\n` +
2012
2122
  (record.result?.trim() || "No output."),
@@ -2018,7 +2128,7 @@ Terse command-style prompts produce shallow, generic work.
2018
2128
 
2019
2129
  // ---- get_subagent_result tool ----
2020
2130
 
2021
- pi.registerTool(
2131
+ registerToolReportingUsage(
2022
2132
  defineTool({
2023
2133
  name: SUBAGENT_TOOL_NAMES.GET_RESULT,
2024
2134
  label: "Get Agent Result",
@@ -2077,6 +2187,10 @@ Terse command-style prompts produce shallow, generic work.
2077
2187
  const contextPercent = getSessionContextPercent(record.session)
2078
2188
  const statsParts = [`Tool uses: ${record.toolUses}`]
2079
2189
  if (tokens) statsParts.push(tokens)
2190
+ if (showCost) {
2191
+ const costText = formatCost(getLifetimeCost(record.lifetimeUsage))
2192
+ if (costText) statsParts.push(`Cost: ${costText}`)
2193
+ }
2080
2194
  if (contextPercent !== null)
2081
2195
  statsParts.push(`Context: ${Math.round(contextPercent)}%`)
2082
2196
  if (record.compactionCount)
@@ -2118,7 +2232,7 @@ Terse command-style prompts produce shallow, generic work.
2118
2232
 
2119
2233
  // ---- steer_subagent tool ----
2120
2234
 
2121
- pi.registerTool(
2235
+ registerToolReportingUsage(
2122
2236
  defineTool({
2123
2237
  name: SUBAGENT_TOOL_NAMES.STEER,
2124
2238
  label: "Steer Agent",
@@ -2422,6 +2536,7 @@ Terse command-style prompts produce shallow, generic work.
2422
2536
  },
2423
2537
  keybindings,
2424
2538
  (message: string) => manager.steer(record.id, message),
2539
+ showCost,
2425
2540
  )
2426
2541
  },
2427
2542
  {
@@ -2680,11 +2795,14 @@ extensions: <true (inherit all MCP/extension tools), false (none), or comma-sepa
2680
2795
  skills: <true (inherit all), false (none), or comma-separated skill names to preload into prompt. Default: true>
2681
2796
  disallowed_tools: <comma-separated tool names to block, even if otherwise available. Omit for none>
2682
2797
  inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
2683
- run_in_background: <true to run in background by default. Default: false>
2798
+ run_in_background: <pin this agent to background (true) or foreground (false). Omit to follow backgroundByDefault>
2684
2799
  output_transcript: <false to write no transcript file or path for this agent. Independent of persist_session. Default: true>
2685
2800
  isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
2686
- memory: <"user" (global), "project" (per-project), or "local" (gitignored per-project) for persistent memory. Omit for none>
2687
- isolation: <"worktree" to run in isolated git worktree. Omit for normal>
2801
+ memory: <"user" (global), "project" (per-project), or "local" (gitignored per-project) for persistent memory. Omit for none>${
2802
+ isWorktreeIsolationEnabled()
2803
+ ? `\nisolation: <"worktree" to run in an isolated git worktree; "off" to refuse one. Omit for normal>`
2804
+ : ""
2805
+ }
2688
2806
  ---
2689
2807
 
2690
2808
  <system prompt body — instructions for the agent>
@@ -2839,6 +2957,7 @@ Write the file using the write tool. Only write the file, nothing else.`
2839
2957
  defaultMaxTurns: getDefaultMaxTurns() ?? 0,
2840
2958
  graceTurns: getGraceTurns(),
2841
2959
  defaultJoinMode: getDefaultJoinMode(),
2960
+ backgroundByDefault: getBackgroundByDefault(),
2842
2961
  schedulingEnabled: isSchedulingEnabled(),
2843
2962
  scopeModels: isScopeModelsEnabled(),
2844
2963
  strictAgentFiles,
@@ -2847,12 +2966,15 @@ Write the file using the write tool. Only write the file, nothing else.`
2847
2966
  fleetView: isFleetViewEnabled(),
2848
2967
  widgetMode: getWidgetMode(),
2849
2968
  outputTranscript: getOutputTranscriptDefault(),
2969
+ worktreeIsolation: isWorktreeIsolationEnabled(),
2850
2970
  maxSubagentDepth: getMaxSubagentDepth(),
2851
2971
  // Deliberately NOT `?? "general-purpose"`: every settings change writes the
2852
2972
  // whole snapshot, and materializing the implicit default would turn it into
2853
2973
  // explicit configuration — which then fails loudly if general-purpose later
2854
2974
  // goes away. undefined is dropped by JSON.stringify.
2855
2975
  fallbackSubagent: getFallbackSubagent(),
2976
+ reportUsage: isReportUsageEnabled(),
2977
+ showCost: isShowCostEnabled(),
2856
2978
  } satisfies SubagentsSettings
2857
2979
  }
2858
2980
 
@@ -2929,6 +3051,14 @@ Write the file using the write tool. Only write the file, nothing else.`
2929
3051
  currentValue: getDefaultJoinMode(),
2930
3052
  values: ["smart", "async", "group"],
2931
3053
  },
3054
+ {
3055
+ id: "backgroundByDefault",
3056
+ label: "Background by default",
3057
+ description:
3058
+ "Unqualified top-level Agent calls run detached (off = block and return inline)",
3059
+ currentValue: getBackgroundByDefault() ? "on" : "off",
3060
+ values: ["on", "off"],
3061
+ },
2932
3062
  {
2933
3063
  id: "schedulingEnabled",
2934
3064
  label: "Scheduling",
@@ -2976,6 +3106,28 @@ Write the file using the write tool. Only write the file, nothing else.`
2976
3106
  currentValue: getOutputTranscriptDefault() ? "on" : "off",
2977
3107
  values: ["on", "off"],
2978
3108
  },
3109
+ {
3110
+ id: "worktreeIsolation",
3111
+ label: "Worktree isolation",
3112
+ description:
3113
+ "Allow isolation: worktree (off removes the Agent parameter on next pi session)",
3114
+ currentValue: isWorktreeIsolationEnabled() ? "on" : "off",
3115
+ values: ["on", "off"],
3116
+ },
3117
+ {
3118
+ id: "reportUsage",
3119
+ label: "Report usage",
3120
+ description: "Include subagent spend in parent session accounting",
3121
+ currentValue: isReportUsageEnabled() ? "on" : "off",
3122
+ values: ["on", "off"],
3123
+ },
3124
+ {
3125
+ id: "showCost",
3126
+ label: "Show cost",
3127
+ description: "Show estimated USD cost beside subagent token counts",
3128
+ currentValue: isShowCostEnabled() ? "on" : "off",
3129
+ values: ["on", "off"],
3130
+ },
2979
3131
  {
2980
3132
  id: "fleetView",
2981
3133
  label: "Fleet view",
@@ -3039,6 +3191,15 @@ Write the file using the write tool. Only write the file, nothing else.`
3039
3191
  } else if (id === "joinMode") {
3040
3192
  setDefaultJoinMode(value as JoinMode)
3041
3193
  notifyApplied(ctx, `Default join mode set to ${value}`)
3194
+ } else if (id === "backgroundByDefault") {
3195
+ const enabled = value === "on"
3196
+ setBackgroundByDefault(enabled)
3197
+ notifyApplied(
3198
+ ctx,
3199
+ enabled
3200
+ ? "Agent calls run in the background unless explicitly set false"
3201
+ : "Agent calls block unless explicitly set true",
3202
+ )
3042
3203
  } else if (id === "schedulingEnabled") {
3043
3204
  const enabled = value === "on"
3044
3205
  if (enabled === isSchedulingEnabled()) {
@@ -3087,6 +3248,24 @@ Write the file using the write tool. Only write the file, nothing else.`
3087
3248
  ctx,
3088
3249
  `Output transcript ${enabled ? "enabled" : "disabled"} by default`,
3089
3250
  )
3251
+ } else if (id === "worktreeIsolation") {
3252
+ const enabled = value === "on"
3253
+ setWorktreeIsolationEnabled(enabled)
3254
+ notifyApplied(
3255
+ ctx,
3256
+ `Worktree isolation ${enabled ? "enabled" : "disabled"}. Tool parameter updates on next pi session.`,
3257
+ )
3258
+ } else if (id === "reportUsage") {
3259
+ const enabled = value === "on"
3260
+ setReportUsage(enabled)
3261
+ notifyApplied(
3262
+ ctx,
3263
+ `Usage reporting ${enabled ? "enabled" : "disabled"}`,
3264
+ )
3265
+ } else if (id === "showCost") {
3266
+ const enabled = value === "on"
3267
+ setShowCost(enabled)
3268
+ notifyApplied(ctx, `Cost display ${enabled ? "enabled" : "disabled"}`)
3090
3269
  } else if (id === "toolDescriptionMode") {
3091
3270
  setToolDescriptionMode(value as ToolDescriptionMode)
3092
3271
  notifyApplied(