@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.
@@ -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)this.outputManager.emitAgentStream(chunk);
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)this.outputManager.emitAgentStream(chunk);
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;
@@ -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();
@@ -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;
@@ -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();
@@ -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;
@@ -42,6 +42,7 @@ const MessageTypeSchema = external_zod_namespaceObject.z["enum"]([
42
42
  "connect",
43
43
  "res",
44
44
  "req:agent",
45
+ "req:agent:cancel",
45
46
  "event:agent",
46
47
  "session_subscribe",
47
48
  "session_unsubscribe",
@@ -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";
@@ -3,6 +3,7 @@ const MessageTypeSchema = z["enum"]([
3
3
  "connect",
4
4
  "res",
5
5
  "req:agent",
6
+ "req:agent:cancel",
6
7
  "event:agent",
7
8
  "session_subscribe",
8
9
  "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",