@herbertgao/pi-subagents 0.15.5 → 0.16.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/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(),
@@ -827,6 +890,20 @@ export default function (pi: ExtensionAPI) {
827
890
  const record = manager.getRecord(id)
828
891
  return !record?.parentAgentId && manager.abort(id)
829
892
  },
893
+ consumeResult: (id) => {
894
+ const record = manager.getRecord(id)
895
+ if (
896
+ !record ||
897
+ record.parentAgentId ||
898
+ record.status === "running" ||
899
+ record.status === "queued"
900
+ ) {
901
+ return false
902
+ }
903
+ record.resultConsumed = true
904
+ cancelNudge(record.id)
905
+ return true
906
+ },
830
907
  },
831
908
  })
832
909
  // Broadcast readiness so extensions loaded alongside us can discover us.
@@ -848,6 +925,7 @@ export default function (pi: ExtensionAPI) {
848
925
  rpcHandle?.unsubSpawn()
849
926
  rpcHandle?.unsubStop()
850
927
  rpcHandle?.unsubPing()
928
+ rpcHandle?.unsubConsume()
851
929
  rpcHandle = undefined
852
930
  currentCtx = undefined
853
931
  // Only release the global slot if this activation claimed it — a child
@@ -863,7 +941,7 @@ export default function (pi: ExtensionAPI) {
863
941
  for (const timer of pendingNudges.values()) clearTimeout(timer)
864
942
  pendingNudges.clear()
865
943
  fleet.dispose()
866
- manager.dispose()
944
+ await manager.dispose()
867
945
  })
868
946
 
869
947
  // Live widget: show running agents above editor.
@@ -875,14 +953,19 @@ export default function (pi: ExtensionAPI) {
875
953
  function getWidgetMode(): WidgetMode {
876
954
  return widgetMode
877
955
  }
878
- const widget = new AgentWidget(manager, agentActivity, getWidgetMode)
956
+ const widget = new AgentWidget(
957
+ manager,
958
+ agentActivity,
959
+ getWidgetMode,
960
+ isShowCostEnabled,
961
+ )
879
962
  function setWidgetMode(m: WidgetMode): void {
880
963
  widgetMode = m
881
964
  widget.update()
882
965
  }
883
966
 
884
967
  // Claude Code-style FleetView: navigable list of main + subagents below the editor.
885
- const fleet = new FleetList(manager, agentActivity)
968
+ const fleet = new FleetList(manager, agentActivity, isShowCostEnabled)
886
969
  let fleetViewEnabled = true
887
970
  function isFleetViewEnabled(): boolean {
888
971
  return fleetViewEnabled
@@ -906,6 +989,14 @@ export default function (pi: ExtensionAPI) {
906
989
  defaultJoinMode = mode
907
990
  }
908
991
 
992
+ let backgroundByDefault = true
993
+ function getBackgroundByDefault(): boolean {
994
+ return backgroundByDefault
995
+ }
996
+ function setBackgroundByDefault(enabled: boolean): void {
997
+ backgroundByDefault = enabled
998
+ }
999
+
909
1000
  // Master switch for the schedule subagent feature. Defaults to enabled.
910
1001
  // Read once at extension init (before tool registration) so the Agent tool's
911
1002
  // param schema reflects the persisted setting. Runtime toggles via /agents
@@ -992,6 +1083,7 @@ export default function (pi: ExtensionAPI) {
992
1083
  // Grab UI context from first tool execution + clear lingering widget on new turn
993
1084
  pi.on("tool_execution_start", async (_event, ctx) => {
994
1085
  widget.setUICtx(ctx.ui as UICtx)
1086
+ // SAFETY: both UI adapters receive the same Pi ExtensionContext UI surface.
995
1087
  fleet.setUICtx(ctx.ui as unknown as FleetUICtx)
996
1088
  widget.onTurnStart()
997
1089
  })
@@ -1044,6 +1136,7 @@ export default function (pi: ExtensionAPI) {
1044
1136
  setDefaultMaxTurns,
1045
1137
  setGraceTurns,
1046
1138
  setDefaultJoinMode,
1139
+ setBackgroundByDefault,
1047
1140
  setSchedulingEnabled,
1048
1141
  setScopeModels: setScopeModelsEnabled,
1049
1142
  setStrictAgentFiles: (enabled) => {
@@ -1054,8 +1147,11 @@ export default function (pi: ExtensionAPI) {
1054
1147
  setFleetView: setFleetViewEnabled,
1055
1148
  setWidgetMode: setWidgetMode,
1056
1149
  setOutputTranscript: setOutputTranscriptDefault,
1150
+ setWorktreeIsolation: setWorktreeIsolationEnabled,
1057
1151
  setMaxSubagentDepth: setMaxSubagentDepth,
1058
1152
  setFallbackSubagent: setFallbackSubagent,
1153
+ setReportUsage,
1154
+ setShowCost,
1059
1155
  },
1060
1156
  (event, payload) => pi.events.emit(event, payload),
1061
1157
  )
@@ -1084,6 +1180,13 @@ export default function (pi: ExtensionAPI) {
1084
1180
  ? `\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
1181
  : ""
1086
1182
 
1183
+ const isolationGuideline = isWorktreeIsolationEnabled()
1184
+ ? `\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.`
1185
+ : ""
1186
+ const isolationCompactGuideline = isWorktreeIsolationEnabled()
1187
+ ? `\n- isolation: "worktree" gives the agent its own git worktree; "off" leaves it in the current checkout.`
1188
+ : ""
1189
+
1087
1190
  // Compact Agent tool description (#91, `toolDescriptionMode: "compact"`) —
1088
1191
  // the same load-bearing facts as the full version at ~75% fewer tokens, for
1089
1192
  // small/local models. Per-option details live in the param descriptions.
@@ -1094,10 +1197,10 @@ Custom agents: .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.
1094
1197
 
1095
1198
  Notes:
1096
1199
  - 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.
1200
+ - Parallel work: one message, multiple Agent calls — they run concurrently.
1201
+ - 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
1202
  - 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.`
1203
+ - resume continues a previous agent by ID; steer_subagent messages a running one.${isolationCompactGuideline}`
1101
1204
 
1102
1205
  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
1206
 
@@ -1115,19 +1218,19 @@ If the target is already known, use a direct tool — \`read\` for a known path,
1115
1218
  ## Usage notes
1116
1219
 
1117
1220
  - 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.
1221
+ - When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.
1119
1222
  - 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
1223
  - 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.
1224
+ - Agents run in the background by default. You will be notified when one completes — do NOT poll or sleep waiting for it.
1225
+ - Pass \`run_in_background: false\` only when your very next action depends on the result and nothing else could usefully happen while it runs.
1226
+ - Never fabricate or predict a pending agent's results; if asked before completion, say it is still running.
1123
1227
  - 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
1228
  - Use steer_subagent to send mid-run messages to a running background agent.
1125
1229
  - 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
1230
  - 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
1231
  - Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
1128
1232
  - 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}
1233
+ - Use inherit_context if the agent needs the parent conversation history.${isolationGuideline}${scheduleGuideline}
1131
1234
 
1132
1235
  ## Writing the prompt
1133
1236
 
@@ -1151,6 +1254,7 @@ Terse command-style prompts produce shallow, generic work.
1151
1254
  typeList: buildTypeListText,
1152
1255
  compactTypeList: buildCompactTypeListText,
1153
1256
  agentDir: getAgentDir,
1257
+ isolationGuideline: () => isolationGuideline,
1154
1258
  scheduleGuideline: () => scheduleGuideline,
1155
1259
  }
1156
1260
  // Replacement callback (not a string) — agent descriptions may contain `$&` etc.
@@ -1195,7 +1299,19 @@ Terse command-style prompts produce shallow, generic work.
1195
1299
  return fullAgentToolDescription
1196
1300
  })()
1197
1301
 
1198
- pi.registerTool(
1302
+ function registerToolReportingUsage(tool: any): void {
1303
+ pi.registerTool({
1304
+ ...tool,
1305
+ execute: async (toolCallId: string | undefined, ...args: any[]) => {
1306
+ const result = await tool.execute(toolCallId, ...args)
1307
+ if (!reportUsage || !toolCallId) return result
1308
+ const usage = pendingUsage.drain()
1309
+ return usage ? { ...result, usage } : result
1310
+ },
1311
+ })
1312
+ }
1313
+
1314
+ registerToolReportingUsage(
1199
1315
  defineTool({
1200
1316
  name: SUBAGENT_TOOL_NAMES.AGENT,
1201
1317
  label: "Agent",
@@ -1240,13 +1356,13 @@ Terse command-style prompts produce shallow, generic work.
1240
1356
  run_in_background: Type.Optional(
1241
1357
  Type.Boolean({
1242
1358
  description:
1243
- "Set to true to run in background. Returns agent ID immediately. You will be notified on completion.",
1359
+ "Defaults to true: returns the agent ID immediately and notifies on completion. Set false to block and return the full output inline.",
1244
1360
  }),
1245
1361
  ),
1246
1362
  resume: Type.Optional(
1247
1363
  Type.String({
1248
1364
  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.",
1365
+ "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
1366
  }),
1251
1367
  ),
1252
1368
  isolated: Type.Optional(
@@ -1261,12 +1377,7 @@ Terse command-style prompts produce shallow, generic work.
1261
1377
  "If true, fork parent conversation into the agent. Default: false (fresh context).",
1262
1378
  }),
1263
1379
  ),
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
- ),
1380
+ ...isolationParam(isWorktreeIsolationEnabled()),
1270
1381
  ...scheduleParam,
1271
1382
  }),
1272
1383
 
@@ -1325,6 +1436,10 @@ Terse command-style prompts produce shallow, generic work.
1325
1436
  if (d.toolUses > 0)
1326
1437
  parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`)
1327
1438
  if (d.tokens) parts.push(d.tokens)
1439
+ if (showCost) {
1440
+ const costText = formatCost(d.cost ?? 0)
1441
+ if (costText) parts.push(costText)
1442
+ }
1328
1443
  return parts
1329
1444
  .map((p) => fgPreservingNestedStyles(theme, "dim", p))
1330
1445
  .join(" " + theme.fg("dim", "·") + " ")
@@ -1460,6 +1575,10 @@ Terse command-style prompts produce shallow, generic work.
1460
1575
  const resolvedConfig = resolveAgentInvocationConfig(
1461
1576
  customConfig,
1462
1577
  params,
1578
+ {
1579
+ worktreeAllowed: isWorktreeIsolationEnabled(),
1580
+ defaultRunInBackground: getBackgroundByDefault(),
1581
+ },
1463
1582
  )
1464
1583
 
1465
1584
  // Resolve model from agent config first; tool-call params only fill gaps.
@@ -1890,7 +2009,10 @@ Terse command-style prompts produce shallow, generic work.
1890
2009
  const details: AgentDetails = {
1891
2010
  ...detailBase,
1892
2011
  toolUses: fgState.toolUses,
1893
- tokens: formatLifetimeTokens(fgState),
2012
+ tokens: fgId ? formatLifetimeTokens(manager.getRecord(fgId)!) : "",
2013
+ cost: fgId
2014
+ ? getLifetimeCost(manager.getRecord(fgId)?.lifetimeUsage)
2015
+ : 0,
1894
2016
  turnCount: fgState.turnCount,
1895
2017
  maxTurns: fgState.maxTurns,
1896
2018
  durationMs: Date.now() - startedAt,
@@ -1990,7 +2112,7 @@ Terse command-style prompts produce shallow, generic work.
1990
2112
  }
1991
2113
 
1992
2114
  // Get final token count
1993
- const tokenText = formatLifetimeTokens(fgState)
2115
+ const tokenText = formatLifetimeTokens(record)
1994
2116
 
1995
2117
  const details = buildDetails(detailBase, record, fgState, {
1996
2118
  tokens: tokenText,
@@ -2007,6 +2129,10 @@ Terse command-style prompts produce shallow, generic work.
2007
2129
  const durationMs = (record.completedAt ?? Date.now()) - record.startedAt
2008
2130
  const statsParts = [`${record.toolUses} tool uses`]
2009
2131
  if (tokenText) statsParts.push(tokenText)
2132
+ if (showCost) {
2133
+ const costText = formatCost(getLifetimeCost(record.lifetimeUsage))
2134
+ if (costText) statsParts.push(costText)
2135
+ }
2010
2136
  return textResult(
2011
2137
  `${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getForegroundOutcomeNote(record.status)}.\n\n` +
2012
2138
  (record.result?.trim() || "No output."),
@@ -2018,7 +2144,7 @@ Terse command-style prompts produce shallow, generic work.
2018
2144
 
2019
2145
  // ---- get_subagent_result tool ----
2020
2146
 
2021
- pi.registerTool(
2147
+ registerToolReportingUsage(
2022
2148
  defineTool({
2023
2149
  name: SUBAGENT_TOOL_NAMES.GET_RESULT,
2024
2150
  label: "Get Agent Result",
@@ -2077,6 +2203,10 @@ Terse command-style prompts produce shallow, generic work.
2077
2203
  const contextPercent = getSessionContextPercent(record.session)
2078
2204
  const statsParts = [`Tool uses: ${record.toolUses}`]
2079
2205
  if (tokens) statsParts.push(tokens)
2206
+ if (showCost) {
2207
+ const costText = formatCost(getLifetimeCost(record.lifetimeUsage))
2208
+ if (costText) statsParts.push(`Cost: ${costText}`)
2209
+ }
2080
2210
  if (contextPercent !== null)
2081
2211
  statsParts.push(`Context: ${Math.round(contextPercent)}%`)
2082
2212
  if (record.compactionCount)
@@ -2118,7 +2248,7 @@ Terse command-style prompts produce shallow, generic work.
2118
2248
 
2119
2249
  // ---- steer_subagent tool ----
2120
2250
 
2121
- pi.registerTool(
2251
+ registerToolReportingUsage(
2122
2252
  defineTool({
2123
2253
  name: SUBAGENT_TOOL_NAMES.STEER,
2124
2254
  label: "Steer Agent",
@@ -2422,6 +2552,7 @@ Terse command-style prompts produce shallow, generic work.
2422
2552
  },
2423
2553
  keybindings,
2424
2554
  (message: string) => manager.steer(record.id, message),
2555
+ showCost,
2425
2556
  )
2426
2557
  },
2427
2558
  {
@@ -2680,11 +2811,14 @@ extensions: <true (inherit all MCP/extension tools), false (none), or comma-sepa
2680
2811
  skills: <true (inherit all), false (none), or comma-separated skill names to preload into prompt. Default: true>
2681
2812
  disallowed_tools: <comma-separated tool names to block, even if otherwise available. Omit for none>
2682
2813
  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>
2814
+ run_in_background: <pin this agent to background (true) or foreground (false). Omit to follow backgroundByDefault>
2684
2815
  output_transcript: <false to write no transcript file or path for this agent. Independent of persist_session. Default: true>
2685
2816
  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>
2817
+ memory: <"user" (global), "project" (per-project), or "local" (gitignored per-project) for persistent memory. Omit for none>${
2818
+ isWorktreeIsolationEnabled()
2819
+ ? `\nisolation: <"worktree" to run in an isolated git worktree; "off" to refuse one. Omit for normal>`
2820
+ : ""
2821
+ }
2688
2822
  ---
2689
2823
 
2690
2824
  <system prompt body — instructions for the agent>
@@ -2839,6 +2973,7 @@ Write the file using the write tool. Only write the file, nothing else.`
2839
2973
  defaultMaxTurns: getDefaultMaxTurns() ?? 0,
2840
2974
  graceTurns: getGraceTurns(),
2841
2975
  defaultJoinMode: getDefaultJoinMode(),
2976
+ backgroundByDefault: getBackgroundByDefault(),
2842
2977
  schedulingEnabled: isSchedulingEnabled(),
2843
2978
  scopeModels: isScopeModelsEnabled(),
2844
2979
  strictAgentFiles,
@@ -2847,12 +2982,15 @@ Write the file using the write tool. Only write the file, nothing else.`
2847
2982
  fleetView: isFleetViewEnabled(),
2848
2983
  widgetMode: getWidgetMode(),
2849
2984
  outputTranscript: getOutputTranscriptDefault(),
2985
+ worktreeIsolation: isWorktreeIsolationEnabled(),
2850
2986
  maxSubagentDepth: getMaxSubagentDepth(),
2851
2987
  // Deliberately NOT `?? "general-purpose"`: every settings change writes the
2852
2988
  // whole snapshot, and materializing the implicit default would turn it into
2853
2989
  // explicit configuration — which then fails loudly if general-purpose later
2854
2990
  // goes away. undefined is dropped by JSON.stringify.
2855
2991
  fallbackSubagent: getFallbackSubagent(),
2992
+ reportUsage: isReportUsageEnabled(),
2993
+ showCost: isShowCostEnabled(),
2856
2994
  } satisfies SubagentsSettings
2857
2995
  }
2858
2996
 
@@ -2929,6 +3067,14 @@ Write the file using the write tool. Only write the file, nothing else.`
2929
3067
  currentValue: getDefaultJoinMode(),
2930
3068
  values: ["smart", "async", "group"],
2931
3069
  },
3070
+ {
3071
+ id: "backgroundByDefault",
3072
+ label: "Background by default",
3073
+ description:
3074
+ "Unqualified top-level Agent calls run detached (off = block and return inline)",
3075
+ currentValue: getBackgroundByDefault() ? "on" : "off",
3076
+ values: ["on", "off"],
3077
+ },
2932
3078
  {
2933
3079
  id: "schedulingEnabled",
2934
3080
  label: "Scheduling",
@@ -2976,6 +3122,28 @@ Write the file using the write tool. Only write the file, nothing else.`
2976
3122
  currentValue: getOutputTranscriptDefault() ? "on" : "off",
2977
3123
  values: ["on", "off"],
2978
3124
  },
3125
+ {
3126
+ id: "worktreeIsolation",
3127
+ label: "Worktree isolation",
3128
+ description:
3129
+ "Allow isolation: worktree (off removes the Agent parameter on next pi session)",
3130
+ currentValue: isWorktreeIsolationEnabled() ? "on" : "off",
3131
+ values: ["on", "off"],
3132
+ },
3133
+ {
3134
+ id: "reportUsage",
3135
+ label: "Report usage",
3136
+ description: "Include subagent spend in parent session accounting",
3137
+ currentValue: isReportUsageEnabled() ? "on" : "off",
3138
+ values: ["on", "off"],
3139
+ },
3140
+ {
3141
+ id: "showCost",
3142
+ label: "Show cost",
3143
+ description: "Show estimated USD cost beside subagent token counts",
3144
+ currentValue: isShowCostEnabled() ? "on" : "off",
3145
+ values: ["on", "off"],
3146
+ },
2979
3147
  {
2980
3148
  id: "fleetView",
2981
3149
  label: "Fleet view",
@@ -3039,6 +3207,15 @@ Write the file using the write tool. Only write the file, nothing else.`
3039
3207
  } else if (id === "joinMode") {
3040
3208
  setDefaultJoinMode(value as JoinMode)
3041
3209
  notifyApplied(ctx, `Default join mode set to ${value}`)
3210
+ } else if (id === "backgroundByDefault") {
3211
+ const enabled = value === "on"
3212
+ setBackgroundByDefault(enabled)
3213
+ notifyApplied(
3214
+ ctx,
3215
+ enabled
3216
+ ? "Agent calls run in the background unless explicitly set false"
3217
+ : "Agent calls block unless explicitly set true",
3218
+ )
3042
3219
  } else if (id === "schedulingEnabled") {
3043
3220
  const enabled = value === "on"
3044
3221
  if (enabled === isSchedulingEnabled()) {
@@ -3087,6 +3264,24 @@ Write the file using the write tool. Only write the file, nothing else.`
3087
3264
  ctx,
3088
3265
  `Output transcript ${enabled ? "enabled" : "disabled"} by default`,
3089
3266
  )
3267
+ } else if (id === "worktreeIsolation") {
3268
+ const enabled = value === "on"
3269
+ setWorktreeIsolationEnabled(enabled)
3270
+ notifyApplied(
3271
+ ctx,
3272
+ `Worktree isolation ${enabled ? "enabled" : "disabled"}. Tool parameter updates on next pi session.`,
3273
+ )
3274
+ } else if (id === "reportUsage") {
3275
+ const enabled = value === "on"
3276
+ setReportUsage(enabled)
3277
+ notifyApplied(
3278
+ ctx,
3279
+ `Usage reporting ${enabled ? "enabled" : "disabled"}`,
3280
+ )
3281
+ } else if (id === "showCost") {
3282
+ const enabled = value === "on"
3283
+ setShowCost(enabled)
3284
+ notifyApplied(ctx, `Cost display ${enabled ? "enabled" : "disabled"}`)
3090
3285
  } else if (id === "toolDescriptionMode") {
3091
3286
  setToolDescriptionMode(value as ToolDescriptionMode)
3092
3287
  notifyApplied(