@diffexai/diffex 0.2.6 → 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 (52) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/core/agent-session.d.ts +5 -0
  3. package/dist/core/agent-session.js +89 -19
  4. package/dist/core/compaction/compaction.js +3 -1
  5. package/dist/core/model-runtime.js +3 -3
  6. package/dist/core/provider-composer.js +1 -1
  7. package/dist/core/sdk.d.ts +2 -1
  8. package/dist/core/sdk.js +18 -6
  9. package/dist/core/subagents/controller.d.ts +4 -3
  10. package/dist/core/subagents/controller.js +173 -37
  11. package/dist/core/subagents/types.d.ts +13 -0
  12. package/dist/core/tools/bash.d.ts +4 -0
  13. package/dist/core/tools/bash.js +14 -5
  14. package/dist/core/tools/subagents.d.ts +12 -10
  15. package/dist/core/tools/subagents.js +61 -49
  16. package/dist/node_modules/@diffexai/diffex-agent-core/distribution-components.json +4 -4
  17. package/dist/node_modules/@diffexai/diffex-agent-core/distribution-files.json +1 -1
  18. package/dist/node_modules/@diffexai/diffex-agent-core/package.json +1 -1
  19. package/dist/node_modules/@diffexai/diffex-ai/dist/api/lazy.d.ts +1 -1
  20. package/dist/node_modules/@diffexai/diffex-ai/dist/api/lazy.js +8 -8
  21. package/dist/node_modules/@diffexai/diffex-ai/dist/models.js +4 -4
  22. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/.manifest.json +1 -1
  23. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/opencode.json +1 -1
  24. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/openrouter.json +1 -1
  25. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  26. package/dist/node_modules/@diffexai/diffex-ai/distribution-components.json +3 -3
  27. package/dist/node_modules/@diffexai/diffex-ai/distribution-files.json +8 -8
  28. package/dist/node_modules/@diffexai/diffex-ai/package.json +1 -1
  29. package/dist/node_modules/@diffexai/diffex-client/distribution-components.json +3 -3
  30. package/dist/node_modules/@diffexai/diffex-client/distribution-files.json +1 -1
  31. package/dist/node_modules/@diffexai/diffex-client/package.json +1 -1
  32. package/dist/node_modules/@diffexai/diffex-harness-state/distribution-components.json +2 -2
  33. package/dist/node_modules/@diffexai/diffex-harness-state/distribution-files.json +1 -1
  34. package/dist/node_modules/@diffexai/diffex-harness-state/package.json +1 -1
  35. package/dist/node_modules/@diffexai/diffex-protocol/distribution-components.json +2 -2
  36. package/dist/node_modules/@diffexai/diffex-protocol/distribution-files.json +1 -1
  37. package/dist/node_modules/@diffexai/diffex-protocol/package.json +1 -1
  38. package/dist/node_modules/@diffexai/diffex-telemetry/distribution-components.json +2 -2
  39. package/dist/node_modules/@diffexai/diffex-telemetry/distribution-files.json +1 -1
  40. package/dist/node_modules/@diffexai/diffex-telemetry/package.json +1 -1
  41. package/dist/node_modules/@diffexai/diffex-tui/distribution-components.json +2 -2
  42. package/dist/node_modules/@diffexai/diffex-tui/distribution-files.json +1 -1
  43. package/dist/node_modules/@diffexai/diffex-tui/package.json +1 -1
  44. package/dist/utils/owned-shell-processes.d.ts +8 -0
  45. package/dist/utils/owned-shell-processes.js +61 -0
  46. package/distribution-components.json +11 -11
  47. package/distribution-files.json +51 -43
  48. package/npm-shrinkwrap.json +2 -2
  49. package/package.json +1 -1
  50. package/release/distribution-manifest.json +4 -4
  51. package/release/install-package-lock.json +5 -5
  52. package/release/install-package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  import { NOOP_TELEMETRY_CONTEXT, uuidv7 } from "@diffexai/diffex-agent-core";
2
2
  import { startCodingAgentSpan } from "../telemetry.js";
3
- import { createAgentActionTool, createAgentsTool, createSpawnAgentTool } from "../tools/subagents.js";
3
+ import { createAgentActionTool, createAgentStatusTool, createSpawnAgentTool } from "../tools/subagents.js";
4
4
  import { reportSubagentRuntimeWarning } from "./runtime-diagnostics.js";
5
5
  import { SUBAGENT_NAME_PATTERN } from "./types.js";
6
6
  function normalizeError(error) {
@@ -57,6 +57,9 @@ function captureSpawnOrigin(parent, toolCallId) {
57
57
  const MAX_TIMER_MS = 2_147_483_647;
58
58
  export class SubagentController {
59
59
  #maxConcurrent;
60
+ #leaseMs;
61
+ #supervisionIntervalMs;
62
+ #parentDeadline;
60
63
  #captureSession;
61
64
  #persistRoster;
62
65
  #restoreRoster;
@@ -83,6 +86,20 @@ export class SubagentController {
83
86
  throw new RangeError("maxConcurrent must be a positive integer");
84
87
  }
85
88
  this.#maxConcurrent = options.maxConcurrent;
89
+ this.#leaseMs = options.leaseMs ?? 15 * 60_000;
90
+ this.#supervisionIntervalMs = options.supervisionIntervalMs ?? 120_000;
91
+ for (const [name, value] of [
92
+ ["leaseMs", this.#leaseMs],
93
+ ["supervisionIntervalMs", this.#supervisionIntervalMs],
94
+ ]) {
95
+ if (!Number.isInteger(value) || value <= 0 || value > MAX_TIMER_MS)
96
+ throw new RangeError(`${name} must be a positive timer-safe integer`);
97
+ }
98
+ if (options.parentDeadline !== undefined &&
99
+ (!Number.isSafeInteger(options.parentDeadline) || options.parentDeadline <= 0)) {
100
+ throw new RangeError("parentDeadline must be a positive Unix timestamp in milliseconds");
101
+ }
102
+ this.#parentDeadline = options.parentDeadline;
86
103
  this.#captureSession = options.captureSession;
87
104
  this.#telemetryContext = options.telemetryContext ?? NOOP_TELEMETRY_CONTEXT;
88
105
  this.#persistRoster = options.persistRoster;
@@ -102,12 +119,14 @@ export class SubagentController {
102
119
  }, () => this.#assertParentPolicyCompatible()),
103
120
  createAgentActionTool((request) => {
104
121
  const parent = this.#getBoundParent();
122
+ if (request.action === "renew")
123
+ return this.renew(request.target, request.reason);
105
124
  return request.action === "message"
106
- ? this.message(request.target, request.message, request.wake, parent.isStreaming)
125
+ ? this.message(request.target, request.message, request.wake, parent.isStreaming, request.delivery)
107
126
  : this.interrupt(request.target);
108
127
  }),
109
- createAgentsTool(async (options) => {
110
- const result = await this.queryAgents(options);
128
+ createAgentStatusTool(async (options) => {
129
+ const result = await this.queryAgents({ ...options, timeoutMs: 0 });
111
130
  this.#consumeTerminalQueryResults(result);
112
131
  return result;
113
132
  }),
@@ -236,14 +255,84 @@ export class SubagentController {
236
255
  });
237
256
  this.#scheduleRun(record, task);
238
257
  }
239
- message(name, message, wake = false, coordinated = false) {
258
+ message(name, message, wake = false, coordinated = false, delivery = "steer") {
240
259
  if (message.trim().length === 0) {
241
260
  return Promise.reject(new Error("Subagent message must be non-empty"));
242
261
  }
243
- return this.#serializeAction(name, () => this.#traceAction(name, "message", () => this.#message(name, message, wake, coordinated)));
262
+ return this.#serializeAction(name, () => this.#traceAction(name, "message", () => this.#message(name, message, wake, coordinated, delivery)));
244
263
  }
245
264
  interrupt(name) {
246
- return this.#serializeAction(name, () => this.#traceAction(name, "interrupt", () => this.#interrupt(name)));
265
+ // Cancellation must not wait behind a message waiting for construction or a permit.
266
+ return this.#traceAction(name, "interrupt", () => this.#interrupt(name));
267
+ }
268
+ async renew(name, reason) {
269
+ const record = this.#getRecordForAction(name);
270
+ if (!reason.trim())
271
+ throw new Error("Lease renewal requires a justification");
272
+ const supervision = record.supervision;
273
+ if (!supervision ||
274
+ isTerminalStatus(record.status) ||
275
+ record.interruptionRequested ||
276
+ supervision.deadline <= Date.now()) {
277
+ throw new Error(`Cannot renew inactive or expired subagent: ${name}`);
278
+ }
279
+ const deadline = Math.min(supervision.deadline + this.#leaseMs, this.#parentDeadline ?? Infinity);
280
+ if (deadline > supervision.deadline) {
281
+ supervision.deadline = deadline;
282
+ supervision.checkpointRequested = false;
283
+ this.#armLease(record);
284
+ }
285
+ return { target: name, action: "renew", status: record.status, deadline: supervision.deadline };
286
+ }
287
+ #armLease(record) {
288
+ const supervision = record.supervision;
289
+ if (!supervision)
290
+ return;
291
+ clearTimeout(supervision.leaseTimer);
292
+ supervision.leaseTimer = setTimeout(() => {
293
+ if (record.supervision !== supervision || isTerminalStatus(record.status))
294
+ return;
295
+ if (supervision.deadline > Date.now()) {
296
+ this.#armLease(record);
297
+ return;
298
+ }
299
+ record.runtimeWarning = "Subagent lease or parent deadline expired; cancellation requested";
300
+ void this.interrupt(record.name).catch((error) => {
301
+ record.runtimeWarning = `Subagent cancellation failed: ${normalizeError(error)}`;
302
+ });
303
+ }, Math.min(MAX_TIMER_MS, Math.max(0, supervision.deadline - Date.now())));
304
+ supervision.leaseTimer.unref();
305
+ }
306
+ #stopSupervision(record) {
307
+ clearTimeout(record.supervision?.leaseTimer);
308
+ clearInterval(record.supervision?.reportTimer);
309
+ }
310
+ async #reportActivity(record) {
311
+ const supervision = record.supervision;
312
+ if (!supervision || supervision.reporting || isTerminalStatus(record.status) || this.#closed || this.#resetting)
313
+ return;
314
+ supervision.reporting = true;
315
+ try {
316
+ const now = Date.now();
317
+ const remaining = Math.max(0, supervision.deadline - now);
318
+ await this.#deliverEscalation?.(record.name, `Runtime supervision: ${JSON.stringify({
319
+ status: record.status,
320
+ elapsed_ms: now - supervision.startedAt,
321
+ inactive_ms: now - supervision.lastActivityAt,
322
+ })}`);
323
+ if (!supervision.checkpointRequested &&
324
+ remaining <= Math.max(this.#supervisionIntervalMs * 2, this.#leaseMs / 5) &&
325
+ record.status === "running") {
326
+ supervision.checkpointRequested = true;
327
+ await record.session?.steer("Your work allowance is nearly exhausted. If needed work remains, call escalate_to_parent with a concise checkpoint: current progress, remaining work, the goal, and the decision or extension needed. Otherwise finish.");
328
+ }
329
+ }
330
+ catch (error) {
331
+ record.runtimeWarning = `Subagent supervision delivery failed: ${normalizeError(error)}`;
332
+ }
333
+ finally {
334
+ supervision.reporting = false;
335
+ }
247
336
  }
248
337
  attachSession(name, session) {
249
338
  const record = this.#getRecordForUpdate(name);
@@ -424,7 +513,11 @@ export class SubagentController {
424
513
  (options.until !== undefined && this.#queryConditionMet(result.agents, options.until))) {
425
514
  return result;
426
515
  }
427
- const deadline = performance.now() + options.timeoutMs;
516
+ // Return control periodically so supervision can reach a parent inside a long wait tool.
517
+ const timeoutMs = this.#deliverEscalation
518
+ ? Math.min(options.timeoutMs, this.#supervisionIntervalMs)
519
+ : options.timeoutMs;
520
+ const deadline = performance.now() + timeoutMs;
428
521
  while (true) {
429
522
  const remainingMs = Math.min(deadline - performance.now(), MAX_TIMER_MS);
430
523
  if (remainingMs <= 0) {
@@ -703,6 +796,7 @@ export class SubagentController {
703
796
  record.error = undefined;
704
797
  }
705
798
  else {
799
+ this.#stopSupervision(record);
706
800
  record.completedAt = timestamp;
707
801
  record.finalText = fields.finalText;
708
802
  record.error = status === "failed" ? fields.error : undefined;
@@ -1030,8 +1124,9 @@ export class SubagentController {
1030
1124
  }
1031
1125
  });
1032
1126
  }
1033
- async #message(name, message, wake, coordinated) {
1127
+ async #message(name, message, wake, coordinated, delivery) {
1034
1128
  const record = this.#getRecordForAction(name);
1129
+ const admittedRun = record.runController;
1035
1130
  while (record.status === "queued" && record.session) {
1036
1131
  const snapshot = this.snapshot();
1037
1132
  if (record.status !== "queued")
@@ -1045,6 +1140,9 @@ export class SubagentController {
1045
1140
  await record.run;
1046
1141
  this.#assertOpen();
1047
1142
  }
1143
+ admittedRun?.signal.throwIfAborted();
1144
+ if (admittedRun && record.interruptionRequested)
1145
+ throw new Error(`Subagent interrupted: ${name}`);
1048
1146
  if (wake && isTerminalStatus(record.status) && record.coordinatedRun?.delivery === "frozen") {
1049
1147
  throw new Error(`Cannot wake subagent while its coordinated result is pending delivery: ${name}`);
1050
1148
  }
@@ -1055,6 +1153,9 @@ export class SubagentController {
1055
1153
  if (!wake)
1056
1154
  throw new Error(`Cannot message restored subagent without wake: ${name}`);
1057
1155
  const prepared = await record.prepareWake();
1156
+ admittedRun?.signal.throwIfAborted();
1157
+ if (admittedRun && record.interruptionRequested)
1158
+ throw new Error(`Subagent interrupted: ${name}`);
1058
1159
  record.createSession = prepared.createSession;
1059
1160
  record.sessionManager = prepared.sessionManager;
1060
1161
  record.persistenceConfig = prepared.config;
@@ -1076,13 +1177,15 @@ export class SubagentController {
1076
1177
  record.coordinatedRun = this.#createCoordinatedRun("retry");
1077
1178
  }
1078
1179
  else {
1079
- record.coordinatedRun = undefined;
1180
+ record.coordinatedRun =
1181
+ coordinated && record.coordinatedRun ? this.#createCoordinatedRun("initial") : undefined;
1080
1182
  }
1081
1183
  this.#scheduleRun(record, message);
1082
1184
  return { target: name, action: "message", status: "queued", delivery: "prompt" };
1083
1185
  }
1084
1186
  if (record.status === "idle" && wake) {
1085
- record.coordinatedRun = undefined;
1187
+ record.coordinatedRun =
1188
+ coordinated && record.coordinatedRun ? this.#createCoordinatedRun("initial") : undefined;
1086
1189
  this.#scheduleRun(record, message);
1087
1190
  return { target: name, action: "message", status: "queued", delivery: "prompt" };
1088
1191
  }
@@ -1091,10 +1194,12 @@ export class SubagentController {
1091
1194
  throw new Error(`Subagent session is unavailable: ${name}`);
1092
1195
  }
1093
1196
  if (record.status === "idle") {
1197
+ if (delivery === "steer")
1198
+ throw new Error(`Cannot steer idle subagent without wake: ${name}`);
1094
1199
  await session.followUp(message);
1095
1200
  return { target: name, action: "message", status: "idle", delivery: "follow_up" };
1096
1201
  }
1097
- if (wake) {
1202
+ if (delivery === "steer") {
1098
1203
  await session.steer(message);
1099
1204
  return { target: name, action: "message", status: "running", delivery: "steer" };
1100
1205
  }
@@ -1113,12 +1218,7 @@ export class SubagentController {
1113
1218
  }
1114
1219
  record.interruptionRequested = true;
1115
1220
  const run = record.run;
1116
- try {
1117
- await this.#requestRunCancellation(record, new Error(`Subagent interrupted: ${name}`));
1118
- }
1119
- catch {
1120
- // The captured run still determines the settled controller state.
1121
- }
1221
+ await this.#requestRunCancellation(record, new Error(`Subagent interrupted: ${name}`));
1122
1222
  await run;
1123
1223
  if (record.status === "queued" || record.status === "running") {
1124
1224
  this.markInterrupted(name);
@@ -1134,6 +1234,23 @@ export class SubagentController {
1134
1234
  const runController = new AbortController();
1135
1235
  record.runController = runController;
1136
1236
  record.runCancellation = undefined;
1237
+ const now = Date.now();
1238
+ record.supervision = {
1239
+ deadline: Math.min(now + this.#leaseMs, this.#parentDeadline ?? Infinity),
1240
+ startedAt: now,
1241
+ lastActivityAt: now,
1242
+ checkpointRequested: false,
1243
+ reporting: false,
1244
+ };
1245
+ if (record.supervision.deadline <= now) {
1246
+ record.interruptionRequested = true;
1247
+ runController.abort(new Error("Parent deadline has already expired"));
1248
+ }
1249
+ this.#armLease(record);
1250
+ record.supervision.reportTimer = setInterval(() => {
1251
+ void this.#reportActivity(record);
1252
+ }, this.#supervisionIntervalMs);
1253
+ record.supervision.reportTimer.unref();
1137
1254
  const run = this.#runPrompt(record, prompt, runController);
1138
1255
  record.run = run;
1139
1256
  void run.catch(() => { });
@@ -1164,6 +1281,7 @@ export class SubagentController {
1164
1281
  let session = record.session;
1165
1282
  let createdSession;
1166
1283
  const assistantMessages = [];
1284
+ let shellTerminationFailed = false;
1167
1285
  let unsubscribe;
1168
1286
  try {
1169
1287
  permit = await this.acquirePermit(runController.signal);
@@ -1193,10 +1311,22 @@ export class SubagentController {
1193
1311
  const activeSession = session;
1194
1312
  let running = false;
1195
1313
  unsubscribe = activeSession.subscribe((event) => {
1314
+ const supervision = record.supervision;
1315
+ if (supervision) {
1316
+ if ([
1317
+ "message_update",
1318
+ "message_end",
1319
+ "tool_execution_start",
1320
+ "tool_execution_update",
1321
+ "tool_execution_end",
1322
+ ].includes(event.type))
1323
+ supervision.lastActivityAt = Date.now();
1324
+ }
1196
1325
  if (event.type === "agent_start" && !running) {
1197
1326
  running = true;
1198
- if (runController.signal.aborted) {
1199
- void activeSession.abort().catch(() => { });
1327
+ if (runController.signal.aborted || record.interruptionRequested) {
1328
+ if (!record.runCancellation)
1329
+ void activeSession.abort().catch(() => { });
1200
1330
  }
1201
1331
  else {
1202
1332
  this.markRunning(record.name);
@@ -1206,7 +1336,18 @@ export class SubagentController {
1206
1336
  assistantMessages.push(event.message);
1207
1337
  }
1208
1338
  });
1209
- await activeSession.prompt(prompt, { signal: runController.signal });
1339
+ const [promptResult] = await Promise.allSettled([
1340
+ activeSession.prompt(prompt, { signal: runController.signal }),
1341
+ ]);
1342
+ try {
1343
+ await activeSession.terminateOwnedShellProcesses();
1344
+ }
1345
+ catch (error) {
1346
+ shellTerminationFailed = true;
1347
+ throw error;
1348
+ }
1349
+ if (promptResult.status === "rejected")
1350
+ throw promptResult.reason;
1210
1351
  if (this.#closed)
1211
1352
  return;
1212
1353
  const terminalMessage = assistantMessages.at(-1);
@@ -1230,7 +1371,7 @@ export class SubagentController {
1230
1371
  catch (error) {
1231
1372
  if (!this.#closed) {
1232
1373
  const finalText = getAssistantText(assistantMessages.at(-1));
1233
- if (record.interruptionRequested) {
1374
+ if (record.interruptionRequested && !shellTerminationFailed) {
1234
1375
  this.markInterrupted(record.name, finalText);
1235
1376
  }
1236
1377
  else {
@@ -1270,6 +1411,7 @@ export class SubagentController {
1270
1411
  #detachRecords(reason) {
1271
1412
  const records = [...this.#records.values()];
1272
1413
  for (const record of records) {
1414
+ this.#stopSupervision(record);
1273
1415
  void this.#requestRunCancellation(record, reason);
1274
1416
  }
1275
1417
  this.#records.clear();
@@ -1278,23 +1420,17 @@ export class SubagentController {
1278
1420
  #requestRunCancellation(record, reason) {
1279
1421
  if (record.runCancellation)
1280
1422
  return record.runCancellation;
1423
+ record.interruptionRequested = true;
1281
1424
  let cancellation;
1282
- // AgentSession marks streaming before awaiting agent_start hooks, while this record can still be queued.
1283
- if (record.runController && !record.session?.isStreaming) {
1284
- record.runController.abort(reason);
1285
- cancellation = Promise.resolve();
1286
- }
1287
- else if (record.session) {
1288
- try {
1289
- cancellation = record.session.abort();
1290
- }
1291
- catch (error) {
1292
- cancellation = Promise.reject(error);
1293
- }
1425
+ try {
1426
+ // Active prompts own their cancellation and verification. Abort the run signal
1427
+ // only during construction/preflight, avoiding a second abort via its listener.
1428
+ if (!record.session?.isStreaming)
1429
+ record.runController?.abort(reason);
1430
+ cancellation = record.session?.abort() ?? Promise.resolve();
1294
1431
  }
1295
- else {
1296
- record.runController?.abort(reason);
1297
- cancellation = Promise.resolve();
1432
+ catch (error) {
1433
+ cancellation = Promise.reject(error);
1298
1434
  }
1299
1435
  record.runCancellation = cancellation;
1300
1436
  void cancellation.catch(() => {
@@ -3,6 +3,14 @@ import type { AgentSession } from "../agent-session.ts";
3
3
  import type { SessionManager } from "../session-manager.ts";
4
4
  import type { ToolName } from "../tools/index.ts";
5
5
  export declare const SUBAGENT_NAME_PATTERN: RegExp;
6
+ export interface SubagentSupervisionOptions {
7
+ /** Renewable allowance per child run, including queueing. Default: 15 minutes. */
8
+ leaseMs?: number;
9
+ /** Runtime activity reports to the parent. Default: 2 minutes. */
10
+ supervisionIntervalMs?: number;
11
+ /** Absolute parent deadline, in milliseconds since the Unix epoch. */
12
+ parentDeadline?: number;
13
+ }
6
14
  export type SubagentStatus = "queued" | "running" | "idle" | "failed" | "interrupted";
7
15
  export interface SubagentSummary {
8
16
  readonly name: string;
@@ -41,6 +49,11 @@ export type SubagentSpawnOrigin = {
41
49
  export type CaptureSubagentSession = (parent: AgentSession, childName: string, origin: SubagentSpawnOrigin) => CapturedSubagentSession;
42
50
  export type SubagentMessageDelivery = "steer" | "follow_up" | "prompt";
43
51
  export type SubagentActionResult = {
52
+ readonly target: string;
53
+ readonly action: "renew";
54
+ readonly status: SubagentStatus;
55
+ readonly deadline: number;
56
+ } | {
44
57
  readonly target: string;
45
58
  readonly action: "message";
46
59
  readonly status: SubagentStatus;
@@ -1,5 +1,6 @@
1
1
  import type { AgentTool } from "@diffexai/diffex-agent-core";
2
2
  import { type Static, Type } from "typebox";
3
+ import type { OwnedShellProcesses } from "../../utils/owned-shell-processes.ts";
3
4
  import type { ToolDefinition } from "../extensions/types.ts";
4
5
  import { type TruncationResult } from "./truncate.ts";
5
6
  declare const bashSchema: Type.TObject<{
@@ -44,6 +45,7 @@ export interface BashOperations {
44
45
  */
45
46
  export declare function createLocalBashOperations(options?: {
46
47
  shellPath?: string;
48
+ owner?: OwnedShellProcesses;
47
49
  }): BashOperations;
48
50
  export interface BashSpawnContext {
49
51
  command: string;
@@ -52,6 +54,8 @@ export interface BashSpawnContext {
52
54
  }
53
55
  export type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;
54
56
  export interface BashToolOptions {
57
+ /** Session ownership for cancellation of background shell jobs. */
58
+ owner?: OwnedShellProcesses;
55
59
  /** Custom operations for command execution. Default: local shell */
56
60
  operations?: BashOperations;
57
61
  /** Command prefix prepended to every command (for example shell setup commands) */
@@ -54,6 +54,7 @@ export function createLocalBashOperations(options) {
54
54
  catch {
55
55
  throw new Error(`Working directory does not exist: ${cwd}\nCannot execute bash commands.`);
56
56
  }
57
+ signal?.throwIfAborted();
57
58
  const commandFromStdin = shellConfig.commandTransport === "stdin";
58
59
  const child = spawn(shellConfig.shell, commandFromStdin ? shellConfig.args : [...shellConfig.args, command], {
59
60
  cwd,
@@ -66,8 +67,12 @@ export function createLocalBashOperations(options) {
66
67
  child.stdin?.on("error", () => { });
67
68
  child.stdin?.end(command);
68
69
  }
69
- if (child.pid)
70
- trackDetachedChildPid(child.pid);
70
+ if (child.pid) {
71
+ if (options?.owner)
72
+ options.owner.track(child.pid);
73
+ else
74
+ trackDetachedChildPid(child.pid);
75
+ }
71
76
  let timedOut = false;
72
77
  let timeoutHandle;
73
78
  const onAbort = () => {
@@ -105,8 +110,12 @@ export function createLocalBashOperations(options) {
105
110
  return { exitCode };
106
111
  }
107
112
  finally {
108
- if (child.pid)
109
- untrackDetachedChildPid(child.pid);
113
+ if (child.pid) {
114
+ if (options?.owner)
115
+ options.owner.releaseIfExited(child.pid);
116
+ else
117
+ untrackDetachedChildPid(child.pid);
118
+ }
110
119
  if (timeoutHandle)
111
120
  clearTimeout(timeoutHandle);
112
121
  if (signal)
@@ -223,7 +232,7 @@ function rebuildBashResultRenderComponent(component, result, options, showImages
223
232
  }
224
233
  }
225
234
  export function createBashToolDefinition(cwd, options) {
226
- const ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });
235
+ const ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath, owner: options?.owner });
227
236
  const commandPrefix = options?.commandPrefix;
228
237
  const exposeSessionEnvironment = options?.exposeSessionEnvironment ?? true;
229
238
  const spawnHook = options?.spawnHook;
@@ -10,35 +10,37 @@ declare const escalateToParentSchema: Type.TObject<{
10
10
  }>;
11
11
  declare const agentActionSchema: Type.TObject<{
12
12
  target: Type.TString;
13
- action: Type.TUnsafe<"interrupt" | "message">;
13
+ reason: Type.TOptional<Type.TString>;
14
+ action: Type.TUnsafe<"interrupt" | "message" | "renew">;
14
15
  message: Type.TOptional<Type.TString>;
15
16
  wake: Type.TOptional<Type.TBoolean>;
17
+ delivery: Type.TOptional<Type.TUnion<[Type.TLiteral<"steer">, Type.TLiteral<"follow_up">]>>;
16
18
  }>;
17
- declare const agentsSchema: Type.TObject<{
18
- timeout_ms: Type.TInteger;
19
+ declare const agentStatusSchema: Type.TObject<{
19
20
  targets: Type.TOptional<Type.TArray<Type.TString>>;
20
- until: Type.TOptional<Type.TUnion<[Type.TLiteral<"any">, Type.TLiteral<"all">]>>;
21
21
  }>;
22
22
  export type AgentActionRequest = {
23
23
  readonly target: string;
24
24
  readonly action: "message";
25
25
  readonly message: string;
26
26
  readonly wake?: boolean;
27
+ readonly delivery?: "steer" | "follow_up";
27
28
  } | {
28
29
  readonly target: string;
29
30
  readonly action: "interrupt";
31
+ } | {
32
+ readonly target: string;
33
+ readonly action: "renew";
34
+ readonly reason: string;
30
35
  };
31
36
  export interface SpawnAgentDetails {
32
37
  readonly name: string;
33
38
  readonly status: "queued";
34
39
  }
35
- export interface AgentsRequest {
36
- readonly timeout_ms: number;
40
+ export interface AgentStatusRequest {
37
41
  readonly targets?: string[];
38
- readonly until?: "any" | "all";
39
42
  }
40
- export interface AgentsDetails {
41
- readonly timed_out: boolean;
43
+ export interface AgentStatusDetails {
42
44
  readonly agents: readonly SubagentResultView[];
43
45
  }
44
46
  export interface EscalateToParentDetails {
@@ -47,5 +49,5 @@ export interface EscalateToParentDetails {
47
49
  export declare function createEscalateToParentTool(escalate: SubagentEscalationCallback): ToolDefinition<typeof escalateToParentSchema, EscalateToParentDetails>;
48
50
  export declare function createSpawnAgentTool(spawn: (name: string, task: string, toolCallId: string) => void, assertCanSpawn?: () => void): ToolDefinition<typeof spawnAgentSchema, SpawnAgentDetails>;
49
51
  export declare function createAgentActionTool(action: (request: AgentActionRequest) => Promise<SubagentActionResult>): ToolDefinition<typeof agentActionSchema, SubagentActionResult>;
50
- export declare function createAgentsTool(query: (options: SubagentQueryOptions) => Promise<SubagentQueryResult>): ToolDefinition<typeof agentsSchema, AgentsDetails>;
52
+ export declare function createAgentStatusTool(query: (options: Pick<SubagentQueryOptions, "targets" | "signal">) => Promise<SubagentQueryResult>): ToolDefinition<typeof agentStatusSchema, AgentStatusDetails>;
51
53
  export {};