@copilotkit/runtime 1.56.2 → 1.56.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/dist/graphql/resolvers/copilot.resolver.cjs +2 -1
  2. package/dist/graphql/resolvers/copilot.resolver.cjs.map +1 -1
  3. package/dist/graphql/resolvers/copilot.resolver.mjs +2 -1
  4. package/dist/graphql/resolvers/copilot.resolver.mjs.map +1 -1
  5. package/dist/graphql/resolvers/resolve-message-id.cjs +19 -0
  6. package/dist/graphql/resolvers/resolve-message-id.cjs.map +1 -0
  7. package/dist/graphql/resolvers/resolve-message-id.mjs +18 -0
  8. package/dist/graphql/resolvers/resolve-message-id.mjs.map +1 -0
  9. package/dist/lib/runtime/copilot-runtime.cjs +4 -2
  10. package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
  11. package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
  12. package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
  13. package/dist/lib/runtime/copilot-runtime.mjs +4 -2
  14. package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
  15. package/dist/package.cjs +2 -2
  16. package/dist/package.mjs +2 -2
  17. package/dist/v2/index.d.cts +2 -2
  18. package/dist/v2/index.d.mts +2 -2
  19. package/dist/v2/runtime/core/debug-event-bus.cjs +36 -0
  20. package/dist/v2/runtime/core/debug-event-bus.cjs.map +1 -0
  21. package/dist/v2/runtime/core/debug-event-bus.d.cts +19 -0
  22. package/dist/v2/runtime/core/debug-event-bus.d.cts.map +1 -0
  23. package/dist/v2/runtime/core/debug-event-bus.d.mts +19 -0
  24. package/dist/v2/runtime/core/debug-event-bus.d.mts.map +1 -0
  25. package/dist/v2/runtime/core/debug-event-bus.mjs +35 -0
  26. package/dist/v2/runtime/core/debug-event-bus.mjs.map +1 -0
  27. package/dist/v2/runtime/core/fetch-handler.cjs +8 -0
  28. package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
  29. package/dist/v2/runtime/core/fetch-handler.d.cts.map +1 -1
  30. package/dist/v2/runtime/core/fetch-handler.d.mts.map +1 -1
  31. package/dist/v2/runtime/core/fetch-handler.mjs +8 -0
  32. package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
  33. package/dist/v2/runtime/core/fetch-router.cjs +1 -0
  34. package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
  35. package/dist/v2/runtime/core/fetch-router.mjs +1 -0
  36. package/dist/v2/runtime/core/fetch-router.mjs.map +1 -1
  37. package/dist/v2/runtime/core/hooks.cjs.map +1 -1
  38. package/dist/v2/runtime/core/hooks.d.cts +2 -0
  39. package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
  40. package/dist/v2/runtime/core/hooks.d.mts +2 -0
  41. package/dist/v2/runtime/core/hooks.d.mts.map +1 -1
  42. package/dist/v2/runtime/core/hooks.mjs.map +1 -1
  43. package/dist/v2/runtime/core/runtime.cjs +5 -0
  44. package/dist/v2/runtime/core/runtime.cjs.map +1 -1
  45. package/dist/v2/runtime/core/runtime.d.cts +5 -0
  46. package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
  47. package/dist/v2/runtime/core/runtime.d.mts +5 -1
  48. package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
  49. package/dist/v2/runtime/core/runtime.mjs +5 -0
  50. package/dist/v2/runtime/core/runtime.mjs.map +1 -1
  51. package/dist/v2/runtime/handlers/handle-connect.cjs +3 -2
  52. package/dist/v2/runtime/handlers/handle-connect.cjs.map +1 -1
  53. package/dist/v2/runtime/handlers/handle-connect.mjs +3 -2
  54. package/dist/v2/runtime/handlers/handle-connect.mjs.map +1 -1
  55. package/dist/v2/runtime/handlers/handle-debug-events.cjs +33 -0
  56. package/dist/v2/runtime/handlers/handle-debug-events.cjs.map +1 -0
  57. package/dist/v2/runtime/handlers/handle-debug-events.mjs +32 -0
  58. package/dist/v2/runtime/handlers/handle-debug-events.mjs.map +1 -0
  59. package/dist/v2/runtime/handlers/handle-run.cjs +1 -0
  60. package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
  61. package/dist/v2/runtime/handlers/handle-run.mjs +1 -0
  62. package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
  63. package/dist/v2/runtime/handlers/intelligence/connect.cjs +24 -4
  64. package/dist/v2/runtime/handlers/intelligence/connect.cjs.map +1 -1
  65. package/dist/v2/runtime/handlers/intelligence/connect.mjs +25 -5
  66. package/dist/v2/runtime/handlers/intelligence/connect.mjs.map +1 -1
  67. package/dist/v2/runtime/handlers/intelligence/run.cjs +111 -26
  68. package/dist/v2/runtime/handlers/intelligence/run.cjs.map +1 -1
  69. package/dist/v2/runtime/handlers/intelligence/run.mjs +111 -26
  70. package/dist/v2/runtime/handlers/intelligence/run.mjs.map +1 -1
  71. package/dist/v2/runtime/handlers/shared/intelligence-utils.cjs +7 -3
  72. package/dist/v2/runtime/handlers/shared/intelligence-utils.cjs.map +1 -1
  73. package/dist/v2/runtime/handlers/shared/intelligence-utils.mjs +7 -3
  74. package/dist/v2/runtime/handlers/shared/intelligence-utils.mjs.map +1 -1
  75. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.cjs +5 -1
  76. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.cjs.map +1 -1
  77. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.mjs +5 -1
  78. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.mjs.map +1 -1
  79. package/dist/v2/runtime/handlers/shared/sse-response.cjs +21 -1
  80. package/dist/v2/runtime/handlers/shared/sse-response.cjs.map +1 -1
  81. package/dist/v2/runtime/handlers/shared/sse-response.mjs +21 -1
  82. package/dist/v2/runtime/handlers/shared/sse-response.mjs.map +1 -1
  83. package/dist/v2/runtime/handlers/sse/connect.cjs +3 -1
  84. package/dist/v2/runtime/handlers/sse/connect.cjs.map +1 -1
  85. package/dist/v2/runtime/handlers/sse/connect.mjs +3 -1
  86. package/dist/v2/runtime/handlers/sse/connect.mjs.map +1 -1
  87. package/dist/v2/runtime/handlers/sse/run.cjs +3 -1
  88. package/dist/v2/runtime/handlers/sse/run.cjs.map +1 -1
  89. package/dist/v2/runtime/handlers/sse/run.mjs +3 -1
  90. package/dist/v2/runtime/handlers/sse/run.mjs.map +1 -1
  91. package/dist/v2/runtime/index.d.cts +1 -1
  92. package/dist/v2/runtime/index.d.mts +1 -2
  93. package/dist/v2/runtime/index.d.mts.map +1 -1
  94. package/dist/v2/runtime/intelligence-platform/client.cjs +6 -8
  95. package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
  96. package/dist/v2/runtime/intelligence-platform/client.d.cts +16 -21
  97. package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
  98. package/dist/v2/runtime/intelligence-platform/client.d.mts +16 -21
  99. package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
  100. package/dist/v2/runtime/intelligence-platform/client.mjs +6 -8
  101. package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
  102. package/dist/v2/runtime/runner/agent-runner.cjs.map +1 -1
  103. package/dist/v2/runtime/runner/agent-runner.d.cts +0 -1
  104. package/dist/v2/runtime/runner/agent-runner.d.cts.map +1 -1
  105. package/dist/v2/runtime/runner/agent-runner.d.mts +0 -1
  106. package/dist/v2/runtime/runner/agent-runner.d.mts.map +1 -1
  107. package/dist/v2/runtime/runner/agent-runner.mjs.map +1 -1
  108. package/dist/v2/runtime/runner/index.d.cts +1 -1
  109. package/dist/v2/runtime/runner/index.d.mts +1 -1
  110. package/dist/v2/runtime/runner/intelligence.cjs +47 -10
  111. package/dist/v2/runtime/runner/intelligence.cjs.map +1 -1
  112. package/dist/v2/runtime/runner/intelligence.d.cts +8 -1
  113. package/dist/v2/runtime/runner/intelligence.d.cts.map +1 -1
  114. package/dist/v2/runtime/runner/intelligence.d.mts +8 -1
  115. package/dist/v2/runtime/runner/intelligence.d.mts.map +1 -1
  116. package/dist/v2/runtime/runner/intelligence.mjs +47 -10
  117. package/dist/v2/runtime/runner/intelligence.mjs.map +1 -1
  118. package/dist/v2/runtime/telemetry/instance-created.cjs +33 -0
  119. package/dist/v2/runtime/telemetry/instance-created.cjs.map +1 -0
  120. package/dist/v2/runtime/telemetry/instance-created.mjs +33 -0
  121. package/dist/v2/runtime/telemetry/instance-created.mjs.map +1 -0
  122. package/dist/v2/runtime/telemetry/telemetry-client.cjs +1 -38
  123. package/dist/v2/runtime/telemetry/telemetry-client.cjs.map +1 -1
  124. package/dist/v2/runtime/telemetry/telemetry-client.mjs +1 -37
  125. package/dist/v2/runtime/telemetry/telemetry-client.mjs.map +1 -1
  126. package/package.json +3 -3
  127. package/src/agents/langgraph/__tests__/event-source.test.ts +256 -0
  128. package/src/graphql/resolvers/__tests__/resolve-message-id.test.ts +25 -0
  129. package/src/graphql/resolvers/copilot.resolver.ts +2 -1
  130. package/src/graphql/resolvers/resolve-message-id.ts +14 -0
  131. package/src/lib/runtime/__tests__/handle-service-adapter.test.ts +108 -0
  132. package/src/lib/runtime/__tests__/retry-utils.test.ts +137 -0
  133. package/src/lib/runtime/agent-integrations/langgraph/__tests__/dispatch-event-filtering.test.ts +190 -0
  134. package/src/lib/runtime/copilot-runtime.ts +20 -4
  135. package/src/lib/runtime/retry-utils.ts +41 -1
  136. package/src/v2/runtime/__tests__/express-single-telemetry.integration.test.ts +65 -0
  137. package/src/v2/runtime/__tests__/express-telemetry.integration.test.ts +101 -0
  138. package/src/v2/runtime/__tests__/fetch-router.test.ts +22 -0
  139. package/src/v2/runtime/__tests__/handle-connect.test.ts +183 -23
  140. package/src/v2/runtime/__tests__/handle-run.test.ts +411 -33
  141. package/src/v2/runtime/__tests__/handle-threads.test.ts +66 -4
  142. package/src/v2/runtime/__tests__/hono-single-telemetry.integration.test.ts +46 -0
  143. package/src/v2/runtime/__tests__/hono-telemetry.integration.test.ts +99 -0
  144. package/src/v2/runtime/__tests__/integration/node-servers.integration.test.ts +19 -0
  145. package/src/v2/runtime/__tests__/integration/suites/debug-events.suite.ts +253 -0
  146. package/src/v2/runtime/__tests__/intelligence-run-telemetry.test.ts +194 -0
  147. package/src/v2/runtime/__tests__/runtime.test.ts +3 -1
  148. package/src/v2/runtime/__tests__/sse-response-telemetry.test.ts +108 -0
  149. package/src/v2/runtime/__tests__/telemetry.test.ts +0 -61
  150. package/src/v2/runtime/core/__tests__/debug-event-bus.test.ts +156 -0
  151. package/src/v2/runtime/core/debug-event-bus.ts +45 -0
  152. package/src/v2/runtime/core/fetch-handler.ts +7 -0
  153. package/src/v2/runtime/core/fetch-router.ts +11 -0
  154. package/src/v2/runtime/core/hooks.ts +2 -1
  155. package/src/v2/runtime/core/runtime.ts +12 -0
  156. package/src/v2/runtime/handlers/__tests__/handle-debug-events.test.ts +176 -0
  157. package/src/v2/runtime/handlers/handle-connect.ts +2 -1
  158. package/src/v2/runtime/handlers/handle-debug-events.ts +52 -0
  159. package/src/v2/runtime/handlers/handle-run.ts +1 -0
  160. package/src/v2/runtime/handlers/intelligence/connect.ts +48 -11
  161. package/src/v2/runtime/handlers/intelligence/run.ts +162 -21
  162. package/src/v2/runtime/handlers/shared/intelligence-utils.ts +21 -1
  163. package/src/v2/runtime/handlers/shared/resolve-intelligence-user.ts +4 -1
  164. package/src/v2/runtime/handlers/shared/sse-response.ts +46 -0
  165. package/src/v2/runtime/handlers/sse/__tests__/sse-connect-agent-id.test.ts +71 -0
  166. package/src/v2/runtime/handlers/sse/connect.ts +6 -0
  167. package/src/v2/runtime/handlers/sse/run.ts +4 -0
  168. package/src/v2/runtime/intelligence-platform/__tests__/client.test.ts +33 -37
  169. package/src/v2/runtime/intelligence-platform/client.ts +37 -40
  170. package/src/v2/runtime/runner/__tests__/intelligence-runner.test.ts +66 -8
  171. package/src/v2/runtime/runner/agent-runner.ts +0 -1
  172. package/src/v2/runtime/runner/intelligence.ts +74 -15
  173. package/src/v2/runtime/telemetry/__tests__/instance-created.test.ts +96 -0
  174. package/src/v2/runtime/telemetry/instance-created.ts +44 -0
  175. package/src/v2/runtime/telemetry/telemetry-client.ts +1 -57
  176. package/dist/v2/runtime/intelligence-platform/index.d.mts +0 -2
  177. package/dist/v2/runtime/telemetry/utils.cjs +0 -15
  178. package/dist/v2/runtime/telemetry/utils.cjs.map +0 -1
  179. package/dist/v2/runtime/telemetry/utils.mjs +0 -14
  180. package/dist/v2/runtime/telemetry/utils.mjs.map +0 -1
  181. package/src/v2/runtime/telemetry/utils.ts +0 -15
@@ -1,6 +1,6 @@
1
1
  import { Observable } from "rxjs";
2
2
  import { describe, it, expect, vi } from "vitest";
3
- import { AbstractAgent, HttpAgent } from "@ag-ui/client";
3
+ import { AbstractAgent, BaseEvent, EventType, HttpAgent } from "@ag-ui/client";
4
4
  import { A2UIMiddleware } from "@ag-ui/a2ui-middleware";
5
5
  import { handleRunAgent } from "../handlers/handle-run";
6
6
  import { CopilotRuntime } from "../core/runtime";
@@ -285,7 +285,7 @@ describe("handleRunAgent", () => {
285
285
  expect(useSpy).not.toHaveBeenCalled();
286
286
  });
287
287
 
288
- describe("IntelligenceAgentRunner join code path", () => {
288
+ describe("IntelligenceAgentRunner realtime credentials path", () => {
289
289
  /** Loose mock type for CopilotKitIntelligence — avoids `as any` while the class has private fields. */
290
290
  interface MockIntelligencePlatform {
291
291
  [key: string]: ((...args: any[]) => any) | undefined;
@@ -296,9 +296,13 @@ describe("handleRunAgent", () => {
296
296
  platform?: MockIntelligencePlatform,
297
297
  options?: {
298
298
  generateThreadNames?: boolean;
299
+ lockHeartbeatIntervalSeconds?: number;
300
+ lockTtlSeconds?: number;
299
301
  identifyUser?: (
300
302
  request: Request,
301
- ) => { id: string } | Promise<{ id: string }>;
303
+ ) =>
304
+ | { id: string; name: string }
305
+ | Promise<{ id: string; name: string }>;
302
306
  },
303
307
  ) => {
304
308
  const runner = Object.create(IntelligenceAgentRunner.prototype);
@@ -316,9 +320,16 @@ describe("handleRunAgent", () => {
316
320
  runner,
317
321
  mode: "intelligence",
318
322
  generateThreadNames: options?.generateThreadNames ?? false,
319
- intelligence: platform,
323
+ lockHeartbeatIntervalSeconds:
324
+ options?.lockHeartbeatIntervalSeconds ?? 15,
325
+ lockTtlSeconds: options?.lockTtlSeconds ?? 20,
326
+ intelligence: {
327
+ ɵgetClientWsUrl: vi.fn(() => "wss://runtime.example/client"),
328
+ ...platform,
329
+ },
320
330
  identifyUser:
321
- options?.identifyUser ?? vi.fn().mockResolvedValue({ id: "user-1" }),
331
+ options?.identifyUser ??
332
+ vi.fn().mockResolvedValue({ id: "user-1", name: "User One" }),
322
333
  } as unknown as CopilotRuntime;
323
334
  };
324
335
 
@@ -328,6 +339,7 @@ describe("handleRunAgent", () => {
328
339
  clone: vi.fn(() => createClone()),
329
340
  setMessages: vi.fn(),
330
341
  setState: vi.fn(),
342
+ abortRun: vi.fn(),
331
343
  threadId: undefined,
332
344
  headers: {},
333
345
  runAgent: vi.fn().mockResolvedValue(undefined),
@@ -337,6 +349,7 @@ describe("handleRunAgent", () => {
337
349
  clone: vi.fn(() => createClone()),
338
350
  setMessages: vi.fn(),
339
351
  setState: vi.fn(),
352
+ abortRun: vi.fn(),
340
353
  threadId: undefined,
341
354
  headers: {},
342
355
  runAgent: vi.fn().mockResolvedValue(undefined),
@@ -352,9 +365,12 @@ describe("handleRunAgent", () => {
352
365
  created: false,
353
366
  }),
354
367
  getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
355
- ɵacquireThreadLock: vi
356
- .fn()
357
- .mockResolvedValue({ joinToken: "jt-123", joinCode: "jc-123" }),
368
+ ɵacquireThreadLock: vi.fn().mockResolvedValue({
369
+ threadId: "thread-1",
370
+ runId: "run-1",
371
+ joinToken: "jt-123",
372
+ }),
373
+ ɵcleanupThreadLock: vi.fn().mockResolvedValue(undefined),
358
374
  };
359
375
  const runtime = createIntelligenceRuntime(agent, platform);
360
376
 
@@ -367,7 +383,15 @@ describe("handleRunAgent", () => {
367
383
  expect(response.status).toBe(200);
368
384
  expect(response.headers.get("Content-Type")).toBe("application/json");
369
385
  const body = await response.json();
370
- expect(body).toEqual({ joinToken: "jt-123" });
386
+ expect(body).toEqual({
387
+ threadId: "thread-1",
388
+ runId: "run-1",
389
+ joinToken: "jt-123",
390
+ realtime: {
391
+ clientUrl: "wss://runtime.example/client",
392
+ topic: "thread:thread-1",
393
+ },
394
+ });
371
395
  expect(platform.getOrCreateThread).toHaveBeenCalledWith({
372
396
  threadId: "thread-1",
373
397
  userId: "user-1",
@@ -377,6 +401,8 @@ describe("handleRunAgent", () => {
377
401
  threadId: "thread-1",
378
402
  runId: "run-1",
379
403
  userId: "user-1",
404
+ agentId: "my-agent",
405
+ ttlSeconds: 20,
380
406
  });
381
407
  expect(platform.getThreadMessages).toHaveBeenCalledWith({
382
408
  threadId: "thread-1",
@@ -391,11 +417,16 @@ describe("handleRunAgent", () => {
391
417
  created: false,
392
418
  }),
393
419
  getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
394
- ɵacquireThreadLock: vi
395
- .fn()
396
- .mockResolvedValue({ joinToken: "jt-123", joinCode: "jc-123" }),
420
+ ɵacquireThreadLock: vi.fn().mockResolvedValue({
421
+ threadId: "thread-1",
422
+ runId: "run-1",
423
+ joinToken: "jt-123",
424
+ }),
425
+ ɵcleanupThreadLock: vi.fn().mockResolvedValue(undefined),
397
426
  };
398
- const identifyUser = vi.fn().mockResolvedValue({ id: "resolved-user" });
427
+ const identifyUser = vi
428
+ .fn()
429
+ .mockResolvedValue({ id: "resolved-user", name: "Resolved User" });
399
430
  const runtime = createIntelligenceRuntime(agent, platform, {
400
431
  identifyUser,
401
432
  });
@@ -419,10 +450,12 @@ describe("handleRunAgent", () => {
419
450
  threadId: "thread-1",
420
451
  runId: "run-1",
421
452
  userId: "resolved-user",
453
+ agentId: "my-agent",
454
+ ttlSeconds: 20,
422
455
  });
423
456
  });
424
457
 
425
- it("passes joinCode to runner.run() when provided", async () => {
458
+ it("starts the runner with canonical threadId and runId from the lock response", async () => {
426
459
  const agent = createAgentForIntelligence();
427
460
  const platform = {
428
461
  getOrCreateThread: vi.fn().mockResolvedValue({
@@ -430,9 +463,11 @@ describe("handleRunAgent", () => {
430
463
  created: false,
431
464
  }),
432
465
  getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
433
- ɵacquireThreadLock: vi
434
- .fn()
435
- .mockResolvedValue({ joinToken: "jt-456", joinCode: "jc-456" }),
466
+ ɵacquireThreadLock: vi.fn().mockResolvedValue({
467
+ threadId: "canonical-thread",
468
+ runId: "canonical-run",
469
+ joinToken: "jt-456",
470
+ }),
436
471
  };
437
472
  const runtime = createIntelligenceRuntime(agent, platform);
438
473
 
@@ -443,11 +478,17 @@ describe("handleRunAgent", () => {
443
478
  });
444
479
 
445
480
  expect(runtime.runner.run).toHaveBeenCalledWith(
446
- expect.objectContaining({ joinCode: "jc-456" }),
481
+ expect.objectContaining({
482
+ threadId: "canonical-thread",
483
+ input: expect.objectContaining({
484
+ threadId: "canonical-thread",
485
+ runId: "canonical-run",
486
+ }),
487
+ }),
447
488
  );
448
489
  });
449
490
 
450
- it("returns 502 when joinToken is missing", async () => {
491
+ it("cleans up the lock and returns 502 when joinToken is missing", async () => {
451
492
  const agent = createAgentForIntelligence();
452
493
  const platform = {
453
494
  getOrCreateThread: vi.fn().mockResolvedValue({
@@ -455,7 +496,11 @@ describe("handleRunAgent", () => {
455
496
  created: false,
456
497
  }),
457
498
  getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
458
- ɵacquireThreadLock: vi.fn().mockResolvedValue({ joinCode: "jc-789" }),
499
+ ɵacquireThreadLock: vi.fn().mockResolvedValue({
500
+ threadId: "thread-1",
501
+ runId: "run-1",
502
+ }),
503
+ ɵcleanupThreadLock: vi.fn().mockResolvedValue(undefined),
459
504
  };
460
505
  const runtime = createIntelligenceRuntime(agent, platform);
461
506
 
@@ -467,7 +512,40 @@ describe("handleRunAgent", () => {
467
512
 
468
513
  expect(response.status).toBe(502);
469
514
  const body = await response.json();
470
- expect(body.error).toBe("Join token not available");
515
+ expect(body.error).toBe("Run connection credentials not available");
516
+ expect(platform.ɵcleanupThreadLock).toHaveBeenCalledWith({
517
+ threadId: "thread-1",
518
+ runId: "run-1",
519
+ });
520
+ expect(runtime.runner.run).not.toHaveBeenCalled();
521
+ });
522
+
523
+ it("uses the requested lock owner when malformed credentials omit canonical IDs", async () => {
524
+ const agent = createAgentForIntelligence();
525
+ const platform = {
526
+ getOrCreateThread: vi.fn().mockResolvedValue({
527
+ thread: { id: "thread-1", name: null },
528
+ created: false,
529
+ }),
530
+ getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
531
+ ɵacquireThreadLock: vi.fn().mockResolvedValue({
532
+ joinToken: "jt-123",
533
+ }),
534
+ ɵcleanupThreadLock: vi.fn().mockResolvedValue(undefined),
535
+ };
536
+ const runtime = createIntelligenceRuntime(agent, platform);
537
+
538
+ const response = await handleRunAgent({
539
+ runtime,
540
+ request: createRunRequest(),
541
+ agentId: "my-agent",
542
+ });
543
+
544
+ expect(response.status).toBe(502);
545
+ expect(platform.ɵcleanupThreadLock).toHaveBeenCalledWith({
546
+ threadId: "thread-1",
547
+ runId: "run-1",
548
+ });
471
549
  expect(runtime.runner.run).not.toHaveBeenCalled();
472
550
  });
473
551
 
@@ -496,6 +574,206 @@ describe("handleRunAgent", () => {
496
574
  expect(body.error).toBe("Thread lock denied");
497
575
  });
498
576
 
577
+ it("cleans up the canonical lock and returns 502 when runner start fails immediately", async () => {
578
+ const agent = createAgentForIntelligence();
579
+ const platform = {
580
+ getOrCreateThread: vi.fn().mockResolvedValue({
581
+ thread: { id: "thread-1", name: null },
582
+ created: false,
583
+ }),
584
+ getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
585
+ ɵacquireThreadLock: vi.fn().mockResolvedValue({
586
+ threadId: "canonical-thread",
587
+ runId: "canonical-run",
588
+ joinToken: "jt-123",
589
+ }),
590
+ ɵcleanupThreadLock: vi.fn().mockResolvedValue(undefined),
591
+ };
592
+ const runtime = createIntelligenceRuntime(agent, platform);
593
+ runtime.runner.run = vi.fn(
594
+ () =>
595
+ new Observable<BaseEvent>((subscriber) => {
596
+ subscriber.next({
597
+ type: EventType.RUN_ERROR,
598
+ message: "join failed",
599
+ } as BaseEvent);
600
+ subscriber.complete();
601
+ }),
602
+ );
603
+
604
+ const response = await handleRunAgent({
605
+ runtime,
606
+ request: createRunRequest(),
607
+ agentId: "my-agent",
608
+ });
609
+
610
+ expect(response.status).toBe(502);
611
+ const body = await response.json();
612
+ expect(body).toEqual({
613
+ error: "Failed to start runner",
614
+ message: "join failed",
615
+ });
616
+ expect(platform.ɵcleanupThreadLock).toHaveBeenCalledWith({
617
+ threadId: "canonical-thread",
618
+ runId: "canonical-run",
619
+ });
620
+ expect(platform.ɵcleanupThreadLock).toHaveBeenCalledTimes(1);
621
+ });
622
+
623
+ it("delays the run success response until the runner startup boundary resolves", async () => {
624
+ const agent = createAgentForIntelligence();
625
+ let resolveStartup: (() => void) | undefined;
626
+ const startup = new Promise<void>((resolve) => {
627
+ resolveStartup = resolve;
628
+ });
629
+ const platform = {
630
+ getOrCreateThread: vi.fn().mockResolvedValue({
631
+ thread: { id: "thread-1", name: null },
632
+ created: false,
633
+ }),
634
+ getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
635
+ ɵacquireThreadLock: vi.fn().mockResolvedValue({
636
+ threadId: "canonical-thread",
637
+ runId: "canonical-run",
638
+ joinToken: "jt-123",
639
+ }),
640
+ ɵcleanupThreadLock: vi.fn().mockResolvedValue(undefined),
641
+ };
642
+ const runtime = createIntelligenceRuntime(agent, platform);
643
+ runtime.runner.runWithStartupBoundary = vi.fn(() => ({
644
+ events: new Observable<BaseEvent>(() => {}),
645
+ startup,
646
+ }));
647
+ let settled = false;
648
+
649
+ const responsePromise = handleRunAgent({
650
+ runtime,
651
+ request: createRunRequest(),
652
+ agentId: "my-agent",
653
+ }).then((response) => {
654
+ settled = true;
655
+ return response;
656
+ });
657
+
658
+ await Promise.resolve();
659
+
660
+ expect(settled).toBe(false);
661
+
662
+ resolveStartup?.();
663
+ const response = await responsePromise;
664
+
665
+ expect(response.status).toBe(200);
666
+ expect(runtime.runner.runWithStartupBoundary).toHaveBeenCalledWith(
667
+ expect.objectContaining({
668
+ threadId: "canonical-thread",
669
+ input: expect.objectContaining({
670
+ threadId: "canonical-thread",
671
+ runId: "canonical-run",
672
+ }),
673
+ }),
674
+ );
675
+ });
676
+
677
+ it("cleans up the lock and returns 502 when the runner startup boundary rejects", async () => {
678
+ const agent = createAgentForIntelligence();
679
+ const platform = {
680
+ getOrCreateThread: vi.fn().mockResolvedValue({
681
+ thread: { id: "thread-1", name: null },
682
+ created: false,
683
+ }),
684
+ getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
685
+ ɵacquireThreadLock: vi.fn().mockResolvedValue({
686
+ threadId: "canonical-thread",
687
+ runId: "canonical-run",
688
+ joinToken: "jt-123",
689
+ }),
690
+ ɵcleanupThreadLock: vi.fn().mockResolvedValue(undefined),
691
+ };
692
+ const runtime = createIntelligenceRuntime(agent, platform);
693
+ runtime.runner.runWithStartupBoundary = vi.fn(() => ({
694
+ events: new Observable<BaseEvent>(() => {}),
695
+ startup: Promise.reject(new Error("Failed to join channel: denied")),
696
+ }));
697
+
698
+ const response = await handleRunAgent({
699
+ runtime,
700
+ request: createRunRequest(),
701
+ agentId: "my-agent",
702
+ });
703
+
704
+ expect(response.status).toBe(502);
705
+ const body = await response.json();
706
+ expect(body).toEqual({
707
+ error: "Failed to start runner",
708
+ message: "Failed to join channel: denied",
709
+ });
710
+ expect(platform.ɵcleanupThreadLock).toHaveBeenCalledWith({
711
+ threadId: "canonical-thread",
712
+ runId: "canonical-run",
713
+ });
714
+ expect(platform.ɵcleanupThreadLock).toHaveBeenCalledTimes(1);
715
+ });
716
+
717
+ it("aborts the agent when lock renewal fails", async () => {
718
+ vi.useFakeTimers();
719
+ const runningAgent = {
720
+ clone: vi.fn(),
721
+ setMessages: vi.fn(),
722
+ setState: vi.fn(),
723
+ abortRun: vi.fn(),
724
+ threadId: undefined,
725
+ headers: {},
726
+ runAgent: vi.fn().mockResolvedValue(undefined),
727
+ } as unknown as AbstractAgent;
728
+ const baseAgent = {
729
+ clone: vi.fn(() => runningAgent),
730
+ setMessages: vi.fn(),
731
+ setState: vi.fn(),
732
+ abortRun: vi.fn(),
733
+ threadId: undefined,
734
+ headers: {},
735
+ runAgent: vi.fn().mockResolvedValue(undefined),
736
+ } as unknown as AbstractAgent;
737
+ const platform = {
738
+ getOrCreateThread: vi.fn().mockResolvedValue({
739
+ thread: { id: "thread-1", name: null },
740
+ created: false,
741
+ }),
742
+ getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
743
+ ɵacquireThreadLock: vi.fn().mockResolvedValue({
744
+ threadId: "canonical-thread",
745
+ runId: "canonical-run",
746
+ joinToken: "jt-123",
747
+ }),
748
+ ɵrenewThreadLock: vi.fn().mockRejectedValue(new Error("lost lock")),
749
+ };
750
+ const runtime = createIntelligenceRuntime(baseAgent, platform, {
751
+ lockHeartbeatIntervalSeconds: 1,
752
+ lockTtlSeconds: 5,
753
+ });
754
+ runtime.runner.run = vi.fn(() => new Observable<BaseEvent>(() => {}));
755
+
756
+ try {
757
+ const response = await handleRunAgent({
758
+ runtime,
759
+ request: createRunRequest(),
760
+ agentId: "my-agent",
761
+ });
762
+ expect(response.status).toBe(200);
763
+
764
+ await vi.advanceTimersByTimeAsync(1_000);
765
+
766
+ expect(platform.ɵrenewThreadLock).toHaveBeenCalledWith({
767
+ threadId: "canonical-thread",
768
+ runId: "canonical-run",
769
+ ttlSeconds: 5,
770
+ });
771
+ expect(runningAgent.abortRun).toHaveBeenCalledTimes(1);
772
+ } finally {
773
+ vi.useRealTimers();
774
+ }
775
+ });
776
+
499
777
  it("passes only unseen input messages to the runner for durable persistence", async () => {
500
778
  const agent = createAgentForIntelligence();
501
779
  const platform = {
@@ -512,9 +790,11 @@ describe("handleRunAgent", () => {
512
790
  },
513
791
  ],
514
792
  }),
515
- ɵacquireThreadLock: vi
516
- .fn()
517
- .mockResolvedValue({ joinToken: "jt-123", joinCode: "jc-123" }),
793
+ ɵacquireThreadLock: vi.fn().mockResolvedValue({
794
+ threadId: "thread-1",
795
+ runId: "run-1",
796
+ joinToken: "jt-123",
797
+ }),
518
798
  };
519
799
  const runtime = createIntelligenceRuntime(agent, platform);
520
800
  const response = await handleRunAgent({
@@ -572,9 +852,12 @@ describe("handleRunAgent", () => {
572
852
  getThreadMessages: vi
573
853
  .fn()
574
854
  .mockRejectedValue(new Error("history unavailable")),
575
- ɵacquireThreadLock: vi
576
- .fn()
577
- .mockResolvedValue({ joinToken: "jt-123", joinCode: "jc-123" }),
855
+ ɵacquireThreadLock: vi.fn().mockResolvedValue({
856
+ threadId: "thread-1",
857
+ runId: "run-1",
858
+ joinToken: "jt-123",
859
+ }),
860
+ ɵcleanupThreadLock: vi.fn().mockResolvedValue(undefined),
578
861
  };
579
862
  const runtime = createIntelligenceRuntime(agent, platform);
580
863
 
@@ -587,6 +870,10 @@ describe("handleRunAgent", () => {
587
870
  expect(response.status).toBe(502);
588
871
  const body = await response.json();
589
872
  expect(body.error).toBe("Thread history lookup failed");
873
+ expect(platform.ɵcleanupThreadLock).toHaveBeenCalledWith({
874
+ threadId: "thread-1",
875
+ runId: "run-1",
876
+ });
590
877
  expect(runtime.runner.run).not.toHaveBeenCalled();
591
878
  });
592
879
 
@@ -599,8 +886,9 @@ describe("handleRunAgent", () => {
599
886
  }),
600
887
  getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
601
888
  ɵacquireThreadLock: vi.fn().mockResolvedValue({
889
+ threadId: "thread-1",
890
+ runId: "run-1",
602
891
  joinToken: "jt-created",
603
- joinCode: "jc-created",
604
892
  }),
605
893
  };
606
894
  const runtime = createIntelligenceRuntime(agent, platform);
@@ -621,6 +909,8 @@ describe("handleRunAgent", () => {
621
909
  threadId: "thread-1",
622
910
  runId: "run-1",
623
911
  userId: "user-1",
912
+ agentId: "my-agent",
913
+ ttlSeconds: 20,
624
914
  });
625
915
  });
626
916
 
@@ -670,8 +960,9 @@ describe("handleRunAgent", () => {
670
960
  }),
671
961
  getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
672
962
  ɵacquireThreadLock: vi.fn().mockResolvedValue({
963
+ threadId: "thread-1",
964
+ runId: "run-1",
673
965
  joinToken: "jt-created",
674
- joinCode: "jc-created",
675
966
  }),
676
967
  };
677
968
  const runtime = createIntelligenceRuntime(baseAgent, platform, {
@@ -725,8 +1016,9 @@ describe("handleRunAgent", () => {
725
1016
  updateThread: vi.fn(),
726
1017
  getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
727
1018
  ɵacquireThreadLock: vi.fn().mockResolvedValue({
1019
+ threadId: "thread-1",
1020
+ runId: "run-1",
728
1021
  joinToken: "jt-created",
729
- joinCode: "jc-created",
730
1022
  }),
731
1023
  };
732
1024
  const runtime = createIntelligenceRuntime(agent, platform, {
@@ -754,8 +1046,9 @@ describe("handleRunAgent", () => {
754
1046
  updateThread: vi.fn(),
755
1047
  getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
756
1048
  ɵacquireThreadLock: vi.fn().mockResolvedValue({
1049
+ threadId: "thread-1",
1050
+ runId: "run-1",
757
1051
  joinToken: "jt-created",
758
- joinCode: "jc-created",
759
1052
  }),
760
1053
  };
761
1054
  const runtime = createIntelligenceRuntime(agent, platform, {
@@ -810,8 +1103,9 @@ describe("handleRunAgent", () => {
810
1103
  updateThread: vi.fn(),
811
1104
  getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
812
1105
  ɵacquireThreadLock: vi.fn().mockResolvedValue({
1106
+ threadId: "thread-1",
1107
+ runId: "run-1",
813
1108
  joinToken: "jt-created",
814
- joinCode: "jc-created",
815
1109
  }),
816
1110
  };
817
1111
  const runtime = createIntelligenceRuntime(baseAgent, platform, {
@@ -870,7 +1164,29 @@ describe("handleRunAgent", () => {
870
1164
  ɵacquireThreadLock: vi.fn(),
871
1165
  };
872
1166
  const runtime = createIntelligenceRuntime(agent, platform, {
873
- identifyUser: vi.fn().mockResolvedValue({ id: "" }),
1167
+ identifyUser: vi.fn().mockResolvedValue({ id: "", name: "User" }),
1168
+ });
1169
+
1170
+ const response = await handleRunAgent({
1171
+ runtime,
1172
+ request: createRunRequest(),
1173
+ agentId: "my-agent",
1174
+ });
1175
+
1176
+ expect(response.status).toBe(400);
1177
+ expect(platform.getOrCreateThread).not.toHaveBeenCalled();
1178
+ expect(platform.ɵacquireThreadLock).not.toHaveBeenCalled();
1179
+ });
1180
+
1181
+ it("returns 400 when identifyUser returns an invalid name", async () => {
1182
+ const agent = createAgentForIntelligence();
1183
+ const platform = {
1184
+ getOrCreateThread: vi.fn(),
1185
+ getThreadMessages: vi.fn(),
1186
+ ɵacquireThreadLock: vi.fn(),
1187
+ };
1188
+ const runtime = createIntelligenceRuntime(agent, platform, {
1189
+ identifyUser: vi.fn().mockResolvedValue({ id: "user-1", name: "" }),
874
1190
  });
875
1191
 
876
1192
  const response = await handleRunAgent({
@@ -911,4 +1227,66 @@ describe("handleRunAgent", () => {
911
1227
  }
912
1228
  });
913
1229
  });
1230
+
1231
+ describe("telemetry", () => {
1232
+ it("captures oss.runtime.copilot_request_created on every invocation", async () => {
1233
+ // Dynamic import so we spy on the module singleton the handler uses.
1234
+ const { telemetry } = await import("../telemetry");
1235
+ const captureSpy = vi
1236
+ .spyOn(telemetry, "capture")
1237
+ .mockResolvedValue(undefined);
1238
+
1239
+ try {
1240
+ const runtime = createMockRuntime({});
1241
+ await handleRunAgent({
1242
+ runtime,
1243
+ request: createMockRequest(),
1244
+ agentId: "nonexistent-agent",
1245
+ });
1246
+
1247
+ expect(captureSpy).toHaveBeenCalledWith(
1248
+ "oss.runtime.copilot_request_created",
1249
+ expect.objectContaining({
1250
+ requestType: "run",
1251
+ "cloud.api_key_provided": false,
1252
+ }),
1253
+ );
1254
+ } finally {
1255
+ captureSpy.mockRestore();
1256
+ }
1257
+ });
1258
+
1259
+ it("includes cloud.public_api_key when x-copilotcloud-public-api-key header is set", async () => {
1260
+ const { telemetry } = await import("../telemetry");
1261
+ const captureSpy = vi
1262
+ .spyOn(telemetry, "capture")
1263
+ .mockResolvedValue(undefined);
1264
+
1265
+ try {
1266
+ const runtime = createMockRuntime({});
1267
+ const request = new Request("https://example.com/agent/test/run", {
1268
+ method: "POST",
1269
+ headers: {
1270
+ "x-copilotcloud-public-api-key": "ck_pub_run_test",
1271
+ },
1272
+ });
1273
+
1274
+ await handleRunAgent({
1275
+ runtime,
1276
+ request,
1277
+ agentId: "nonexistent-agent",
1278
+ });
1279
+
1280
+ expect(captureSpy).toHaveBeenCalledWith(
1281
+ "oss.runtime.copilot_request_created",
1282
+ expect.objectContaining({
1283
+ "cloud.api_key_provided": true,
1284
+ "cloud.public_api_key": "ck_pub_run_test",
1285
+ }),
1286
+ );
1287
+ } finally {
1288
+ captureSpy.mockRestore();
1289
+ }
1290
+ });
1291
+ });
914
1292
  });