pi-subagents 0.82.1 → 0.83.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/bundle/agents.js +4 -4
  3. package/bundle/background-work.js +1 -1
  4. package/bundle/child-tool-plan.js +3 -3
  5. package/bundle/{chunk-FXUXCX2K.js → chunk-4BW55BTL.js} +6 -6
  6. package/bundle/{chunk-PBK2I7WY.js → chunk-527E3D2J.js} +6 -6
  7. package/bundle/{chunk-46XKCPJR.js → chunk-5ESXK62X.js} +7 -1073
  8. package/bundle/{chunk-UTT254JE.js → chunk-6B4ITO2W.js} +8 -4
  9. package/bundle/{chunk-CIZQXJ5Q.js → chunk-7F5SK2U3.js} +39 -6
  10. package/bundle/{chunk-OYF6AFDY.js → chunk-BKFWQ642.js} +2 -2
  11. package/bundle/{chunk-ZIV4J4HG.js → chunk-CDI3GMNV.js} +47 -30
  12. package/bundle/{chunk-3JCDWF4V.js → chunk-CGXWSNPA.js} +12 -18
  13. package/bundle/{chunk-JYUKW2SY.js → chunk-CNLDYEH4.js} +12 -12
  14. package/bundle/{chunk-4SDU2BZX.js → chunk-CSSZRTKY.js} +2 -2
  15. package/bundle/{chunk-VSUSLDNW.js → chunk-CZTMUBUI.js} +10 -8
  16. package/bundle/{chunk-IVAE5Z6Q.js → chunk-HJJAVHFB.js} +28 -24
  17. package/bundle/{chunk-GJECHSAR.js → chunk-HLE22FSH.js} +2 -2
  18. package/bundle/{chunk-4Y2ROR7L.js → chunk-HXVKGS33.js} +5 -5
  19. package/bundle/{chunk-5LFLVSTM.js → chunk-JEZOSEWG.js} +2 -0
  20. package/bundle/{chunk-YKEWIB4M.js → chunk-JHZ6DZ5M.js} +1 -1
  21. package/bundle/{chunk-QE4AGIMG.js → chunk-LDMXJIA6.js} +457 -657
  22. package/bundle/{chunk-43SCHIMD.js → chunk-MBDOYZ2V.js} +1 -1
  23. package/bundle/{chunk-XSWOTIA3.js → chunk-MHX22USZ.js} +1 -1
  24. package/bundle/{chunk-6XJB7PIX.js → chunk-OOFW75PV.js} +4 -4
  25. package/bundle/{chunk-RMQFMMAT.js → chunk-P2NCCTZP.js} +231 -10
  26. package/bundle/{chunk-UCPP4GNU.js → chunk-RZJN63GU.js} +12 -5
  27. package/bundle/{chunk-G4GZD3OJ.js → chunk-TJQJ3MEA.js} +10 -10
  28. package/bundle/{chunk-5TYMSZX3.js → chunk-VTMNBPCQ.js} +2 -2
  29. package/bundle/{chunk-52UBALJL.js → chunk-WG2OTTAK.js} +2 -2
  30. package/bundle/{chunk-N2BN7BF5.js → chunk-XA3DY6UG.js} +7 -5
  31. package/bundle/{chunk-PDZWJCHD.js → chunk-XXKG3P2W.js} +9 -6
  32. package/bundle/{chunk-PLCCBLPY.js → chunk-Y3I6H6DV.js} +2 -2
  33. package/bundle/control-channel.js +3 -3
  34. package/bundle/index.js +1 -1
  35. package/bundle/intercom-bridge.js +3 -3
  36. package/bundle/preflight.js +9 -9
  37. package/bundle/project-panes.js +4 -4
  38. package/bundle/shared-types.js +1 -1
  39. package/docs/configuration.md +1 -1
  40. package/docs/extension-api.md +19 -4
  41. package/docs/observability.md +1 -0
  42. package/docs/tool-reference.md +1 -1
  43. package/package.json +1 -1
  44. package/skills/pi-subagents/SKILL.md +6 -6
  45. package/src/agents/agent-management.js +8 -14
  46. package/src/agents/agent-serializer.js +1 -2
  47. package/src/agents/agents.js +19 -3
  48. package/src/api/background-work.d.ts +9 -6
  49. package/src/api/background-work.js +14 -7
  50. package/src/extension/index.js +4 -1
  51. package/src/extension/public-execution.d.ts +2 -2
  52. package/src/extension/public-execution.js +3 -3
  53. package/src/extension/reply-workflow-script.js +4 -5
  54. package/src/extension/rpc.js +3 -1
  55. package/src/extension/subagent-options.js +17 -4
  56. package/src/runs/background/async-execution.js +4 -1
  57. package/src/runs/background/async-job-tracker.js +2 -1
  58. package/src/runs/background/auto-drain.js +2 -3
  59. package/src/runs/background/fleet-view.js +1 -1
  60. package/src/runs/background/run-child-session.js +22 -6
  61. package/src/runs/background/subagent-wait.d.ts +2 -1
  62. package/src/runs/background/subagent-wait.js +5 -2
  63. package/src/runs/foreground/execution.js +22 -6
  64. package/src/runs/foreground/subagent-executor.js +2 -5
  65. package/src/runs/shared/child-launch.js +5 -2
  66. package/src/runs/shared/child-session.d.ts +6 -0
  67. package/src/runs/shared/child-session.js +20 -4
  68. package/src/runs/shared/herdr-machine.d.ts +2 -0
  69. package/src/runs/shared/herdr-machine.js +2 -0
  70. package/src/runs/shared/herdr-placed-run.js +11 -4
  71. package/src/runs/shared/subagent-prompt-runtime.js +11 -17
  72. package/src/shared/disabled-features.d.ts +3 -1
  73. package/src/shared/disabled-features.js +11 -3
  74. package/src/shared/types.d.ts +3 -1
  75. package/src/shared/types.js +2 -0
  76. package/src/workflows/scripted-workflow.js +9 -1070
  77. package/workflow-script-worker.cjs +1067 -0
@@ -2,22 +2,22 @@ import {
2
2
  buildEffectiveSystemPrompt,
3
3
  canPreferForkFromSnapshot,
4
4
  resolveSubagentLaunchContext
5
- } from "./chunk-GJECHSAR.js";
6
- import "./chunk-PLCCBLPY.js";
5
+ } from "./chunk-HLE22FSH.js";
6
+ import "./chunk-Y3I6H6DV.js";
7
7
  import {
8
8
  loadConfig
9
- } from "./chunk-UCPP4GNU.js";
9
+ } from "./chunk-RZJN63GU.js";
10
10
  import {
11
11
  nestedResultsPath,
12
12
  processTerminalCandidatePath,
13
13
  processTerminalPath,
14
14
  resultFilePath
15
- } from "./chunk-46XKCPJR.js";
15
+ } from "./chunk-5ESXK62X.js";
16
16
  import {
17
17
  applyIntercomBridgeToAgent,
18
18
  resolveIntercomBridge,
19
19
  validateIntercomBridgeConfig
20
- } from "./chunk-XSWOTIA3.js";
20
+ } from "./chunk-MHX22USZ.js";
21
21
  import {
22
22
  AGENT_DEFINITION_PROJECTION_VERSION,
23
23
  resolveLaunchBinding,
@@ -26,7 +26,7 @@ import {
26
26
  import {
27
27
  applyThinkingSuffix,
28
28
  resolvePiLaunchToolPlan
29
- } from "./chunk-4SDU2BZX.js";
29
+ } from "./chunk-CSSZRTKY.js";
30
30
  import {
31
31
  assertRequiredChildExtensionsAdmitted,
32
32
  resolveRequiredChildExtensions
@@ -34,7 +34,7 @@ import {
34
34
  import {
35
35
  getArtifactPaths,
36
36
  getArtifactsDir
37
- } from "./chunk-5TYMSZX3.js";
37
+ } from "./chunk-VTMNBPCQ.js";
38
38
  import {
39
39
  assertClaudeCodeModelScope,
40
40
  assertThinkingWithinCeiling,
@@ -61,12 +61,12 @@ import {
61
61
  resolveSkillsWithFallback,
62
62
  resolveStepBehavior,
63
63
  unknownAgentDiagnosticContext
64
- } from "./chunk-CIZQXJ5Q.js";
64
+ } from "./chunk-7F5SK2U3.js";
65
65
  import {
66
66
  DIRS,
67
67
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
68
68
  TEMP_ROOT_DIR
69
- } from "./chunk-5LFLVSTM.js";
69
+ } from "./chunk-JEZOSEWG.js";
70
70
  import "./chunk-YFCK7H2S.js";
71
71
  import {
72
72
  capabilityCeilingAgentRestrictionMessage,
@@ -8,11 +8,11 @@ import {
8
8
  openProjectPane,
9
9
  projectPaneBindingPath,
10
10
  readProjectPaneBinding
11
- } from "./chunk-52UBALJL.js";
11
+ } from "./chunk-WG2OTTAK.js";
12
12
  import "./chunk-7J4ZXMY7.js";
13
- import "./chunk-5TYMSZX3.js";
14
- import "./chunk-CIZQXJ5Q.js";
15
- import "./chunk-5LFLVSTM.js";
13
+ import "./chunk-VTMNBPCQ.js";
14
+ import "./chunk-7F5SK2U3.js";
15
+ import "./chunk-JEZOSEWG.js";
16
16
  import "./chunk-YFCK7H2S.js";
17
17
  import "./chunk-Z7KVHQHL.js";
18
18
  export {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_FORK_PREAMBLE,
3
3
  wrapForkTask
4
- } from "./chunk-5LFLVSTM.js";
4
+ } from "./chunk-JEZOSEWG.js";
5
5
  export {
6
6
  DEFAULT_FORK_PREAMBLE,
7
7
  wrapForkTask
@@ -124,7 +124,7 @@ Pi records each tool definition and prompt section a session declares, and decla
124
124
  { "disabledFeatures": ["watchdog", "panes", "preflight", "lane-metadata", "gates"] }
125
125
  ```
126
126
 
127
- Removes feature groups you do not use from the `subagent` tool. Each listed feature loses its top-level parameters from the model-facing schema and its `options` keys are rejected, and any request that still uses one of its parameters or actions fails with an error naming this setting. The check covers the parent tool, fanout-child tools, RPC, slash commands, prompt templates, scheduled launches, delegated launches, and workflow `runs.run`/`runs.all`/`runs.lanes` children, which are rejected before they launch. The built-in tool descriptions, the unknown-action list, the fanout-child tool description, and RPC `ping` no longer mention disabled features, and `{ action: "guide", options: { topic: "tool-reference" } }` starts with a notice listing what is disabled. Custom tool descriptions are not changed. Nothing is disabled by default, and the default schema and description are unchanged. An unknown or duplicate feature name fails config loading rather than silently re-enabling every feature.
127
+ Removes feature groups you do not use from the `subagent` tool. Each listed feature loses its top-level parameters from the model-facing schema and its `options` keys are rejected, and any request that still uses one of its parameters or actions fails with an error naming this setting. The check covers the parent tool, fanout-child tools, RPC, slash commands, prompt templates, scheduled launches, delegated launches, and workflow `runs.run`/`runs.all`/`runs.lanes` children, which are rejected before they launch. The built-in tool descriptions, the unknown-action list, the fanout-child tool description, and RPC `ping` no longer mention disabled features. Every `{ action: "guide" }` response starts with a notice: a topic lists what is disabled, and a section points to the `tool-reference` topic for that list. With `workflow-scripts` disabled, the notice also says to use the top-level `tasks` or `chain` fields instead. Custom tool descriptions are not changed. Nothing is disabled by default, and the default schema and description are unchanged. An unknown or duplicate feature name fails config loading rather than silently re-enabling every feature.
128
128
 
129
129
  | Feature | Parameters removed | Actions rejected |
130
130
  |---|---|---|
@@ -129,6 +129,7 @@ Method notes:
129
129
  Capability advertisements on `ping`:
130
130
 
131
131
  - `events.asyncComplete` — exact process-local completion correlation after RPC `spawn`.
132
+ - `events.jobsChanged`, `events.asyncStarted`, `events.childStatus`, `events.processTerminal` — the event names a host can listen to instead of polling `status` (see [Refresh a host view without polling](#refresh-a-host-view-without-polling)).
132
133
  - `managementActions` — exact schedule management actions accepted by RPC `manage`.
133
134
  - `launchResolvedExtensions` — the optional launch-resolved extension projection in status details.
134
135
  - `runtimeAcknowledgedExtensions` — the optional child-runtime acknowledgement projection and event name.
@@ -141,6 +142,20 @@ Capability advertisements on `ping`:
141
142
 
142
143
  Structured delegation progress updates carry `runId` as soon as foreground execution allocates it, so a caller can retain the package-owned revival target even if its own tool turn is interrupted before the terminal response. Foreground `details.results[]` rows also include a numeric `index` that is unique within the run and stable across partial progress snapshots and the final result; use `(runId, index)` instead of row position to correlate single, counted parallel, and chain children.
143
144
 
145
+ ### Refresh a host view without polling
146
+
147
+ A host that shows background subagent activity (an editor panel, a footer, a status bar) can read `status` when something changed instead of on a timer:
148
+
149
+ 1. Subscribe to the names under `ping.events`: `jobsChanged`, `asyncStarted`, `childStatus`, `asyncComplete`, and `processTerminal`.
150
+ 2. Read `status` once, when the host starts or reconnects. Events are not replayed after a host restart, and subscribing first means a change during that read still triggers another one.
151
+ 3. When any of the events fires, read `status` again. Events can arrive in bursts and can repeat, so coalesce them in the host, for example one `status` read per 250 ms, and keep a refresh that arrives while a read is in flight for after it ends.
152
+
153
+ `subagent:jobs-changed` carries only `{ version: 1 }`. It fires whenever the parent's background job view changes: a run starts, moves to another step (no other event reports a chain or parallel run's step changes), reports new activity (a tool, a turn, tokens, or output), finishes, or is cleared from the widget. While a child streams output this can be about once per second per run. Starts and finishes fire right away; step and activity changes fire when the parent next reads the run's status file, which on macOS is a check every 5 seconds. It fires only in sessions with a UI, which covers interactive and RPC mode; print mode (`pi -p`) and SDK embeddings without a UI do not get it. It fires even when the async widget is turned off.
154
+
155
+ Foreground children are not background jobs. Their progress arrives as updates to the parent's `subagent` tool call (Pi's `tool_execution_update` events), with the `details.results[]` rows described above.
156
+
157
+ To rebuild what ran in a session after a reload, read the records the session file already holds: the `subagent` tool result's `details.results[]` for foreground runs and the `subagent-notify` message's `details.runs[]` for background runs (see [Host session lifetime and completion wakes](#host-session-lifetime-and-completion-wakes)). The `cost` method returns per-child rows built from the same records.
158
+
144
159
  ### Direct async launch correlation
145
160
 
146
161
  Direct async runs retain their originating `toolCallId` in status and result
@@ -440,16 +455,16 @@ const dispose = registerBackgroundWorkProvider({
440
455
  name: "my-background-extension",
441
456
  wakeChannels: ["my-extension:job-finished"],
442
457
  listActiveWork: (context) => jobs
443
- .filter((job) => job.status === "running" && (!context || job.ownerSessionId === context.sessionId))
458
+ .filter((job) => job.status === "running" && (!context || context.sessionIds.includes(job.ownerSessionId)))
444
459
  .map((job) => ({ id: job.id, sessionId: job.ownerSessionId })),
445
- reconcile: ({ sessionId, nowMs }) => reconcileJobs(sessionId, nowMs),
460
+ reconcile: ({ sessionIds, nowMs }) => reconcileJobs(sessionIds, nowMs),
446
461
  });
447
462
  ```
448
463
 
449
464
  Semantics:
450
465
 
451
- - Each item needs a stable provider-local ID and the exact Pi session ID that owns it. `bg_wait` captures those identities rather than a count, so one job finishing while another starts still satisfies first-completion waits without losing the replacement.
452
- - `listActiveWork` receives an optional `{ sessionId, nowMs }` context during snapshots. Providers can use `sessionId` to avoid scanning unrelated work; existing zero-argument `() => items` providers continue to work, and returned items are still validated and filtered to the exact requested session.
466
+ - Each item needs a stable provider-local ID and the session that owns it, as either `ctx.sessionManager.getSessionId()` or the session file path (`ctx.sessionManager.getSessionFile()`); both are accepted. `bg_wait` captures those identities rather than a count, so one job finishing while another starts still satisfies first-completion waits without losing the replacement.
467
+ - `listActiveWork` receives an optional `{ sessionId, sessionIds, nowMs }` context during snapshots, and `reconcile` receives the same fields. `sessionIds` holds every form of the current session (its file path when it has one, and its session id); `sessionId` is the first of them. Providers can use them to avoid scanning unrelated work; existing zero-argument `() => items` providers continue to work, and returned items are still validated and filtered to the current session. Each provider is called once per snapshot.
453
468
  - It filters snapshots to the active session, fails closed if a provider disappears while its work is tracked, and surfaces malformed snapshots or provider errors with provider context.
454
469
  - Wake channels only shorten polling; validated snapshots remain authoritative.
455
470
  - Providers share a registry through `Symbol.for("pi-subagents.background-work.v1")`, allowing independently loaded extension modules to meet in one Pi process.
@@ -334,6 +334,7 @@ Async events:
334
334
 
335
335
  - `subagent:async-started`
336
336
  - `subagent:async-complete`
337
+ - `subagent:jobs-changed` (see [extension-api.md](extension-api.md#refresh-a-host-view-without-polling))
337
338
 
338
339
  For regular async starts, the `subagent:async-started` payload includes redacted `task` and `goal` prompt fields. Async workflow script roots emit the same event with `mode: "workflow"` after their initial `status.json` is durable; workflow roots can omit `task`, and any included prompt fields are redacted. Their keyed children are then announced dynamically through `subagent:child-status`. Companion UI extensions can combine those hints with the authoritative live lifecycle artifacts under `asyncDir` without scraping terminal output.
339
340
 
@@ -110,7 +110,7 @@ The `subagent` tool schema lists `agent`, `task`, `workflow`, `args`, `async`, `
110
110
  | `options.checkpointBeforeDeadlineMs` | number | none | Async single-agent runs only. The runner requests that the child "checkpoint and stop" this many milliseconds before the run deadline (finish the current tool call, report changed files, build/test state, remaining work, commit/PR state; start no new work). This best-effort steer uses the normal steering lifecycle at the next tool boundary, so the receipt is visible in status and events; the ordinary deadline kill still applies. Precedence: call value → config `checkpointBeforeDeadlineMs`. Disarmed when the deadline leaves under one second before the checkpoint. |
111
111
  | `options.toolBudget` | object | none | Optional child tool-call budget `{ soft?, hard, block? }`: `hard` is a positive integer and `soft`, if set, is a positive integer no greater than `hard`. At `soft` the child is nudged to finalize. After `hard`, configured tools are blocked; `block` defaults to `read`, `grep`, `find`, and `ls`, and accepts either a nonempty array of tool names or `"*"` for every tool call. Final assistant text is never blocked. |
112
112
  | `options.usageBudget` | object | none | Optional root-only reported-usage budget `{ tokens?: { soft?, hard }, costUsd?: { soft?, hard } }`. Include at least one metric; each `hard` must be positive and each optional `soft` positive and no greater than its `hard`. Soft limits are status-only. Hard limits prevent later child launches after reported usage is reconciled; already-running children are not stopped and no reservations are made. |
113
- | `options.extensionBindings` | object | none | Child-only plain JSON keyed by namespaces like `package.name/1` (positive version): at most 16 namespaces, nesting depth 16, 256 total object properties, and 16 KiB of canonical UTF-8 JSON. Native Pi launches only; external runners and retained resumes reject it. |
113
+ | `options.extensionBindings` | object | none | Child-only plain JSON keyed by namespaces like `package.name/1` (positive version): at most 16 namespaces, nesting depth 16, 256 total object properties, and 16 KiB of canonical UTF-8 JSON. Native Pi launches only; external runners and retained resumes reject it. A child's extensions read it as canonical JSON from `PI_SUBAGENT_EXTENSION_BINDINGS` in their factory or `session_start`; a foreground child sees it only until its session has started, after which the parent's environment is restored. |
114
114
  | `options.preflight` | object | none | Advisory lane hints for raw scripts (`workflow: true` or a script path) only; not accepted with named workflows or direct children. See [bounded workflows](workflows.md#opt-in-bounded-workflows). |
115
115
  | `cwd` | string | runtime cwd | Override working directory. With `machine`, the directory on that machine. |
116
116
  | `options.machine` | string | - | Herdr saved machine (label or profile id) for external-cli agents; see [agents.md](agents.md#running-external-cli-agents-on-a-herdr-saved-machine). |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "pi-subagents",
4
- "version": "0.82.1",
4
+ "version": "0.83.0",
5
5
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
6
6
  "author": "Nico Bailon",
7
7
  "license": "MIT",
@@ -48,12 +48,12 @@ needs.
48
48
  Use a script when later steps depend on earlier results, or for keyed,
49
49
  retried, aggregated, or resumed children. Write one ```` ```js workflow ````
50
50
  block in the reply and call `subagent({ workflow: true })` in the same reply.
51
- For a long script, or when a `workflow: true` call reports the block missing,
52
- write it to a file and pass `workflow: "./path.js"`. On Claude 5.x models (Opus
53
- 5.5, Sonnet 5.5, Fable 5.1), use the file whenever the turn already made a tool
54
- call: text after one reaches the session as a summary without the block. Calls in one reply run at
55
- the same time, so write the file in one reply and `validate` or launch it in
56
- the next.
51
+ For a long script, write it to a file and pass `workflow: "./path.js"`; calls in
52
+ one reply run at the same time, so write the file in one reply and `validate` or
53
+ launch it in the next. When a `workflow: true` call reports the block missing,
54
+ pass the source as `options: { script: "..." }` in place of `workflow: true` for
55
+ the rest of the turn: on some hosts, text after a tool call reaches the session
56
+ as a summary without the block.
57
57
 
58
58
  ```js workflow
59
59
  const scan = await runs.run("scan", { label: "Map auth flow", agent: "scout", task: "..." });
@@ -6,7 +6,7 @@ import { mergeAgentsForScope } from "./agent-selection.js";
6
6
  import { discoverAvailableSkills, resolveSkills } from "./skills.js";
7
7
  import { buildProactiveSkillSubagentRecommendationLines, } from "./proactive-skills.js";
8
8
  import { parseFrontmatter, parseUnquotedList } from "./frontmatter.js";
9
- import { resolveEffectiveThinking, toModelInfo } from "../shared/model-info.js";
9
+ import { resolveEffectiveThinking, THINKING_LEVELS, toModelInfo } from "../shared/model-info.js";
10
10
  import { resolveSubagentModelOverride } from "../runs/shared/model-resolution.js";
11
11
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.js";
12
12
  import { formatReviewGateLabel, validateAcceptanceInput } from "../runs/shared/acceptance.js";
@@ -279,11 +279,8 @@ export function preservedAgentFrontmatterFields(agent, cfg) {
279
279
  changed("subagentOnlyExtensions");
280
280
  if (hasKey(cfg, "mutationTools"))
281
281
  changed("mutationTools");
282
- if (hasKey(cfg, "thinking")) {
282
+ if (hasKey(cfg, "thinking"))
283
283
  changed("thinking");
284
- if (cfg.thinking === "off")
285
- fields.add("thinking");
286
- }
287
284
  if (hasKey(cfg, "systemPromptMode")) {
288
285
  changed("systemPromptMode");
289
286
  fields.add("systemPromptMode");
@@ -528,17 +525,14 @@ function applyAgentConfig(target, cfg) {
528
525
  return "config.mutationTools must be a comma-separated string, empty string, or false when provided.";
529
526
  }
530
527
  if (hasKey(cfg, "thinking")) {
531
- if (cfg.thinking === false || cfg.thinking === "")
528
+ const thinking = typeof cfg.thinking === "string" ? cfg.thinking.trim() : cfg.thinking;
529
+ const level = THINKING_LEVELS.find((candidate) => candidate === thinking);
530
+ if (thinking === false || thinking === "")
532
531
  delete target.thinking;
533
- else if (typeof cfg.thinking === "string") {
534
- const thinking = cfg.thinking.trim();
535
- if (thinking)
536
- target.thinking = thinking;
537
- else
538
- delete target.thinking;
539
- }
532
+ else if (level)
533
+ target.thinking = level;
540
534
  else
541
- return "config.thinking must be a string or false when provided.";
535
+ return `config.thinking must be one of ${THINKING_LEVELS.join(", ")}, or false when provided.`;
542
536
  }
543
537
  if (hasKey(cfg, "systemPromptMode")) {
544
538
  if (cfg.systemPromptMode === "append" || cfg.systemPromptMode === "replace")
@@ -91,9 +91,8 @@ export function serializeAgent(config, options = {}) {
91
91
  lines.push(`model: ${config.model ?? ""}`);
92
92
  if (config.fast === true || preserve("fast"))
93
93
  lines.push(`fast: ${config.fast === undefined ? "" : config.fast ? "true" : "false"}`);
94
- if ((config.thinking && (config.thinking !== "off" || preserve("thinking"))) || (!config.thinking && preserve("thinking"))) {
94
+ if (config.thinking || preserve("thinking"))
95
95
  lines.push(`thinking: ${config.thinking ?? ""}`);
96
- }
97
96
  if (!preservingExistingFrontmatter || preserve("systemPromptMode"))
98
97
  lines.push(`systemPromptMode: ${config.systemPromptMode}`);
99
98
  if (!preservingExistingFrontmatter || preserve("inheritProjectContext"))
@@ -28,6 +28,7 @@ import { parseMemoryFrontmatter } from "./agent-memory.js";
28
28
  import { validateAcceptanceInput } from "../runs/shared/acceptance.js";
29
29
  import { validatePermissionRules } from "../runs/shared/permissions.js";
30
30
  import { parseThinkingLevel } from "../shared/thinking-ceiling.js";
31
+ import { THINKING_LEVELS } from "../shared/model-info.js";
31
32
  import { assertJsonSchemaObject } from "../runs/shared/structured-output.js";
32
33
  import { normalizeCapabilityCeilingAllowedAgents } from "../runs/shared/capability-ceiling.js";
33
34
  export function defaultSystemPromptMode(name) {
@@ -39,6 +40,14 @@ export function defaultInheritProjectContext(name) {
39
40
  export function defaultInheritSkills() {
40
41
  return false;
41
42
  }
43
+ /** Every field an `agentOverrides` entry may set; the Record type keeps it equal to BuiltinAgentOverrideConfig. */
44
+ const BUILTIN_OVERRIDE_FIELDS = {
45
+ description: true, advertise: true, machine: true, output: true, outputMode: true, defaultReads: true, model: true,
46
+ defaultProvider: true, fast: true, thinking: true, systemPromptMode: true, inheritProjectContext: true,
47
+ inheritGlobalContext: true, inheritSkills: true, defaultContext: true, acceptanceRole: true, disabled: true,
48
+ systemPrompt: true, skills: true, tools: true, excludeTools: true, allowNestedSubagents: true, allowedAgents: true,
49
+ extensions: true, subagentOnlyExtensions: true, mutationTools: true, toolBudget: true,
50
+ };
42
51
  const EMPTY_SUBAGENT_SETTINGS = { overrides: {}, providerOverrides: {} };
43
52
  const agentFrontmatterFields = new WeakMap();
44
53
  const AGENT_SOURCE_PRIORITY = {
@@ -763,6 +772,12 @@ function parseBuiltinOverrideEntry(name, value, filePath) {
763
772
  if (Object.hasOwn(input, "launcher")) {
764
773
  throw new Error(`Builtin override '${name}' in '${filePath}' sets 'launcher', which settings overrides do not support; set 'launcher' in a user agent file instead.`);
765
774
  }
775
+ for (const key of Object.keys(input)) {
776
+ if (Object.hasOwn(BUILTIN_OVERRIDE_FIELDS, key))
777
+ continue;
778
+ const suggestion = closestMatch(key, Object.keys(BUILTIN_OVERRIDE_FIELDS));
779
+ throw new Error(`Builtin override '${name}' in '${filePath}' has unknown field '${key}'.${suggestion ? ` Did you mean '${suggestion}'?` : ""}`);
780
+ }
766
781
  const override = {};
767
782
  if ("description" in input) {
768
783
  if (typeof input.description === "string" && input.description.trim()) {
@@ -805,10 +820,11 @@ function parseBuiltinOverrideEntry(name, value, filePath) {
805
820
  throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'fast'; expected a boolean.`);
806
821
  }
807
822
  if ("thinking" in input) {
808
- if (typeof input.thinking === "string" || input.thinking === false)
809
- override.thinking = input.thinking;
823
+ const level = THINKING_LEVELS.find((candidate) => candidate === input.thinking);
824
+ if (level !== undefined || input.thinking === false)
825
+ override.thinking = level ?? false;
810
826
  else
811
- throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'thinking'; expected a string or false.`);
827
+ throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'thinking'; expected one of ${THINKING_LEVELS.join(", ")}, or false.`);
812
828
  }
813
829
  if ("systemPromptMode" in input) {
814
830
  if (input.systemPromptMode === "append" || input.systemPromptMode === "replace") {
@@ -4,14 +4,13 @@ export interface BackgroundWorkItem {
4
4
  id: string;
5
5
  sessionId: string;
6
6
  }
7
- export interface BackgroundWorkReconcileContext {
8
- sessionId: string;
9
- nowMs: number;
10
- }
11
7
  export interface BackgroundWorkListContext {
12
8
  sessionId: string;
9
+ /** Every form the current session is known by: its file path when it has one, and `getSessionId()`. Includes `sessionId`. */
10
+ sessionIds: readonly string[];
13
11
  nowMs: number;
14
12
  }
13
+ export type BackgroundWorkReconcileContext = BackgroundWorkListContext;
15
14
  export interface BackgroundWorkProvider {
16
15
  name: string;
17
16
  listActiveWork(context?: BackgroundWorkListContext): readonly BackgroundWorkItem[];
@@ -34,5 +33,9 @@ export declare function registerBackgroundWorkProvider(provider: BackgroundWorkP
34
33
  export declare function listBackgroundWorkProviders(): readonly BackgroundWorkProvider[];
35
34
  /** Read validated provider wake channels without reconciling or listing work. */
36
35
  export declare function listBackgroundWorkWakeChannels(): readonly string[];
37
- /** Reconcile and snapshot active provider work owned by one exact Pi session. */
38
- export declare function snapshotBackgroundWork(sessionId: string, nowMs?: number): BackgroundWorkSnapshot;
36
+ /**
37
+ * Reconcile and snapshot active provider work owned by one exact Pi session.
38
+ * `sessionIds` lists that session's identity forms; the first is passed to
39
+ * providers as `sessionId`, and items keyed by any of them are kept.
40
+ */
41
+ export declare function snapshotBackgroundWork(sessionIds: readonly string[], nowMs?: number): BackgroundWorkSnapshot;
@@ -4,7 +4,7 @@ const MAX_PROVIDER_NAME_LENGTH = 128;
4
4
  const MAX_PROVIDERS = 100;
5
5
  const MAX_ITEM_ID_LENGTH = 256;
6
6
  /**
7
- * A Pi session id is the session file path, which routinely exceeds a short
7
+ * A session can be identified by its file path, which routinely exceeds a short
8
8
  * identity budget in nested worktrees, so it is bounded like the other paths.
9
9
  */
10
10
  const MAX_SESSION_ID_LENGTH = 4_096;
@@ -120,22 +120,29 @@ export function listBackgroundWorkWakeChannels() {
120
120
  }
121
121
  return [...channels];
122
122
  }
123
- /** Reconcile and snapshot active provider work owned by one exact Pi session. */
124
- export function snapshotBackgroundWork(sessionId, nowMs = Date.now()) {
125
- validateString(sessionId, "Background-work snapshot sessionId", MAX_SESSION_ID_LENGTH);
123
+ /**
124
+ * Reconcile and snapshot active provider work owned by one exact Pi session.
125
+ * `sessionIds` lists that session's identity forms; the first is passed to
126
+ * providers as `sessionId`, and items keyed by any of them are kept.
127
+ */
128
+ export function snapshotBackgroundWork(sessionIds, nowMs = Date.now()) {
129
+ const accepted = [...new Set(sessionIds.map((value) => validateString(value, "Background-work snapshot sessionId", MAX_SESSION_ID_LENGTH)))];
130
+ const sessionId = accepted[0];
131
+ if (sessionId === undefined)
132
+ throw new Error("Background-work snapshot needs at least one session id.");
126
133
  const providers = listBackgroundWorkProviders();
127
134
  const items = [];
128
135
  const identities = new Set();
129
136
  for (const provider of providers) {
130
137
  try {
131
- provider.reconcile?.({ sessionId, nowMs });
138
+ provider.reconcile?.({ sessionId, sessionIds: [...accepted], nowMs });
132
139
  }
133
140
  catch (error) {
134
141
  throw new Error(`Background-work provider '${provider.name}' reconcile failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
135
142
  }
136
143
  let active;
137
144
  try {
138
- active = provider.listActiveWork({ sessionId, nowMs });
145
+ active = provider.listActiveWork({ sessionId, sessionIds: [...accepted], nowMs });
139
146
  }
140
147
  catch (error) {
141
148
  throw new Error(`Background-work provider '${provider.name}' listActiveWork failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
@@ -153,7 +160,7 @@ export function snapshotBackgroundWork(sessionId, nowMs = Date.now()) {
153
160
  throw new Error(`Background-work provider '${provider.name}' returned duplicate item '${item.id}' for session '${item.sessionId}'.`);
154
161
  }
155
162
  identities.add(identity);
156
- if (item.sessionId === sessionId)
163
+ if (accepted.includes(item.sessionId))
157
164
  items.push({ provider: provider.name, ...item });
158
165
  });
159
166
  }
@@ -566,8 +566,11 @@ export default function registerSubagentExtension(pi) {
566
566
  // Pi sends custom messages to the model as plain user messages, so every model-visible
567
567
  // pi-subagents message is wrapped in a marker naming its source. The marker depends only on
568
568
  // the message, so history renders the same bytes every turn and cached prefixes stay valid.
569
+ // It runs on context_with_system, not context: Pi treats any change a context handler makes
570
+ // to the message list as a rewritten conversation and folds every mid-conversation system
571
+ // message into the leading one, which misses the prompt cache on every later turn.
569
572
  const markedMessageTypes = new Set([...SUBAGENT_MESSAGE_TYPES, "subagent-compaction-resume", SLASH_RESULT_TYPE, SLASH_TEXT_RESULT_TYPE, ADMIN_MESSAGE_TYPE]);
570
- pi.on("context", (event) => {
573
+ pi.on("context_with_system", (event) => {
571
574
  const messages = [];
572
575
  for (const message of event.messages) {
573
576
  if (message.role !== "custom" || !markedMessageTypes.has(message.customType)) {
@@ -57,8 +57,8 @@ export type PublicSubagentExecutionNormalization<T> = {
57
57
  /** A workflow value containing "/" or "\" is a script path; named workflow resource names contain neither. */
58
58
  export declare function isWorkflowScriptPath(workflow: string): boolean;
59
59
  /**
60
- * Model tool calls supply script text only through workflow: true (the reply block) or a script path.
61
- * workflowScript remains the internal carrier for slash, prompt-workflow, scheduled, RPC, and named-resource launches.
60
+ * Model tool calls supply script text through workflow: true (the reply block), a script path, or options.script,
61
+ * which flattenSubagentToolOptions lowers to workflowScript, the internal carrier for every other launch path too.
62
62
  */
63
63
  export declare function removedModelWorkflowFieldError(params: object): string | undefined;
64
64
  export declare function validateWorkflowCapacityOverrides(params: PublicSubagentExecutionParams): string | undefined;
@@ -7,8 +7,8 @@ export function isWorkflowScriptPath(workflow) {
7
7
  return workflow.includes("/") || workflow.includes("\\");
8
8
  }
9
9
  /**
10
- * Model tool calls supply script text only through workflow: true (the reply block) or a script path.
11
- * workflowScript remains the internal carrier for slash, prompt-workflow, scheduled, RPC, and named-resource launches.
10
+ * Model tool calls supply script text through workflow: true (the reply block), a script path, or options.script,
11
+ * which flattenSubagentToolOptions lowers to workflowScript, the internal carrier for every other launch path too.
12
12
  */
13
13
  export function removedModelWorkflowFieldError(params) {
14
14
  if (Object.hasOwn(params, "workflowScript"))
@@ -56,7 +56,7 @@ export function normalizePublicSubagentExecution(params, options = {}) {
56
56
  return { ok: false, error: "workflow must be true (the ```js workflow block in this reply), a script path containing '/', or a named workflow resource.", mode: "workflow" };
57
57
  }
58
58
  const hasNamedWorkflow = typeof workflow === "string" && !isWorkflowScriptPath(workflow);
59
- // workflowScript is the internal script carrier; model tool calls cannot set it.
59
+ // workflowScript is the internal script carrier; model tool calls set it only through options.script.
60
60
  if (workflow !== undefined && params.workflowScript !== undefined) {
61
61
  return { ok: false, error: "workflow cannot be combined with an internal workflowScript.", mode: "workflow" };
62
62
  }
@@ -2,10 +2,9 @@ const WORKFLOW_FENCE = /^```(?:js|javascript) workflow[ \t]*$/;
2
2
  const PLAIN_JS_FENCE = /^```(?:js|javascript)[ \t]*$/;
3
3
  const OPEN_FENCE = /^(`{3,}|~{3,})/;
4
4
  const CLOSE_FENCE = /^(`{3,}|~{3,})[ \t]*$/;
5
- // Claude 5.x (Opus 5.5, Sonnet 5.5, Fable 5.1) text written after a tool call in the same turn reaches the session only as a
6
- // server summary, which drops fenced blocks (pi-claude-bridge#35); retrying the block fails again.
7
- const SUMMARY_NOTE = "On some models, text written after a tool call in the same turn reaches the session only as a summary without code blocks, so for the rest of this turn write the script to a file and pass its path, such as workflow: \"./script.js\".";
8
- const FILE_FALLBACK = `If the block is in your reply: ${SUMMARY_NOTE}`;
5
+ // Through the Claude Agent SDK (pi-claude-bridge#35), text written after a tool call in the same turn reaches the session
6
+ // only as a server summary, which drops fenced blocks; retrying the block fails again. Tool arguments arrive intact.
7
+ const SUMMARY_NOTE = "On some hosts, text written after a tool call in the same turn reaches the session only as a summary without code blocks, so for the rest of this turn pass the script's source as a tool argument: options: { script: \"...\" } in place of workflow: true, with the call's other fields unchanged.";
9
8
  /**
10
9
  * Pi persists the whole assistant message before running its tool calls, so the
11
10
  * message that issued this subagent call is on the branch and carries the script.
@@ -40,7 +39,7 @@ function scriptFromReply(content) {
40
39
  const blocks = tagged.length > 0 ? tagged : plain.length === 1 ? plain : [];
41
40
  if (blocks.length !== 1) {
42
41
  const untagged = tagged.length === 0 && plain.length > 1 ? ` This reply has ${plain.length} untagged js blocks; open the script's block with the line "\`\`\`js workflow".` : "";
43
- return { error: `workflow: true needs one fenced block in the same reply as the tool call, opened with the exact line "\`\`\`js workflow" and closed with "\`\`\`"; found ${tagged.length}.${untagged} ${FILE_FALLBACK}` };
42
+ return { error: `workflow: true needs one fenced block in the same reply as the tool call, opened with the exact line "\`\`\`js workflow" and closed with "\`\`\`"; found ${tagged.length}.${untagged} If the block is in your reply: ${SUMMARY_NOTE}` };
44
43
  }
45
44
  const script = blocks[0];
46
45
  if (!script.trim())
@@ -5,7 +5,7 @@ import { deliverAsyncRunStop } from "../runs/foreground/async-stop-action.js";
5
5
  import { reconcileAsyncRun } from "../runs/background/stale-run-reconciler.js";
6
6
  import { resolveCurrentSessionId } from "../shared/session-identity.js";
7
7
  import { ownerSessionIds, ownsRun } from "../runs/shared/run-ownership.js";
8
- import { DIRS, SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_CHILD_STATUS_EVENT, SUBAGENT_PROCESS_TERMINAL_EVENT, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, } from "../shared/types.js";
8
+ import { DIRS, SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_ASYNC_STARTED_EVENT, SUBAGENT_CHILD_STATUS_EVENT, SUBAGENT_JOBS_CHANGED_EVENT, SUBAGENT_PROCESS_TERMINAL_EVENT, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, } from "../shared/types.js";
9
9
  import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.js";
10
10
  import { readStatus } from "../shared/utils.js";
11
11
  import { SubagentFlatParams } from "./schemas.js";
@@ -336,8 +336,10 @@ function pingData(ctx, options) {
336
336
  request: SUBAGENT_RPC_REQUEST_EVENT,
337
337
  replyPrefix: SUBAGENT_RPC_REPLY_EVENT_PREFIX,
338
338
  asyncComplete: SUBAGENT_ASYNC_COMPLETE_EVENT,
339
+ asyncStarted: SUBAGENT_ASYNC_STARTED_EVENT,
339
340
  childStatus: SUBAGENT_CHILD_STATUS_EVENT,
340
341
  processTerminal: SUBAGENT_PROCESS_TERMINAL_EVENT,
342
+ jobsChanged: SUBAGENT_JOBS_CHANGED_EVENT,
341
343
  },
342
344
  session: sessionData(ctx),
343
345
  };
@@ -1,5 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { Compile } from "typebox/compile";
3
+ import { Value } from "typebox/value";
3
4
  import { closestMatch } from "../shared/edit-distance.js";
4
5
  import { removedModelWorkflowFieldError } from "./public-execution.js";
5
6
  import { SUBAGENT_OPTION_KEYS, SubagentOptionParams, createSubagentParamsSchema } from "./schemas.js";
@@ -22,6 +23,15 @@ export function flattenSubagentToolOptions(params, disabled) {
22
23
  const removedField = removedModelWorkflowFieldError(options);
23
24
  if (removedField)
24
25
  throw new Error(removedField);
26
+ // options.script carries script text as a tool argument for hosts that drop the reply's ```js block (SUMMARY_NOTE).
27
+ const { script, ...rest } = options;
28
+ if (script !== undefined) {
29
+ if (typeof script !== "string" || !script.trim())
30
+ throw new Error("subagent: options.script must be the workflow script's source text.");
31
+ if (topLevel.workflow !== undefined)
32
+ throw new Error("subagent: options.script replaces workflow; pass only one of them.");
33
+ return { ...flattenSubagentToolOptions({ ...topLevel, options: rest }, disabled), workflowScript: script };
34
+ }
25
35
  const entries = Object.entries(options);
26
36
  const enabledKeys = SUBAGENT_OPTION_KEYS.filter((key) => !disabled?.params.has(key));
27
37
  const unknown = entries.map(([key]) => key).filter((key) => !optionKeys.has(key));
@@ -40,12 +50,15 @@ export function flattenSubagentToolOptions(params, disabled) {
40
50
  const disabledKey = entries.find(([key]) => disabled?.params.has(key))?.[0];
41
51
  if (disabledKey)
42
52
  throw new Error(`subagent option 'options.${disabledKey}' is disabled by config ${disabled.params.get(disabledKey)}.`);
43
- if (!optionsValidator.Check(options)) {
44
- const errors = [...optionsValidator.Errors(options)].slice(0, 4).map((error) => `options${error.instancePath.replaceAll("/", ".")} ${error.message}`);
53
+ // Pi converts only the declared top-level parameters ("true" -> true); `options` is an untyped bag in the
54
+ // tool schema, so its values get the same conversion here. Some providers send nested scalars as strings.
55
+ const converted = Value.Convert(SubagentOptionParams, structuredClone(options));
56
+ if (!optionsValidator.Check(converted)) {
57
+ const errors = [...optionsValidator.Errors(converted)].slice(0, 4).map((error) => `options${error.instancePath.replaceAll("/", ".")} ${error.message}`);
45
58
  throw new Error(`subagent: ${errors.join("; ")}.`);
46
59
  }
47
- const conflicts = entries.filter(([key, value]) => topLevel[key] !== undefined && !isDeepStrictEqual(topLevel[key], value)).map(([key]) => `'${key}'`);
60
+ const conflicts = Object.entries(converted).filter(([key, value]) => topLevel[key] !== undefined && !isDeepStrictEqual(topLevel[key], value)).map(([key]) => `'${key}'`);
48
61
  if (conflicts.length > 0)
49
62
  throw new Error(`subagent: ${conflicts.join(", ")} given both at the top level and in options with different values; give each once.`);
50
- return { ...topLevel, ...options };
63
+ return { ...topLevel, ...converted };
51
64
  }
@@ -13,7 +13,7 @@ import { childCacheRetentionEnv } from "../../shared/child-cache-retention.js";
13
13
  import { buildEffectiveSystemPrompt } from "../shared/effective-system-prompt.js";
14
14
  import { currentCompletionOwnerId } from "../../shared/completion-owner.js";
15
15
  import { planChildLaunch, projectChainOutputSchemas, resolveStepBehavior, suppressProgressForReadOnlyTask } from "../shared/child-launch-plan.js";
16
- import { formatHerdrMachineRunnerUnsupported, resolveMachinePlacement } from "../shared/herdr-machine.js";
16
+ import { formatHerdrMachineRunnerUnsupported, PLACED_RESTORE_UNSUPPORTED, resolveMachinePlacement } from "../shared/herdr-machine.js";
17
17
  import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.js";
18
18
  import { assertClaudeCodeModelScope, isClaudeCodeAdapterId, resolveClaudeCodeOverride } from "../shared/claude-code-adapter.js";
19
19
  import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.js";
@@ -1612,6 +1612,9 @@ export function executeAsyncSingle(id, params) {
1612
1612
  const machineUnsupported = formatHerdrMachineRunnerUnsupported({ machine: requestedMachine, agentName: agentConfig.name, runnerType: agentConfig.runner?.type, adapter: agentConfig.runner?.type === "external-cli" ? agentConfig.runner.adapter : undefined, worktree: params.worktree });
1613
1613
  if (machineUnsupported)
1614
1614
  return formatAsyncStartError("single", machineUnsupported);
1615
+ // Refuse before the runner starts; the placed launch would refuse only after the run exists.
1616
+ if (requestedMachine && params.revivalLease)
1617
+ return formatAsyncStartError("single", PLACED_RESTORE_UNSUPPORTED);
1615
1618
  const resolvedLauncher = resolveAgentRunnerLauncher(agentConfig, ctx.runnerLaunchers, requestedMachine);
1616
1619
  if (resolvedLauncher.error)
1617
1620
  return formatAsyncStartError("single", resolvedLauncher.error);
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { renderWidget, widgetRenderKey } from "../../tui/render.js";
4
4
  import { formatControlNoticeMessage } from "../shared/subagent-control.js";
5
- import { DIRS, SUBAGENT_CHILD_STATUS_EVENT, SUBAGENT_CONTROL_EVENT, SUBAGENT_CONTROL_INTERCOM_EVENT, SUBAGENT_STEERING_NOTICE_EVENT, WIDGET_ANIMATION_INTERVAL_MS, } from "../../shared/types.js";
5
+ import { DIRS, SUBAGENT_CHILD_STATUS_EVENT, SUBAGENT_CONTROL_EVENT, SUBAGENT_CONTROL_INTERCOM_EVENT, SUBAGENT_JOBS_CHANGED_EVENT, SUBAGENT_STEERING_NOTICE_EVENT, WIDGET_ANIMATION_INTERVAL_MS, } from "../../shared/types.js";
6
6
  import { readStatus, resolveWatchPath } from "../../shared/utils.js";
7
7
  import { normalizeParallelGroups } from "./parallel-groups.js";
8
8
  import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.js";
@@ -63,6 +63,7 @@ export function createAsyncJobTracker(pi, state, asyncDirRoot, options = {}) {
63
63
  };
64
64
  const rerenderWidget = (ctx, jobs = Array.from(state.asyncJobs.values())) => {
65
65
  options.onJobsChanged?.();
66
+ pi.events.emit(SUBAGENT_JOBS_CHANGED_EVENT, { version: 1 });
66
67
  if (state.widgetsSuspended)
67
68
  return;
68
69
  renderWidget(ctx, options.widgetEnabled === false ? [] : jobs, options.widgetCollapsed, options.widgetLayout);
@@ -1,6 +1,5 @@
1
- import { snapshotBackgroundWork } from "../../api/background-work.js";
2
1
  import { listAsyncRuns } from "./async-status.js";
3
- import { waitForSubagents, waitRunScopes } from "./subagent-wait.js";
2
+ import { backgroundWorkForSession, waitForSubagents, waitRunScopes } from "./subagent-wait.js";
4
3
  export const DEFAULT_AUTO_DRAIN_TIMEOUT_MS = 30 * 60 * 1000;
5
4
  function resultText(value) {
6
5
  return value.content.map((part) => part.type === "text" ? part.text : "").join(" ").trim();
@@ -14,7 +13,7 @@ function hasOutstandingWork(deps, sessionId, nowMs, observation) {
14
13
  includeNested: false,
15
14
  }, observation?.status));
16
15
  const detachedForeground = [...(deps.state.foregroundRuns?.values() ?? [])].some((run) => run.sessionId === sessionId && run.children.some((child) => child.status === "detached"));
17
- return asyncRuns.length > 0 || snapshotBackgroundWork(sessionId, nowMs).items.length > 0 || detachedForeground;
16
+ return asyncRuns.length > 0 || backgroundWorkForSession(deps, nowMs).items.length > 0 || detachedForeground;
18
17
  }
19
18
  /** Drain all work owned by the current headless session, including work added while draining. */
20
19
  export async function drainOutstandingWork(deps, observation) {
@@ -164,7 +164,7 @@ function sessionMessageParts(record) {
164
164
  }
165
165
  if (entry.type === "toolCall" || entry.type === "tool_call") {
166
166
  const name = typeof entry.name === "string" ? entry.name : typeof entry.toolName === "string" ? entry.toolName : "tool";
167
- parts.push({ role, kind: "toolCall", name, text: `[tool: ${name}${entry.args === undefined ? "" : ` ${stringifyJsonPreview(entry.args)}`}]` });
167
+ parts.push({ role, kind: "toolCall", name, text: `[tool: ${name}${entry.arguments === undefined ? "" : ` ${stringifyJsonPreview(entry.arguments)}`}]` });
168
168
  continue;
169
169
  }
170
170
  if (entry.type === "toolResult" || entry.type === "tool_result") {