@gajae-code/coding-agent 0.2.5 → 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 (112) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/types/async/job-manager.d.ts +84 -2
  3. package/dist/types/commands/harness.d.ts +37 -0
  4. package/dist/types/config/settings-schema.d.ts +6 -0
  5. package/dist/types/config/settings.d.ts +2 -0
  6. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  7. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  9. package/dist/types/extensibility/shared-events.d.ts +1 -0
  10. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  11. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  12. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  16. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  17. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  18. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  20. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  21. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  22. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  23. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  24. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  25. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  26. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  27. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  28. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  29. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  30. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  31. package/dist/types/harness-control-plane/types.d.ts +162 -0
  32. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  33. package/dist/types/hooks/skill-state.d.ts +2 -29
  34. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  35. package/dist/types/modes/interactive-mode.d.ts +1 -0
  36. package/dist/types/modes/types.d.ts +1 -0
  37. package/dist/types/sdk.d.ts +2 -0
  38. package/dist/types/session/agent-session.d.ts +8 -0
  39. package/dist/types/skill-state/active-state.d.ts +2 -0
  40. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  41. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  42. package/dist/types/task/executor.d.ts +3 -0
  43. package/dist/types/task/types.d.ts +55 -3
  44. package/dist/types/tools/subagent.d.ts +11 -1
  45. package/package.json +7 -7
  46. package/src/async/job-manager.ts +298 -6
  47. package/src/cli/auth-broker-cli.ts +1 -0
  48. package/src/cli/config-cli.ts +10 -2
  49. package/src/cli.ts +2 -0
  50. package/src/commands/harness.ts +592 -0
  51. package/src/commands/team.ts +36 -39
  52. package/src/config/settings-schema.ts +7 -0
  53. package/src/config/settings.ts +5 -0
  54. package/src/deep-interview/render-middleware.ts +366 -0
  55. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  56. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  57. package/src/extensibility/custom-tools/types.ts +1 -0
  58. package/src/extensibility/extensions/types.ts +6 -0
  59. package/src/extensibility/shared-events.ts +1 -0
  60. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  61. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  62. package/src/gjc-runtime/ralplan-runtime.ts +25 -10
  63. package/src/gjc-runtime/state-graph.ts +86 -0
  64. package/src/gjc-runtime/state-migrations.ts +132 -0
  65. package/src/gjc-runtime/state-renderer.ts +345 -0
  66. package/src/gjc-runtime/state-runtime.ts +733 -21
  67. package/src/gjc-runtime/state-validation.ts +49 -0
  68. package/src/gjc-runtime/state-writer.ts +718 -0
  69. package/src/gjc-runtime/team-runtime.ts +1083 -89
  70. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  71. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  72. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  73. package/src/harness-control-plane/classifier.ts +128 -0
  74. package/src/harness-control-plane/control-endpoint.ts +137 -0
  75. package/src/harness-control-plane/finalize.ts +222 -0
  76. package/src/harness-control-plane/frame-mapper.ts +286 -0
  77. package/src/harness-control-plane/operate.ts +225 -0
  78. package/src/harness-control-plane/owner.ts +553 -0
  79. package/src/harness-control-plane/preserve.ts +102 -0
  80. package/src/harness-control-plane/receipts.ts +216 -0
  81. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  82. package/src/harness-control-plane/seams.ts +39 -0
  83. package/src/harness-control-plane/session-lease.ts +388 -0
  84. package/src/harness-control-plane/state-machine.ts +97 -0
  85. package/src/harness-control-plane/storage.ts +257 -0
  86. package/src/harness-control-plane/types.ts +214 -0
  87. package/src/hooks/skill-keywords.ts +4 -2
  88. package/src/hooks/skill-state.ts +24 -41
  89. package/src/internal-urls/docs-index.generated.ts +1 -1
  90. package/src/modes/components/assistant-message.ts +5 -1
  91. package/src/modes/components/hook-selector.ts +72 -2
  92. package/src/modes/controllers/event-controller.ts +71 -6
  93. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  94. package/src/modes/controllers/input-controller.ts +9 -1
  95. package/src/modes/controllers/selector-controller.ts +2 -1
  96. package/src/modes/interactive-mode.ts +1 -0
  97. package/src/modes/types.ts +1 -0
  98. package/src/prompts/agents/executor.md +13 -0
  99. package/src/prompts/tools/subagent.md +33 -3
  100. package/src/sdk.ts +4 -0
  101. package/src/session/agent-session.ts +231 -33
  102. package/src/session/session-manager.ts +13 -1
  103. package/src/skill-state/active-state.ts +58 -65
  104. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  105. package/src/skill-state/initial-phase.ts +2 -0
  106. package/src/skill-state/workflow-state-contract.ts +26 -0
  107. package/src/task/executor.ts +50 -8
  108. package/src/task/index.ts +120 -8
  109. package/src/task/render.ts +6 -3
  110. package/src/task/types.ts +56 -3
  111. package/src/tools/ask.ts +28 -7
  112. package/src/tools/subagent.ts +255 -64
@@ -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
  }