@gajae-code/coding-agent 0.2.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/types/async/job-manager.d.ts +91 -2
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/commands/harness.d.ts +37 -0
  6. package/dist/types/config/keybindings.d.ts +5 -0
  7. package/dist/types/config/settings-schema.d.ts +10 -4
  8. package/dist/types/config/settings.d.ts +2 -0
  9. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  10. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  11. package/dist/types/deep-interview/render-middleware.d.ts +6 -0
  12. package/dist/types/eval/py/executor.d.ts +2 -0
  13. package/dist/types/eval/py/kernel.d.ts +2 -0
  14. package/dist/types/exec/bash-executor.d.ts +10 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  17. package/dist/types/extensibility/shared-events.d.ts +1 -0
  18. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  19. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  20. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  21. package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
  22. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  23. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  25. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  26. package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  28. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  29. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  30. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  31. package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
  32. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  33. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  34. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  35. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  36. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  37. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  38. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  39. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  40. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  41. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  42. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  43. package/dist/types/harness-control-plane/types.d.ts +162 -0
  44. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  45. package/dist/types/hooks/skill-state.d.ts +23 -29
  46. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  47. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  48. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  49. package/dist/types/internal-urls/types.d.ts +4 -0
  50. package/dist/types/lsp/index.d.ts +10 -10
  51. package/dist/types/modes/bridge/auth.d.ts +12 -0
  52. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  53. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  54. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  55. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  56. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  57. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  58. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  59. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  60. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  61. package/dist/types/modes/components/status-line.d.ts +2 -0
  62. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  63. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  64. package/dist/types/modes/index.d.ts +1 -0
  65. package/dist/types/modes/interactive-mode.d.ts +2 -0
  66. package/dist/types/modes/jobs-observer.d.ts +57 -0
  67. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  68. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  69. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  70. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  71. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  72. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  73. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  74. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  75. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  76. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  77. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  78. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  79. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  80. package/dist/types/modes/types.d.ts +2 -0
  81. package/dist/types/sdk.d.ts +4 -0
  82. package/dist/types/session/agent-session.d.ts +19 -1
  83. package/dist/types/skill-state/active-state.d.ts +2 -0
  84. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  85. package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
  86. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  87. package/dist/types/task/executor.d.ts +3 -0
  88. package/dist/types/task/id.d.ts +7 -0
  89. package/dist/types/task/index.d.ts +5 -0
  90. package/dist/types/task/receipt.d.ts +85 -0
  91. package/dist/types/task/spawn-gate.d.ts +38 -0
  92. package/dist/types/task/types.d.ts +198 -14
  93. package/dist/types/tools/cron.d.ts +6 -0
  94. package/dist/types/tools/index.d.ts +2 -0
  95. package/dist/types/tools/path-utils.d.ts +1 -0
  96. package/dist/types/tools/subagent.d.ts +26 -1
  97. package/package.json +7 -7
  98. package/scripts/build-binary.ts +7 -0
  99. package/src/async/job-manager.ts +334 -6
  100. package/src/cli/args.ts +9 -2
  101. package/src/cli/auth-broker-cli.ts +1 -0
  102. package/src/cli/config-cli.ts +10 -2
  103. package/src/cli.ts +2 -0
  104. package/src/commands/deep-interview.ts +1 -0
  105. package/src/commands/harness.ts +862 -0
  106. package/src/commands/launch.ts +2 -2
  107. package/src/commands/state.ts +2 -1
  108. package/src/commands/team.ts +54 -39
  109. package/src/config/keybindings.ts +6 -0
  110. package/src/config/settings-schema.ts +13 -3
  111. package/src/config/settings.ts +5 -0
  112. package/src/dap/client.ts +17 -3
  113. package/src/debug/crash-diagnostics.ts +223 -0
  114. package/src/debug/runtime-gauges.ts +20 -0
  115. package/src/deep-interview/render-middleware.ts +372 -0
  116. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  117. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  118. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  119. package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
  120. package/src/eval/py/executor.ts +21 -1
  121. package/src/eval/py/kernel.ts +15 -0
  122. package/src/exec/bash-executor.ts +41 -0
  123. package/src/extensibility/custom-tools/types.ts +1 -0
  124. package/src/extensibility/extensions/types.ts +6 -0
  125. package/src/extensibility/shared-events.ts +1 -0
  126. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  127. package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
  128. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  129. package/src/gjc-runtime/ralplan-runtime.ts +235 -43
  130. package/src/gjc-runtime/state-graph.ts +86 -0
  131. package/src/gjc-runtime/state-migrations.ts +179 -0
  132. package/src/gjc-runtime/state-renderer.ts +345 -0
  133. package/src/gjc-runtime/state-runtime.ts +1155 -46
  134. package/src/gjc-runtime/state-schema.ts +192 -0
  135. package/src/gjc-runtime/state-validation.ts +49 -0
  136. package/src/gjc-runtime/state-writer.ts +749 -0
  137. package/src/gjc-runtime/team-runtime.ts +1255 -189
  138. package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
  139. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  140. package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
  141. package/src/gjc-runtime/workflow-manifest.ts +427 -0
  142. package/src/harness-control-plane/classifier.ts +128 -0
  143. package/src/harness-control-plane/control-endpoint.ts +148 -0
  144. package/src/harness-control-plane/finalize.ts +222 -0
  145. package/src/harness-control-plane/frame-mapper.ts +286 -0
  146. package/src/harness-control-plane/operate.ts +225 -0
  147. package/src/harness-control-plane/owner.ts +600 -0
  148. package/src/harness-control-plane/preserve.ts +102 -0
  149. package/src/harness-control-plane/receipts.ts +216 -0
  150. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  151. package/src/harness-control-plane/seams.ts +39 -0
  152. package/src/harness-control-plane/session-lease.ts +388 -0
  153. package/src/harness-control-plane/state-machine.ts +98 -0
  154. package/src/harness-control-plane/storage.ts +257 -0
  155. package/src/harness-control-plane/types.ts +214 -0
  156. package/src/hooks/skill-keywords.ts +4 -2
  157. package/src/hooks/skill-state.ts +197 -64
  158. package/src/internal-urls/agent-protocol.ts +68 -21
  159. package/src/internal-urls/artifact-protocol.ts +12 -17
  160. package/src/internal-urls/docs-index.generated.ts +3 -2
  161. package/src/internal-urls/registry-helpers.ts +19 -16
  162. package/src/internal-urls/types.ts +4 -0
  163. package/src/lsp/client.ts +18 -2
  164. package/src/main.ts +21 -5
  165. package/src/modes/bridge/auth.ts +41 -0
  166. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  167. package/src/modes/bridge/bridge-mode.ts +520 -0
  168. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  169. package/src/modes/bridge/event-stream.ts +70 -0
  170. package/src/modes/components/assistant-message.ts +5 -1
  171. package/src/modes/components/custom-editor.ts +101 -0
  172. package/src/modes/components/hook-selector.ts +133 -20
  173. package/src/modes/components/jobs-overlay-model.ts +109 -0
  174. package/src/modes/components/jobs-overlay.ts +172 -0
  175. package/src/modes/components/status-line/presets.ts +7 -5
  176. package/src/modes/components/status-line/segments.ts +25 -0
  177. package/src/modes/components/status-line/types.ts +2 -0
  178. package/src/modes/components/status-line.ts +9 -1
  179. package/src/modes/controllers/event-controller.ts +71 -6
  180. package/src/modes/controllers/extension-ui-controller.ts +43 -1
  181. package/src/modes/controllers/input-controller.ts +105 -9
  182. package/src/modes/controllers/selector-controller.ts +31 -1
  183. package/src/modes/index.ts +1 -0
  184. package/src/modes/interactive-mode.ts +28 -0
  185. package/src/modes/jobs-observer.ts +204 -0
  186. package/src/modes/rpc/host-tools.ts +1 -186
  187. package/src/modes/rpc/host-uris.ts +1 -235
  188. package/src/modes/rpc/rpc-client.ts +25 -10
  189. package/src/modes/rpc/rpc-mode.ts +12 -381
  190. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  191. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  192. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  193. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  194. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  195. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  196. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  197. package/src/modes/shared/agent-wire/responses.ts +17 -0
  198. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  199. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  200. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  201. package/src/modes/types.ts +2 -0
  202. package/src/prompts/agents/executor.md +13 -0
  203. package/src/prompts/tools/subagent.md +39 -4
  204. package/src/prompts/tools/task-summary.md +3 -9
  205. package/src/prompts/tools/task.md +5 -1
  206. package/src/sdk.ts +8 -0
  207. package/src/session/agent-session.ts +445 -71
  208. package/src/session/session-manager.ts +13 -1
  209. package/src/skill-state/active-state.ts +58 -65
  210. package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
  211. package/src/skill-state/initial-phase.ts +2 -0
  212. package/src/skill-state/workflow-state-contract.ts +33 -4
  213. package/src/skill-state/workflow-state-version.ts +3 -0
  214. package/src/slash-commands/builtin-registry.ts +8 -0
  215. package/src/task/executor.ts +79 -13
  216. package/src/task/id.ts +33 -0
  217. package/src/task/index.ts +376 -74
  218. package/src/task/output-manager.ts +5 -4
  219. package/src/task/receipt.ts +297 -0
  220. package/src/task/render.ts +54 -134
  221. package/src/task/spawn-gate.ts +132 -0
  222. package/src/task/types.ts +104 -10
  223. package/src/tools/ask.ts +88 -27
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash.ts +1 -1
  227. package/src/tools/cron.ts +48 -0
  228. package/src/tools/find.ts +4 -1
  229. package/src/tools/index.ts +2 -0
  230. package/src/tools/path-utils.ts +3 -2
  231. package/src/tools/read.ts +1 -0
  232. package/src/tools/search.ts +1 -0
  233. package/src/tools/skill.ts +6 -1
  234. package/src/tools/subagent.ts +423 -79
@@ -1,7 +1,8 @@
1
+ import * as path from "node:path";
1
2
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
2
3
  import { prompt } from "@gajae-code/utils";
3
4
  import * as z from "zod/v4";
4
- import { type AsyncJob, AsyncJobManager } from "../async";
5
+ import { type AsyncJob, AsyncJobManager, type SubagentRecord } from "../async";
5
6
  import subagentDescription from "../prompts/tools/subagent.md" with { type: "text" };
6
7
  import type { AgentSource } from "../task/types";
7
8
  import { Ellipsis, truncateToWidth } from "../tui";
@@ -13,17 +14,38 @@ const DEFAULT_AWAIT_TIMEOUT_MS = 30_000;
13
14
  const MAX_AWAIT_TIMEOUT_MS = 60 * 60 * 1000;
14
15
  const DEFAULT_LIST_LIMIT = 10;
15
16
  const MAX_LIST_LIMIT = 50;
16
- const TEXT_PREVIEW_WIDTH = 12_000;
17
+ const RECEIPT_PREVIEW_WIDTH = 280;
18
+ const PREVIEW_WIDTH = 2_000;
19
+ const FULL_PREVIEW_WIDTH = 12_000;
17
20
 
18
21
  const subagentSchema = z.object({
19
- action: z.enum(["list", "inspect", "await", "cancel"]).describe("subagent control action"),
22
+ action: z
23
+ .enum(["list", "inspect", "await", "cancel", "pause", "resume", "steer"])
24
+ .describe("subagent control action"),
20
25
  ids: z.array(z.string()).optional().describe("subagent ids or backing job ids"),
26
+ id: z.string().optional().describe("single subagent id or backing job id for resume/steer"),
27
+ message: z.string().optional().describe("message to deliver when resuming or steering a subagent"),
28
+ pause: z.boolean().optional().describe("pause after steering a currently running subagent"),
21
29
  timeout_ms: z.number().min(0).max(MAX_AWAIT_TIMEOUT_MS).optional().describe("await timeout in milliseconds"),
22
30
  limit: z.number().min(1).max(MAX_LIST_LIMIT).optional().describe("maximum subagents to return"),
31
+ verbosity: z
32
+ .enum(["receipt", "preview", "full"])
33
+ .optional()
34
+ .describe(
35
+ "output verbosity: receipt (default, <=280-char receipt preview), preview (<=2000 chars), or full (<=12000 chars; requires explicit ids)",
36
+ ),
23
37
  });
24
38
 
25
39
  type SubagentParams = z.infer<typeof subagentSchema>;
26
- type SubagentStatus = "running" | "completed" | "failed" | "cancelled" | "not_found" | "already_completed";
40
+ type SubagentStatus =
41
+ | "running"
42
+ | "paused"
43
+ | "queued"
44
+ | "completed"
45
+ | "failed"
46
+ | "cancelled"
47
+ | "not_found"
48
+ | "already_completed";
27
49
 
28
50
  export interface SubagentSnapshot {
29
51
  id: string;
@@ -37,6 +59,9 @@ export interface SubagentSnapshot {
37
59
  durationMs: number;
38
60
  resultText?: string;
39
61
  errorText?: string;
62
+ resultPreview?: string;
63
+ outputRef?: string;
64
+ truncated?: boolean;
40
65
  guidance?: string;
41
66
  }
42
67
 
@@ -75,19 +100,26 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
75
100
  const ownerId = this.session.getAgentId?.() ?? undefined;
76
101
  const ownerFilter = ownerId ? { ownerId } : undefined;
77
102
  const limit = Math.min(MAX_LIST_LIMIT, Math.max(1, Math.floor(params.limit ?? DEFAULT_LIST_LIMIT)));
103
+ const verbosity = params.verbosity ?? "receipt";
104
+ if (verbosity === "full" && (params.action === "list" || !params.ids?.length)) {
105
+ throw new ToolError(
106
+ "`verbosity=full` cannot be used with `list` and requires explicit `ids` so broad inspection cannot inline retained subagent output.",
107
+ );
108
+ }
78
109
 
79
110
  if (params.action === "list") {
80
- const jobs = this.#listSubagentJobs(manager, ownerFilter, limit);
81
- return this.#buildResult(manager, jobs, { title: "Subagents" });
111
+ const records = this.#listSubagentRecords(manager, ownerFilter, limit);
112
+ return await this.#buildRecordResult(manager, records, { title: "Subagents", verbosity });
82
113
  }
83
114
 
84
115
  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, {
116
+ const records = params.ids?.length
117
+ ? this.#visibleRecordsByIds(manager, params.ids, ownerFilter)
118
+ : this.#runningRecords(manager, ownerFilter);
119
+ return await this.#buildRecordResult(manager, records, {
89
120
  title: "Subagent inspection",
90
- notFoundIds: this.#notFoundIds(manager, params.ids ?? [], ownerId),
121
+ notFoundIds: this.#notFoundRecordIds(manager, params.ids ?? [], ownerFilter),
122
+ verbosity,
91
123
  });
92
124
  }
93
125
 
@@ -96,48 +128,190 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
96
128
  if (ids.length === 0) {
97
129
  throw new ToolError("`cancel` requires at least one subagent id.");
98
130
  }
99
- const snapshots: SubagentSnapshot[] = [];
131
+ const records: SubagentRecord[] = [];
132
+ const missing: SubagentSnapshot[] = [];
133
+ for (const id of ids) {
134
+ const record = this.#findVisibleRecord(manager, id, ownerFilter);
135
+ if (!record) {
136
+ missing.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
137
+ continue;
138
+ }
139
+ const cancelled = manager.cancelSubagent(record.subagentId, ownerFilter);
140
+ if (!cancelled && record.currentJobId) manager.cancel(record.currentJobId, ownerFilter);
141
+ records.push(this.#findVisibleRecord(manager, id, ownerFilter) ?? record);
142
+ }
143
+ const verifiedOutputIds = await this.#verifiedOutputIds(records);
144
+ return this.#buildSnapshotResult(
145
+ [
146
+ ...records.map(record => this.#recordSnapshot(manager, record, false, verbosity, verifiedOutputIds)),
147
+ ...missing,
148
+ ],
149
+ "Subagent cancellation",
150
+ );
151
+ }
152
+
153
+ if (params.action === "pause") {
154
+ const ids = params.ids ?? [];
155
+ if (ids.length === 0) {
156
+ throw new ToolError("`pause` requires at least one subagent id.");
157
+ }
158
+ const records: SubagentRecord[] = [];
159
+ const missing: SubagentSnapshot[] = [];
100
160
  for (const id of ids) {
101
- const job = this.#findVisibleJob(manager, id, ownerId);
102
- if (!job) {
103
- snapshots.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
161
+ const record = this.#findVisibleRecord(manager, id, ownerFilter);
162
+ if (!record) {
163
+ missing.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
104
164
  continue;
105
165
  }
106
- if (job.status !== "running") {
107
- snapshots.push({ ...this.#snapshot(job), status: "already_completed" });
166
+ const result = manager.pauseSubagent(record.subagentId, ownerFilter);
167
+ if (!result.ok && result.reason === "not_found") {
168
+ missing.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
108
169
  continue;
109
170
  }
110
- manager.cancel(job.id, ownerFilter);
111
- snapshots.push(this.#snapshot(manager.getJob(job.id) ?? job));
171
+ records.push(manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record);
172
+ }
173
+ const verifiedOutputIds = await this.#verifiedOutputIds(records);
174
+ return this.#buildSnapshotResult(
175
+ [
176
+ ...records.map(record => this.#recordSnapshot(manager, record, false, verbosity, verifiedOutputIds)),
177
+ ...missing,
178
+ ],
179
+ "Subagent pause",
180
+ );
181
+ }
182
+
183
+ if (params.action === "resume") {
184
+ const id = this.#singleTargetId(params, "resume");
185
+ const records: SubagentRecord[] = [];
186
+ const missing: SubagentSnapshot[] = [];
187
+ const terminalGuidanceIds = new Set<string>();
188
+ const record = this.#findVisibleRecord(manager, id, ownerFilter);
189
+ const verifiedOutputIds = await this.#verifiedOutputIds(record ? [record] : []);
190
+ if (!record) {
191
+ missing.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
192
+ } else if (record.status === "running") {
193
+ records.push(record);
194
+ } else if (params.message === undefined && isTerminalStatus(record.status)) {
195
+ records.push(record);
196
+ terminalGuidanceIds.add(record.subagentId);
197
+ } else {
198
+ const result = manager.resumeSubagent(record.subagentId, ownerFilter, params.message);
199
+ if (!result.ok && result.reason === "context_unavailable") throw new ToolError("context unavailable");
200
+ if (!result.ok && result.reason === "not_found") {
201
+ missing.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
202
+ } else {
203
+ records.push(manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record);
204
+ }
205
+ }
206
+
207
+ return this.#buildSnapshotResult(
208
+ [
209
+ ...records.map(record => {
210
+ const snapshot = this.#recordSnapshot(manager, record, false, verbosity, verifiedOutputIds);
211
+ return terminalGuidanceIds.has(record.subagentId)
212
+ ? {
213
+ ...snapshot,
214
+ guidance:
215
+ "This subagent is terminal. Provide `message` to start a follow-up resume run from its saved context.",
216
+ }
217
+ : snapshot;
218
+ }),
219
+ ...missing,
220
+ ],
221
+ "Subagent resume",
222
+ );
223
+ }
224
+
225
+ if (params.action === "steer") {
226
+ const id = this.#singleTargetId(params, "steer");
227
+ const message = params.message;
228
+ if (message === undefined || message.trim() === "") {
229
+ throw new ToolError("`steer` requires a non-empty message.");
230
+ }
231
+ const records: SubagentRecord[] = [];
232
+ const missing: SubagentSnapshot[] = [];
233
+ const record = this.#findVisibleRecord(manager, id, ownerFilter);
234
+ const verifiedOutputIds = await this.#verifiedOutputIds(record ? [record] : []);
235
+ if (!record) {
236
+ missing.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
237
+ } else {
238
+ if (!record.sessionFile) throw new ToolError(`Subagent ${record.subagentId} has no session file.`);
239
+ if (record.status === "running") {
240
+ const handle = manager.getLiveHandle(record.subagentId);
241
+ if (!handle) throw new ToolError(`Subagent ${record.subagentId} has no live handle.`);
242
+ await handle.injectMessage(message, "steer");
243
+ if (params.pause === true) manager.pauseSubagent(record.subagentId, ownerFilter);
244
+ } else {
245
+ const result = manager.resumeSubagent(record.subagentId, ownerFilter, message);
246
+ if (!result.ok && result.reason === "context_unavailable") throw new ToolError("context unavailable");
247
+ if (!result.ok && result.reason === "not_found") {
248
+ missing.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
249
+ } else {
250
+ records.push(manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record);
251
+ }
252
+ }
253
+ if (record.status === "running")
254
+ records.push(manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record);
112
255
  }
113
- return this.#buildSnapshotResult(snapshots, "Subagent cancellation");
256
+ return this.#buildSnapshotResult(
257
+ [
258
+ ...records.map(record => this.#recordSnapshot(manager, record, false, verbosity, verifiedOutputIds)),
259
+ ...missing,
260
+ ],
261
+ "Subagent steer",
262
+ );
114
263
  }
115
264
 
116
- return this.#awaitSubagents(manager, params, ownerId, ownerFilter, signal, onUpdate);
265
+ return this.#awaitSubagents(manager, params, ownerFilter, signal, onUpdate);
266
+ }
267
+
268
+ #singleTargetId(params: SubagentParams, action: "resume" | "steer"): string {
269
+ const id = params.id?.trim();
270
+ const ids = (params.ids ?? []).map(value => value.trim()).filter(value => value.length > 0);
271
+ if (id && ids.length > 0) {
272
+ if (ids.length === 1 && ids[0] === id) return id;
273
+ throw new ToolError(
274
+ `\`${action}\` accepts exactly one target; provide \`id\` or a single-item \`ids\`, not both.`,
275
+ );
276
+ }
277
+ if (id) return id;
278
+ if (ids.length === 1) return ids[0]!;
279
+ if (ids.length > 1) {
280
+ throw new ToolError(
281
+ `\`${action}\` accepts exactly one target because \`message\` is delivered to one subagent.`,
282
+ );
283
+ }
284
+ throw new ToolError(`\`${action}\` requires a single subagent id via \`id\`.`);
117
285
  }
118
286
 
119
287
  async #awaitSubagents(
120
288
  manager: AsyncJobManager,
121
289
  params: SubagentParams,
122
- ownerId: string | undefined,
123
290
  ownerFilter: { ownerId: string } | undefined,
124
291
  signal: AbortSignal | undefined,
125
292
  onUpdate: AgentToolUpdateCallback<SubagentToolDetails> | undefined,
126
293
  ): 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) {
294
+ const records = params.ids?.length
295
+ ? this.#visibleRecordsByIds(manager, params.ids, ownerFilter)
296
+ : this.#runningRecords(manager, ownerFilter);
297
+ const notFoundIds = this.#notFoundRecordIds(manager, params.ids ?? [], ownerFilter);
298
+ if (records.length === 0) {
132
299
  const missing = notFoundIds.map(id =>
133
300
  this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."),
134
301
  );
135
302
  return this.#buildSnapshotResult(missing, "Subagent await");
136
303
  }
137
304
 
138
- const runningJobs = jobs.filter(job => job.status === "running");
305
+ const runningJobs = records
306
+ .filter(record => record.status === "running" && record.currentJobId)
307
+ .map(record => manager.getJob(record.currentJobId!))
308
+ .filter((job): job is AsyncJob => job !== undefined);
139
309
  if (runningJobs.length === 0) {
140
- return this.#buildResult(manager, jobs, { title: "Subagent await", notFoundIds });
310
+ return await this.#buildRecordResult(manager, records, {
311
+ title: "Subagent await",
312
+ notFoundIds,
313
+ verbosity: params.verbosity ?? "receipt",
314
+ });
141
315
  }
142
316
 
143
317
  const timeoutMs = Math.min(
@@ -148,10 +322,10 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
148
322
  manager.watchJobs(watchedJobIds);
149
323
  const progressTimer = onUpdate
150
324
  ? setInterval(() => {
151
- onUpdate(this.#progressResult(manager, jobs));
325
+ onUpdate(this.#progressResult(manager, records));
152
326
  }, 500)
153
327
  : undefined;
154
- onUpdate?.(this.#progressResult(manager, jobs));
328
+ onUpdate?.(this.#progressResult(manager, records));
155
329
 
156
330
  let timedOut = false;
157
331
  try {
@@ -176,70 +350,136 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
176
350
  if (progressTimer) clearInterval(progressTimer);
177
351
  }
178
352
 
179
- return this.#buildResult(manager, jobs, { title: "Subagent await", notFoundIds, timedOut });
353
+ return await this.#buildRecordResult(manager, records, {
354
+ title: "Subagent await",
355
+ notFoundIds,
356
+ timedOut,
357
+ verbosity: params.verbosity ?? "receipt",
358
+ });
180
359
  }
181
360
 
182
- #listSubagentJobs(
361
+ #mergedRecords(
183
362
  manager: AsyncJobManager,
184
363
  ownerFilter: { ownerId: string } | undefined,
185
364
  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);
365
+ ): SubagentRecord[] {
366
+ const merged = [...manager.getSubagentRecords(ownerFilter)];
367
+ const known = new Set(merged.map(record => record.subagentId));
368
+ const jobs = [...manager.getRunningJobs(ownerFilter), ...manager.getRecentJobs(limit, ownerFilter)].filter(
369
+ isSubagentJob,
370
+ );
371
+ for (const job of jobs) {
372
+ const subagentId = job.metadata?.subagent?.id ?? job.id;
373
+ if (known.has(subagentId)) continue;
374
+ known.add(subagentId);
375
+ merged.push(this.#jobToRecord(job));
376
+ }
377
+ merged.sort((a, b) => {
378
+ const aJob = a.currentJobId ? manager.getJob(a.currentJobId) : undefined;
379
+ const bJob = b.currentJobId ? manager.getJob(b.currentJobId) : undefined;
380
+ return (bJob?.startTime ?? 0) - (aJob?.startTime ?? 0);
381
+ });
382
+ return merged.slice(0, limit);
191
383
  }
192
384
 
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);
385
+ #listSubagentRecords(
386
+ manager: AsyncJobManager,
387
+ ownerFilter: { ownerId: string } | undefined,
388
+ limit: number,
389
+ ): SubagentRecord[] {
390
+ return this.#mergedRecords(manager, ownerFilter, limit);
200
391
  }
201
392
 
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);
393
+ #runningRecords(manager: AsyncJobManager, ownerFilter: { ownerId: string } | undefined): SubagentRecord[] {
394
+ return this.#mergedRecords(manager, ownerFilter, MAX_LIST_LIMIT).filter(record => record.status === "running");
395
+ }
396
+
397
+ /** Synthesize a record from a subagent job that has no registered SubagentRecord (backward compat). */
398
+ #jobToRecord(job: AsyncJob): SubagentRecord {
399
+ return {
400
+ subagentId: job.metadata?.subagent?.id ?? job.id,
401
+ ownerId: job.ownerId,
402
+ currentJobId: job.id,
403
+ historicalJobIds: [],
404
+ status: job.status,
405
+ sessionFile: null,
406
+ resumable: false,
407
+ };
408
+ }
409
+
410
+ #findSubagentJob(manager: AsyncJobManager, id: string, ownerId: string | undefined): AsyncJob | undefined {
411
+ const direct = manager.getJob(id);
206
412
  if (direct && isSubagentJob(direct) && (!ownerId || direct.ownerId === ownerId)) return direct;
207
413
  return manager
208
414
  .getAllJobs(ownerId ? { ownerId } : undefined)
209
- .find(job => isSubagentJob(job) && job.metadata?.subagent?.id === trimmedId);
415
+ .find(job => isSubagentJob(job) && job.metadata?.subagent?.id === id);
210
416
  }
211
417
 
212
- #notFoundIds(manager: AsyncJobManager, ids: string[], ownerId: string | undefined): string[] {
213
- return ids.filter(id => !this.#findVisibleJob(manager, id, ownerId));
418
+ #visibleRecordsByIds(
419
+ manager: AsyncJobManager,
420
+ ids: string[],
421
+ ownerFilter: { ownerId: string } | undefined,
422
+ ): SubagentRecord[] {
423
+ const records: SubagentRecord[] = [];
424
+ const seen = new Set<string>();
425
+ for (const id of ids) {
426
+ const record = this.#findVisibleRecord(manager, id, ownerFilter);
427
+ if (!record || seen.has(record.subagentId)) continue;
428
+ seen.add(record.subagentId);
429
+ records.push(record);
430
+ }
431
+ return records;
214
432
  }
215
433
 
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
- });
434
+ #findVisibleRecord(
435
+ manager: AsyncJobManager,
436
+ id: string,
437
+ ownerFilter: { ownerId: string } | undefined,
438
+ ): SubagentRecord | undefined {
439
+ const trimmedId = id.trim();
440
+ if (!trimmedId) return undefined;
441
+ const direct = manager.getSubagentRecord(trimmedId, ownerFilter);
442
+ if (direct) return direct;
443
+ const byJobId = manager.getSubagentRecords(ownerFilter).find(record => record.currentJobId === trimmedId);
444
+ if (byJobId) return byJobId;
445
+ const job = this.#findSubagentJob(manager, trimmedId, ownerFilter?.ownerId);
446
+ return job ? this.#jobToRecord(job) : undefined;
223
447
  }
224
448
 
225
- #progressResult(manager: AsyncJobManager, jobs: AsyncJob[]): AgentToolResult<SubagentToolDetails> {
449
+ #notFoundRecordIds(manager: AsyncJobManager, ids: string[], ownerFilter: { ownerId: string } | undefined): string[] {
450
+ return ids.filter(id => !this.#findVisibleRecord(manager, id, ownerFilter));
451
+ }
452
+
453
+ #progressResult(manager: AsyncJobManager, records: SubagentRecord[]): AgentToolResult<SubagentToolDetails> {
226
454
  return {
227
455
  content: [{ type: "text", text: "" }],
228
- details: { subagents: this.#snapshots(manager, jobs) },
456
+ details: { subagents: this.#recordSnapshots(manager, records, false, "receipt", new Set()) },
229
457
  };
230
458
  }
231
459
 
232
- #buildResult(
460
+ async #buildRecordResult(
233
461
  manager: AsyncJobManager,
234
- jobs: AsyncJob[],
235
- options: { title: string; notFoundIds?: string[]; timedOut?: boolean },
236
- ): AgentToolResult<SubagentToolDetails> {
237
- const snapshots = this.#snapshots(manager, jobs, options.timedOut);
462
+ records: SubagentRecord[],
463
+ options: { title: string; notFoundIds?: string[]; timedOut?: boolean; verbosity?: SubagentParams["verbosity"] },
464
+ ): Promise<AgentToolResult<SubagentToolDetails>> {
465
+ const verifiedOutputIds = await this.#verifiedOutputIds(records);
466
+ const snapshots = this.#recordSnapshots(
467
+ manager,
468
+ records,
469
+ options.timedOut,
470
+ options.verbosity ?? "receipt",
471
+ verifiedOutputIds,
472
+ );
238
473
  for (const id of options.notFoundIds ?? []) {
239
474
  snapshots.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
240
475
  }
241
476
  manager.acknowledgeDeliveries(
242
- snapshots.filter(s => s.status !== "running" && s.status !== "not_found").map(s => s.jobId),
477
+ snapshots
478
+ .filter(
479
+ s =>
480
+ s.status !== "running" && s.status !== "paused" && s.status !== "queued" && s.status !== "not_found",
481
+ )
482
+ .map(s => s.jobId),
243
483
  );
244
484
  return this.#buildSnapshotResult(snapshots, options.title);
245
485
  }
@@ -251,9 +491,13 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
251
491
  if (snapshot.jobId !== snapshot.id) lines.push(`Job: ${snapshot.jobId}`);
252
492
  if (snapshot.agent) lines.push(`Agent: ${snapshot.agent} (${snapshot.agentSource})`);
253
493
  if (snapshot.description) lines.push(`Description: ${snapshot.description}`);
494
+ if (snapshot.outputRef) lines.push(`Output: ${snapshot.outputRef}`);
254
495
  if (snapshot.assignment) lines.push("Assignment:", "```", snapshot.assignment, "```");
255
- if (snapshot.resultText) lines.push("Result:", "```", snapshot.resultText, "```");
256
- if (snapshot.errorText) lines.push("Error:", "```", snapshot.errorText, "```");
496
+ if (snapshot.resultPreview) {
497
+ lines.push(snapshot.errorText ? "Error preview:" : "Result preview:", "```", snapshot.resultPreview, "```");
498
+ if (snapshot.truncated)
499
+ lines.push("Preview truncated; use the output ref or explicit ids with `verbosity=full` for more.");
500
+ }
257
501
  if (snapshot.guidance) lines.push(`Guidance: ${snapshot.guidance}`);
258
502
  lines.push("");
259
503
  }
@@ -263,32 +507,111 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
263
507
  };
264
508
  }
265
509
 
266
- #snapshots(manager: AsyncJobManager, jobs: AsyncJob[], timedOut = false): SubagentSnapshot[] {
267
- return jobs.map(job => this.#snapshot(manager.getJob(job.id) ?? job, timedOut));
510
+ #recordSnapshots(
511
+ manager: AsyncJobManager,
512
+ records: SubagentRecord[],
513
+ timedOut = false,
514
+ verbosity: SubagentParams["verbosity"] = "receipt",
515
+ verifiedOutputIds: ReadonlySet<string>,
516
+ ): SubagentSnapshot[] {
517
+ return records.map(record => this.#recordSnapshot(manager, record, timedOut, verbosity, verifiedOutputIds));
518
+ }
519
+
520
+ #recordSnapshot(
521
+ manager: AsyncJobManager,
522
+ record: SubagentRecord,
523
+ timedOut = false,
524
+ verbosity: SubagentParams["verbosity"] = "receipt",
525
+ verifiedOutputIds: ReadonlySet<string>,
526
+ ): SubagentSnapshot {
527
+ const job = record.currentJobId ? manager.getJob(record.currentJobId) : undefined;
528
+ if (job) {
529
+ return {
530
+ ...this.#snapshot(job, timedOut, verbosity, verifiedOutputIds, record),
531
+ id: record.subagentId,
532
+ jobId: record.currentJobId ?? job.id,
533
+ status: record.status,
534
+ };
535
+ }
536
+ return {
537
+ id: record.subagentId,
538
+ jobId: record.currentJobId ?? record.subagentId,
539
+ status: record.status,
540
+ label: "subagent",
541
+ agent: "unknown",
542
+ agentSource: "bundled",
543
+ durationMs: 0,
544
+ ...(verifiedOutputIds.has(record.subagentId) ? { outputRef: `agent://${record.subagentId}` } : {}),
545
+ };
268
546
  }
269
547
 
270
- #snapshot(job: AsyncJob, timedOut = false): SubagentSnapshot {
548
+ #snapshot(
549
+ job: AsyncJob,
550
+ timedOut = false,
551
+ verbosity: SubagentParams["verbosity"] = "receipt",
552
+ verifiedOutputIds: ReadonlySet<string>,
553
+ record?: SubagentRecord,
554
+ ): SubagentSnapshot {
271
555
  const subagent = job.metadata?.subagent;
272
556
  const runningTimeoutGuidance =
273
557
  timedOut && job.status === "running"
274
558
  ? "Still running after the await timeout; timeout only bounded this wait and is not a failure. Inspect progress, continue independent work, and never cancel just because an await timed out; cancel only if the subagent has actually failed, gone off-track, or become unrecoverably wrong."
275
559
  : undefined;
560
+ const output = previewJobOutput(job, verbosity);
561
+ const outputRef = record && verifiedOutputIds.has(record.subagentId) ? `agent://${record.subagentId}` : undefined;
276
562
  return {
277
563
  id: subagent?.id ?? job.id,
278
564
  jobId: job.id,
279
565
  status: job.status,
280
- label: sanitizeText(job.label),
566
+ label: sanitizeText(job.label, RECEIPT_PREVIEW_WIDTH),
281
567
  agent: subagent?.agent ?? "unknown",
282
568
  agentSource: subagent?.agentSource ?? "bundled",
283
569
  durationMs: Math.max(0, Date.now() - job.startTime),
284
- ...(subagent?.description ? { description: sanitizeText(subagent.description) } : {}),
285
- ...(subagent?.assignment ? { assignment: sanitizeText(subagent.assignment) } : {}),
286
- ...(job.resultText ? { resultText: sanitizeText(job.resultText) } : {}),
287
- ...(job.errorText ? { errorText: sanitizeText(job.errorText) } : {}),
570
+ ...(subagent?.description ? { description: sanitizeText(subagent.description, RECEIPT_PREVIEW_WIDTH) } : {}),
571
+ ...(verbosity === "full" && subagent?.assignment
572
+ ? { assignment: sanitizeText(subagent.assignment, FULL_PREVIEW_WIDTH) }
573
+ : {}),
574
+ ...(output
575
+ ? {
576
+ ...(output.type === "error" ? { errorText: output.preview } : { resultText: output.preview }),
577
+ resultPreview: output.preview,
578
+ truncated: output.truncated,
579
+ }
580
+ : {}),
581
+ ...(outputRef ? { outputRef } : {}),
288
582
  ...(runningTimeoutGuidance ? { guidance: runningTimeoutGuidance } : {}),
289
583
  };
290
584
  }
291
585
 
586
+ async #verifiedOutputIds(records: SubagentRecord[]): Promise<Set<string>> {
587
+ const ids = new Set(records.map(record => record.subagentId));
588
+ const dirs = this.#artifactDirsForRecords(records);
589
+ const verified = new Set<string>();
590
+ await Promise.all(
591
+ [...ids].map(async id => {
592
+ for (const dir of dirs) {
593
+ if (await Bun.file(path.join(dir, `${id}.md.meta.json`)).exists()) {
594
+ verified.add(id);
595
+ return;
596
+ }
597
+ }
598
+ }),
599
+ );
600
+ return verified;
601
+ }
602
+
603
+ #artifactDirsForRecords(records: SubagentRecord[]): string[] {
604
+ const dirs: string[] = [];
605
+ for (const record of records) {
606
+ if (!record.sessionFile) continue;
607
+ const dir = path.dirname(record.sessionFile);
608
+ if (!dirs.includes(dir)) dirs.push(dir);
609
+ }
610
+ const sessionDir = this.session.getArtifactsDir?.();
611
+ if (sessionDir && !dirs.includes(sessionDir)) dirs.push(sessionDir);
612
+ return dirs;
613
+ }
614
+
292
615
  #missingSnapshot(id: string, status: "not_found", guidance: string): SubagentSnapshot {
293
616
  return {
294
617
  id,
@@ -303,10 +626,31 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
303
626
  }
304
627
  }
305
628
 
629
+ function isTerminalStatus(status: SubagentStatus): boolean {
630
+ return status === "completed" || status === "failed" || status === "cancelled";
631
+ }
632
+
306
633
  function isSubagentJob(job: AsyncJob): boolean {
307
634
  return job.type === "task" && job.metadata?.subagent !== undefined;
308
635
  }
309
636
 
310
- function sanitizeText(text: string): string {
311
- return truncateToWidth(replaceTabs(text), TEXT_PREVIEW_WIDTH, Ellipsis.Unicode);
637
+ function sanitizeText(text: string, width: number): string {
638
+ return truncateToWidth(replaceTabs(text), width, Ellipsis.Unicode);
639
+ }
640
+
641
+ function previewJobOutput(
642
+ job: AsyncJob,
643
+ verbosity: SubagentParams["verbosity"] = "receipt",
644
+ ): { type: "result" | "error"; preview: string; truncated: boolean } | undefined {
645
+ const source = job.errorText
646
+ ? { type: "error" as const, text: job.errorText }
647
+ : job.resultText
648
+ ? { type: "result" as const, text: job.resultText }
649
+ : undefined;
650
+ if (!source) return undefined;
651
+ const width =
652
+ verbosity === "full" ? FULL_PREVIEW_WIDTH : verbosity === "preview" ? PREVIEW_WIDTH : RECEIPT_PREVIEW_WIDTH;
653
+ const normalized = replaceTabs(source.text);
654
+ const preview = truncateToWidth(normalized, width, Ellipsis.Unicode);
655
+ return { type: source.type, preview, truncated: preview !== normalized };
312
656
  }