@diffexai/diffex 0.2.7 → 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.
- package/CHANGELOG.md +17 -0
- package/dist/core/agent-session.d.ts +5 -0
- package/dist/core/agent-session.js +58 -8
- package/dist/core/compaction/compaction.js +3 -1
- package/dist/core/sdk.d.ts +2 -1
- package/dist/core/sdk.js +18 -6
- package/dist/core/subagents/controller.d.ts +4 -3
- package/dist/core/subagents/controller.js +173 -37
- package/dist/core/subagents/types.d.ts +13 -0
- package/dist/core/tools/bash.d.ts +4 -0
- package/dist/core/tools/bash.js +14 -5
- package/dist/core/tools/subagents.d.ts +12 -10
- package/dist/core/tools/subagents.js +61 -49
- package/dist/node_modules/@diffexai/diffex-agent-core/distribution-components.json +4 -4
- package/dist/node_modules/@diffexai/diffex-agent-core/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-agent-core/package.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/.manifest.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/opencode.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/openrouter.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/distribution-components.json +3 -3
- package/dist/node_modules/@diffexai/diffex-ai/distribution-files.json +5 -5
- package/dist/node_modules/@diffexai/diffex-ai/package.json +1 -1
- package/dist/node_modules/@diffexai/diffex-client/distribution-components.json +3 -3
- package/dist/node_modules/@diffexai/diffex-client/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-client/package.json +1 -1
- package/dist/node_modules/@diffexai/diffex-harness-state/distribution-components.json +2 -2
- package/dist/node_modules/@diffexai/diffex-harness-state/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-harness-state/package.json +1 -1
- package/dist/node_modules/@diffexai/diffex-protocol/distribution-components.json +2 -2
- package/dist/node_modules/@diffexai/diffex-protocol/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-protocol/package.json +1 -1
- package/dist/node_modules/@diffexai/diffex-telemetry/distribution-components.json +2 -2
- package/dist/node_modules/@diffexai/diffex-telemetry/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-telemetry/package.json +1 -1
- package/dist/node_modules/@diffexai/diffex-tui/distribution-components.json +2 -2
- package/dist/node_modules/@diffexai/diffex-tui/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-tui/package.json +1 -1
- package/dist/utils/owned-shell-processes.d.ts +8 -0
- package/dist/utils/owned-shell-processes.js +61 -0
- package/distribution-components.json +11 -11
- package/distribution-files.json +46 -38
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/release/distribution-manifest.json +4 -4
- package/release/install-package-lock.json +5 -5
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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
|
-
|
|
1296
|
-
|
|
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) */
|
package/dist/core/tools/bash.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
36
|
-
readonly timeout_ms: number;
|
|
40
|
+
export interface AgentStatusRequest {
|
|
37
41
|
readonly targets?: string[];
|
|
38
|
-
readonly until?: "any" | "all";
|
|
39
42
|
}
|
|
40
|
-
export interface
|
|
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
|
|
52
|
+
export declare function createAgentStatusTool(query: (options: Pick<SubagentQueryOptions, "targets" | "signal">) => Promise<SubagentQueryResult>): ToolDefinition<typeof agentStatusSchema, AgentStatusDetails>;
|
|
51
53
|
export {};
|