@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.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 (291) hide show
  1. package/CHANGELOG.md +304 -6
  2. package/dist/cli.js +1015 -881
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/types.d.ts +1 -1
  8. package/dist/types/cli/args.d.ts +19 -1
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -1
  11. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  12. package/dist/types/collab/protocol.d.ts +1 -1
  13. package/dist/types/commands/say.d.ts +24 -0
  14. package/dist/types/config/keybindings.d.ts +3 -3
  15. package/dist/types/config/model-registry.d.ts +10 -0
  16. package/dist/types/config/models-config-schema.d.ts +12 -0
  17. package/dist/types/config/models-config.d.ts +8 -2
  18. package/dist/types/config/settings-schema.d.ts +261 -58
  19. package/dist/types/export/html/index.d.ts +2 -1
  20. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  22. package/dist/types/extensibility/extensions/types.d.ts +47 -1
  23. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  25. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  26. package/dist/types/extensibility/shared-events.d.ts +1 -1
  27. package/dist/types/extensibility/skills.d.ts +10 -0
  28. package/dist/types/goals/guided-setup.d.ts +18 -0
  29. package/dist/types/goals/state.d.ts +1 -1
  30. package/dist/types/hindsight/transcript.d.ts +1 -1
  31. package/dist/types/index.d.ts +5 -0
  32. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  33. package/dist/types/main.d.ts +4 -3
  34. package/dist/types/mcp/startup-events.d.ts +11 -0
  35. package/dist/types/memories/index.d.ts +7 -0
  36. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  37. package/dist/types/mnemopi/config.d.ts +4 -4
  38. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  39. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  40. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  42. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  43. package/dist/types/modes/components/session-selector.d.ts +1 -1
  44. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  45. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  46. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  47. package/dist/types/modes/components/usage-row.d.ts +3 -0
  48. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  49. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  51. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  52. package/dist/types/modes/image-references.d.ts +6 -0
  53. package/dist/types/modes/interactive-mode.d.ts +27 -3
  54. package/dist/types/modes/magic-keywords.d.ts +13 -1
  55. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  56. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  57. package/dist/types/modes/runtime-init.d.ts +4 -0
  58. package/dist/types/modes/theme/theme.d.ts +13 -2
  59. package/dist/types/modes/types.d.ts +8 -2
  60. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  61. package/dist/types/registry/agent-registry.d.ts +17 -0
  62. package/dist/types/secrets/obfuscator.d.ts +1 -1
  63. package/dist/types/session/agent-session.d.ts +14 -2
  64. package/dist/types/session/indexed-session-storage.d.ts +3 -4
  65. package/dist/types/session/session-context.d.ts +39 -0
  66. package/dist/types/session/session-entries.d.ts +159 -0
  67. package/dist/types/session/session-listing.d.ts +69 -0
  68. package/dist/types/session/session-loader.d.ts +16 -0
  69. package/dist/types/session/session-manager.d.ts +82 -474
  70. package/dist/types/session/session-migrations.d.ts +12 -0
  71. package/dist/types/session/session-paths.d.ts +25 -0
  72. package/dist/types/session/session-persistence.d.ts +8 -0
  73. package/dist/types/session/session-storage.d.ts +11 -12
  74. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  75. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  76. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  77. package/dist/types/stt/asr-client.d.ts +90 -0
  78. package/dist/types/stt/asr-protocol.d.ts +97 -0
  79. package/dist/types/stt/asr-worker.d.ts +2 -0
  80. package/dist/types/stt/downloader.d.ts +38 -0
  81. package/dist/types/stt/endpointer.d.ts +59 -0
  82. package/dist/types/stt/index.d.ts +5 -1
  83. package/dist/types/stt/models.d.ts +120 -0
  84. package/dist/types/stt/recorder.d.ts +17 -0
  85. package/dist/types/stt/stt-controller.d.ts +6 -0
  86. package/dist/types/stt/transcriber.d.ts +5 -7
  87. package/dist/types/stt/wav.d.ts +29 -0
  88. package/dist/types/system-prompt.d.ts +4 -0
  89. package/dist/types/task/executor.d.ts +2 -0
  90. package/dist/types/task/index.d.ts +9 -1
  91. package/dist/types/task/types.d.ts +36 -0
  92. package/dist/types/tools/bash.d.ts +2 -2
  93. package/dist/types/tools/eval-render.d.ts +1 -1
  94. package/dist/types/tools/index.d.ts +11 -1
  95. package/dist/types/tools/irc.d.ts +1 -0
  96. package/dist/types/tools/learn.d.ts +51 -0
  97. package/dist/types/tools/manage-skill.d.ts +40 -0
  98. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  99. package/dist/types/tools/renderers.d.ts +7 -11
  100. package/dist/types/tools/ssh.d.ts +1 -1
  101. package/dist/types/tools/todo.d.ts +1 -1
  102. package/dist/types/tools/tts.d.ts +25 -0
  103. package/dist/types/tools/write.d.ts +1 -1
  104. package/dist/types/tts/downloader.d.ts +20 -0
  105. package/dist/types/tts/index.d.ts +8 -0
  106. package/dist/types/tts/models.d.ts +82 -0
  107. package/dist/types/tts/player.d.ts +32 -0
  108. package/dist/types/tts/runtime.d.ts +6 -0
  109. package/dist/types/tts/streaming-player.d.ts +41 -0
  110. package/dist/types/tts/tts-client.d.ts +93 -0
  111. package/dist/types/tts/tts-protocol.d.ts +95 -0
  112. package/dist/types/tts/tts-worker.d.ts +2 -0
  113. package/dist/types/tts/vocalizer.d.ts +41 -0
  114. package/dist/types/tts/wav.d.ts +8 -0
  115. package/dist/types/utils/tool-choice.d.ts +8 -0
  116. package/dist/types/utils/tools-manager.d.ts +2 -1
  117. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  118. package/dist/types/web/scrapers/github.d.ts +1 -1
  119. package/package.json +15 -14
  120. package/src/async/job-manager.ts +49 -0
  121. package/src/autolearn/controller.ts +139 -0
  122. package/src/autolearn/managed-skills.ts +257 -0
  123. package/src/autoresearch/state.ts +1 -1
  124. package/src/autoresearch/types.ts +1 -1
  125. package/src/cli/args.ts +56 -2
  126. package/src/cli/session-picker.ts +2 -1
  127. package/src/cli/setup-cli.ts +148 -47
  128. package/src/cli/setup-model-picker.ts +43 -0
  129. package/src/cli-commands.ts +1 -0
  130. package/src/cli.ts +45 -13
  131. package/src/collab/host.ts +1 -1
  132. package/src/collab/protocol.ts +1 -1
  133. package/src/commands/say.ts +102 -0
  134. package/src/commands/setup.ts +1 -1
  135. package/src/commit/agentic/tools/analyze-file.ts +3 -0
  136. package/src/config/keybindings.ts +2 -2
  137. package/src/config/model-discovery.ts +11 -5
  138. package/src/config/model-registry.ts +64 -9
  139. package/src/config/models-config-schema.ts +4 -1
  140. package/src/config/models-config.ts +2 -1
  141. package/src/config/settings-schema.ts +248 -32
  142. package/src/config/settings.ts +10 -0
  143. package/src/discovery/builtin.ts +23 -1
  144. package/src/discovery/claude-plugins.ts +44 -5
  145. package/src/discovery/helpers.ts +41 -1
  146. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  147. package/src/eval/js/shared/prelude.txt +69 -17
  148. package/src/export/html/index.ts +3 -6
  149. package/src/extensibility/extensions/model-api.ts +41 -0
  150. package/src/extensibility/extensions/runner.ts +4 -0
  151. package/src/extensibility/extensions/types.ts +52 -1
  152. package/src/extensibility/extensions/wrapper.ts +41 -5
  153. package/src/extensibility/hooks/index.ts +2 -1
  154. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  155. package/src/extensibility/plugins/loader.ts +30 -19
  156. package/src/extensibility/plugins/manager.ts +221 -90
  157. package/src/extensibility/shared-events.ts +1 -1
  158. package/src/extensibility/skills.ts +96 -15
  159. package/src/goals/guided-setup.ts +133 -0
  160. package/src/goals/state.ts +1 -1
  161. package/src/hindsight/transcript.ts +1 -1
  162. package/src/index.ts +5 -0
  163. package/src/internal-urls/docs-index.generated.ts +10 -10
  164. package/src/internal-urls/history-protocol.ts +1 -1
  165. package/src/internal-urls/local-protocol.ts +29 -7
  166. package/src/main.ts +27 -7
  167. package/src/mcp/startup-events.ts +21 -0
  168. package/src/mcp/transports/stdio.ts +2 -1
  169. package/src/memories/index.ts +146 -11
  170. package/src/memory-backend/local-backend.ts +11 -5
  171. package/src/mnemopi/backend.ts +1 -0
  172. package/src/mnemopi/config.ts +26 -10
  173. package/src/modes/acp/acp-agent.ts +3 -5
  174. package/src/modes/components/agent-hub.ts +49 -4
  175. package/src/modes/components/assistant-message.ts +4 -37
  176. package/src/modes/components/compaction-summary-message.ts +125 -26
  177. package/src/modes/components/custom-editor.test.ts +96 -0
  178. package/src/modes/components/custom-editor.ts +164 -8
  179. package/src/modes/components/session-selector.ts +1 -1
  180. package/src/modes/components/settings-defs.ts +7 -0
  181. package/src/modes/components/tool-execution.ts +82 -43
  182. package/src/modes/components/transcript-container.ts +70 -1
  183. package/src/modes/components/tree-selector.ts +1 -1
  184. package/src/modes/components/usage-row.ts +18 -0
  185. package/src/modes/components/user-message.ts +4 -2
  186. package/src/modes/controllers/command-controller.ts +14 -4
  187. package/src/modes/controllers/event-controller.ts +78 -11
  188. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  189. package/src/modes/controllers/input-controller.ts +258 -27
  190. package/src/modes/controllers/selector-controller.ts +12 -2
  191. package/src/modes/gradient-highlight.ts +21 -9
  192. package/src/modes/image-references.ts +20 -0
  193. package/src/modes/interactive-mode.ts +286 -40
  194. package/src/modes/magic-keywords.ts +27 -5
  195. package/src/modes/rpc/rpc-mode.ts +146 -14
  196. package/src/modes/rpc/rpc-subagents.ts +2 -2
  197. package/src/modes/rpc/rpc-types.ts +8 -2
  198. package/src/modes/runtime-init.ts +28 -3
  199. package/src/modes/theme/theme.ts +98 -50
  200. package/src/modes/types.ts +6 -2
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  202. package/src/modes/utils/ui-helpers.ts +34 -6
  203. package/src/priority.json +5 -1
  204. package/src/prompts/agents/task.md +1 -0
  205. package/src/prompts/goals/guided-goal-interview.md +8 -0
  206. package/src/prompts/goals/guided-goal-system.md +12 -0
  207. package/src/prompts/memories/read-path.md +6 -0
  208. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  209. package/src/prompts/system/autolearn-guidance.md +7 -0
  210. package/src/prompts/system/autolearn-nudge.md +3 -0
  211. package/src/prompts/system/eager-task.md +7 -0
  212. package/src/prompts/system/eager-todo.md +11 -6
  213. package/src/prompts/system/subagent-system-prompt.md +4 -0
  214. package/src/prompts/system/system-prompt.md +10 -5
  215. package/src/prompts/system/title-marker-instruction.md +1 -0
  216. package/src/prompts/system/title-system-marker.md +16 -0
  217. package/src/prompts/tools/job.md +1 -0
  218. package/src/prompts/tools/learn.md +7 -0
  219. package/src/prompts/tools/manage-skill.md +9 -0
  220. package/src/prompts/tools/task.md +3 -0
  221. package/src/registry/agent-registry.ts +30 -0
  222. package/src/sdk.ts +88 -24
  223. package/src/secrets/obfuscator.ts +1 -1
  224. package/src/session/agent-session.ts +209 -87
  225. package/src/session/history-storage.ts +2 -2
  226. package/src/session/indexed-session-storage.ts +7 -17
  227. package/src/session/session-context.ts +352 -0
  228. package/src/session/session-entries.ts +194 -0
  229. package/src/session/session-listing.ts +588 -0
  230. package/src/session/session-loader.ts +106 -0
  231. package/src/session/session-manager.ts +933 -3145
  232. package/src/session/session-migrations.ts +78 -0
  233. package/src/session/session-paths.ts +193 -0
  234. package/src/session/session-persistence.ts +131 -0
  235. package/src/session/session-storage.ts +91 -50
  236. package/src/session/snapcompact-inline.ts +21 -1
  237. package/src/session/snapcompact-savings-journal.ts +113 -0
  238. package/src/session/tool-choice-queue.ts +23 -11
  239. package/src/slash-commands/builtin-registry.ts +25 -3
  240. package/src/stt/asr-client.ts +520 -0
  241. package/src/stt/asr-protocol.ts +65 -0
  242. package/src/stt/asr-worker.ts +790 -0
  243. package/src/stt/downloader.ts +107 -47
  244. package/src/stt/endpointer.ts +259 -0
  245. package/src/stt/index.ts +5 -1
  246. package/src/stt/models.ts +150 -0
  247. package/src/stt/recorder.ts +247 -60
  248. package/src/stt/stt-controller.ts +201 -22
  249. package/src/stt/transcriber.ts +37 -68
  250. package/src/stt/wav.ts +173 -0
  251. package/src/system-prompt.ts +8 -0
  252. package/src/task/agents.ts +1 -2
  253. package/src/task/executor.ts +49 -15
  254. package/src/task/index.ts +60 -6
  255. package/src/task/render.ts +83 -8
  256. package/src/task/types.ts +53 -0
  257. package/src/tools/ask.ts +8 -0
  258. package/src/tools/bash.ts +4 -3
  259. package/src/tools/eval-render.ts +4 -3
  260. package/src/tools/index.ts +40 -4
  261. package/src/tools/irc.ts +10 -2
  262. package/src/tools/job.ts +14 -2
  263. package/src/tools/learn.ts +144 -0
  264. package/src/tools/manage-skill.ts +104 -0
  265. package/src/tools/plan-mode-guard.ts +53 -19
  266. package/src/tools/renderers.ts +7 -11
  267. package/src/tools/ssh.ts +4 -3
  268. package/src/tools/todo.ts +1 -1
  269. package/src/tools/tts.ts +203 -92
  270. package/src/tools/write.ts +18 -2
  271. package/src/tts/downloader.ts +64 -0
  272. package/src/tts/index.ts +8 -0
  273. package/src/tts/models.ts +137 -0
  274. package/src/tts/player.ts +137 -0
  275. package/src/tts/runtime.ts +21 -0
  276. package/src/tts/streaming-player.ts +266 -0
  277. package/src/tts/tts-client.ts +647 -0
  278. package/src/tts/tts-protocol.ts +60 -0
  279. package/src/tts/tts-worker.ts +497 -0
  280. package/src/tts/vocalizer.ts +162 -0
  281. package/src/tts/wav.ts +58 -0
  282. package/src/utils/title-generator.ts +48 -5
  283. package/src/utils/tool-choice.ts +16 -0
  284. package/src/utils/tools-manager.test.ts +25 -0
  285. package/src/utils/tools-manager.ts +19 -1
  286. package/src/web/scrapers/github.ts +96 -0
  287. package/src/web/search/index.ts +13 -0
  288. package/src/web/search/providers/searxng.ts +13 -1
  289. package/dist/types/stt/setup.d.ts +0 -18
  290. package/src/stt/setup.ts +0 -52
  291. package/src/stt/transcribe.py +0 -70
@@ -1,5 +1,7 @@
1
- import { $which, logger } from "@oh-my-pi/pi-utils";
2
- import transcribeScript from "./transcribe.py" with { type: "text" };
1
+ import { logger } from "@oh-my-pi/pi-utils";
2
+ import { sttClient } from "./asr-client";
3
+ import { resolveSttModelSpec } from "./models";
4
+ import { decodeWavToMono16k } from "./wav";
3
5
 
4
6
  export interface TranscribeOptions {
5
7
  modelName?: string;
@@ -10,82 +12,49 @@ export interface TranscribeOptions {
10
12
  const TRANSCRIBE_TIMEOUT_MS = 120_000;
11
13
 
12
14
  /**
13
- * Find a usable Python command.
14
- */
15
- export function resolvePython(): string | null {
16
- for (const cmd of ["python", "py", "python3"]) {
17
- if ($which(cmd)) return cmd;
18
- }
19
- return null;
20
- }
21
-
22
- /**
23
- * Transcribe a WAV file using Python openai-whisper.
15
+ * Transcribe a WAV file using the local ONNX Whisper worker.
24
16
  *
25
- * Reads the WAV via Python's built-in `wave` module (no ffmpeg needed),
26
- * resamples to 16 kHz mono, and passes the numpy array directly to whisper.
17
+ * Decodes the WAV to a 16 kHz mono Float32Array in-process (no Python, no
18
+ * ffmpeg) and routes it to the warm speech worker, which keeps the model loaded
19
+ * across calls. Honors `options.signal` (abort) and applies an internal timeout
20
+ * with the same semantics as the previous Python path.
27
21
  */
28
22
  export async function transcribe(audioPath: string, options?: TranscribeOptions): Promise<string> {
29
23
  const audioFile = Bun.file(audioPath);
30
24
  if (audioFile.size < 100) {
31
25
  throw new Error(`Audio file is empty or too small (${audioFile.size} bytes). Check microphone.`);
32
26
  }
33
-
34
- const pythonCmd = resolvePython();
35
- if (!pythonCmd) {
36
- throw new Error("Python not found. Install Python 3.8+ from https://python.org");
37
- }
38
-
39
- const modelName = options?.modelName ?? "base.en";
40
- const language = options?.language ?? "en";
41
-
42
- logger.debug("Transcribing with Python whisper", { pythonCmd, audioPath, modelName, language });
43
-
44
- const proc = Bun.spawn([pythonCmd, "-c", transcribeScript, audioPath, modelName, language], {
45
- stdout: "pipe",
46
- stderr: "pipe",
47
- });
48
-
49
- if (options?.signal?.aborted) {
50
- proc.kill();
51
- options.signal.throwIfAborted();
52
- }
53
-
54
- const onAbort = () => proc.kill();
55
- options?.signal?.addEventListener("abort", onAbort, { once: true });
56
-
57
- let timedOut = false;
58
-
59
- const killTimer = setTimeout(() => {
60
- timedOut = true;
61
- logger.error("Python whisper transcription timed out, killing process", { timeoutMs: TRANSCRIBE_TIMEOUT_MS });
62
- proc.kill();
63
- }, TRANSCRIBE_TIMEOUT_MS);
64
-
65
- const exitCode = await proc.exited;
66
- clearTimeout(killTimer);
67
- options?.signal?.removeEventListener("abort", onAbort);
68
-
69
27
  options?.signal?.throwIfAborted();
70
28
 
71
- const stdout = await new Response(proc.stdout).text();
72
- const stderr = await new Response(proc.stderr).text();
73
-
74
- if (timedOut) {
75
- throw new Error(`Transcription timed out after ${Math.round(TRANSCRIBE_TIMEOUT_MS / 1000)}s`);
76
- }
29
+ const spec = resolveSttModelSpec(options?.modelName);
30
+ const language = options?.language || undefined;
31
+ const audio = decodeWavToMono16k(await audioFile.arrayBuffer());
32
+ if (audio.length === 0) return "";
33
+
34
+ logger.debug("Transcribing with local ONNX whisper", {
35
+ audioPath,
36
+ modelKey: spec.key,
37
+ repo: spec.repo,
38
+ language,
39
+ samples: audio.length,
40
+ });
77
41
 
78
- if (exitCode !== 0) {
79
- logger.error("Python whisper transcription failed", { exitCode, stderr: stderr.trim() });
80
- if (stderr.includes("No module named 'whisper'")) {
81
- throw new Error("openai-whisper not installed. Run: pip install openai-whisper");
42
+ // Bound runaway inference. Abort the request on timeout; the warm worker
43
+ // keeps the model loaded (the request promise just rejects).
44
+ const timeout = new AbortController();
45
+ const timer = setTimeout(() => timeout.abort(), TRANSCRIBE_TIMEOUT_MS);
46
+ const signal = options?.signal ? AbortSignal.any([options.signal, timeout.signal]) : timeout.signal;
47
+ try {
48
+ const text = (await sttClient.transcribe(spec.key, audio, { language, signal })).trim();
49
+ logger.debug("Transcription complete", { length: text.length });
50
+ return text;
51
+ } catch (error) {
52
+ if (timeout.signal.aborted && !options?.signal?.aborted) {
53
+ logger.error("Local whisper transcription timed out", { timeoutMs: TRANSCRIBE_TIMEOUT_MS });
54
+ throw new Error(`Transcription timed out after ${Math.round(TRANSCRIBE_TIMEOUT_MS / 1000)}s`);
82
55
  }
83
- // Show last line of stderr (the actual error, not the full traceback)
84
- const lastLine = stderr.trim().split("\n").pop() ?? "";
85
- throw new Error(`Transcription failed: ${lastLine}`);
56
+ throw error;
57
+ } finally {
58
+ clearTimeout(timer);
86
59
  }
87
-
88
- const text = stdout.trim();
89
- logger.debug("Transcription complete", { length: text.length });
90
- return text;
91
60
  }
package/src/stt/wav.ts ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Minimal WAV (RIFF/PCM) decoder producing the Float32Array @ 16 kHz mono that
3
+ * transformers.js `automatic-speech-recognition` expects. Ports the decode/
4
+ * mono-mix/resample logic from the retired Python `transcribe.py` (which read
5
+ * via the stdlib `wave` module) so STT no longer shells out to Python.
6
+ *
7
+ * Supported sample formats: PCM uint8 (8-bit), int16 (16-bit), int32 (32-bit),
8
+ * and IEEE float32 (format tag 3). Any number of channels is mixed down to mono.
9
+ */
10
+
11
+ /** transformers.js Whisper feature extractor operates at 16 kHz. */
12
+ export const TARGET_SAMPLE_RATE = 16_000;
13
+
14
+ const WAV_FORMAT_PCM = 1;
15
+ const WAV_FORMAT_IEEE_FLOAT = 3;
16
+ const WAV_FORMAT_EXTENSIBLE = 0xfffe;
17
+
18
+ interface WavData {
19
+ format: number;
20
+ channels: number;
21
+ sampleRate: number;
22
+ bitsPerSample: number;
23
+ /** Raw PCM/float bytes from the `data` chunk. */
24
+ samples: DataView;
25
+ }
26
+
27
+ function readFourCc(view: DataView, offset: number): string {
28
+ return String.fromCharCode(
29
+ view.getUint8(offset),
30
+ view.getUint8(offset + 1),
31
+ view.getUint8(offset + 2),
32
+ view.getUint8(offset + 3),
33
+ );
34
+ }
35
+
36
+ /** Parse the RIFF container, returning the `fmt ` parameters and `data` bytes. */
37
+ function parseWav(buffer: ArrayBuffer): WavData {
38
+ const view = new DataView(buffer);
39
+ if (buffer.byteLength < 12 || readFourCc(view, 0) !== "RIFF" || readFourCc(view, 8) !== "WAVE") {
40
+ throw new Error("Not a RIFF/WAVE file");
41
+ }
42
+
43
+ let format: number | undefined;
44
+ let channels = 0;
45
+ let sampleRate = 0;
46
+ let bitsPerSample = 0;
47
+ let samples: DataView | undefined;
48
+
49
+ // Chunks begin after the 12-byte RIFF/WAVE header; each is an 8-byte header
50
+ // (4-char id + uint32 LE size) followed by `size` bytes padded to even.
51
+ let offset = 12;
52
+ while (offset + 8 <= buffer.byteLength) {
53
+ const id = readFourCc(view, offset);
54
+ const size = view.getUint32(offset + 4, true);
55
+ const body = offset + 8;
56
+ if (id === "fmt ") {
57
+ format = view.getUint16(body, true);
58
+ channels = view.getUint16(body + 2, true);
59
+ sampleRate = view.getUint32(body + 4, true);
60
+ bitsPerSample = view.getUint16(body + 14, true);
61
+ // WAVE_FORMAT_EXTENSIBLE (ffmpeg & friends): the real codec is the
62
+ // first 2 bytes of the SubFormat GUID in the fmt extension.
63
+ if (format === WAV_FORMAT_EXTENSIBLE && size >= 40) format = view.getUint16(body + 24, true);
64
+ } else if (id === "data") {
65
+ const length = Math.min(size, buffer.byteLength - body);
66
+ samples = new DataView(buffer, body, length);
67
+ }
68
+ offset = body + size + (size % 2);
69
+ }
70
+
71
+ if (format === undefined || samples === undefined || channels < 1 || sampleRate < 1) {
72
+ throw new Error("WAV file missing fmt/data chunks");
73
+ }
74
+ return { format, channels, sampleRate, bitsPerSample, samples };
75
+ }
76
+
77
+ /** Decode raw PCM/float bytes into interleaved normalized [-1, 1] float samples. */
78
+ function decodeSamples(wav: WavData): Float32Array {
79
+ const { format, bitsPerSample, samples } = wav;
80
+ const view = samples;
81
+ if (format === WAV_FORMAT_IEEE_FLOAT && bitsPerSample === 32) {
82
+ const count = Math.floor(view.byteLength / 4);
83
+ const out = new Float32Array(count);
84
+ for (let i = 0; i < count; i += 1) out[i] = view.getFloat32(i * 4, true);
85
+ return out;
86
+ }
87
+ if (format !== WAV_FORMAT_PCM) {
88
+ throw new Error(`Unsupported WAV format tag: ${format}`);
89
+ }
90
+ if (bitsPerSample === 16) {
91
+ const count = Math.floor(view.byteLength / 2);
92
+ const out = new Float32Array(count);
93
+ for (let i = 0; i < count; i += 1) out[i] = view.getInt16(i * 2, true) / 32_768;
94
+ return out;
95
+ }
96
+ if (bitsPerSample === 8) {
97
+ // 8-bit PCM is unsigned, centered at 128.
98
+ const count = view.byteLength;
99
+ const out = new Float32Array(count);
100
+ for (let i = 0; i < count; i += 1) out[i] = (view.getUint8(i) - 128) / 128;
101
+ return out;
102
+ }
103
+ if (bitsPerSample === 32) {
104
+ const count = Math.floor(view.byteLength / 4);
105
+ const out = new Float32Array(count);
106
+ for (let i = 0; i < count; i += 1) out[i] = view.getInt32(i * 4, true) / 2_147_483_648;
107
+ return out;
108
+ }
109
+ throw new Error(`Unsupported PCM sample width: ${bitsPerSample} bits`);
110
+ }
111
+
112
+ /** Average interleaved channels down to a single mono track. */
113
+ function mixToMono(interleaved: Float32Array, channels: number): Float32Array {
114
+ if (channels <= 1) return interleaved;
115
+ const frames = Math.floor(interleaved.length / channels);
116
+ const out = new Float32Array(frames);
117
+ for (let frame = 0; frame < frames; frame += 1) {
118
+ let sum = 0;
119
+ for (let channel = 0; channel < channels; channel += 1) sum += interleaved[frame * channels + channel]!;
120
+ out[frame] = sum / channels;
121
+ }
122
+ return out;
123
+ }
124
+
125
+ /**
126
+ * Resample via linear interpolation, mirroring the Python `np.interp` over
127
+ * `linspace(0, n-1, targetLen)` against `arange(n)`.
128
+ */
129
+ export function resampleLinear(input: Float32Array, fromRate: number, toRate: number): Float32Array {
130
+ if (fromRate === toRate || input.length === 0) return input;
131
+ const n = input.length;
132
+ const targetLen = Math.max(1, Math.floor((n * toRate) / fromRate));
133
+ const out = new Float32Array(targetLen);
134
+ if (targetLen === 1) {
135
+ out[0] = input[0]!;
136
+ return out;
137
+ }
138
+ const step = (n - 1) / (targetLen - 1);
139
+ for (let i = 0; i < targetLen; i += 1) {
140
+ const pos = i * step;
141
+ const lo = Math.floor(pos);
142
+ const hi = Math.min(lo + 1, n - 1);
143
+ const frac = pos - lo;
144
+ out[i] = input[lo]! * (1 - frac) + input[hi]! * frac;
145
+ }
146
+ return out;
147
+ }
148
+
149
+ /**
150
+ * Decode a WAV byte buffer into a 16 kHz mono Float32Array suitable for the
151
+ * transformers.js Whisper pipeline.
152
+ */
153
+ export function decodeWavToMono16k(buffer: ArrayBuffer): Float32Array {
154
+ const wav = parseWav(buffer);
155
+ const interleaved = decodeSamples(wav);
156
+ const mono = mixToMono(interleaved, wav.channels);
157
+ return resampleLinear(mono, wav.sampleRate, TARGET_SAMPLE_RATE);
158
+ }
159
+
160
+ /**
161
+ * Decode interleaved little-endian signed 16-bit PCM bytes into normalized
162
+ * [-1, 1] mono float samples. The live recorder streams raw s16le frames from
163
+ * sox/ffmpeg/arecord stdout (no RIFF container), so this is the hot-path
164
+ * counterpart to {@link decodeWavToMono16k}. `bytes` MUST be 2-byte aligned;
165
+ * callers buffer any trailing odd byte across chunk boundaries.
166
+ */
167
+ export function decodePcmS16LE(bytes: Uint8Array): Float32Array {
168
+ const count = bytes.length >>> 1;
169
+ const view = new DataView(bytes.buffer, bytes.byteOffset, count * 2);
170
+ const out = new Float32Array(count);
171
+ for (let i = 0; i < count; i += 1) out[i] = view.getInt16(i * 2, true) / 32_768;
172
+ return out;
173
+ }
@@ -385,6 +385,10 @@ export interface BuildSystemPromptOptions {
385
385
  mcpDiscoveryServerSummaries?: string[];
386
386
  /** Encourage the agent to delegate via tasks unless changes are trivial. */
387
387
  eagerTasks?: boolean;
388
+ /** When true, the Eager Tasks section uses the hard MUST/ONLY wording (`task.eager: always`) rather than the softer `preferred` nudge. */
389
+ eagerTasksAlways?: boolean;
390
+ /** Whether `task.batch` is enabled; gates batch-call guidance in the Eager Tasks section. */
391
+ taskBatch?: boolean;
388
392
  /** Rules with alwaysApply=true — their full content is injected into the prompt. */
389
393
  alwaysApplyRules?: AlwaysApplyRule[];
390
394
  /** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
@@ -427,6 +431,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
427
431
  mcpDiscoveryMode = false,
428
432
  mcpDiscoveryServerSummaries = [],
429
433
  eagerTasks = false,
434
+ eagerTasksAlways = false,
435
+ taskBatch = true,
430
436
  secretsEnabled = false,
431
437
  workspaceTree: providedWorkspaceTree,
432
438
  memoryRootEnabled = false,
@@ -610,6 +616,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
610
616
  hasMCPDiscoveryServers: mcpDiscoveryServerSummaries.length > 0,
611
617
  mcpDiscoveryServerSummaries,
612
618
  eagerTasks,
619
+ eagerTasksAlways,
620
+ taskBatch,
613
621
  secretsEnabled,
614
622
  hasMemoryRoot: memoryRootEnabled,
615
623
  hasObsidian: hasObsidian(),
@@ -55,7 +55,6 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
55
55
  description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
56
56
  spawns: "*",
57
57
  model: "pi/task",
58
- thinkingLevel: Effort.Medium,
59
58
  },
60
59
  template: taskMd,
61
60
  },
@@ -65,7 +64,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
65
64
  name: "quick_task",
66
65
  description: "Low-reasoning agent for strictly mechanical updates or data collection only",
67
66
  model: "pi/smol",
68
- thinkingLevel: Effort.Minimal,
67
+ thinkingLevel: Effort.Medium,
69
68
  },
70
69
  template: taskMd,
71
70
  },
@@ -8,7 +8,7 @@ import path from "node:path";
8
8
  import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
9
9
  import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
10
10
  import type { Usage } from "@oh-my-pi/pi-ai";
11
- import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
11
+ import { logger, popLoopPhase, prompt, pushLoopPhase, untilAborted } from "@oh-my-pi/pi-utils";
12
12
  import type { Rule } from "../capability/rule";
13
13
  import { ModelRegistry } from "../config/model-registry";
14
14
  import { resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
@@ -56,7 +56,9 @@ import {
56
56
  type AgentProgress,
57
57
  MAX_OUTPUT_BYTES,
58
58
  MAX_OUTPUT_LINES,
59
+ oneLineLabel,
59
60
  type ReviewFinding,
61
+ resolveSubagentDisplayName,
60
62
  type SingleResult,
61
63
  TASK_SUBAGENT_EVENT_CHANNEL,
62
64
  TASK_SUBAGENT_LIFECYCLE_CHANNEL,
@@ -123,7 +125,10 @@ function renderIrcPeerRoster(selfId: string): string {
123
125
  .list()
124
126
  .filter(ref => ref.id !== selfId && ref.status !== "aborted");
125
127
  if (peers.length === 0) return "- (no other agents)";
126
- const lines = peers.map(peer => `- \`${peer.id}\` — ${peer.displayName} (${peer.kind}, ${peer.status})`);
128
+ const lines = peers.map(
129
+ peer =>
130
+ `- \`${peer.id}\` — ${peer.displayName} (${peer.kind}, ${peer.status})${peer.activity ? `: ${peer.activity}` : ""}`,
131
+ );
127
132
  if (peers.some(peer => peer.status === "idle" || peer.status === "parked")) {
128
133
  lines.push("Idle/parked peers are not gone: messaging them wakes (or revives) them.");
129
134
  }
@@ -192,6 +197,8 @@ export interface ExecutorOptions {
192
197
  */
193
198
  planReference?: { path: string; content: string };
194
199
  description?: string;
200
+ /** Specialist role/expertise for this spawn; drives the system-prompt preamble, display name, and telemetry identity. */
201
+ role?: string;
195
202
  index: number;
196
203
  id: string;
197
204
  parentToolCallId?: string;
@@ -837,6 +844,9 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
837
844
  const emitProgressNow = () => {
838
845
  progress.durationMs = Date.now() - startTime;
839
846
  onProgress?.({ ...progress });
847
+ const activityGist =
848
+ progress.lastIntent ?? (progress.currentTool ? `running ${progress.currentTool}` : undefined);
849
+ if (activityGist) AgentRegistry.global().setActivity(id, activityGist);
840
850
  if (args.eventBus) {
841
851
  args.eventBus.emit(TASK_SUBAGENT_PROGRESS_CHANNEL, {
842
852
  index,
@@ -1198,6 +1208,9 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1198
1208
  return;
1199
1209
  }
1200
1210
  if (isAgentEvent(event)) {
1211
+ // Breadcrumb the synchronous subagent event handling so the loop
1212
+ // watchdog can attribute any block to this in-process subagent.
1213
+ pushLoopPhase(`subagent:${id}`);
1201
1214
  try {
1202
1215
  processEvent(event);
1203
1216
  } catch (err) {
@@ -1205,6 +1218,8 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
1205
1218
  error: err instanceof Error ? err.message : String(err),
1206
1219
  });
1207
1220
  requestAbort("terminate");
1221
+ } finally {
1222
+ popLoopPhase();
1208
1223
  }
1209
1224
  }
1210
1225
  });
@@ -1444,16 +1459,24 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
1444
1459
  const yieldItems = progress.extractedToolData?.yield as YieldItem[] | undefined;
1445
1460
  const reportFindingDetails = progress.extractedToolData?.report_finding as ReportFindingDetails[] | undefined;
1446
1461
  const reportFindings: ReviewFinding[] | undefined = reportFindingDetails?.map(toReviewFinding);
1447
- const finalized = finalizeSubprocessOutput({
1448
- rawOutput,
1449
- exitCode,
1450
- stderr,
1451
- doneAborted: Boolean(done.aborted),
1452
- signalAborted: Boolean(signal?.aborted),
1453
- yieldItems,
1454
- reportFindings,
1455
- outputSchema: args.outputSchema,
1456
- });
1462
+ // Breadcrumb the synchronous yield-payload shaping (O(rawOutput)) so a block
1463
+ // here is attributed to this subagent rather than logged as "unknown".
1464
+ pushLoopPhase(`subagent:${id}`);
1465
+ let finalized: FinalizeSubprocessOutputResult;
1466
+ try {
1467
+ finalized = finalizeSubprocessOutput({
1468
+ rawOutput,
1469
+ exitCode,
1470
+ stderr,
1471
+ doneAborted: Boolean(done.aborted),
1472
+ signalAborted: Boolean(signal?.aborted),
1473
+ yieldItems,
1474
+ reportFindings,
1475
+ outputSchema: args.outputSchema,
1476
+ });
1477
+ } finally {
1478
+ popLoopPhase();
1479
+ }
1457
1480
  rawOutput = finalized.rawOutput;
1458
1481
  exitCode = finalized.exitCode;
1459
1482
  stderr = finalized.stderr;
@@ -1618,6 +1641,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1618
1641
  agent.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
1619
1642
  );
1620
1643
  const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
1644
+ // Tailored specialist identity for this spawn. `subagentRole` is the full
1645
+ // (trimmed) role text fed to the system-prompt preamble; `subagentDisplayName`
1646
+ // is the label-normalized form the registry/roster show, falling back to the
1647
+ // agent type name when no role was given.
1648
+ const subagentRole = options.role?.trim() || undefined;
1649
+ const subagentDisplayName = resolveSubagentDisplayName(options.role, agent.name);
1621
1650
  const maxRuntimeMs = Math.max(
1622
1651
  0,
1623
1652
  Math.trunc(Number(options.maxRuntimeMs ?? settings.get("task.maxRuntimeMs") ?? 0) || 0),
@@ -1816,7 +1845,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1816
1845
  // carry the subagent's own agent identity, and use the subagent's
1817
1846
  // own session id for `gen_ai.conversation.id`.
1818
1847
  const subagentAgentIdentity: AgentIdentity | undefined = options.parentTelemetry
1819
- ? { id, name: agent.name, description: agent.description }
1848
+ ? {
1849
+ id,
1850
+ name: subagentDisplayName,
1851
+ description: subagentRole ? oneLineLabel(subagentRole) : agent.description,
1852
+ }
1820
1853
  : undefined;
1821
1854
  const subagentTelemetry: AgentTelemetryConfig | undefined =
1822
1855
  options.parentTelemetry && subagentAgentIdentity
@@ -1866,6 +1899,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1866
1899
  systemPrompt: defaultPrompt => {
1867
1900
  const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
1868
1901
  agent: agent.systemPrompt,
1902
+ role: subagentRole ? oneLineLabel(subagentRole) : "",
1869
1903
  context: options.context?.trim() ?? "",
1870
1904
  planReference: options.planReference?.content ?? "",
1871
1905
  planReferencePath: options.planReference?.path ?? "",
@@ -1886,7 +1920,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1886
1920
  parentMnemopiSessionState: options.parentMnemopiSessionState,
1887
1921
  parentTaskPrefix: id,
1888
1922
  agentId: id,
1889
- agentDisplayName: agent.name,
1923
+ agentDisplayName: subagentDisplayName,
1890
1924
  enableLsp: lspEnabled,
1891
1925
  skipPythonPreflight,
1892
1926
  enableMCP,
@@ -1971,7 +2005,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1971
2005
  }
1972
2006
 
1973
2007
  const extensionRunner = session.extensionRunner;
1974
- const pendingExtensionMessages: Promise<void>[] = [];
2008
+ const pendingExtensionMessages: Promise<unknown>[] = [];
1975
2009
  if (extensionRunner) {
1976
2010
  extensionRunner.initialize(
1977
2011
  {
package/src/task/index.ts CHANGED
@@ -33,6 +33,7 @@ import { formatBytes, formatDuration } from "../tools/render-utils";
33
33
  import {
34
34
  type AgentDefinition,
35
35
  type AgentProgress,
36
+ canSpawnAtDepth,
36
37
  getTaskSchema,
37
38
  type SingleResult,
38
39
  type TaskItem,
@@ -310,7 +311,7 @@ function resolveSpawnItems(params: TaskParams): TaskItem[] {
310
311
  if (Array.isArray(params.tasks) && params.tasks.length > 0) {
311
312
  return params.tasks;
312
313
  }
313
- return [{ id: params.id, description: params.description, assignment: params.assignment }];
314
+ return [{ id: params.id, description: params.description, role: params.role, assignment: params.assignment }];
314
315
  }
315
316
 
316
317
  /**
@@ -324,6 +325,7 @@ function spawnParamsFor(params: TaskParams, item: TaskItem): TaskParams {
324
325
  const spawn: TaskParams = { agent: params.agent };
325
326
  if (item.id !== undefined) spawn.id = item.id;
326
327
  if (item.description !== undefined) spawn.description = item.description;
328
+ if (item.role !== undefined) spawn.role = item.role;
327
329
  if (item.assignment !== undefined) spawn.assignment = item.assignment;
328
330
  if (params.context !== undefined) spawn.context = params.context;
329
331
  if (item.isolated !== undefined) {
@@ -334,6 +336,36 @@ function spawnParamsFor(params: TaskParams, item: TaskItem): TaskParams {
334
336
  return spawn;
335
337
  }
336
338
 
339
+ /** Generic worker agents whose output sharpens with a tailored `role` rather than the bare type. */
340
+ const GENERIC_SPAWN_AGENTS: ReadonlySet<string> = new Set(["task", "quick_task"]);
341
+
342
+ /**
343
+ * Advisory — never a rejection — nudging the spawner toward tailored
344
+ * specialists when it spawns generic role-less workers and still holds spawn
345
+ * capacity (DepthCapacity: it currently has the `task` tool). Fires when a
346
+ * generic `task`/`quick_task` spawn carries no `role`, or when one call clones
347
+ * the same agent ≥2× all without roles. Returns undefined when no nudge applies.
348
+ */
349
+ export function buildSpecializationAdvisory(
350
+ agentName: string | undefined,
351
+ items: TaskItem[],
352
+ depthCapacity: boolean,
353
+ ): string | undefined {
354
+ if (!depthCapacity) return undefined;
355
+ const rolelessCount = items.filter(item => !item.role?.trim()).length;
356
+ if (rolelessCount === 0) return undefined;
357
+ const generic = agentName !== undefined && GENERIC_SPAWN_AGENTS.has(agentName);
358
+ const cloned = items.length >= 2 && rolelessCount === items.length;
359
+ if (!generic && !cloned) return undefined;
360
+ const label = agentName ?? "task";
361
+ return (
362
+ `Tip: spawned ${rolelessCount} \`${label}\` worker${rolelessCount === 1 ? "" : "s"} without a \`role\`. ` +
363
+ `Tailored specialists outperform generic workers — give each spawn a \`role\` naming its expertise ` +
364
+ `(e.g. "Auth-flow security reviewer"). Depth budget remains, so decompose into named specialists ` +
365
+ `rather than cloning one generic worker.`
366
+ );
367
+ }
368
+
337
369
  /** Sentinel for async jobs whose subagent finished with a failing result; progress is already updated. */
338
370
  class TaskJobError extends Error {}
339
371
 
@@ -388,6 +420,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
388
420
  if (typeof params.agent === "string") {
389
421
  lines.push(`Agent: ${truncateForPrompt(params.agent)}`);
390
422
  }
423
+ if (typeof params.role === "string" && params.role.trim()) {
424
+ lines.push(`Role: ${truncateForPrompt(params.role)}`);
425
+ }
391
426
  if (typeof params.id === "string" && params.id.trim()) {
392
427
  lines.push(`Task: ${truncateForPrompt(params.id)}`);
393
428
  }
@@ -403,6 +438,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
403
438
  if (typeof firstTask.id === "string" && firstTask.id.trim()) {
404
439
  lines.push(`Task: ${truncateForPrompt(firstTask.id)}`);
405
440
  }
441
+ if (typeof firstTask.role === "string" && firstTask.role.trim()) {
442
+ lines.push(`Role: ${truncateForPrompt(firstTask.role)}`);
443
+ }
406
444
  if (typeof firstTask.assignment === "string") {
407
445
  lines.push(`Assignment:\n${truncateForPrompt(firstTask.assignment)}`);
408
446
  }
@@ -497,6 +535,21 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
497
535
  const selectedAgent = this.#discoveredAgents.find(agent => agent.name === params.agent);
498
536
  const asyncEnabled = this.session.settings.get("async.enabled");
499
537
  const manager = asyncEnabled ? this.session.asyncJobManager : undefined;
538
+ const depthCapacity = canSpawnAtDepth(
539
+ this.session.settings.get("task.maxRecursionDepth") ?? 2,
540
+ this.session.taskDepth ?? 0,
541
+ );
542
+ const advisory = buildSpecializationAdvisory(params.agent, spawnItems, depthCapacity);
543
+ const withAdvisory = (result: AgentToolResult<TaskToolDetails>): AgentToolResult<TaskToolDetails> => {
544
+ if (!advisory) return result;
545
+ const textPart = result.content.find(part => part.type === "text");
546
+ if (textPart && typeof textPart.text === "string") {
547
+ textPart.text = `${textPart.text}\n\n${advisory}`;
548
+ } else {
549
+ result.content.push({ type: "text", text: advisory });
550
+ }
551
+ return result;
552
+ };
500
553
  if (!asyncEnabled || !manager || selectedAgent?.blocking === true) {
501
554
  // Sync fallback: async execution disabled, orphaned host that never
502
555
  // wired a job manager, or an agent definition that declares
@@ -505,7 +558,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
505
558
  if (asyncEnabled && !manager) {
506
559
  logger.warn("task: no AsyncJobManager registered; falling back to sync execution");
507
560
  }
508
- return this.#executeSyncFanout(toolCallId, params, spawnItems, signal, onUpdate);
561
+ return withAdvisory(await this.#executeSyncFanout(toolCallId, params, spawnItems, signal, onUpdate));
509
562
  }
510
563
 
511
564
  // Resolve agent ids up front so the immediate result can name them.
@@ -613,7 +666,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
613
666
  content: [{ type: "text", text: `Spawned agent \`${agentId}\`...` }],
614
667
  details: buildAsyncDetails("running", jobId),
615
668
  });
616
- return {
669
+ return withAdvisory({
617
670
  content: [
618
671
  {
619
672
  type: "text",
@@ -621,7 +674,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
621
674
  },
622
675
  ],
623
676
  details: buildAsyncDetails("running", jobId),
624
- };
677
+ });
625
678
  }
626
679
 
627
680
  const coordinationHint = ircEnabled
@@ -641,7 +694,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
641
694
  content: [{ type: "text", text: `Spawned ${started.length} agents...` }],
642
695
  details: buildAsyncDetails("running", primaryJobId),
643
696
  });
644
- return {
697
+ return withAdvisory({
645
698
  content: [
646
699
  {
647
700
  type: "text",
@@ -649,7 +702,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
649
702
  },
650
703
  ],
651
704
  details: buildAsyncDetails("running", primaryJobId),
652
- };
705
+ });
653
706
  }
654
707
 
655
708
  /**
@@ -1146,6 +1199,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1146
1199
  context: sharedContext,
1147
1200
  planReference,
1148
1201
  description: params.description,
1202
+ role: params.role,
1149
1203
  index: spawnIndex,
1150
1204
  parentToolCallId: toolCallId,
1151
1205
  detached,