@gajae-code/coding-agent 0.2.4 → 0.3.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 (266) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +145 -2
  4. package/dist/types/commands/harness.d.ts +37 -0
  5. package/dist/types/config/settings-schema.d.ts +13 -3
  6. package/dist/types/config/settings.d.ts +3 -1
  7. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  8. package/dist/types/discovery/helpers.d.ts +1 -0
  9. package/dist/types/exec/bash-executor.d.ts +8 -1
  10. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  12. package/dist/types/extensibility/shared-events.d.ts +1 -0
  13. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  16. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  17. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  18. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  19. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  20. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  21. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  22. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  23. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  24. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  25. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  26. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  27. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  28. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  29. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  30. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  31. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  32. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  33. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  34. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  35. package/dist/types/harness-control-plane/types.d.ts +162 -0
  36. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  37. package/dist/types/hooks/skill-state.d.ts +2 -29
  38. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  39. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  40. package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
  41. package/dist/types/modes/interactive-mode.d.ts +2 -0
  42. package/dist/types/modes/theme/defaults/index.d.ts +45 -9477
  43. package/dist/types/modes/theme/theme.d.ts +1 -5
  44. package/dist/types/modes/types.d.ts +2 -0
  45. package/dist/types/sdk.d.ts +4 -0
  46. package/dist/types/session/agent-session.d.ts +8 -0
  47. package/dist/types/session/streaming-output.d.ts +11 -0
  48. package/dist/types/skill-state/active-state.d.ts +3 -0
  49. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  50. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  51. package/dist/types/task/executor.d.ts +3 -0
  52. package/dist/types/task/types.d.ts +56 -3
  53. package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
  54. package/dist/types/tools/bash.d.ts +24 -0
  55. package/dist/types/tools/cron.d.ts +110 -0
  56. package/dist/types/tools/index.d.ts +4 -0
  57. package/dist/types/tools/monitor.d.ts +54 -0
  58. package/dist/types/tools/subagent.d.ts +11 -1
  59. package/dist/types/web/search/index.d.ts +1 -0
  60. package/dist/types/web/search/provider.d.ts +11 -4
  61. package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
  62. package/dist/types/web/search/types.d.ts +1 -1
  63. package/package.json +7 -7
  64. package/src/async/job-manager.ts +522 -6
  65. package/src/cli/agents-cli.ts +3 -0
  66. package/src/cli/auth-broker-cli.ts +1 -0
  67. package/src/cli/config-cli.ts +10 -2
  68. package/src/cli.ts +2 -0
  69. package/src/commands/harness.ts +592 -0
  70. package/src/commands/team.ts +36 -39
  71. package/src/config/settings-schema.ts +15 -2
  72. package/src/config/settings.ts +49 -7
  73. package/src/deep-interview/render-middleware.ts +366 -0
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +9 -2
  75. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  76. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  77. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  78. package/src/discovery/helpers.ts +5 -0
  79. package/src/eval/js/shared/rewrite-imports.ts +1 -2
  80. package/src/exec/bash-executor.ts +20 -9
  81. package/src/extensibility/custom-tools/types.ts +1 -0
  82. package/src/extensibility/extensions/types.ts +6 -0
  83. package/src/extensibility/shared-events.ts +1 -0
  84. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  85. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  86. package/src/gjc-runtime/ralplan-runtime.ts +27 -10
  87. package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
  88. package/src/gjc-runtime/state-graph.ts +86 -0
  89. package/src/gjc-runtime/state-migrations.ts +132 -0
  90. package/src/gjc-runtime/state-renderer.ts +345 -0
  91. package/src/gjc-runtime/state-runtime.ts +733 -21
  92. package/src/gjc-runtime/state-validation.ts +49 -0
  93. package/src/gjc-runtime/state-writer.ts +718 -0
  94. package/src/gjc-runtime/team-runtime.ts +1083 -89
  95. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  96. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  97. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  98. package/src/harness-control-plane/classifier.ts +128 -0
  99. package/src/harness-control-plane/control-endpoint.ts +137 -0
  100. package/src/harness-control-plane/finalize.ts +222 -0
  101. package/src/harness-control-plane/frame-mapper.ts +286 -0
  102. package/src/harness-control-plane/operate.ts +225 -0
  103. package/src/harness-control-plane/owner.ts +553 -0
  104. package/src/harness-control-plane/preserve.ts +102 -0
  105. package/src/harness-control-plane/receipts.ts +216 -0
  106. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  107. package/src/harness-control-plane/seams.ts +39 -0
  108. package/src/harness-control-plane/session-lease.ts +388 -0
  109. package/src/harness-control-plane/state-machine.ts +97 -0
  110. package/src/harness-control-plane/storage.ts +257 -0
  111. package/src/harness-control-plane/types.ts +214 -0
  112. package/src/hooks/skill-keywords.ts +4 -2
  113. package/src/hooks/skill-state.ts +25 -42
  114. package/src/internal-urls/docs-index.generated.ts +6 -4
  115. package/src/lsp/render.ts +1 -1
  116. package/src/modes/acp/acp-agent.ts +1 -1
  117. package/src/modes/acp/acp-client-bridge.ts +1 -1
  118. package/src/modes/components/agent-dashboard.ts +1 -1
  119. package/src/modes/components/assistant-message.ts +5 -1
  120. package/src/modes/components/diff.ts +2 -2
  121. package/src/modes/components/hook-selector.ts +72 -2
  122. package/src/modes/components/skill-hud/render.ts +7 -2
  123. package/src/modes/controllers/event-controller.ts +71 -6
  124. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  125. package/src/modes/controllers/input-controller.ts +19 -3
  126. package/src/modes/controllers/selector-controller.ts +3 -2
  127. package/src/modes/interactive-mode.ts +21 -2
  128. package/src/modes/theme/defaults/index.ts +0 -196
  129. package/src/modes/theme/theme.ts +35 -35
  130. package/src/modes/types.ts +2 -0
  131. package/src/prompts/agents/architect.md +5 -1
  132. package/src/prompts/agents/critic.md +5 -1
  133. package/src/prompts/agents/executor.md +13 -0
  134. package/src/prompts/agents/frontmatter.md +1 -0
  135. package/src/prompts/agents/planner.md +5 -1
  136. package/src/prompts/tools/bash.md +9 -0
  137. package/src/prompts/tools/cron.md +25 -0
  138. package/src/prompts/tools/monitor.md +30 -0
  139. package/src/prompts/tools/subagent.md +33 -3
  140. package/src/runtime-mcp/oauth-flow.ts +4 -2
  141. package/src/sdk.ts +7 -0
  142. package/src/session/agent-session.ts +247 -38
  143. package/src/session/session-manager.ts +13 -1
  144. package/src/session/streaming-output.ts +21 -0
  145. package/src/skill-state/active-state.ts +222 -78
  146. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  147. package/src/skill-state/initial-phase.ts +2 -0
  148. package/src/skill-state/workflow-state-contract.ts +26 -0
  149. package/src/task/agents.ts +1 -0
  150. package/src/task/executor.ts +51 -8
  151. package/src/task/index.ts +120 -8
  152. package/src/task/render.ts +6 -3
  153. package/src/task/types.ts +57 -3
  154. package/src/tools/ask.ts +28 -7
  155. package/src/tools/bash-allowed-prefixes.ts +169 -0
  156. package/src/tools/bash.ts +190 -29
  157. package/src/tools/browser/tab-worker.ts +1 -1
  158. package/src/tools/cron.ts +665 -0
  159. package/src/tools/index.ts +20 -2
  160. package/src/tools/monitor.ts +136 -0
  161. package/src/tools/subagent.ts +255 -64
  162. package/src/vim/engine.ts +3 -3
  163. package/src/web/search/index.ts +31 -18
  164. package/src/web/search/provider.ts +57 -12
  165. package/src/web/search/providers/duckduckgo.ts +279 -0
  166. package/src/web/search/types.ts +2 -0
  167. package/src/modes/theme/dark.json +0 -95
  168. package/src/modes/theme/defaults/alabaster.json +0 -93
  169. package/src/modes/theme/defaults/amethyst.json +0 -96
  170. package/src/modes/theme/defaults/anthracite.json +0 -93
  171. package/src/modes/theme/defaults/basalt.json +0 -91
  172. package/src/modes/theme/defaults/birch.json +0 -95
  173. package/src/modes/theme/defaults/dark-abyss.json +0 -91
  174. package/src/modes/theme/defaults/dark-arctic.json +0 -104
  175. package/src/modes/theme/defaults/dark-aurora.json +0 -95
  176. package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
  177. package/src/modes/theme/defaults/dark-cavern.json +0 -91
  178. package/src/modes/theme/defaults/dark-copper.json +0 -95
  179. package/src/modes/theme/defaults/dark-cosmos.json +0 -90
  180. package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
  181. package/src/modes/theme/defaults/dark-dracula.json +0 -98
  182. package/src/modes/theme/defaults/dark-eclipse.json +0 -91
  183. package/src/modes/theme/defaults/dark-ember.json +0 -95
  184. package/src/modes/theme/defaults/dark-equinox.json +0 -90
  185. package/src/modes/theme/defaults/dark-forest.json +0 -96
  186. package/src/modes/theme/defaults/dark-github.json +0 -105
  187. package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
  188. package/src/modes/theme/defaults/dark-lavender.json +0 -95
  189. package/src/modes/theme/defaults/dark-lunar.json +0 -89
  190. package/src/modes/theme/defaults/dark-midnight.json +0 -95
  191. package/src/modes/theme/defaults/dark-monochrome.json +0 -94
  192. package/src/modes/theme/defaults/dark-monokai.json +0 -98
  193. package/src/modes/theme/defaults/dark-nebula.json +0 -90
  194. package/src/modes/theme/defaults/dark-nord.json +0 -97
  195. package/src/modes/theme/defaults/dark-ocean.json +0 -101
  196. package/src/modes/theme/defaults/dark-one.json +0 -100
  197. package/src/modes/theme/defaults/dark-poimandres.json +0 -141
  198. package/src/modes/theme/defaults/dark-rainforest.json +0 -91
  199. package/src/modes/theme/defaults/dark-reef.json +0 -91
  200. package/src/modes/theme/defaults/dark-retro.json +0 -92
  201. package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
  202. package/src/modes/theme/defaults/dark-sakura.json +0 -95
  203. package/src/modes/theme/defaults/dark-slate.json +0 -95
  204. package/src/modes/theme/defaults/dark-solarized.json +0 -97
  205. package/src/modes/theme/defaults/dark-solstice.json +0 -90
  206. package/src/modes/theme/defaults/dark-starfall.json +0 -91
  207. package/src/modes/theme/defaults/dark-sunset.json +0 -99
  208. package/src/modes/theme/defaults/dark-swamp.json +0 -90
  209. package/src/modes/theme/defaults/dark-synthwave.json +0 -103
  210. package/src/modes/theme/defaults/dark-taiga.json +0 -91
  211. package/src/modes/theme/defaults/dark-terminal.json +0 -95
  212. package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
  213. package/src/modes/theme/defaults/dark-tundra.json +0 -91
  214. package/src/modes/theme/defaults/dark-twilight.json +0 -91
  215. package/src/modes/theme/defaults/dark-volcanic.json +0 -91
  216. package/src/modes/theme/defaults/graphite.json +0 -92
  217. package/src/modes/theme/defaults/light-arctic.json +0 -107
  218. package/src/modes/theme/defaults/light-aurora-day.json +0 -91
  219. package/src/modes/theme/defaults/light-canyon.json +0 -91
  220. package/src/modes/theme/defaults/light-catppuccin.json +0 -106
  221. package/src/modes/theme/defaults/light-cirrus.json +0 -90
  222. package/src/modes/theme/defaults/light-coral.json +0 -95
  223. package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
  224. package/src/modes/theme/defaults/light-dawn.json +0 -90
  225. package/src/modes/theme/defaults/light-dunes.json +0 -91
  226. package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
  227. package/src/modes/theme/defaults/light-forest.json +0 -100
  228. package/src/modes/theme/defaults/light-frost.json +0 -95
  229. package/src/modes/theme/defaults/light-github.json +0 -115
  230. package/src/modes/theme/defaults/light-glacier.json +0 -91
  231. package/src/modes/theme/defaults/light-gruvbox.json +0 -108
  232. package/src/modes/theme/defaults/light-haze.json +0 -90
  233. package/src/modes/theme/defaults/light-honeycomb.json +0 -95
  234. package/src/modes/theme/defaults/light-lagoon.json +0 -91
  235. package/src/modes/theme/defaults/light-lavender.json +0 -95
  236. package/src/modes/theme/defaults/light-meadow.json +0 -91
  237. package/src/modes/theme/defaults/light-mint.json +0 -95
  238. package/src/modes/theme/defaults/light-monochrome.json +0 -101
  239. package/src/modes/theme/defaults/light-ocean.json +0 -99
  240. package/src/modes/theme/defaults/light-one.json +0 -99
  241. package/src/modes/theme/defaults/light-opal.json +0 -91
  242. package/src/modes/theme/defaults/light-orchard.json +0 -91
  243. package/src/modes/theme/defaults/light-paper.json +0 -95
  244. package/src/modes/theme/defaults/light-poimandres.json +0 -141
  245. package/src/modes/theme/defaults/light-prism.json +0 -90
  246. package/src/modes/theme/defaults/light-retro.json +0 -98
  247. package/src/modes/theme/defaults/light-sand.json +0 -95
  248. package/src/modes/theme/defaults/light-savanna.json +0 -91
  249. package/src/modes/theme/defaults/light-solarized.json +0 -102
  250. package/src/modes/theme/defaults/light-soleil.json +0 -90
  251. package/src/modes/theme/defaults/light-sunset.json +0 -99
  252. package/src/modes/theme/defaults/light-synthwave.json +0 -98
  253. package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
  254. package/src/modes/theme/defaults/light-wetland.json +0 -91
  255. package/src/modes/theme/defaults/light-zenith.json +0 -89
  256. package/src/modes/theme/defaults/limestone.json +0 -94
  257. package/src/modes/theme/defaults/mahogany.json +0 -97
  258. package/src/modes/theme/defaults/marble.json +0 -93
  259. package/src/modes/theme/defaults/obsidian.json +0 -91
  260. package/src/modes/theme/defaults/onyx.json +0 -91
  261. package/src/modes/theme/defaults/pearl.json +0 -93
  262. package/src/modes/theme/defaults/porcelain.json +0 -91
  263. package/src/modes/theme/defaults/quartz.json +0 -96
  264. package/src/modes/theme/defaults/sandstone.json +0 -95
  265. package/src/modes/theme/defaults/titanium.json +0 -90
  266. package/src/modes/theme/light.json +0 -93
@@ -31,6 +31,7 @@ import { BashTool } from "./bash";
31
31
  import { BrowserTool } from "./browser";
32
32
  import { CalculatorTool } from "./calculator";
33
33
  import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
34
+ import { CronCreateTool, CronDeleteTool, CronListTool } from "./cron";
34
35
  import { DebugTool } from "./debug";
35
36
  import { EvalTool } from "./eval";
36
37
  import { FindTool } from "./find";
@@ -41,6 +42,7 @@ import { HindsightRetainTool } from "./hindsight-retain";
41
42
  import { InspectImageTool } from "./inspect-image";
42
43
  import { IrcTool } from "./irc";
43
44
  import { JobTool } from "./job";
45
+ import { MonitorTool } from "./monitor";
44
46
  import { wrapToolWithMetaNotice } from "./output-meta";
45
47
  import { ReadTool } from "./read";
46
48
  import { RecipeTool } from "./recipe";
@@ -69,6 +71,7 @@ export * from "./bash";
69
71
  export * from "./browser";
70
72
  export * from "./calculator";
71
73
  export * from "./checkpoint";
74
+ export * from "./cron";
72
75
  export * from "./debug";
73
76
  export * from "./eval";
74
77
  export * from "./find";
@@ -80,6 +83,7 @@ export * from "./image-gen";
80
83
  export * from "./inspect-image";
81
84
  export * from "./irc";
82
85
  export * from "./job";
86
+ export * from "./monitor";
83
87
  export * from "./read";
84
88
  export * from "./recipe";
85
89
  export * from "./render-mermaid";
@@ -164,6 +168,8 @@ export interface ToolSession {
164
168
  getToolByName?: (name: string) => AgentTool | undefined;
165
169
  /** Agent registry for IRC routing across live sessions. */
166
170
  agentRegistry?: AgentRegistry;
171
+ /** Optional restricted bash command prefixes for read-only role agents. */
172
+ bashAllowedPrefixes?: string[];
167
173
  /** Get artifacts directory for artifact:// URLs */
168
174
  getArtifactsDir?: () => string | null;
169
175
  /** Get the ArtifactManager backing this session (shared across parent + subagents). */
@@ -319,6 +325,10 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
319
325
  task: s => TaskTool.create(s),
320
326
  subagent: s => new SubagentTool(s),
321
327
  job: JobTool.createIf,
328
+ monitor: MonitorTool.createIf,
329
+ CronCreate: CronCreateTool.createIf,
330
+ CronList: CronListTool.createIf,
331
+ CronDelete: CronDeleteTool.createIf,
322
332
  recipe: RecipeTool.createIf,
323
333
  irc: IrcTool.createIf,
324
334
  todo_write: s => new TodoWriteTool(s),
@@ -466,6 +476,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
466
476
  const discoveryActive = effectiveDiscoveryMode !== "off";
467
477
 
468
478
  const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
479
+ const allToolFactoryEntries = Object.entries(allTools) as Array<[string, ToolFactory]>;
480
+ const allToolsByRequestName = new Map<string, [string, ToolFactory]>();
481
+ for (const [name, factory] of allToolFactoryEntries) {
482
+ allToolsByRequestName.set(name.toLowerCase(), [name, factory]);
483
+ }
469
484
  const isToolAllowed = (name: string) => {
470
485
  if (name === "goal") return goalEnabled && session.getGoalRuntime !== undefined;
471
486
  if (goalStateToolNames.includes(name as (typeof GOAL_MODE_TOOL_NAMES)[number])) return goalEnabled;
@@ -509,10 +524,13 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
509
524
  requestedTools.push("yield");
510
525
  }
511
526
 
512
- const filteredRequestedTools = requestedTools?.filter(name => name in allTools && isToolAllowed(name));
527
+ const filteredRequestedTools = requestedTools
528
+ ?.map(name => allToolsByRequestName.get(name))
529
+ .filter((entry): entry is [string, ToolFactory] => entry !== undefined)
530
+ .filter(([name]) => isToolAllowed(name));
513
531
  const baseEntries =
514
532
  filteredRequestedTools !== undefined
515
- ? filteredRequestedTools.filter(name => name !== "resolve").map(name => [name, allTools[name]] as const)
533
+ ? filteredRequestedTools.filter(([name]) => name !== "resolve")
516
534
  : [
517
535
  ...Object.entries(BUILTIN_TOOLS)
518
536
  .filter(([name]) => isToolAllowed(name))
@@ -0,0 +1,136 @@
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
2
+ import { logger, prompt } from "@gajae-code/utils";
3
+ import * as z from "zod/v4";
4
+ import { AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
5
+ import monitorDescription from "../prompts/tools/monitor.md" with { type: "text" };
6
+ import { BashTool } from "./bash";
7
+ import type { ToolSession } from "./index";
8
+ import { ToolError } from "./tool-errors";
9
+
10
+ const monitorKindEnum = z.enum(["log", "poll", "watch", "other"]);
11
+
12
+ const monitorSchema = z.object({
13
+ command: z
14
+ .string()
15
+ .describe(
16
+ "Shell command to run as a background monitor. Each stdout line is delivered as a separate task-notification event.",
17
+ ),
18
+ kind: monitorKindEnum.describe(
19
+ "Category of monitor. 'log' tails a log file, 'poll' polls a status endpoint, 'watch' watches a directory, 'other' for arbitrary streams.",
20
+ ),
21
+ description: z
22
+ .string()
23
+ .describe("Short human-readable description of what is being monitored. Appears in task listings."),
24
+ timeout: z
25
+ .number()
26
+ .min(1)
27
+ .optional()
28
+ .describe(
29
+ "Optional maximum wall-clock seconds the monitor may run before automatic shutdown. Omit for indefinite (subject to session lifetime).",
30
+ ),
31
+ persistent: z
32
+ .boolean()
33
+ .optional()
34
+ .describe(
35
+ "Whether to keep the monitor running past the originating turn. Persistent monitors survive until session end or explicit kill via the background-task stop tool.",
36
+ ),
37
+ });
38
+
39
+ export type MonitorParams = z.infer<typeof monitorSchema>;
40
+
41
+ export interface MonitorToolDetails {
42
+ taskId: string;
43
+ kind: z.infer<typeof monitorKindEnum>;
44
+ description: string;
45
+ command: string;
46
+ persistent: boolean;
47
+ }
48
+
49
+ const MONITOR_LABEL_MAX = 120;
50
+
51
+ function buildMonitorLabel(params: MonitorParams): string {
52
+ const base = `[monitor:${params.kind}] ${params.description}`;
53
+ if (base.length <= MONITOR_LABEL_MAX) return base;
54
+ return `${base.slice(0, MONITOR_LABEL_MAX - 3)}...`;
55
+ }
56
+
57
+ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolDetails> {
58
+ readonly name = "monitor";
59
+ readonly label = "Monitor";
60
+ readonly summary = "Start a background monitor that streams stdout lines as task notifications";
61
+ readonly description: string;
62
+ readonly parameters = monitorSchema;
63
+ readonly strict = true;
64
+ readonly loadMode = "discoverable";
65
+
66
+ constructor(private readonly session: ToolSession) {
67
+ this.description = prompt.render(monitorDescription);
68
+ }
69
+
70
+ static createIf(session: ToolSession): MonitorTool | null {
71
+ if (!isBackgroundJobSupportEnabled(session.settings)) return null;
72
+ return new MonitorTool(session);
73
+ }
74
+
75
+ async execute(
76
+ _toolCallId: string,
77
+ params: MonitorParams,
78
+ _signal?: AbortSignal,
79
+ _onUpdate?: AgentToolUpdateCallback<MonitorToolDetails>,
80
+ context?: AgentToolContext,
81
+ ): Promise<AgentToolResult<MonitorToolDetails>> {
82
+ const manager = AsyncJobManager.instance();
83
+ if (!manager) {
84
+ throw new ToolError("Async execution is disabled; the monitor tool is unavailable in this session.");
85
+ }
86
+
87
+ const persistent = params.persistent ?? false;
88
+ const label = buildMonitorLabel(params);
89
+ const ownerId = this.session.getAgentId?.() ?? undefined;
90
+ const bash = new BashTool(this.session);
91
+ let deliveredFirstLine = false;
92
+ const monitorJob = await bash.startMonitorJob(
93
+ { command: params.command, timeout: params.timeout },
94
+ {
95
+ ownerId,
96
+ label,
97
+ ctx: context,
98
+ onRawLine: (line, jobId) => {
99
+ if (!persistent && deliveredFirstLine) return;
100
+ deliveredFirstLine = true;
101
+ const content = `<task-notification>\nMonitor task ${jobId} (${params.kind}: ${params.description}) emitted:\n${line}\n</task-notification>`;
102
+ const details = { taskId: jobId, kind: params.kind, description: params.description };
103
+ const sendPromise = this.session.sendCustomMessage?.(
104
+ { customType: "task-notification", content, display: false, attribution: "agent", details },
105
+ { triggerTurn: true, deliverAs: "followUp" },
106
+ );
107
+ if (sendPromise) {
108
+ void sendPromise.catch(error => {
109
+ logger.warn("Monitor task-notification delivery failed", {
110
+ error: error instanceof Error ? error.message : String(error),
111
+ });
112
+ });
113
+ } else {
114
+ this.session.steer?.({ customType: "task-notification", content, details });
115
+ }
116
+ if (!persistent) {
117
+ manager.cancel(jobId, ownerId ? { ownerId } : undefined);
118
+ }
119
+ },
120
+ },
121
+ );
122
+
123
+ const startedText = `Monitor started · task ${monitorJob.jobId} · persistent: ${persistent}`;
124
+
125
+ return {
126
+ content: [{ type: "text", text: startedText }],
127
+ details: {
128
+ taskId: monitorJob.jobId,
129
+ kind: params.kind,
130
+ description: params.description,
131
+ command: params.command,
132
+ persistent,
133
+ },
134
+ };
135
+ }
136
+ }
@@ -1,7 +1,7 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
2
2
  import { prompt } from "@gajae-code/utils";
3
3
  import * as z from "zod/v4";
4
- import { type AsyncJob, AsyncJobManager } from "../async";
4
+ import { type AsyncJob, AsyncJobManager, type SubagentRecord } from "../async";
5
5
  import subagentDescription from "../prompts/tools/subagent.md" with { type: "text" };
6
6
  import type { AgentSource } from "../task/types";
7
7
  import { Ellipsis, truncateToWidth } from "../tui";
@@ -16,14 +16,26 @@ const MAX_LIST_LIMIT = 50;
16
16
  const TEXT_PREVIEW_WIDTH = 12_000;
17
17
 
18
18
  const subagentSchema = z.object({
19
- action: z.enum(["list", "inspect", "await", "cancel"]).describe("subagent control action"),
19
+ action: z
20
+ .enum(["list", "inspect", "await", "cancel", "pause", "resume", "steer"])
21
+ .describe("subagent control action"),
20
22
  ids: z.array(z.string()).optional().describe("subagent ids or backing job ids"),
23
+ message: z.string().optional().describe("message to deliver when resuming or steering a subagent"),
24
+ pause: z.boolean().optional().describe("pause after steering a currently running subagent"),
21
25
  timeout_ms: z.number().min(0).max(MAX_AWAIT_TIMEOUT_MS).optional().describe("await timeout in milliseconds"),
22
26
  limit: z.number().min(1).max(MAX_LIST_LIMIT).optional().describe("maximum subagents to return"),
23
27
  });
24
28
 
25
29
  type SubagentParams = z.infer<typeof subagentSchema>;
26
- type SubagentStatus = "running" | "completed" | "failed" | "cancelled" | "not_found" | "already_completed";
30
+ type SubagentStatus =
31
+ | "running"
32
+ | "paused"
33
+ | "queued"
34
+ | "completed"
35
+ | "failed"
36
+ | "cancelled"
37
+ | "not_found"
38
+ | "already_completed";
27
39
 
28
40
  export interface SubagentSnapshot {
29
41
  id: string;
@@ -77,17 +89,17 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
77
89
  const limit = Math.min(MAX_LIST_LIMIT, Math.max(1, Math.floor(params.limit ?? DEFAULT_LIST_LIMIT)));
78
90
 
79
91
  if (params.action === "list") {
80
- const jobs = this.#listSubagentJobs(manager, ownerFilter, limit);
81
- return this.#buildResult(manager, jobs, { title: "Subagents" });
92
+ const records = this.#listSubagentRecords(manager, ownerFilter, limit);
93
+ return this.#buildRecordResult(manager, records, { title: "Subagents" });
82
94
  }
83
95
 
84
96
  if (params.action === "inspect") {
85
- const jobs = params.ids?.length
86
- ? this.#visibleJobsByIds(manager, params.ids, ownerId)
87
- : manager.getRunningJobs(ownerFilter).filter(isSubagentJob);
88
- return this.#buildResult(manager, jobs, {
97
+ const records = params.ids?.length
98
+ ? this.#visibleRecordsByIds(manager, params.ids, ownerFilter)
99
+ : this.#runningRecords(manager, ownerFilter);
100
+ return this.#buildRecordResult(manager, records, {
89
101
  title: "Subagent inspection",
90
- notFoundIds: this.#notFoundIds(manager, params.ids ?? [], ownerId),
102
+ notFoundIds: this.#notFoundRecordIds(manager, params.ids ?? [], ownerFilter),
91
103
  });
92
104
  }
93
105
 
@@ -98,46 +110,146 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
98
110
  }
99
111
  const snapshots: SubagentSnapshot[] = [];
100
112
  for (const id of ids) {
101
- const job = this.#findVisibleJob(manager, id, ownerId);
102
- if (!job) {
113
+ const record = this.#findVisibleRecord(manager, id, ownerFilter);
114
+ if (!record) {
103
115
  snapshots.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
104
116
  continue;
105
117
  }
106
- if (job.status !== "running") {
107
- snapshots.push({ ...this.#snapshot(job), status: "already_completed" });
118
+ const cancelled = manager.cancelSubagent(record.subagentId, ownerFilter);
119
+ if (!cancelled && record.currentJobId) manager.cancel(record.currentJobId, ownerFilter);
120
+ const updated = this.#findVisibleRecord(manager, id, ownerFilter) ?? record;
121
+ snapshots.push(this.#recordSnapshot(manager, updated));
122
+ }
123
+ return this.#buildSnapshotResult(snapshots, "Subagent cancellation");
124
+ }
125
+
126
+ if (params.action === "pause") {
127
+ const ids = params.ids ?? [];
128
+ if (ids.length === 0) {
129
+ throw new ToolError("`pause` requires at least one subagent id.");
130
+ }
131
+ const snapshots: SubagentSnapshot[] = [];
132
+ for (const id of ids) {
133
+ const record = this.#findVisibleRecord(manager, id, ownerFilter);
134
+ if (!record) {
135
+ snapshots.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
136
+ continue;
137
+ }
138
+ const result = manager.pauseSubagent(record.subagentId, ownerFilter);
139
+ if (!result.ok && result.reason === "not_found") {
140
+ snapshots.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
108
141
  continue;
109
142
  }
110
- manager.cancel(job.id, ownerFilter);
111
- snapshots.push(this.#snapshot(manager.getJob(job.id) ?? job));
143
+ snapshots.push(
144
+ this.#recordSnapshot(manager, manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record),
145
+ );
112
146
  }
113
- return this.#buildSnapshotResult(snapshots, "Subagent cancellation");
147
+ return this.#buildSnapshotResult(snapshots, "Subagent pause");
148
+ }
149
+
150
+ if (params.action === "resume") {
151
+ const ids = params.ids ?? [];
152
+ if (ids.length === 0) {
153
+ throw new ToolError("`resume` requires at least one subagent id.");
154
+ }
155
+ const snapshots: SubagentSnapshot[] = [];
156
+ for (const id of ids) {
157
+ const record = this.#findVisibleRecord(manager, id, ownerFilter);
158
+ if (!record) {
159
+ snapshots.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
160
+ continue;
161
+ }
162
+ if (record.status === "running") {
163
+ snapshots.push(this.#recordSnapshot(manager, record));
164
+ continue;
165
+ }
166
+ if (params.message === undefined && isTerminalStatus(record.status)) {
167
+ snapshots.push({
168
+ ...this.#recordSnapshot(manager, record),
169
+ guidance:
170
+ "This subagent is terminal. Provide `message` to start a follow-up resume run from its saved context.",
171
+ });
172
+ continue;
173
+ }
174
+ const result = manager.resumeSubagent(record.subagentId, ownerFilter, params.message);
175
+ if (!result.ok && result.reason === "context_unavailable") throw new ToolError("context unavailable");
176
+ if (!result.ok && result.reason === "not_found") {
177
+ snapshots.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
178
+ continue;
179
+ }
180
+ snapshots.push(
181
+ this.#recordSnapshot(manager, manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record),
182
+ );
183
+ }
184
+ return this.#buildSnapshotResult(snapshots, "Subagent resume");
114
185
  }
115
186
 
116
- return this.#awaitSubagents(manager, params, ownerId, ownerFilter, signal, onUpdate);
187
+ if (params.action === "steer") {
188
+ const ids = params.ids ?? [];
189
+ const message = params.message;
190
+ if (ids.length === 0) {
191
+ throw new ToolError("`steer` requires at least one subagent id.");
192
+ }
193
+ if (message === undefined || message.trim() === "") {
194
+ throw new ToolError("`steer` requires a non-empty message.");
195
+ }
196
+ const snapshots: SubagentSnapshot[] = [];
197
+ for (const id of ids) {
198
+ const record = this.#findVisibleRecord(manager, id, ownerFilter);
199
+ if (!record) {
200
+ snapshots.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
201
+ continue;
202
+ }
203
+ if (!record.sessionFile) throw new ToolError(`Subagent ${record.subagentId} has no session file.`);
204
+ if (record.status === "running") {
205
+ const handle = manager.getLiveHandle(record.subagentId);
206
+ if (!handle) throw new ToolError(`Subagent ${record.subagentId} has no live handle.`);
207
+ await handle.injectMessage(message, "steer");
208
+ if (params.pause === true) manager.pauseSubagent(record.subagentId, ownerFilter);
209
+ } else {
210
+ const result = manager.resumeSubagent(record.subagentId, ownerFilter, message);
211
+ if (!result.ok && result.reason === "context_unavailable") throw new ToolError("context unavailable");
212
+ if (!result.ok && result.reason === "not_found") {
213
+ snapshots.push(
214
+ this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."),
215
+ );
216
+ continue;
217
+ }
218
+ }
219
+ snapshots.push(
220
+ this.#recordSnapshot(manager, manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record),
221
+ );
222
+ }
223
+ return this.#buildSnapshotResult(snapshots, "Subagent steer");
224
+ }
225
+
226
+ return this.#awaitSubagents(manager, params, ownerFilter, signal, onUpdate);
117
227
  }
118
228
 
119
229
  async #awaitSubagents(
120
230
  manager: AsyncJobManager,
121
231
  params: SubagentParams,
122
- ownerId: string | undefined,
123
232
  ownerFilter: { ownerId: string } | undefined,
124
233
  signal: AbortSignal | undefined,
125
234
  onUpdate: AgentToolUpdateCallback<SubagentToolDetails> | undefined,
126
235
  ): Promise<AgentToolResult<SubagentToolDetails>> {
127
- const jobs = params.ids?.length
128
- ? this.#visibleJobsByIds(manager, params.ids, ownerId)
129
- : manager.getRunningJobs(ownerFilter).filter(isSubagentJob);
130
- const notFoundIds = this.#notFoundIds(manager, params.ids ?? [], ownerId);
131
- if (jobs.length === 0) {
236
+ const records = params.ids?.length
237
+ ? this.#visibleRecordsByIds(manager, params.ids, ownerFilter)
238
+ : this.#runningRecords(manager, ownerFilter);
239
+ const notFoundIds = this.#notFoundRecordIds(manager, params.ids ?? [], ownerFilter);
240
+ if (records.length === 0) {
132
241
  const missing = notFoundIds.map(id =>
133
242
  this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."),
134
243
  );
135
244
  return this.#buildSnapshotResult(missing, "Subagent await");
136
245
  }
137
246
 
138
- const runningJobs = jobs.filter(job => job.status === "running");
247
+ const runningJobs = records
248
+ .filter(record => record.status === "running" && record.currentJobId)
249
+ .map(record => manager.getJob(record.currentJobId!))
250
+ .filter((job): job is AsyncJob => job !== undefined);
139
251
  if (runningJobs.length === 0) {
140
- return this.#buildResult(manager, jobs, { title: "Subagent await", notFoundIds });
252
+ return this.#buildRecordResult(manager, records, { title: "Subagent await", notFoundIds });
141
253
  }
142
254
 
143
255
  const timeoutMs = Math.min(
@@ -148,10 +260,10 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
148
260
  manager.watchJobs(watchedJobIds);
149
261
  const progressTimer = onUpdate
150
262
  ? setInterval(() => {
151
- onUpdate(this.#progressResult(manager, jobs));
263
+ onUpdate(this.#progressResult(manager, records));
152
264
  }, 500)
153
265
  : undefined;
154
- onUpdate?.(this.#progressResult(manager, jobs));
266
+ onUpdate?.(this.#progressResult(manager, records));
155
267
 
156
268
  let timedOut = false;
157
269
  try {
@@ -176,70 +288,124 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
176
288
  if (progressTimer) clearInterval(progressTimer);
177
289
  }
178
290
 
179
- return this.#buildResult(manager, jobs, { title: "Subagent await", notFoundIds, timedOut });
291
+ return this.#buildRecordResult(manager, records, { title: "Subagent await", notFoundIds, timedOut });
180
292
  }
181
293
 
182
- #listSubagentJobs(
294
+ #mergedRecords(
183
295
  manager: AsyncJobManager,
184
296
  ownerFilter: { ownerId: string } | undefined,
185
297
  limit: number,
186
- ): AsyncJob[] {
187
- const running = manager.getRunningJobs(ownerFilter).filter(isSubagentJob);
188
- const recent = manager.getRecentJobs(limit, ownerFilter).filter(isSubagentJob);
189
- const jobs = [...running, ...recent];
190
- return this.#dedupeJobs(jobs).slice(0, limit);
298
+ ): SubagentRecord[] {
299
+ const merged = [...manager.getSubagentRecords(ownerFilter)];
300
+ const known = new Set(merged.map(record => record.subagentId));
301
+ const jobs = [...manager.getRunningJobs(ownerFilter), ...manager.getRecentJobs(limit, ownerFilter)].filter(
302
+ isSubagentJob,
303
+ );
304
+ for (const job of jobs) {
305
+ const subagentId = job.metadata?.subagent?.id ?? job.id;
306
+ if (known.has(subagentId)) continue;
307
+ known.add(subagentId);
308
+ merged.push(this.#jobToRecord(job));
309
+ }
310
+ merged.sort((a, b) => {
311
+ const aJob = a.currentJobId ? manager.getJob(a.currentJobId) : undefined;
312
+ const bJob = b.currentJobId ? manager.getJob(b.currentJobId) : undefined;
313
+ return (bJob?.startTime ?? 0) - (aJob?.startTime ?? 0);
314
+ });
315
+ return merged.slice(0, limit);
191
316
  }
192
317
 
193
- #visibleJobsByIds(manager: AsyncJobManager, ids: string[], ownerId: string | undefined): AsyncJob[] {
194
- const jobs: AsyncJob[] = [];
195
- for (const id of ids) {
196
- const job = this.#findVisibleJob(manager, id, ownerId);
197
- if (job) jobs.push(job);
198
- }
199
- return this.#dedupeJobs(jobs);
318
+ #listSubagentRecords(
319
+ manager: AsyncJobManager,
320
+ ownerFilter: { ownerId: string } | undefined,
321
+ limit: number,
322
+ ): SubagentRecord[] {
323
+ return this.#mergedRecords(manager, ownerFilter, limit);
200
324
  }
201
325
 
202
- #findVisibleJob(manager: AsyncJobManager, id: string, ownerId: string | undefined): AsyncJob | undefined {
203
- const trimmedId = id.trim();
204
- if (!trimmedId) return undefined;
205
- const direct = manager.getJob(trimmedId);
326
+ #runningRecords(manager: AsyncJobManager, ownerFilter: { ownerId: string } | undefined): SubagentRecord[] {
327
+ return this.#mergedRecords(manager, ownerFilter, MAX_LIST_LIMIT).filter(record => record.status === "running");
328
+ }
329
+
330
+ /** Synthesize a record from a subagent job that has no registered SubagentRecord (backward compat). */
331
+ #jobToRecord(job: AsyncJob): SubagentRecord {
332
+ return {
333
+ subagentId: job.metadata?.subagent?.id ?? job.id,
334
+ ownerId: job.ownerId,
335
+ currentJobId: job.id,
336
+ historicalJobIds: [],
337
+ status: job.status,
338
+ sessionFile: null,
339
+ resumable: false,
340
+ };
341
+ }
342
+
343
+ #findSubagentJob(manager: AsyncJobManager, id: string, ownerId: string | undefined): AsyncJob | undefined {
344
+ const direct = manager.getJob(id);
206
345
  if (direct && isSubagentJob(direct) && (!ownerId || direct.ownerId === ownerId)) return direct;
207
346
  return manager
208
347
  .getAllJobs(ownerId ? { ownerId } : undefined)
209
- .find(job => isSubagentJob(job) && job.metadata?.subagent?.id === trimmedId);
348
+ .find(job => isSubagentJob(job) && job.metadata?.subagent?.id === id);
210
349
  }
211
350
 
212
- #notFoundIds(manager: AsyncJobManager, ids: string[], ownerId: string | undefined): string[] {
213
- return ids.filter(id => !this.#findVisibleJob(manager, id, ownerId));
351
+ #visibleRecordsByIds(
352
+ manager: AsyncJobManager,
353
+ ids: string[],
354
+ ownerFilter: { ownerId: string } | undefined,
355
+ ): SubagentRecord[] {
356
+ const records: SubagentRecord[] = [];
357
+ const seen = new Set<string>();
358
+ for (const id of ids) {
359
+ const record = this.#findVisibleRecord(manager, id, ownerFilter);
360
+ if (!record || seen.has(record.subagentId)) continue;
361
+ seen.add(record.subagentId);
362
+ records.push(record);
363
+ }
364
+ return records;
214
365
  }
215
366
 
216
- #dedupeJobs(jobs: AsyncJob[]): AsyncJob[] {
217
- const seen = new Set<string>();
218
- return jobs.filter(job => {
219
- if (seen.has(job.id)) return false;
220
- seen.add(job.id);
221
- return true;
222
- });
367
+ #findVisibleRecord(
368
+ manager: AsyncJobManager,
369
+ id: string,
370
+ ownerFilter: { ownerId: string } | undefined,
371
+ ): SubagentRecord | undefined {
372
+ const trimmedId = id.trim();
373
+ if (!trimmedId) return undefined;
374
+ const direct = manager.getSubagentRecord(trimmedId, ownerFilter);
375
+ if (direct) return direct;
376
+ const byJobId = manager.getSubagentRecords(ownerFilter).find(record => record.currentJobId === trimmedId);
377
+ if (byJobId) return byJobId;
378
+ const job = this.#findSubagentJob(manager, trimmedId, ownerFilter?.ownerId);
379
+ return job ? this.#jobToRecord(job) : undefined;
380
+ }
381
+
382
+ #notFoundRecordIds(manager: AsyncJobManager, ids: string[], ownerFilter: { ownerId: string } | undefined): string[] {
383
+ return ids.filter(id => !this.#findVisibleRecord(manager, id, ownerFilter));
223
384
  }
224
385
 
225
- #progressResult(manager: AsyncJobManager, jobs: AsyncJob[]): AgentToolResult<SubagentToolDetails> {
386
+ #progressResult(manager: AsyncJobManager, records: SubagentRecord[]): AgentToolResult<SubagentToolDetails> {
226
387
  return {
227
388
  content: [{ type: "text", text: "" }],
228
- details: { subagents: this.#snapshots(manager, jobs) },
389
+ details: { subagents: this.#recordSnapshots(manager, records) },
229
390
  };
230
391
  }
231
392
 
232
- #buildResult(
393
+ #buildRecordResult(
233
394
  manager: AsyncJobManager,
234
- jobs: AsyncJob[],
395
+ records: SubagentRecord[],
235
396
  options: { title: string; notFoundIds?: string[]; timedOut?: boolean },
236
397
  ): AgentToolResult<SubagentToolDetails> {
237
- const snapshots = this.#snapshots(manager, jobs, options.timedOut);
398
+ const snapshots = this.#recordSnapshots(manager, records, options.timedOut);
238
399
  for (const id of options.notFoundIds ?? []) {
239
400
  snapshots.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
240
401
  }
241
402
  manager.acknowledgeDeliveries(
242
- snapshots.filter(s => s.status !== "running" && s.status !== "not_found").map(s => s.jobId),
403
+ snapshots
404
+ .filter(
405
+ s =>
406
+ s.status !== "running" && s.status !== "paused" && s.status !== "queued" && s.status !== "not_found",
407
+ )
408
+ .map(s => s.jobId),
243
409
  );
244
410
  return this.#buildSnapshotResult(snapshots, options.title);
245
411
  }
@@ -263,8 +429,29 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
263
429
  };
264
430
  }
265
431
 
266
- #snapshots(manager: AsyncJobManager, jobs: AsyncJob[], timedOut = false): SubagentSnapshot[] {
267
- return jobs.map(job => this.#snapshot(manager.getJob(job.id) ?? job, timedOut));
432
+ #recordSnapshots(manager: AsyncJobManager, records: SubagentRecord[], timedOut = false): SubagentSnapshot[] {
433
+ return records.map(record => this.#recordSnapshot(manager, record, timedOut));
434
+ }
435
+
436
+ #recordSnapshot(manager: AsyncJobManager, record: SubagentRecord, timedOut = false): SubagentSnapshot {
437
+ const job = record.currentJobId ? manager.getJob(record.currentJobId) : undefined;
438
+ if (job) {
439
+ return {
440
+ ...this.#snapshot(job, timedOut),
441
+ id: record.subagentId,
442
+ jobId: record.currentJobId ?? job.id,
443
+ status: record.status,
444
+ };
445
+ }
446
+ return {
447
+ id: record.subagentId,
448
+ jobId: record.currentJobId ?? record.subagentId,
449
+ status: record.status,
450
+ label: "subagent",
451
+ agent: "unknown",
452
+ agentSource: "bundled",
453
+ durationMs: 0,
454
+ };
268
455
  }
269
456
 
270
457
  #snapshot(job: AsyncJob, timedOut = false): SubagentSnapshot {
@@ -303,6 +490,10 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
303
490
  }
304
491
  }
305
492
 
493
+ function isTerminalStatus(status: SubagentStatus): boolean {
494
+ return status === "completed" || status === "failed" || status === "cancelled";
495
+ }
496
+
306
497
  function isSubagentJob(job: AsyncJob): boolean {
307
498
  return job.type === "task" && job.metadata?.subagent !== undefined;
308
499
  }