@wingman-ai/gateway 0.2.0 → 0.2.2
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/.wingman/agents/README.md +1 -0
- package/.wingman/agents/coding/agent.md +76 -14
- package/.wingman/agents/coding/implementor.md +25 -2
- package/README.md +310 -0
- package/dist/agent/config/agentConfig.cjs +29 -0
- package/dist/agent/config/agentConfig.js +29 -0
- package/dist/agent/tests/agentConfig.test.cjs +39 -0
- package/dist/agent/tests/agentConfig.test.js +39 -0
- package/dist/cli/core/agentInvoker.cjs +51 -4
- package/dist/cli/core/agentInvoker.d.ts +4 -1
- package/dist/cli/core/agentInvoker.js +51 -4
- package/dist/gateway/server.cjs +53 -1
- package/dist/gateway/server.d.ts +3 -0
- package/dist/gateway/server.js +53 -1
- package/dist/gateway/types.d.ts +4 -1
- package/dist/gateway/validation.cjs +1 -0
- package/dist/gateway/validation.d.ts +2 -0
- package/dist/gateway/validation.js +1 -0
- package/dist/tests/gateway.test.cjs +66 -1
- package/dist/tests/gateway.test.js +66 -1
- package/dist/webui/assets/index-CPhfGPHc.js +182 -0
- package/dist/webui/assets/index-DDsMIOTX.css +11 -0
- package/dist/webui/index.html +2 -2
- package/package.json +3 -1
- package/dist/webui/assets/index-BytPznA_.css +0 -1
- package/dist/webui/assets/index-u_5qlVip.js +0 -176
|
@@ -67,6 +67,45 @@ describe("Agent Configuration Schema", ()=>{
|
|
|
67
67
|
expect(result.success).toBe(true);
|
|
68
68
|
if (result.success) expect(result.data.subAgents?.[0].model).toBe("openai:gpt-4o");
|
|
69
69
|
});
|
|
70
|
+
it("should fail when a sub-agent shares the same name as its parent", ()=>{
|
|
71
|
+
const config = {
|
|
72
|
+
name: "coding",
|
|
73
|
+
description: "Parent coding agent",
|
|
74
|
+
systemPrompt: "You are the parent coding agent",
|
|
75
|
+
subAgents: [
|
|
76
|
+
{
|
|
77
|
+
name: "coding",
|
|
78
|
+
description: "Nested coding worker",
|
|
79
|
+
systemPrompt: "You are a worker"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
const result = validateAgentConfig(config);
|
|
84
|
+
expect(result.success).toBe(false);
|
|
85
|
+
if (!result.success) expect(result.error).toContain("Sub-agent name must be different from parent agent name");
|
|
86
|
+
});
|
|
87
|
+
it("should fail when sub-agent names are duplicated", ()=>{
|
|
88
|
+
const config = {
|
|
89
|
+
name: "parent-agent",
|
|
90
|
+
description: "Parent agent",
|
|
91
|
+
systemPrompt: "You are the parent agent",
|
|
92
|
+
subAgents: [
|
|
93
|
+
{
|
|
94
|
+
name: "implementor",
|
|
95
|
+
description: "First implementor",
|
|
96
|
+
systemPrompt: "You implement changes"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "IMPLEMENTOR",
|
|
100
|
+
description: "Duplicate implementor",
|
|
101
|
+
systemPrompt: "You implement more changes"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
};
|
|
105
|
+
const result = validateAgentConfig(config);
|
|
106
|
+
expect(result.success).toBe(false);
|
|
107
|
+
if (!result.success) expect(result.error).toContain("Sub-agent names must be unique within the same parent agent");
|
|
108
|
+
});
|
|
70
109
|
it("should fail validation for missing required fields", ()=>{
|
|
71
110
|
const config = {
|
|
72
111
|
name: "test-agent"
|
|
@@ -96,7 +96,9 @@ class AgentInvoker {
|
|
|
96
96
|
async findAgent(name) {
|
|
97
97
|
return await this.loader.loadAgent(name);
|
|
98
98
|
}
|
|
99
|
-
async invokeAgent(agentName, prompt, sessionId, attachments) {
|
|
99
|
+
async invokeAgent(agentName, prompt, sessionId, attachments, options) {
|
|
100
|
+
let cancellationHandled = false;
|
|
101
|
+
const isCancelled = ()=>options?.signal?.aborted === true;
|
|
100
102
|
try {
|
|
101
103
|
const executionWorkspace = resolveExecutionWorkspace(this.workspace, this.workdir);
|
|
102
104
|
const effectiveWorkdir = this.workdir ? executionWorkspace : null;
|
|
@@ -211,9 +213,29 @@ class AgentInvoker {
|
|
|
211
213
|
configurable: {
|
|
212
214
|
thread_id: sessionId
|
|
213
215
|
},
|
|
214
|
-
version: "v2"
|
|
216
|
+
version: "v2",
|
|
217
|
+
signal: options?.signal
|
|
215
218
|
});
|
|
216
|
-
for await (const chunk of stream)
|
|
219
|
+
for await (const chunk of stream){
|
|
220
|
+
if (isCancelled()) {
|
|
221
|
+
cancellationHandled = true;
|
|
222
|
+
this.logger.info("Agent invocation cancelled");
|
|
223
|
+
this.outputManager.emitAgentError("Request cancelled");
|
|
224
|
+
if ("function" == typeof stream?.return) await stream.return();
|
|
225
|
+
return {
|
|
226
|
+
cancelled: true
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
this.outputManager.emitAgentStream(chunk);
|
|
230
|
+
}
|
|
231
|
+
if (isCancelled()) {
|
|
232
|
+
cancellationHandled = true;
|
|
233
|
+
this.logger.info("Agent invocation cancelled");
|
|
234
|
+
this.outputManager.emitAgentError("Request cancelled");
|
|
235
|
+
return {
|
|
236
|
+
cancelled: true
|
|
237
|
+
};
|
|
238
|
+
}
|
|
217
239
|
this.logger.info("Agent streaming completed successfully");
|
|
218
240
|
this.outputManager.emitAgentComplete({
|
|
219
241
|
streaming: true
|
|
@@ -224,6 +246,14 @@ class AgentInvoker {
|
|
|
224
246
|
}
|
|
225
247
|
{
|
|
226
248
|
this.logger.debug("Using blocking invoke (no session manager)");
|
|
249
|
+
if (isCancelled()) {
|
|
250
|
+
cancellationHandled = true;
|
|
251
|
+
this.logger.info("Agent invocation cancelled");
|
|
252
|
+
this.outputManager.emitAgentError("Request cancelled");
|
|
253
|
+
return {
|
|
254
|
+
cancelled: true
|
|
255
|
+
};
|
|
256
|
+
}
|
|
227
257
|
const result = await standaloneAgent.invoke({
|
|
228
258
|
messages: [
|
|
229
259
|
{
|
|
@@ -232,13 +262,30 @@ class AgentInvoker {
|
|
|
232
262
|
}
|
|
233
263
|
]
|
|
234
264
|
}, {
|
|
235
|
-
recursionLimit: this.wingmanConfig.recursionLimit
|
|
265
|
+
recursionLimit: this.wingmanConfig.recursionLimit,
|
|
266
|
+
signal: options?.signal
|
|
236
267
|
});
|
|
268
|
+
if (isCancelled()) {
|
|
269
|
+
cancellationHandled = true;
|
|
270
|
+
this.logger.info("Agent invocation cancelled");
|
|
271
|
+
this.outputManager.emitAgentError("Request cancelled");
|
|
272
|
+
return {
|
|
273
|
+
cancelled: true
|
|
274
|
+
};
|
|
275
|
+
}
|
|
237
276
|
this.logger.info("Agent completed successfully");
|
|
238
277
|
this.outputManager.emitAgentComplete(result);
|
|
239
278
|
return result;
|
|
240
279
|
}
|
|
241
280
|
} catch (error) {
|
|
281
|
+
const abortError = isCancelled() || error instanceof Error && ("AbortError" === error.name || "CancelledError" === error.name || /abort|cancel/i.test(error.message));
|
|
282
|
+
if (abortError) {
|
|
283
|
+
if (!cancellationHandled) this.outputManager.emitAgentError("Request cancelled");
|
|
284
|
+
this.logger.info("Agent invocation cancelled");
|
|
285
|
+
return {
|
|
286
|
+
cancelled: true
|
|
287
|
+
};
|
|
288
|
+
}
|
|
242
289
|
this.logger.error(`Agent invocation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
243
290
|
this.outputManager.emitAgentError(error);
|
|
244
291
|
throw error;
|
|
@@ -12,6 +12,9 @@ export interface AgentInvokerOptions {
|
|
|
12
12
|
workdir?: string | null;
|
|
13
13
|
defaultOutputDir?: string | null;
|
|
14
14
|
}
|
|
15
|
+
export interface InvokeAgentOptions {
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}
|
|
15
18
|
export type ImageAttachment = {
|
|
16
19
|
kind?: "image";
|
|
17
20
|
dataUrl: string;
|
|
@@ -99,7 +102,7 @@ export declare class AgentInvoker {
|
|
|
99
102
|
/**
|
|
100
103
|
* Invoke a specific agent directly (bypassing main orchestration)
|
|
101
104
|
*/
|
|
102
|
-
invokeAgent(agentName: string, prompt: string, sessionId?: string, attachments?: MediaAttachment[]): Promise<any>;
|
|
105
|
+
invokeAgent(agentName: string, prompt: string, sessionId?: string, attachments?: MediaAttachment[], options?: InvokeAgentOptions): Promise<any>;
|
|
103
106
|
/**
|
|
104
107
|
* List all available agents with their descriptions
|
|
105
108
|
*/
|
|
@@ -62,7 +62,9 @@ class AgentInvoker {
|
|
|
62
62
|
async findAgent(name) {
|
|
63
63
|
return await this.loader.loadAgent(name);
|
|
64
64
|
}
|
|
65
|
-
async invokeAgent(agentName, prompt, sessionId, attachments) {
|
|
65
|
+
async invokeAgent(agentName, prompt, sessionId, attachments, options) {
|
|
66
|
+
let cancellationHandled = false;
|
|
67
|
+
const isCancelled = ()=>options?.signal?.aborted === true;
|
|
66
68
|
try {
|
|
67
69
|
const executionWorkspace = resolveExecutionWorkspace(this.workspace, this.workdir);
|
|
68
70
|
const effectiveWorkdir = this.workdir ? executionWorkspace : null;
|
|
@@ -177,9 +179,29 @@ class AgentInvoker {
|
|
|
177
179
|
configurable: {
|
|
178
180
|
thread_id: sessionId
|
|
179
181
|
},
|
|
180
|
-
version: "v2"
|
|
182
|
+
version: "v2",
|
|
183
|
+
signal: options?.signal
|
|
181
184
|
});
|
|
182
|
-
for await (const chunk of stream)
|
|
185
|
+
for await (const chunk of stream){
|
|
186
|
+
if (isCancelled()) {
|
|
187
|
+
cancellationHandled = true;
|
|
188
|
+
this.logger.info("Agent invocation cancelled");
|
|
189
|
+
this.outputManager.emitAgentError("Request cancelled");
|
|
190
|
+
if ("function" == typeof stream?.return) await stream.return();
|
|
191
|
+
return {
|
|
192
|
+
cancelled: true
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
this.outputManager.emitAgentStream(chunk);
|
|
196
|
+
}
|
|
197
|
+
if (isCancelled()) {
|
|
198
|
+
cancellationHandled = true;
|
|
199
|
+
this.logger.info("Agent invocation cancelled");
|
|
200
|
+
this.outputManager.emitAgentError("Request cancelled");
|
|
201
|
+
return {
|
|
202
|
+
cancelled: true
|
|
203
|
+
};
|
|
204
|
+
}
|
|
183
205
|
this.logger.info("Agent streaming completed successfully");
|
|
184
206
|
this.outputManager.emitAgentComplete({
|
|
185
207
|
streaming: true
|
|
@@ -190,6 +212,14 @@ class AgentInvoker {
|
|
|
190
212
|
}
|
|
191
213
|
{
|
|
192
214
|
this.logger.debug("Using blocking invoke (no session manager)");
|
|
215
|
+
if (isCancelled()) {
|
|
216
|
+
cancellationHandled = true;
|
|
217
|
+
this.logger.info("Agent invocation cancelled");
|
|
218
|
+
this.outputManager.emitAgentError("Request cancelled");
|
|
219
|
+
return {
|
|
220
|
+
cancelled: true
|
|
221
|
+
};
|
|
222
|
+
}
|
|
193
223
|
const result = await standaloneAgent.invoke({
|
|
194
224
|
messages: [
|
|
195
225
|
{
|
|
@@ -198,13 +228,30 @@ class AgentInvoker {
|
|
|
198
228
|
}
|
|
199
229
|
]
|
|
200
230
|
}, {
|
|
201
|
-
recursionLimit: this.wingmanConfig.recursionLimit
|
|
231
|
+
recursionLimit: this.wingmanConfig.recursionLimit,
|
|
232
|
+
signal: options?.signal
|
|
202
233
|
});
|
|
234
|
+
if (isCancelled()) {
|
|
235
|
+
cancellationHandled = true;
|
|
236
|
+
this.logger.info("Agent invocation cancelled");
|
|
237
|
+
this.outputManager.emitAgentError("Request cancelled");
|
|
238
|
+
return {
|
|
239
|
+
cancelled: true
|
|
240
|
+
};
|
|
241
|
+
}
|
|
203
242
|
this.logger.info("Agent completed successfully");
|
|
204
243
|
this.outputManager.emitAgentComplete(result);
|
|
205
244
|
return result;
|
|
206
245
|
}
|
|
207
246
|
} catch (error) {
|
|
247
|
+
const abortError = isCancelled() || error instanceof Error && ("AbortError" === error.name || "CancelledError" === error.name || /abort|cancel/i.test(error.message));
|
|
248
|
+
if (abortError) {
|
|
249
|
+
if (!cancellationHandled) this.outputManager.emitAgentError("Request cancelled");
|
|
250
|
+
this.logger.info("Agent invocation cancelled");
|
|
251
|
+
return {
|
|
252
|
+
cancelled: true
|
|
253
|
+
};
|
|
254
|
+
}
|
|
208
255
|
this.logger.error(`Agent invocation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
209
256
|
this.outputManager.emitAgentError(error);
|
|
210
257
|
throw error;
|
package/dist/gateway/server.cjs
CHANGED
|
@@ -249,6 +249,7 @@ class GatewayServer {
|
|
|
249
249
|
const nodeId = ws.data.nodeId;
|
|
250
250
|
if ("connect" === msg.type) return void this.handleConnect(ws, msg);
|
|
251
251
|
if ("req:agent" === msg.type) return void this.handleAgentRequest(ws, msg);
|
|
252
|
+
if ("req:agent:cancel" === msg.type) return void this.handleAgentCancel(ws, msg);
|
|
252
253
|
if (nodeId && "register" !== msg.type && "ping" !== msg.type && "pong" !== msg.type) {
|
|
253
254
|
if (this.nodeManager.isRateLimited(nodeId)) return void this.sendError(ws, "RATE_LIMITED", "Too many messages. Please slow down.");
|
|
254
255
|
this.nodeManager.recordMessage(nodeId);
|
|
@@ -303,6 +304,7 @@ class GatewayServer {
|
|
|
303
304
|
}
|
|
304
305
|
this.connectedClients.delete(ws);
|
|
305
306
|
this.clearSessionSubscriptions(ws);
|
|
307
|
+
this.cancelSocketAgentRequests(ws);
|
|
306
308
|
}
|
|
307
309
|
handleDrain(ws) {
|
|
308
310
|
this.log("debug", "WebSocket drained");
|
|
@@ -343,6 +345,11 @@ class GatewayServer {
|
|
|
343
345
|
async handleAgentRequest(ws, msg) {
|
|
344
346
|
if (!msg.id) return void this.sendError(ws, "INVALID_REQUEST", "Missing request id");
|
|
345
347
|
if (!ws.data.authenticated) return void this.sendAgentError(ws, msg.id, "Client is not authenticated");
|
|
348
|
+
if (this.activeAgentRequests.has(msg.id)) {
|
|
349
|
+
const existing = this.activeAgentRequests.get(msg.id);
|
|
350
|
+
existing?.abortController.abort();
|
|
351
|
+
this.activeAgentRequests.delete(msg.id);
|
|
352
|
+
}
|
|
346
353
|
const payload = msg.payload;
|
|
347
354
|
const content = "string" == typeof payload?.content ? payload.content : "";
|
|
348
355
|
const attachments = Array.isArray(payload?.attachments) ? payload.attachments : [];
|
|
@@ -428,8 +435,15 @@ class GatewayServer {
|
|
|
428
435
|
workdir,
|
|
429
436
|
defaultOutputDir
|
|
430
437
|
});
|
|
438
|
+
const abortController = new AbortController();
|
|
439
|
+
this.activeAgentRequests.set(msg.id, {
|
|
440
|
+
socket: ws,
|
|
441
|
+
abortController
|
|
442
|
+
});
|
|
431
443
|
try {
|
|
432
|
-
await invoker.invokeAgent(agentId, content, sessionKey, attachments
|
|
444
|
+
await invoker.invokeAgent(agentId, content, sessionKey, attachments, {
|
|
445
|
+
signal: abortController.signal
|
|
446
|
+
});
|
|
433
447
|
const updated = sessionManager.getSession(sessionKey);
|
|
434
448
|
if (updated) sessionManager.updateSession(sessionKey, {
|
|
435
449
|
messageCount: updated.messageCount + 1
|
|
@@ -437,9 +451,40 @@ class GatewayServer {
|
|
|
437
451
|
} catch (error) {
|
|
438
452
|
this.logger.error("Agent invocation failed", error);
|
|
439
453
|
} finally{
|
|
454
|
+
this.activeAgentRequests.delete(msg.id);
|
|
440
455
|
outputManager.off("output-event", outputHandler);
|
|
441
456
|
}
|
|
442
457
|
}
|
|
458
|
+
handleAgentCancel(ws, msg) {
|
|
459
|
+
if (!ws.data.authenticated) return void this.sendError(ws, "AUTH_FAILED", "Client is not authenticated");
|
|
460
|
+
const payload = msg.payload;
|
|
461
|
+
const requestId = "string" == typeof payload?.requestId && payload.requestId || void 0;
|
|
462
|
+
if (!requestId) return void this.sendError(ws, "INVALID_REQUEST", "Missing requestId for cancellation");
|
|
463
|
+
const active = this.activeAgentRequests.get(requestId);
|
|
464
|
+
if (!active) return void this.sendMessage(ws, {
|
|
465
|
+
type: "ack",
|
|
466
|
+
id: msg.id,
|
|
467
|
+
payload: {
|
|
468
|
+
action: "req:agent:cancel",
|
|
469
|
+
requestId,
|
|
470
|
+
status: "not_found"
|
|
471
|
+
},
|
|
472
|
+
timestamp: Date.now()
|
|
473
|
+
});
|
|
474
|
+
if (active.socket !== ws) return void this.sendError(ws, "FORBIDDEN", "Cannot cancel a request started by another client");
|
|
475
|
+
active.abortController.abort();
|
|
476
|
+
this.activeAgentRequests.delete(requestId);
|
|
477
|
+
this.sendMessage(ws, {
|
|
478
|
+
type: "ack",
|
|
479
|
+
id: msg.id,
|
|
480
|
+
payload: {
|
|
481
|
+
action: "req:agent:cancel",
|
|
482
|
+
requestId,
|
|
483
|
+
status: "cancelled"
|
|
484
|
+
},
|
|
485
|
+
timestamp: Date.now()
|
|
486
|
+
});
|
|
487
|
+
}
|
|
443
488
|
handleRegister(ws, msg) {
|
|
444
489
|
const payload = msg.payload;
|
|
445
490
|
if (!this.auth.validate({
|
|
@@ -614,6 +659,12 @@ class GatewayServer {
|
|
|
614
659
|
timestamp: Date.now()
|
|
615
660
|
});
|
|
616
661
|
}
|
|
662
|
+
cancelSocketAgentRequests(ws) {
|
|
663
|
+
for (const [requestId, active] of this.activeAgentRequests)if (active.socket === ws) {
|
|
664
|
+
active.abortController.abort();
|
|
665
|
+
this.activeAgentRequests.delete(requestId);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
617
668
|
attachSessionContext(event, sessionId, agentId) {
|
|
618
669
|
if (event && "object" == typeof event && !Array.isArray(event)) return {
|
|
619
670
|
...event,
|
|
@@ -1129,6 +1180,7 @@ class GatewayServer {
|
|
|
1129
1180
|
_define_property(this, "sessionSubscriptions", new Map());
|
|
1130
1181
|
_define_property(this, "socketSubscriptions", new Map());
|
|
1131
1182
|
_define_property(this, "connectedClients", new Set());
|
|
1183
|
+
_define_property(this, "activeAgentRequests", new Map());
|
|
1132
1184
|
_define_property(this, "bridgeQueues", new Map());
|
|
1133
1185
|
_define_property(this, "bridgePollWaiters", new Map());
|
|
1134
1186
|
this.workspace = config.workspace || process.cwd();
|
package/dist/gateway/server.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export declare class GatewayServer {
|
|
|
33
33
|
private sessionSubscriptions;
|
|
34
34
|
private socketSubscriptions;
|
|
35
35
|
private connectedClients;
|
|
36
|
+
private activeAgentRequests;
|
|
36
37
|
private bridgeQueues;
|
|
37
38
|
private bridgePollWaiters;
|
|
38
39
|
constructor(config?: Partial<GatewayConfig>);
|
|
@@ -75,6 +76,7 @@ export declare class GatewayServer {
|
|
|
75
76
|
* Handle agent execution request
|
|
76
77
|
*/
|
|
77
78
|
private handleAgentRequest;
|
|
79
|
+
private handleAgentCancel;
|
|
78
80
|
/**
|
|
79
81
|
* Handle node registration
|
|
80
82
|
*/
|
|
@@ -124,6 +126,7 @@ export declare class GatewayServer {
|
|
|
124
126
|
*/
|
|
125
127
|
private sendError;
|
|
126
128
|
private sendAgentError;
|
|
129
|
+
private cancelSocketAgentRequests;
|
|
127
130
|
private attachSessionContext;
|
|
128
131
|
private broadcastSessionEvent;
|
|
129
132
|
private broadcastToClients;
|
package/dist/gateway/server.js
CHANGED
|
@@ -218,6 +218,7 @@ class GatewayServer {
|
|
|
218
218
|
const nodeId = ws.data.nodeId;
|
|
219
219
|
if ("connect" === msg.type) return void this.handleConnect(ws, msg);
|
|
220
220
|
if ("req:agent" === msg.type) return void this.handleAgentRequest(ws, msg);
|
|
221
|
+
if ("req:agent:cancel" === msg.type) return void this.handleAgentCancel(ws, msg);
|
|
221
222
|
if (nodeId && "register" !== msg.type && "ping" !== msg.type && "pong" !== msg.type) {
|
|
222
223
|
if (this.nodeManager.isRateLimited(nodeId)) return void this.sendError(ws, "RATE_LIMITED", "Too many messages. Please slow down.");
|
|
223
224
|
this.nodeManager.recordMessage(nodeId);
|
|
@@ -272,6 +273,7 @@ class GatewayServer {
|
|
|
272
273
|
}
|
|
273
274
|
this.connectedClients.delete(ws);
|
|
274
275
|
this.clearSessionSubscriptions(ws);
|
|
276
|
+
this.cancelSocketAgentRequests(ws);
|
|
275
277
|
}
|
|
276
278
|
handleDrain(ws) {
|
|
277
279
|
this.log("debug", "WebSocket drained");
|
|
@@ -312,6 +314,11 @@ class GatewayServer {
|
|
|
312
314
|
async handleAgentRequest(ws, msg) {
|
|
313
315
|
if (!msg.id) return void this.sendError(ws, "INVALID_REQUEST", "Missing request id");
|
|
314
316
|
if (!ws.data.authenticated) return void this.sendAgentError(ws, msg.id, "Client is not authenticated");
|
|
317
|
+
if (this.activeAgentRequests.has(msg.id)) {
|
|
318
|
+
const existing = this.activeAgentRequests.get(msg.id);
|
|
319
|
+
existing?.abortController.abort();
|
|
320
|
+
this.activeAgentRequests.delete(msg.id);
|
|
321
|
+
}
|
|
315
322
|
const payload = msg.payload;
|
|
316
323
|
const content = "string" == typeof payload?.content ? payload.content : "";
|
|
317
324
|
const attachments = Array.isArray(payload?.attachments) ? payload.attachments : [];
|
|
@@ -397,8 +404,15 @@ class GatewayServer {
|
|
|
397
404
|
workdir,
|
|
398
405
|
defaultOutputDir
|
|
399
406
|
});
|
|
407
|
+
const abortController = new AbortController();
|
|
408
|
+
this.activeAgentRequests.set(msg.id, {
|
|
409
|
+
socket: ws,
|
|
410
|
+
abortController
|
|
411
|
+
});
|
|
400
412
|
try {
|
|
401
|
-
await invoker.invokeAgent(agentId, content, sessionKey, attachments
|
|
413
|
+
await invoker.invokeAgent(agentId, content, sessionKey, attachments, {
|
|
414
|
+
signal: abortController.signal
|
|
415
|
+
});
|
|
402
416
|
const updated = sessionManager.getSession(sessionKey);
|
|
403
417
|
if (updated) sessionManager.updateSession(sessionKey, {
|
|
404
418
|
messageCount: updated.messageCount + 1
|
|
@@ -406,9 +420,40 @@ class GatewayServer {
|
|
|
406
420
|
} catch (error) {
|
|
407
421
|
this.logger.error("Agent invocation failed", error);
|
|
408
422
|
} finally{
|
|
423
|
+
this.activeAgentRequests.delete(msg.id);
|
|
409
424
|
outputManager.off("output-event", outputHandler);
|
|
410
425
|
}
|
|
411
426
|
}
|
|
427
|
+
handleAgentCancel(ws, msg) {
|
|
428
|
+
if (!ws.data.authenticated) return void this.sendError(ws, "AUTH_FAILED", "Client is not authenticated");
|
|
429
|
+
const payload = msg.payload;
|
|
430
|
+
const requestId = "string" == typeof payload?.requestId && payload.requestId || void 0;
|
|
431
|
+
if (!requestId) return void this.sendError(ws, "INVALID_REQUEST", "Missing requestId for cancellation");
|
|
432
|
+
const active = this.activeAgentRequests.get(requestId);
|
|
433
|
+
if (!active) return void this.sendMessage(ws, {
|
|
434
|
+
type: "ack",
|
|
435
|
+
id: msg.id,
|
|
436
|
+
payload: {
|
|
437
|
+
action: "req:agent:cancel",
|
|
438
|
+
requestId,
|
|
439
|
+
status: "not_found"
|
|
440
|
+
},
|
|
441
|
+
timestamp: Date.now()
|
|
442
|
+
});
|
|
443
|
+
if (active.socket !== ws) return void this.sendError(ws, "FORBIDDEN", "Cannot cancel a request started by another client");
|
|
444
|
+
active.abortController.abort();
|
|
445
|
+
this.activeAgentRequests.delete(requestId);
|
|
446
|
+
this.sendMessage(ws, {
|
|
447
|
+
type: "ack",
|
|
448
|
+
id: msg.id,
|
|
449
|
+
payload: {
|
|
450
|
+
action: "req:agent:cancel",
|
|
451
|
+
requestId,
|
|
452
|
+
status: "cancelled"
|
|
453
|
+
},
|
|
454
|
+
timestamp: Date.now()
|
|
455
|
+
});
|
|
456
|
+
}
|
|
412
457
|
handleRegister(ws, msg) {
|
|
413
458
|
const payload = msg.payload;
|
|
414
459
|
if (!this.auth.validate({
|
|
@@ -583,6 +628,12 @@ class GatewayServer {
|
|
|
583
628
|
timestamp: Date.now()
|
|
584
629
|
});
|
|
585
630
|
}
|
|
631
|
+
cancelSocketAgentRequests(ws) {
|
|
632
|
+
for (const [requestId, active] of this.activeAgentRequests)if (active.socket === ws) {
|
|
633
|
+
active.abortController.abort();
|
|
634
|
+
this.activeAgentRequests.delete(requestId);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
586
637
|
attachSessionContext(event, sessionId, agentId) {
|
|
587
638
|
if (event && "object" == typeof event && !Array.isArray(event)) return {
|
|
588
639
|
...event,
|
|
@@ -1098,6 +1149,7 @@ class GatewayServer {
|
|
|
1098
1149
|
_define_property(this, "sessionSubscriptions", new Map());
|
|
1099
1150
|
_define_property(this, "socketSubscriptions", new Map());
|
|
1100
1151
|
_define_property(this, "connectedClients", new Set());
|
|
1152
|
+
_define_property(this, "activeAgentRequests", new Map());
|
|
1101
1153
|
_define_property(this, "bridgeQueues", new Map());
|
|
1102
1154
|
_define_property(this, "bridgePollWaiters", new Map());
|
|
1103
1155
|
this.workspace = config.workspace || process.cwd();
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ServerWebSocket } from "bun";
|
|
|
2
2
|
/**
|
|
3
3
|
* Message types for gateway communication
|
|
4
4
|
*/
|
|
5
|
-
export type MessageType = "connect" | "res" | "req:agent" | "event:agent" | "session_subscribe" | "session_unsubscribe" | "register" | "registered" | "unregister" | "join_group" | "leave_group" | "broadcast" | "direct" | "ping" | "pong" | "error" | "ack" | "upgrade";
|
|
5
|
+
export type MessageType = "connect" | "res" | "req:agent" | "req:agent:cancel" | "event:agent" | "session_subscribe" | "session_unsubscribe" | "register" | "registered" | "unregister" | "join_group" | "leave_group" | "broadcast" | "direct" | "ping" | "pong" | "error" | "ack" | "upgrade";
|
|
6
6
|
/**
|
|
7
7
|
* Gateway message structure
|
|
8
8
|
*/
|
|
@@ -61,6 +61,9 @@ export interface AgentRequestPayload {
|
|
|
61
61
|
routing?: RoutingInfo;
|
|
62
62
|
sessionKey?: string;
|
|
63
63
|
}
|
|
64
|
+
export interface AgentCancelPayload {
|
|
65
|
+
requestId: string;
|
|
66
|
+
}
|
|
64
67
|
export interface RoutingPeer {
|
|
65
68
|
kind: "dm" | "group" | "channel";
|
|
66
69
|
id: string;
|
|
@@ -8,6 +8,7 @@ export declare const MessageTypeSchema: z.ZodEnum<{
|
|
|
8
8
|
connect: "connect";
|
|
9
9
|
res: "res";
|
|
10
10
|
"req:agent": "req:agent";
|
|
11
|
+
"req:agent:cancel": "req:agent:cancel";
|
|
11
12
|
"event:agent": "event:agent";
|
|
12
13
|
session_subscribe: "session_subscribe";
|
|
13
14
|
session_unsubscribe: "session_unsubscribe";
|
|
@@ -31,6 +32,7 @@ export declare const GatewayMessageSchema: z.ZodObject<{
|
|
|
31
32
|
connect: "connect";
|
|
32
33
|
res: "res";
|
|
33
34
|
"req:agent": "req:agent";
|
|
35
|
+
"req:agent:cancel": "req:agent:cancel";
|
|
34
36
|
"event:agent": "event:agent";
|
|
35
37
|
session_subscribe: "session_subscribe";
|
|
36
38
|
session_unsubscribe: "session_unsubscribe";
|
|
@@ -2,15 +2,52 @@
|
|
|
2
2
|
var __webpack_exports__ = {};
|
|
3
3
|
const external_vitest_namespaceObject = require("vitest");
|
|
4
4
|
const index_cjs_namespaceObject = require("../gateway/index.cjs");
|
|
5
|
+
function _define_property(obj, key, value) {
|
|
6
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
7
|
+
value: value,
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true
|
|
11
|
+
});
|
|
12
|
+
else obj[key] = value;
|
|
13
|
+
return obj;
|
|
14
|
+
}
|
|
5
15
|
const isBun = void 0 !== globalThis.Bun;
|
|
6
16
|
const describeIfBun = isBun ? external_vitest_namespaceObject.describe : external_vitest_namespaceObject.describe.skip;
|
|
7
17
|
external_vitest_namespaceObject.vi.mock("@/cli/core/agentInvoker.js", ()=>({
|
|
8
18
|
AgentInvoker: class {
|
|
9
|
-
async invokeAgent() {
|
|
19
|
+
async invokeAgent(_agentId, _content, _sessionId, _attachments, options) {
|
|
20
|
+
const signal = options?.signal;
|
|
21
|
+
await new Promise((resolve)=>{
|
|
22
|
+
const timer = setTimeout(resolve, 75);
|
|
23
|
+
if (signal) {
|
|
24
|
+
const onAbort = ()=>{
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
resolve();
|
|
27
|
+
};
|
|
28
|
+
if (signal.aborted) return void onAbort();
|
|
29
|
+
signal.addEventListener("abort", onAbort, {
|
|
30
|
+
once: true
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (signal?.aborted) {
|
|
35
|
+
this.outputManager?.emitAgentError?.("Request cancelled");
|
|
36
|
+
return {
|
|
37
|
+
cancelled: true
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
this.outputManager?.emitAgentComplete?.({
|
|
41
|
+
streaming: true
|
|
42
|
+
});
|
|
10
43
|
return {
|
|
11
44
|
streaming: true
|
|
12
45
|
};
|
|
13
46
|
}
|
|
47
|
+
constructor(options){
|
|
48
|
+
_define_property(this, "outputManager", void 0);
|
|
49
|
+
this.outputManager = options?.outputManager;
|
|
50
|
+
}
|
|
14
51
|
}
|
|
15
52
|
}));
|
|
16
53
|
describeIfBun("Gateway", ()=>{
|
|
@@ -296,6 +333,34 @@ describeIfBun("Gateway", ()=>{
|
|
|
296
333
|
desktopClient.close();
|
|
297
334
|
requester.close();
|
|
298
335
|
});
|
|
336
|
+
(0, external_vitest_namespaceObject.it)("should cancel an in-flight agent request", async ()=>{
|
|
337
|
+
const requester = await connectClient("session-cancel-requester");
|
|
338
|
+
const requestId = "req-cancel-test";
|
|
339
|
+
requester.send(JSON.stringify({
|
|
340
|
+
type: "req:agent",
|
|
341
|
+
id: requestId,
|
|
342
|
+
payload: {
|
|
343
|
+
agentId: "main",
|
|
344
|
+
sessionKey: "session-cancel-test",
|
|
345
|
+
content: "cancel me"
|
|
346
|
+
},
|
|
347
|
+
timestamp: Date.now()
|
|
348
|
+
}));
|
|
349
|
+
requester.send(JSON.stringify({
|
|
350
|
+
type: "req:agent:cancel",
|
|
351
|
+
id: "cancel-req-cancel-test",
|
|
352
|
+
payload: {
|
|
353
|
+
requestId
|
|
354
|
+
},
|
|
355
|
+
timestamp: Date.now()
|
|
356
|
+
}));
|
|
357
|
+
const ack = await waitForMessage(requester, (msg)=>"ack" === msg.type && msg.payload?.action === "req:agent:cancel" && msg.payload?.requestId === requestId);
|
|
358
|
+
(0, external_vitest_namespaceObject.expect)([
|
|
359
|
+
"cancelled",
|
|
360
|
+
"not_found"
|
|
361
|
+
]).toContain(ack.payload?.status);
|
|
362
|
+
requester.close();
|
|
363
|
+
});
|
|
299
364
|
(0, external_vitest_namespaceObject.it)("should clear session messages via API", async ()=>{
|
|
300
365
|
const createRes = await fetch(`http://localhost:${port}/api/sessions`, {
|
|
301
366
|
method: "POST",
|