chainlesschain 0.45.66 → 0.45.67
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/package.json +1 -1
- package/src/gateways/ws/message-dispatcher.js +12 -0
- package/src/gateways/ws/session-protocol.js +757 -0
- package/src/lib/abort-utils.js +20 -0
- package/src/lib/agent-core.js +83 -4
- package/src/lib/interaction-adapter.js +40 -21
- package/src/lib/sub-agent-registry.js +34 -0
- package/src/lib/ws-agent-handler.js +48 -0
- package/src/lib/ws-server.js +66 -0
- package/src/lib/ws-session-manager.js +369 -0
- package/src/runtime/coding-agent-events.cjs +31 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function createAbortError(message = "Agent loop interrupted") {
|
|
2
|
+
const error = new Error(message);
|
|
3
|
+
error.name = "AbortError";
|
|
4
|
+
return error;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isAbortError(error) {
|
|
8
|
+
return (
|
|
9
|
+
error?.name === "AbortError" ||
|
|
10
|
+
error?.code === "ABORT_ERR" ||
|
|
11
|
+
(typeof error?.message === "string" &&
|
|
12
|
+
/aborted|interrupted/i.test(error.message))
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function throwIfAborted(signal, message = "Agent loop interrupted") {
|
|
17
|
+
if (signal?.aborted) {
|
|
18
|
+
throw signal.reason || createAbortError(message);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/lib/agent-core.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { createToolContext } from "../tools/tool-context.js";
|
|
34
34
|
import { createToolTelemetryRecord } from "../tools/tool-telemetry.js";
|
|
35
35
|
import { DEFAULT_TOOL_DESCRIPTORS } from "../tools/registry.js";
|
|
36
|
+
import { isAbortError, throwIfAborted } from "./abort-utils.js";
|
|
36
37
|
|
|
37
38
|
const { isReadOnlyGitCommand, normalizeGitCommand } = sharedCodingAgentPolicy;
|
|
38
39
|
const { evaluateShellCommandPolicy } = sharedShellPolicy;
|
|
@@ -719,6 +720,7 @@ export async function executeTool(name, args, context = {}) {
|
|
|
719
720
|
cwd,
|
|
720
721
|
parentMessages: context.parentMessages,
|
|
721
722
|
interaction: context.interaction,
|
|
723
|
+
sessionId: context.sessionId || null,
|
|
722
724
|
hostManagedToolPolicy: context.hostManagedToolPolicy || null,
|
|
723
725
|
externalToolDescriptors: context.externalToolDescriptors || null,
|
|
724
726
|
externalToolExecutors: context.externalToolExecutors || null,
|
|
@@ -784,6 +786,7 @@ async function executeToolInner(
|
|
|
784
786
|
cwd,
|
|
785
787
|
parentMessages,
|
|
786
788
|
interaction,
|
|
789
|
+
sessionId,
|
|
787
790
|
hostManagedToolPolicy,
|
|
788
791
|
externalToolDescriptors,
|
|
789
792
|
externalToolExecutors,
|
|
@@ -957,7 +960,13 @@ async function executeToolInner(
|
|
|
957
960
|
|
|
958
961
|
case "spawn_sub_agent": {
|
|
959
962
|
return attachDescriptor(
|
|
960
|
-
await _executeSpawnSubAgent(args, {
|
|
963
|
+
await _executeSpawnSubAgent(args, {
|
|
964
|
+
skillLoader,
|
|
965
|
+
cwd,
|
|
966
|
+
parentMessages,
|
|
967
|
+
interaction,
|
|
968
|
+
sessionId,
|
|
969
|
+
}),
|
|
961
970
|
);
|
|
962
971
|
}
|
|
963
972
|
|
|
@@ -1393,7 +1402,7 @@ async function _executeRunCode(args, cwd) {
|
|
|
1393
1402
|
* Creates an isolated SubAgentContext, runs it, and returns only the summary.
|
|
1394
1403
|
*
|
|
1395
1404
|
* @param {object} args - { role, task, context?, tools? }
|
|
1396
|
-
* @param {object} ctx - { skillLoader, cwd }
|
|
1405
|
+
* @param {object} ctx - { skillLoader, cwd, parentMessages, interaction, sessionId }
|
|
1397
1406
|
* @returns {Promise<object>}
|
|
1398
1407
|
*/
|
|
1399
1408
|
async function _executeSpawnSubAgent(args, ctx) {
|
|
@@ -1415,14 +1424,35 @@ async function _executeSpawnSubAgent(args, ctx) {
|
|
|
1415
1424
|
}
|
|
1416
1425
|
}
|
|
1417
1426
|
|
|
1427
|
+
// Link child to parent session so registry-scoped queries and
|
|
1428
|
+
// session-close cascade cleanup can find it.
|
|
1429
|
+
const parentSessionId = ctx.sessionId || null;
|
|
1430
|
+
const interaction = ctx.interaction || null;
|
|
1431
|
+
|
|
1418
1432
|
const subCtx = SubAgentContext.create({
|
|
1419
1433
|
role,
|
|
1420
1434
|
task,
|
|
1435
|
+
parentId: parentSessionId,
|
|
1421
1436
|
inheritedContext: resolvedContext,
|
|
1422
1437
|
allowedTools: allowedTools || null,
|
|
1423
1438
|
cwd: ctx.cwd,
|
|
1424
1439
|
});
|
|
1425
1440
|
|
|
1441
|
+
const emit = (type, payload) => {
|
|
1442
|
+
if (!interaction || typeof interaction.emit !== "function") return;
|
|
1443
|
+
try {
|
|
1444
|
+
interaction.emit(type, {
|
|
1445
|
+
sessionId: parentSessionId,
|
|
1446
|
+
subAgentId: subCtx.id,
|
|
1447
|
+
parentSessionId,
|
|
1448
|
+
role: subCtx.role,
|
|
1449
|
+
...payload,
|
|
1450
|
+
});
|
|
1451
|
+
} catch (_err) {
|
|
1452
|
+
// Event emission is best-effort — never break the tool call
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1426
1456
|
try {
|
|
1427
1457
|
// Notify registry if available
|
|
1428
1458
|
const { SubAgentRegistry } = await import("./sub-agent-registry.js").catch(
|
|
@@ -1436,6 +1466,13 @@ async function _executeSpawnSubAgent(args, ctx) {
|
|
|
1436
1466
|
}
|
|
1437
1467
|
}
|
|
1438
1468
|
|
|
1469
|
+
emit("sub-agent.started", {
|
|
1470
|
+
task: subCtx.task,
|
|
1471
|
+
allowedTools: allowedTools || null,
|
|
1472
|
+
maxIterations: subCtx.maxIterations,
|
|
1473
|
+
createdAt: subCtx.createdAt,
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1439
1476
|
const result = await subCtx.run(task);
|
|
1440
1477
|
|
|
1441
1478
|
// Complete in registry
|
|
@@ -1447,10 +1484,21 @@ async function _executeSpawnSubAgent(args, ctx) {
|
|
|
1447
1484
|
}
|
|
1448
1485
|
}
|
|
1449
1486
|
|
|
1487
|
+
emit("sub-agent.completed", {
|
|
1488
|
+
status: subCtx.status,
|
|
1489
|
+
summary: result.summary,
|
|
1490
|
+
toolsUsed: result.toolsUsed,
|
|
1491
|
+
iterationCount: result.iterationCount,
|
|
1492
|
+
tokenCount: result.tokenCount,
|
|
1493
|
+
artifactCount: result.artifacts.length,
|
|
1494
|
+
completedAt: subCtx.completedAt,
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1450
1497
|
return {
|
|
1451
1498
|
success: true,
|
|
1452
1499
|
subAgentId: subCtx.id,
|
|
1453
1500
|
role: subCtx.role,
|
|
1501
|
+
parentSessionId,
|
|
1454
1502
|
summary: result.summary,
|
|
1455
1503
|
toolsUsed: result.toolsUsed,
|
|
1456
1504
|
iterationCount: result.iterationCount,
|
|
@@ -1458,10 +1506,18 @@ async function _executeSpawnSubAgent(args, ctx) {
|
|
|
1458
1506
|
};
|
|
1459
1507
|
} catch (err) {
|
|
1460
1508
|
subCtx.forceComplete(err.message);
|
|
1509
|
+
|
|
1510
|
+
emit("sub-agent.failed", {
|
|
1511
|
+
status: subCtx.status,
|
|
1512
|
+
error: err.message,
|
|
1513
|
+
completedAt: subCtx.completedAt,
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1461
1516
|
return {
|
|
1462
1517
|
error: `Sub-agent failed: ${err.message}`,
|
|
1463
1518
|
subAgentId: subCtx.id,
|
|
1464
1519
|
role: subCtx.role,
|
|
1520
|
+
parentSessionId,
|
|
1465
1521
|
};
|
|
1466
1522
|
}
|
|
1467
1523
|
}
|
|
@@ -1477,7 +1533,14 @@ async function _executeSpawnSubAgent(args, ctx) {
|
|
|
1477
1533
|
* @returns {Promise<object>} response with .message
|
|
1478
1534
|
*/
|
|
1479
1535
|
export async function chatWithTools(rawMessages, options) {
|
|
1480
|
-
const {
|
|
1536
|
+
const {
|
|
1537
|
+
provider,
|
|
1538
|
+
model,
|
|
1539
|
+
baseUrl,
|
|
1540
|
+
apiKey,
|
|
1541
|
+
contextEngine: ce,
|
|
1542
|
+
signal,
|
|
1543
|
+
} = options;
|
|
1481
1544
|
|
|
1482
1545
|
const persona = _loadProjectPersona(options.cwd);
|
|
1483
1546
|
const tools = getAgentToolDefinitions({
|
|
@@ -1496,10 +1559,13 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1496
1559
|
})
|
|
1497
1560
|
: rawMessages;
|
|
1498
1561
|
|
|
1562
|
+
throwIfAborted(signal);
|
|
1563
|
+
|
|
1499
1564
|
if (provider === "ollama") {
|
|
1500
1565
|
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
1501
1566
|
method: "POST",
|
|
1502
1567
|
headers: { "Content-Type": "application/json" },
|
|
1568
|
+
signal,
|
|
1503
1569
|
body: JSON.stringify({
|
|
1504
1570
|
model,
|
|
1505
1571
|
messages,
|
|
@@ -1548,6 +1614,7 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1548
1614
|
"x-api-key": key,
|
|
1549
1615
|
"anthropic-version": "2023-06-01",
|
|
1550
1616
|
},
|
|
1617
|
+
signal,
|
|
1551
1618
|
body: JSON.stringify(body),
|
|
1552
1619
|
});
|
|
1553
1620
|
|
|
@@ -1608,6 +1675,7 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1608
1675
|
"Content-Type": "application/json",
|
|
1609
1676
|
Authorization: `Bearer ${key}`,
|
|
1610
1677
|
},
|
|
1678
|
+
signal,
|
|
1611
1679
|
body: JSON.stringify({
|
|
1612
1680
|
model: model || defaultModels[provider] || "gpt-4o-mini",
|
|
1613
1681
|
messages,
|
|
@@ -1667,6 +1735,7 @@ function _normalizeAnthropicResponse(data) {
|
|
|
1667
1735
|
*/
|
|
1668
1736
|
export async function* agentLoop(messages, options) {
|
|
1669
1737
|
const MAX_ITERATIONS = 15;
|
|
1738
|
+
const signal = options.signal || null;
|
|
1670
1739
|
const toolContext = {
|
|
1671
1740
|
hookDb: options.hookDb || null,
|
|
1672
1741
|
skillLoader: options.skillLoader || _defaultSkillLoader,
|
|
@@ -1681,6 +1750,8 @@ export async function* agentLoop(messages, options) {
|
|
|
1681
1750
|
interaction: options.interaction || null,
|
|
1682
1751
|
};
|
|
1683
1752
|
|
|
1753
|
+
throwIfAborted(signal);
|
|
1754
|
+
|
|
1684
1755
|
// ── Slot-filling phase ──────────────────────────────────────────────
|
|
1685
1756
|
// Before calling the LLM, check if the user's message matches a known
|
|
1686
1757
|
// intent with missing required parameters. If so, interactively fill them
|
|
@@ -1721,14 +1792,19 @@ export async function* agentLoop(messages, options) {
|
|
|
1721
1792
|
}
|
|
1722
1793
|
}
|
|
1723
1794
|
}
|
|
1724
|
-
} catch (
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
if (isAbortError(error) || signal?.aborted) {
|
|
1797
|
+
throw error;
|
|
1798
|
+
}
|
|
1725
1799
|
// Slot-filling failure is non-critical — proceed to LLM
|
|
1726
1800
|
}
|
|
1727
1801
|
}
|
|
1728
1802
|
}
|
|
1729
1803
|
|
|
1730
1804
|
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
1805
|
+
throwIfAborted(signal);
|
|
1731
1806
|
const result = await chatWithTools(messages, options);
|
|
1807
|
+
throwIfAborted(signal);
|
|
1732
1808
|
const msg = result?.message;
|
|
1733
1809
|
|
|
1734
1810
|
if (!msg) {
|
|
@@ -1747,6 +1823,7 @@ export async function* agentLoop(messages, options) {
|
|
|
1747
1823
|
messages.push(msg);
|
|
1748
1824
|
|
|
1749
1825
|
for (const call of toolCalls) {
|
|
1826
|
+
throwIfAborted(signal);
|
|
1750
1827
|
const fn = call.function;
|
|
1751
1828
|
const toolName = fn.name;
|
|
1752
1829
|
let toolArgs;
|
|
@@ -1771,6 +1848,8 @@ export async function* agentLoop(messages, options) {
|
|
|
1771
1848
|
toolError = err.message;
|
|
1772
1849
|
}
|
|
1773
1850
|
|
|
1851
|
+
throwIfAborted(signal);
|
|
1852
|
+
|
|
1774
1853
|
yield {
|
|
1775
1854
|
type: "tool-result",
|
|
1776
1855
|
tool: toolName,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
CODING_AGENT_EVENT_TYPES,
|
|
14
14
|
LEGACY_TO_UNIFIED_TYPE,
|
|
15
15
|
} from "../runtime/runtime-events.js";
|
|
16
|
+
import { createAbortError } from "./abort-utils.js";
|
|
16
17
|
|
|
17
18
|
// Whitelist of event types the CLI runtime should emit as unified envelopes
|
|
18
19
|
// (with source: "cli-runtime"). Anything not in this set keeps the legacy
|
|
@@ -104,7 +105,7 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
104
105
|
super();
|
|
105
106
|
this.ws = ws;
|
|
106
107
|
this.sessionId = sessionId;
|
|
107
|
-
/** @type {Map<string, {resolve: Function, reject: Function}>} */
|
|
108
|
+
/** @type {Map<string, {resolve: Function, reject: Function, timeoutId: ReturnType<typeof setTimeout>|null}>} */
|
|
108
109
|
this._pending = new Map();
|
|
109
110
|
// Per-instance sequence tracker so monotonic sequences are scoped to
|
|
110
111
|
// this WS session instead of leaking across sessions via the process-
|
|
@@ -127,24 +128,24 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
127
128
|
_request(message, options = {}) {
|
|
128
129
|
return new Promise((resolve, reject) => {
|
|
129
130
|
const requestId = this._requestId();
|
|
130
|
-
|
|
131
|
+
const timeoutId = setTimeout(
|
|
132
|
+
() => {
|
|
133
|
+
const pending = this._pending.get(requestId);
|
|
134
|
+
if (!pending) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this._pending.delete(requestId);
|
|
138
|
+
reject(new Error("Question timed out"));
|
|
139
|
+
},
|
|
140
|
+
options.timeoutMs || 5 * 60 * 1000,
|
|
141
|
+
);
|
|
142
|
+
this._pending.set(requestId, { resolve, reject, timeoutId });
|
|
131
143
|
|
|
132
144
|
this._sendWs({
|
|
133
145
|
...message,
|
|
134
146
|
sessionId: this.sessionId,
|
|
135
147
|
requestId,
|
|
136
148
|
});
|
|
137
|
-
|
|
138
|
-
// Timeout after 5 minutes
|
|
139
|
-
setTimeout(
|
|
140
|
-
() => {
|
|
141
|
-
if (this._pending.has(requestId)) {
|
|
142
|
-
this._pending.delete(requestId);
|
|
143
|
-
reject(new Error("Question timed out"));
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
options.timeoutMs || 5 * 60 * 1000,
|
|
147
|
-
);
|
|
148
149
|
});
|
|
149
150
|
}
|
|
150
151
|
|
|
@@ -179,11 +180,7 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
179
180
|
* Resolves the corresponding pending promise.
|
|
180
181
|
*/
|
|
181
182
|
resolveAnswer(requestId, answer) {
|
|
182
|
-
|
|
183
|
-
if (pending) {
|
|
184
|
-
this._pending.delete(requestId);
|
|
185
|
-
pending.resolve(answer);
|
|
186
|
-
}
|
|
183
|
+
this._resolvePending(requestId, answer);
|
|
187
184
|
}
|
|
188
185
|
|
|
189
186
|
async requestHostTool(toolName, args = {}, extra = {}) {
|
|
@@ -199,10 +196,19 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
199
196
|
}
|
|
200
197
|
|
|
201
198
|
resolveHostTool(requestId, payload) {
|
|
202
|
-
|
|
203
|
-
|
|
199
|
+
this._resolvePending(requestId, payload);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
rejectAllPending(reason = createAbortError("Interaction interrupted")) {
|
|
203
|
+
const error =
|
|
204
|
+
reason instanceof Error ? reason : createAbortError(String(reason));
|
|
205
|
+
|
|
206
|
+
for (const [requestId, pending] of this._pending.entries()) {
|
|
204
207
|
this._pending.delete(requestId);
|
|
205
|
-
pending.
|
|
208
|
+
if (pending.timeoutId) {
|
|
209
|
+
clearTimeout(pending.timeoutId);
|
|
210
|
+
}
|
|
211
|
+
pending.reject(error);
|
|
206
212
|
}
|
|
207
213
|
}
|
|
208
214
|
|
|
@@ -245,4 +251,17 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
245
251
|
}
|
|
246
252
|
}
|
|
247
253
|
}
|
|
254
|
+
|
|
255
|
+
_resolvePending(requestId, payload) {
|
|
256
|
+
const pending = this._pending.get(requestId);
|
|
257
|
+
if (!pending) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this._pending.delete(requestId);
|
|
262
|
+
if (pending.timeoutId) {
|
|
263
|
+
clearTimeout(pending.timeoutId);
|
|
264
|
+
}
|
|
265
|
+
pending.resolve(payload);
|
|
266
|
+
}
|
|
248
267
|
}
|
|
@@ -131,6 +131,40 @@ export class SubAgentRegistry {
|
|
|
131
131
|
return [...this._active.values()].map((ctx) => ctx.toJSON());
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Get a single sub-agent snapshot by id — checks active first, then history.
|
|
136
|
+
* @param {string} id
|
|
137
|
+
* @returns {object|null}
|
|
138
|
+
*/
|
|
139
|
+
getById(id) {
|
|
140
|
+
if (!id) return null;
|
|
141
|
+
const active = this._active.get(id);
|
|
142
|
+
if (active) return active.toJSON();
|
|
143
|
+
const historyEntry = this._completed
|
|
144
|
+
.toArray()
|
|
145
|
+
.find((record) => record.id === id);
|
|
146
|
+
return historyEntry || null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get active + recent sub-agents belonging to a parent session.
|
|
151
|
+
* Used by the WS sub-agent-list query and by UI consumers that need to
|
|
152
|
+
* render only the child agents spawned from a specific parent turn.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} parentId
|
|
155
|
+
* @returns {{ active: Array<object>, history: Array<object> }}
|
|
156
|
+
*/
|
|
157
|
+
getByParent(parentId) {
|
|
158
|
+
if (!parentId) return { active: [], history: [] };
|
|
159
|
+
const active = [...this._active.values()]
|
|
160
|
+
.filter((ctx) => ctx.parentId === parentId)
|
|
161
|
+
.map((ctx) => ctx.toJSON());
|
|
162
|
+
const history = this._completed
|
|
163
|
+
.toArray()
|
|
164
|
+
.filter((record) => record.parentId === parentId);
|
|
165
|
+
return { active, history };
|
|
166
|
+
}
|
|
167
|
+
|
|
134
168
|
/**
|
|
135
169
|
* Get completion history (most recent last).
|
|
136
170
|
* @returns {Array<object>}
|
|
@@ -10,6 +10,7 @@ import { agentLoop, formatToolArgs } from "./agent-core.js";
|
|
|
10
10
|
import { detectTaskType, selectModelForTask } from "./task-model-selector.js";
|
|
11
11
|
import { PlanState } from "./plan-mode.js";
|
|
12
12
|
import { CLISlotFiller } from "./slot-filler.js";
|
|
13
|
+
import { createAbortError, isAbortError } from "./abort-utils.js";
|
|
13
14
|
|
|
14
15
|
export class WSAgentHandler {
|
|
15
16
|
/**
|
|
@@ -23,6 +24,8 @@ export class WSAgentHandler {
|
|
|
23
24
|
this.interaction = interaction;
|
|
24
25
|
this.db = db || null;
|
|
25
26
|
this._processing = false;
|
|
27
|
+
this._abortController = null;
|
|
28
|
+
this._activeRequestId = null;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -42,6 +45,9 @@ export class WSAgentHandler {
|
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
this._processing = true;
|
|
48
|
+
const abortController = new AbortController();
|
|
49
|
+
this._abortController = abortController;
|
|
50
|
+
this._activeRequestId = requestId || null;
|
|
45
51
|
|
|
46
52
|
try {
|
|
47
53
|
const { session } = this;
|
|
@@ -93,6 +99,7 @@ export class WSAgentHandler {
|
|
|
93
99
|
mcpClient: session.mcpClient || null,
|
|
94
100
|
slotFiller,
|
|
95
101
|
interaction: this.interaction,
|
|
102
|
+
signal: abortController.signal,
|
|
96
103
|
};
|
|
97
104
|
|
|
98
105
|
for await (const event of agentLoop(session.messages, loopOptions)) {
|
|
@@ -141,6 +148,10 @@ export class WSAgentHandler {
|
|
|
141
148
|
// Update last activity
|
|
142
149
|
session.lastActivity = new Date().toISOString();
|
|
143
150
|
} catch (err) {
|
|
151
|
+
if (isAbortError(err) || abortController.signal.aborted) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
144
155
|
this.interaction.emit("error", {
|
|
145
156
|
requestId,
|
|
146
157
|
code: "AGENT_ERROR",
|
|
@@ -156,6 +167,43 @@ export class WSAgentHandler {
|
|
|
156
167
|
}
|
|
157
168
|
} finally {
|
|
158
169
|
this._processing = false;
|
|
170
|
+
if (this._abortController === abortController) {
|
|
171
|
+
this._abortController = null;
|
|
172
|
+
}
|
|
173
|
+
if (this._activeRequestId === requestId) {
|
|
174
|
+
this._activeRequestId = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async interrupt() {
|
|
180
|
+
const wasProcessing = this._processing;
|
|
181
|
+
const interruptedRequestId = this._activeRequestId || null;
|
|
182
|
+
const reason = createAbortError("Session interrupted by client");
|
|
183
|
+
|
|
184
|
+
if (this._abortController && !this._abortController.signal.aborted) {
|
|
185
|
+
this._abortController.abort(reason);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (typeof this.interaction?.rejectAllPending === "function") {
|
|
189
|
+
this.interaction.rejectAllPending(reason);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
sessionId: this.session?.id || null,
|
|
194
|
+
interrupted: true,
|
|
195
|
+
wasProcessing,
|
|
196
|
+
interruptedRequestId,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
destroy() {
|
|
201
|
+
const reason = createAbortError("Session closed");
|
|
202
|
+
if (this._abortController && !this._abortController.signal.aborted) {
|
|
203
|
+
this._abortController.abort(reason);
|
|
204
|
+
}
|
|
205
|
+
if (typeof this.interaction?.rejectAllPending === "function") {
|
|
206
|
+
this.interaction.rejectAllPending(reason);
|
|
159
207
|
}
|
|
160
208
|
}
|
|
161
209
|
|
package/src/lib/ws-server.js
CHANGED
|
@@ -28,8 +28,19 @@ import {
|
|
|
28
28
|
handleSessionPolicyUpdate,
|
|
29
29
|
handleSessionList,
|
|
30
30
|
handleSessionClose,
|
|
31
|
+
handleSessionInterrupt,
|
|
31
32
|
handleSessionAnswer,
|
|
32
33
|
handleHostToolResult,
|
|
34
|
+
handleSubAgentList,
|
|
35
|
+
handleSubAgentGet,
|
|
36
|
+
handleReviewEnter,
|
|
37
|
+
handleReviewSubmit,
|
|
38
|
+
handleReviewResolve,
|
|
39
|
+
handleReviewStatus,
|
|
40
|
+
handlePatchPropose,
|
|
41
|
+
handlePatchApply,
|
|
42
|
+
handlePatchReject,
|
|
43
|
+
handlePatchSummary,
|
|
33
44
|
} from "../gateways/ws/session-protocol.js";
|
|
34
45
|
import {
|
|
35
46
|
handleSlashCommand,
|
|
@@ -599,6 +610,11 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
599
610
|
return handleSessionClose(this, id, ws, message);
|
|
600
611
|
}
|
|
601
612
|
|
|
613
|
+
/** @private */
|
|
614
|
+
_handleSessionInterrupt(id, ws, message) {
|
|
615
|
+
return handleSessionInterrupt(this, id, ws, message);
|
|
616
|
+
}
|
|
617
|
+
|
|
602
618
|
/** @private */
|
|
603
619
|
_handleSlashCommand(id, ws, message) {
|
|
604
620
|
return handleSlashCommand(this, id, ws, message);
|
|
@@ -613,6 +629,56 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
613
629
|
return handleHostToolResult(this, id, ws, message);
|
|
614
630
|
}
|
|
615
631
|
|
|
632
|
+
/** @private */
|
|
633
|
+
_handleSubAgentList(id, ws, message) {
|
|
634
|
+
return handleSubAgentList(this, id, ws, message);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** @private */
|
|
638
|
+
_handleSubAgentGet(id, ws, message) {
|
|
639
|
+
return handleSubAgentGet(this, id, ws, message);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** @private */
|
|
643
|
+
_handleReviewEnter(id, ws, message) {
|
|
644
|
+
return handleReviewEnter(this, id, ws, message);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** @private */
|
|
648
|
+
_handleReviewSubmit(id, ws, message) {
|
|
649
|
+
return handleReviewSubmit(this, id, ws, message);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** @private */
|
|
653
|
+
_handleReviewResolve(id, ws, message) {
|
|
654
|
+
return handleReviewResolve(this, id, ws, message);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** @private */
|
|
658
|
+
_handleReviewStatus(id, ws, message) {
|
|
659
|
+
return handleReviewStatus(this, id, ws, message);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** @private */
|
|
663
|
+
_handlePatchPropose(id, ws, message) {
|
|
664
|
+
return handlePatchPropose(this, id, ws, message);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** @private */
|
|
668
|
+
_handlePatchApply(id, ws, message) {
|
|
669
|
+
return handlePatchApply(this, id, ws, message);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** @private */
|
|
673
|
+
_handlePatchReject(id, ws, message) {
|
|
674
|
+
return handlePatchReject(this, id, ws, message);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** @private */
|
|
678
|
+
_handlePatchSummary(id, ws, message) {
|
|
679
|
+
return handlePatchSummary(this, id, ws, message);
|
|
680
|
+
}
|
|
681
|
+
|
|
616
682
|
/** @private — ping/pong heartbeat to detect dead connections */
|
|
617
683
|
async _ensureTaskManager() {
|
|
618
684
|
if (this._taskManager) return this._taskManager;
|