@copilotkit/runtime 1.56.5 → 1.57.0-canary.1778078321

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 (84) hide show
  1. package/dist/agent/index.cjs +20 -2
  2. package/dist/agent/index.cjs.map +1 -1
  3. package/dist/agent/index.d.cts +9 -16
  4. package/dist/agent/index.d.cts.map +1 -1
  5. package/dist/agent/index.d.mts +9 -16
  6. package/dist/agent/index.d.mts.map +1 -1
  7. package/dist/agent/index.mjs +21 -3
  8. package/dist/agent/index.mjs.map +1 -1
  9. package/dist/package.cjs +1 -1
  10. package/dist/package.mjs +1 -1
  11. package/dist/v2/index.d.cts +3 -3
  12. package/dist/v2/index.d.mts +3 -3
  13. package/dist/v2/runtime/core/fetch-handler.cjs +16 -0
  14. package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
  15. package/dist/v2/runtime/core/fetch-handler.d.cts.map +1 -1
  16. package/dist/v2/runtime/core/fetch-handler.d.mts.map +1 -1
  17. package/dist/v2/runtime/core/fetch-handler.mjs +17 -1
  18. package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
  19. package/dist/v2/runtime/core/fetch-router.cjs +18 -1
  20. package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
  21. package/dist/v2/runtime/core/fetch-router.mjs +18 -1
  22. package/dist/v2/runtime/core/fetch-router.mjs.map +1 -1
  23. package/dist/v2/runtime/core/hooks.cjs.map +1 -1
  24. package/dist/v2/runtime/core/hooks.d.cts +8 -0
  25. package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
  26. package/dist/v2/runtime/core/hooks.d.mts +8 -0
  27. package/dist/v2/runtime/core/hooks.d.mts.map +1 -1
  28. package/dist/v2/runtime/core/hooks.mjs.map +1 -1
  29. package/dist/v2/runtime/handlers/handle-run.cjs +1 -0
  30. package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
  31. package/dist/v2/runtime/handlers/handle-run.mjs +1 -0
  32. package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
  33. package/dist/v2/runtime/handlers/intelligence/run.cjs +10 -1
  34. package/dist/v2/runtime/handlers/intelligence/run.cjs.map +1 -1
  35. package/dist/v2/runtime/handlers/intelligence/run.mjs +10 -1
  36. package/dist/v2/runtime/handlers/intelligence/run.mjs.map +1 -1
  37. package/dist/v2/runtime/handlers/intelligence/threads.cjs +124 -12
  38. package/dist/v2/runtime/handlers/intelligence/threads.cjs.map +1 -1
  39. package/dist/v2/runtime/handlers/intelligence/threads.mjs +122 -13
  40. package/dist/v2/runtime/handlers/intelligence/threads.mjs.map +1 -1
  41. package/dist/v2/runtime/handlers/shared/agent-utils.cjs.map +1 -1
  42. package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
  43. package/dist/v2/runtime/index.d.cts +3 -2
  44. package/dist/v2/runtime/index.d.cts.map +1 -1
  45. package/dist/v2/runtime/index.d.mts +3 -2
  46. package/dist/v2/runtime/index.d.mts.map +1 -1
  47. package/dist/v2/runtime/intelligence-platform/client.cjs +40 -0
  48. package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
  49. package/dist/v2/runtime/intelligence-platform/client.d.cts +83 -0
  50. package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
  51. package/dist/v2/runtime/intelligence-platform/client.d.mts +83 -0
  52. package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
  53. package/dist/v2/runtime/intelligence-platform/client.mjs +40 -0
  54. package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
  55. package/dist/v2/runtime/runner/in-memory.cjs +94 -22
  56. package/dist/v2/runtime/runner/in-memory.cjs.map +1 -1
  57. package/dist/v2/runtime/runner/in-memory.d.cts +65 -2
  58. package/dist/v2/runtime/runner/in-memory.d.cts.map +1 -1
  59. package/dist/v2/runtime/runner/in-memory.d.mts +65 -2
  60. package/dist/v2/runtime/runner/in-memory.d.mts.map +1 -1
  61. package/dist/v2/runtime/runner/in-memory.mjs +94 -22
  62. package/dist/v2/runtime/runner/in-memory.mjs.map +1 -1
  63. package/dist/v2/runtime/runner/index.d.cts +1 -1
  64. package/dist/v2/runtime/runner/index.d.mts +1 -1
  65. package/package.json +2 -2
  66. package/src/agent/__tests__/mcp-clients.test.ts +11 -25
  67. package/src/agent/index.ts +67 -32
  68. package/src/v2/runtime/__tests__/fetch-handler-validation.test.ts +68 -0
  69. package/src/v2/runtime/__tests__/fetch-router.test.ts +46 -0
  70. package/src/v2/runtime/__tests__/handle-run.test.ts +97 -1
  71. package/src/v2/runtime/__tests__/handle-threads.test.ts +493 -13
  72. package/src/v2/runtime/core/fetch-handler.ts +19 -0
  73. package/src/v2/runtime/core/fetch-router.ts +33 -1
  74. package/src/v2/runtime/core/hooks.ts +3 -0
  75. package/src/v2/runtime/handlers/handle-run.ts +4 -0
  76. package/src/v2/runtime/handlers/handle-threads.ts +3 -0
  77. package/src/v2/runtime/handlers/intelligence/run.ts +27 -5
  78. package/src/v2/runtime/handlers/intelligence/threads.ts +200 -41
  79. package/src/v2/runtime/handlers/shared/agent-utils.ts +4 -6
  80. package/src/v2/runtime/index.ts +5 -0
  81. package/src/v2/runtime/intelligence-platform/__tests__/intelligence-mcp-helper.test.ts +239 -0
  82. package/src/v2/runtime/intelligence-platform/client.ts +113 -0
  83. package/src/v2/runtime/runner/__tests__/in-memory-runner.test.ts +417 -3
  84. package/src/v2/runtime/runner/in-memory.ts +137 -51
@@ -1,10 +1,17 @@
1
1
  import { Observable } from "rxjs";
2
2
  import { describe, it, expect, vi } from "vitest";
3
- import { AbstractAgent, BaseEvent, EventType, HttpAgent } from "@ag-ui/client";
3
+ import {
4
+ AbstractAgent,
5
+ BaseEvent,
6
+ EventType,
7
+ HttpAgent,
8
+ RunAgentInput,
9
+ } from "@ag-ui/client";
4
10
  import { A2UIMiddleware } from "@ag-ui/a2ui-middleware";
5
11
  import { handleRunAgent } from "../handlers/handle-run";
6
12
  import { CopilotRuntime } from "../core/runtime";
7
13
  import { IntelligenceAgentRunner } from "../runner/intelligence";
14
+ import { InMemoryAgentRunner } from "../runner/in-memory";
8
15
 
9
16
  describe("handleRunAgent", () => {
10
17
  const createMockRuntime = (
@@ -1289,4 +1296,93 @@ describe("handleRunAgent", () => {
1289
1296
  }
1290
1297
  });
1291
1298
  });
1299
+
1300
+ describe("agentId tagging on cloned agents", () => {
1301
+ /**
1302
+ * Pins handle-run.ts:40 — `agent.agentId = agentId` is set on the clone
1303
+ * BEFORE the agent reaches the runner. Without it, InMemoryAgentRunner
1304
+ * falls back to "default" when stamping historic runs, and listThreads
1305
+ * returns rows with the wrong agentId. This breaks the agentId filter
1306
+ * in `GET /threads?agentId=...` for the local-dev fallback.
1307
+ *
1308
+ * This test runs the full flow through InMemoryAgentRunner with an
1309
+ * AbstractAgent whose own `agentId` field is undefined (matches the
1310
+ * shape after `clone()` returns a fresh instance), and asserts the
1311
+ * runner records the registry key, NOT "default".
1312
+ */
1313
+ class TaggingTestAgent extends AbstractAgent {
1314
+ async runAgent(
1315
+ _input: RunAgentInput,
1316
+ options: { onEvent: (event: { event: BaseEvent }) => void },
1317
+ ): Promise<void> {
1318
+ // Emit a single TEXT_MESSAGE_END event so the run produces at least
1319
+ // one event and gets persisted to historicRuns. RUN_STARTED /
1320
+ // RUN_FINISHED are appended by the runner itself.
1321
+ options.onEvent({
1322
+ event: {
1323
+ type: EventType.TEXT_MESSAGE_END,
1324
+ messageId: "msg-1",
1325
+ } as BaseEvent,
1326
+ });
1327
+ }
1328
+
1329
+ clone(): AbstractAgent {
1330
+ // The fresh clone has NO agentId — the only way the runner can know
1331
+ // the registry key is if handle-run.ts:40 stamps it before the run.
1332
+ return new TaggingTestAgent();
1333
+ }
1334
+ }
1335
+
1336
+ const createRunRequestForAgent = (agentId: string, threadId: string) =>
1337
+ new Request(`https://example.com/agent/${agentId}/run`, {
1338
+ method: "POST",
1339
+ headers: { "Content-Type": "application/json" },
1340
+ body: JSON.stringify({
1341
+ threadId,
1342
+ runId: `run-${threadId}`,
1343
+ state: {},
1344
+ messages: [],
1345
+ tools: [],
1346
+ context: [],
1347
+ forwardedProps: {},
1348
+ }),
1349
+ });
1350
+
1351
+ it("propagates the registry agentId onto historic runs (NOT 'default')", async () => {
1352
+ const runner = new InMemoryAgentRunner();
1353
+ const agent = new TaggingTestAgent();
1354
+ const runtime = new CopilotRuntime({
1355
+ agents: { tagged: agent },
1356
+ runner,
1357
+ });
1358
+
1359
+ // Use a unique threadId so this test does not collide with other
1360
+ // tests that share the InMemoryAgentRunner GLOBAL_STORE.
1361
+ const threadId = `thread-tagged-${Date.now()}-${Math.random()}`;
1362
+
1363
+ const response = await handleRunAgent({
1364
+ runtime,
1365
+ request: createRunRequestForAgent("tagged", threadId),
1366
+ agentId: "tagged",
1367
+ });
1368
+ expect(response.status).toBe(200);
1369
+
1370
+ // Drain the SSE stream so the underlying observable run completes —
1371
+ // historicRuns is only populated AFTER the run finalizes.
1372
+ const reader = response.body!.getReader();
1373
+ while (true) {
1374
+ const { done } = await reader.read();
1375
+ if (done) break;
1376
+ }
1377
+
1378
+ const threads = runner.listThreads();
1379
+ const thisThread = threads.find((t) => t.id === threadId);
1380
+ expect(thisThread).toBeDefined();
1381
+ expect(thisThread!.agentId).toBe("tagged");
1382
+ // Negative assertion locks the regression: a future change that drops
1383
+ // the `agent.agentId = agentId` line in handle-run will surface as
1384
+ // "default" here, not as a missing thread.
1385
+ expect(thisThread!.agentId).not.toBe("default");
1386
+ });
1387
+ });
1292
1388
  });
@@ -2,12 +2,17 @@ import { describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import {
4
4
  handleArchiveThread,
5
+ handleClearThreads,
5
6
  handleDeleteThread,
7
+ handleGetThreadEvents,
8
+ handleGetThreadMessages,
9
+ handleGetThreadState,
6
10
  handleListThreads,
7
11
  handleSubscribeToThreads,
8
12
  handleUpdateThread,
9
13
  } from "../handlers/handle-threads";
10
14
  import { CopilotRuntime } from "../core/runtime";
15
+ import { InMemoryAgentRunner } from "../runner/in-memory";
11
16
 
12
17
  describe("thread handlers", () => {
13
18
  const createIdentifyUser = () =>
@@ -47,7 +52,7 @@ describe("thread handlers", () => {
47
52
  body: JSON.stringify(body),
48
53
  });
49
54
 
50
- it("returns 422 when intelligence is not configured for listThreads", async () => {
55
+ it("returns empty thread list when intelligence is not configured for listThreads", async () => {
51
56
  const runtime = new CopilotRuntime({ agents: {} });
52
57
 
53
58
  const response = await handleListThreads({
@@ -55,10 +60,10 @@ describe("thread handlers", () => {
55
60
  request: new Request("https://example.com/threads?agentId=agent-1"),
56
61
  });
57
62
 
58
- expect(response.status).toBe(422);
63
+ expect(response.status).toBe(200);
59
64
  await expect(response.json()).resolves.toEqual({
60
- error:
61
- "Missing CopilotKitIntelligence configuration. Thread operations require a CopilotKitIntelligence instance to be provided in CopilotRuntime options.",
65
+ threads: [],
66
+ nextCursor: null,
62
67
  });
63
68
  });
64
69
 
@@ -143,6 +148,16 @@ describe("thread handlers", () => {
143
148
 
144
149
  expect(response.status).toBe(500);
145
150
  expect(intelligence.ɵsubscribeToThreads).not.toHaveBeenCalled();
151
+ // The handler must log the auth failure so an operator looking at
152
+ // the runtime logs sees why the request 500ed. Asserting the
153
+ // operation name ("identifying intelligence user") catches a
154
+ // regression that swaps the diagnostic for a generic placeholder.
155
+ // The throw originates inside `resolveIntelligenceUser`, which
156
+ // logs and returns 500 before `subscribeToThreads` is reached.
157
+ expect(errorSpy).toHaveBeenCalledWith(
158
+ expect.stringContaining("Error identifying intelligence user"),
159
+ expect.any(Error),
160
+ );
146
161
  } finally {
147
162
  errorSpy.mockRestore();
148
163
  }
@@ -357,37 +372,247 @@ describe("thread handlers", () => {
357
372
 
358
373
  it("returns 422 when intelligence is not configured for thread mutations", async () => {
359
374
  const runtime = new CopilotRuntime({ agents: {} });
360
- const mutationRequest = new Request(
361
- "https://example.com/threads/thread-1",
362
- {
363
- method: "POST",
375
+ const buildRequest = (method: "PATCH" | "POST" | "DELETE") =>
376
+ new Request("https://example.com/threads/thread-1", {
377
+ method,
364
378
  headers: { "Content-Type": "application/json" },
365
379
  body: JSON.stringify({ userId: "user-1", agentId: "agent-1" }),
366
- },
367
- );
380
+ });
368
381
 
369
382
  const updateResponse = await handleUpdateThread({
370
383
  runtime,
371
- request: mutationRequest.clone(),
384
+ request: buildRequest("PATCH"),
372
385
  threadId: "thread-1",
373
386
  });
374
387
  expect(updateResponse.status).toBe(422);
375
388
 
376
389
  const archiveResponse = await handleArchiveThread({
377
390
  runtime,
378
- request: mutationRequest.clone(),
391
+ request: buildRequest("POST"),
379
392
  threadId: "thread-1",
380
393
  });
381
394
  expect(archiveResponse.status).toBe(422);
382
395
 
396
+ // Use the real DELETE method here — Request.clone() preserves the
397
+ // method of the original, so re-using a POST clone for the delete
398
+ // path silently exercises the wrong verb.
383
399
  const deleteResponse = await handleDeleteThread({
384
400
  runtime,
385
- request: mutationRequest.clone(),
401
+ request: buildRequest("DELETE"),
386
402
  threadId: "thread-1",
387
403
  });
388
404
  expect(deleteResponse.status).toBe(422);
389
405
  });
390
406
 
407
+ describe("handleClearThreads", () => {
408
+ // handleClearThreads is intentionally synchronous — it has no I/O on
409
+ // either branch (in-memory map mutation or platform no-op), so it
410
+ // returns a plain Response rather than a Promise. The other handlers
411
+ // in this suite are awaited because they either parse JSON or hit
412
+ // the Intelligence platform; this one does neither.
413
+ it("clears in-memory threads and returns 204 for InMemoryAgentRunner", () => {
414
+ const runner = new InMemoryAgentRunner();
415
+ const clearThreadsSpy = vi.spyOn(runner, "clearThreads");
416
+ const runtime = new CopilotRuntime({ agents: {}, runner });
417
+
418
+ const response = handleClearThreads({
419
+ runtime,
420
+ request: new Request("https://example.com/threads"),
421
+ });
422
+
423
+ // Lock the synchronous contract: a regression that starts awaiting
424
+ // I/O inside this handler must update the call sites that rely on
425
+ // the synchronous return shape. Asserting `not.toBeInstanceOf(Promise)`
426
+ // catches that drift at runtime.
427
+ expect(response).not.toBeInstanceOf(Promise);
428
+ expect(response.status).toBe(204);
429
+ expect(clearThreadsSpy).toHaveBeenCalledTimes(1);
430
+ });
431
+
432
+ it("returns 204 without touching state when intelligence runtime is configured", () => {
433
+ const intelligence = { listThreads: vi.fn() };
434
+ const runtime = createIntelligenceRuntime({ intelligence });
435
+
436
+ const response = handleClearThreads({
437
+ runtime,
438
+ request: new Request("https://example.com/threads"),
439
+ });
440
+
441
+ // Same synchronous-contract guard as the in-memory branch above.
442
+ expect(response).not.toBeInstanceOf(Promise);
443
+ expect(response.status).toBe(204);
444
+ expect(intelligence.listThreads).not.toHaveBeenCalled();
445
+ });
446
+ });
447
+
448
+ describe("handleGetThreadMessages", () => {
449
+ it("returns messages from the in-memory runner for a known thread", async () => {
450
+ const runner = new InMemoryAgentRunner();
451
+ vi.spyOn(runner, "getThreadMessages").mockReturnValue([
452
+ { id: "m1", role: "user", content: "hello" } as never,
453
+ { id: "m2", role: "assistant", content: "hi there" } as never,
454
+ ]);
455
+ const runtime = new CopilotRuntime({ agents: {}, runner });
456
+
457
+ const response = await handleGetThreadMessages({
458
+ runtime,
459
+ request: new Request("https://example.com/threads/thread-1/messages"),
460
+ threadId: "thread-1",
461
+ });
462
+
463
+ expect(response.status).toBe(200);
464
+ const body = await response.json();
465
+ expect(body.messages).toHaveLength(2);
466
+ expect(body.messages[0]).toMatchObject({
467
+ id: "m1",
468
+ role: "user",
469
+ content: "hello",
470
+ });
471
+ expect(body.messages[1]).toMatchObject({
472
+ id: "m2",
473
+ role: "assistant",
474
+ content: "hi there",
475
+ });
476
+ });
477
+
478
+ it("returns empty messages for an unknown threadId", async () => {
479
+ const runtime = new CopilotRuntime({ agents: {} });
480
+
481
+ const response = await handleGetThreadMessages({
482
+ runtime,
483
+ request: new Request(
484
+ "https://example.com/threads/nonexistent/messages",
485
+ ),
486
+ threadId: "nonexistent",
487
+ });
488
+
489
+ expect(response.status).toBe(200);
490
+ const body = await response.json();
491
+ expect(body.messages).toEqual([]);
492
+ });
493
+
494
+ it("delegates to intelligence.getThreadMessages when intelligence is configured", async () => {
495
+ const intelligence = {
496
+ getThreadMessages: vi
497
+ .fn()
498
+ .mockResolvedValue({ messages: [{ id: "m1" }] }),
499
+ };
500
+ const identifyUser = createIdentifyUser();
501
+ const runtime = createIntelligenceRuntime({ intelligence, identifyUser });
502
+
503
+ const response = await handleGetThreadMessages({
504
+ runtime,
505
+ request: new Request("https://example.com/threads/thread-1/messages"),
506
+ threadId: "thread-1",
507
+ });
508
+
509
+ expect(response.status).toBe(200);
510
+ // The handler must propagate the platform's response body verbatim —
511
+ // assert it explicitly so a regression that swaps in a stubbed body
512
+ // (e.g. `{ messages: [] }`) is caught.
513
+ const body = await response.json();
514
+ expect(body.messages).toEqual([{ id: "m1" }]);
515
+ expect(intelligence.getThreadMessages).toHaveBeenCalledWith({
516
+ threadId: "thread-1",
517
+ });
518
+ expect(identifyUser).toHaveBeenCalledTimes(1);
519
+ expect(identifyUser).toHaveBeenCalledWith(
520
+ expect.objectContaining({ url: expect.stringContaining("thread-1") }),
521
+ );
522
+ });
523
+
524
+ it("returns 500 when identifyUser throws for getThreadMessages", async () => {
525
+ const intelligence = {
526
+ getThreadMessages: vi.fn(),
527
+ };
528
+ const runtime = createIntelligenceRuntime({
529
+ intelligence,
530
+ identifyUser: vi.fn().mockRejectedValue(new Error("auth failed")),
531
+ });
532
+ // resolveIntelligenceUser logs via console.error on rejection — silence
533
+ // it for the duration of this test so the suite output stays clean,
534
+ // matching the pattern used in the subscribe-throw test above.
535
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
536
+
537
+ try {
538
+ const response = await handleGetThreadMessages({
539
+ runtime,
540
+ request: new Request("https://example.com/threads/thread-1/messages"),
541
+ threadId: "thread-1",
542
+ });
543
+
544
+ expect(response.status).toBe(500);
545
+ expect(intelligence.getThreadMessages).not.toHaveBeenCalled();
546
+ } finally {
547
+ errorSpy.mockRestore();
548
+ }
549
+ });
550
+
551
+ it("returns 422 when neither in-memory nor intelligence is configured", async () => {
552
+ // A CopilotRuntime with no runner defaults to InMemoryAgentRunner,
553
+ // so simulate a non-InMemory, non-intelligence setup via a custom runner stub.
554
+ // Use the intelligence path but omit intelligence config.
555
+ const runtime = createIntelligenceRuntime({ intelligence: undefined });
556
+
557
+ const response = await handleGetThreadMessages({
558
+ runtime,
559
+ request: new Request("https://example.com/threads/thread-1/messages"),
560
+ threadId: "thread-1",
561
+ });
562
+
563
+ expect(response.status).toBe(422);
564
+ });
565
+
566
+ it("maps tool-call and tool-result messages from the in-memory runner without as-never casts", async () => {
567
+ const runner = new InMemoryAgentRunner();
568
+ const messages = [
569
+ {
570
+ id: "m1",
571
+ role: "assistant" as const,
572
+ toolCalls: [
573
+ {
574
+ id: "tc-1",
575
+ type: "function" as const,
576
+ function: { name: "get_weather", arguments: '{"city":"Paris"}' },
577
+ },
578
+ ],
579
+ },
580
+ {
581
+ id: "m2",
582
+ role: "tool" as const,
583
+ toolCallId: "tc-1",
584
+ content: '{"temp":18}',
585
+ },
586
+ ];
587
+ vi.spyOn(runner, "getThreadMessages").mockReturnValue(messages);
588
+ const runtime = new CopilotRuntime({ agents: {}, runner });
589
+
590
+ const response = await handleGetThreadMessages({
591
+ runtime,
592
+ request: new Request("https://example.com/threads/thread-1/messages"),
593
+ threadId: "thread-1",
594
+ });
595
+
596
+ expect(response.status).toBe(200);
597
+ const body = await response.json();
598
+ expect(body.messages).toHaveLength(2);
599
+
600
+ const assistantMsg = body.messages[0];
601
+ expect(assistantMsg.role).toBe("assistant");
602
+ expect(assistantMsg.toolCalls).toHaveLength(1);
603
+ expect(assistantMsg.toolCalls[0]).toMatchObject({
604
+ id: "tc-1",
605
+ name: "get_weather",
606
+ args: '{"city":"Paris"}',
607
+ });
608
+
609
+ const toolResultMsg = body.messages[1];
610
+ expect(toolResultMsg.role).toBe("tool");
611
+ expect(toolResultMsg.toolCallId).toBe("tc-1");
612
+ expect(toolResultMsg.content).toBe('{"temp":18}');
613
+ });
614
+ });
615
+
391
616
  it("returns 422 when intelligence is not configured for thread subscription", async () => {
392
617
  const runtime = new CopilotRuntime({ agents: {} });
393
618
 
@@ -447,4 +672,259 @@ describe("thread handlers", () => {
447
672
  agentId: "agent-1",
448
673
  });
449
674
  });
675
+
676
+ describe("handleGetThreadEvents", () => {
677
+ it("returns events from the in-memory runner for a known thread", async () => {
678
+ const runner = new InMemoryAgentRunner();
679
+ const fakeEvents = [
680
+ { type: "RUN_STARTED", runId: "r1", threadId: "thread-1" },
681
+ {
682
+ type: "TEXT_MESSAGE_START",
683
+ messageId: "m1",
684
+ role: "assistant",
685
+ },
686
+ ];
687
+ vi.spyOn(runner, "getThreadEvents").mockReturnValue(fakeEvents as never);
688
+ const runtime = new CopilotRuntime({ agents: {}, runner });
689
+
690
+ const response = await handleGetThreadEvents({
691
+ runtime,
692
+ request: new Request("https://example.com/threads/thread-1/events"),
693
+ threadId: "thread-1",
694
+ });
695
+
696
+ expect(response.status).toBe(200);
697
+ const body = await response.json();
698
+ expect(body.events).toHaveLength(2);
699
+ expect(body.events[0]).toMatchObject({ type: "RUN_STARTED" });
700
+ });
701
+
702
+ it("returns empty events for an unknown threadId via the in-memory runner", async () => {
703
+ const runtime = new CopilotRuntime({ agents: {} });
704
+
705
+ const response = await handleGetThreadEvents({
706
+ runtime,
707
+ request: new Request("https://example.com/threads/nonexistent/events"),
708
+ threadId: "nonexistent",
709
+ });
710
+
711
+ expect(response.status).toBe(200);
712
+ const body = await response.json();
713
+ expect(body.events).toEqual([]);
714
+ });
715
+
716
+ it("delegates to intelligence.getThreadEvents when intelligence is configured", async () => {
717
+ // Mirrors the platform's `_inspect/threads/:id/events` response shape
718
+ // (Intelligence PR #144). The handler strips the platform-internal
719
+ // `decodeErrorRowIds` and `truncated` fields before returning, so the
720
+ // wire shape stays `{ events }` to match the in-memory branch.
721
+ const platformEvents = [
722
+ { type: "RUN_STARTED", threadId: "thread-1", runId: "run-a" },
723
+ { type: "TEXT_MESSAGE_CONTENT", messageId: "m1", delta: "hello" },
724
+ ];
725
+ const intelligence = {
726
+ getThreadEvents: vi.fn().mockResolvedValue({
727
+ events: platformEvents,
728
+ decodeErrorRowIds: [],
729
+ truncated: false,
730
+ }),
731
+ };
732
+ const identifyUser = createIdentifyUser();
733
+ const runtime = createIntelligenceRuntime({ intelligence, identifyUser });
734
+
735
+ const response = await handleGetThreadEvents({
736
+ runtime,
737
+ request: new Request("https://example.com/threads/thread-1/events"),
738
+ threadId: "thread-1",
739
+ });
740
+
741
+ expect(response.status).toBe(200);
742
+ expect(intelligence.getThreadEvents).toHaveBeenCalledWith({
743
+ threadId: "thread-1",
744
+ });
745
+ expect(identifyUser).toHaveBeenCalledTimes(1);
746
+ const body = await response.json();
747
+ expect(body).toEqual({ events: platformEvents });
748
+ });
749
+
750
+ it("returns 500 when intelligence.getThreadEvents throws", async () => {
751
+ const intelligence = {
752
+ getThreadEvents: vi
753
+ .fn()
754
+ .mockRejectedValue(new Error("platform unavailable")),
755
+ };
756
+ const runtime = createIntelligenceRuntime({ intelligence });
757
+
758
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
759
+ try {
760
+ const response = await handleGetThreadEvents({
761
+ runtime,
762
+ request: new Request("https://example.com/threads/thread-1/events"),
763
+ threadId: "thread-1",
764
+ });
765
+
766
+ expect(response.status).toBe(500);
767
+ expect(intelligence.getThreadEvents).toHaveBeenCalledTimes(1);
768
+ } finally {
769
+ errorSpy.mockRestore();
770
+ }
771
+ });
772
+
773
+ it("returns 500 when the runner throws", async () => {
774
+ const runner = new InMemoryAgentRunner();
775
+ vi.spyOn(runner, "getThreadEvents").mockImplementation(() => {
776
+ throw new Error("boom");
777
+ });
778
+ const runtime = new CopilotRuntime({ agents: {}, runner });
779
+
780
+ const response = await handleGetThreadEvents({
781
+ runtime,
782
+ request: new Request("https://example.com/threads/thread-1/events"),
783
+ threadId: "thread-1",
784
+ });
785
+
786
+ expect(response.status).toBe(500);
787
+ });
788
+ });
789
+
790
+ describe("handleGetThreadState", () => {
791
+ it("returns the state from the in-memory runner", async () => {
792
+ const runner = new InMemoryAgentRunner();
793
+ const snapshot = { counter: 3, label: "alpha" };
794
+ vi.spyOn(runner, "getThreadState").mockReturnValue(snapshot as never);
795
+ const runtime = new CopilotRuntime({ agents: {}, runner });
796
+
797
+ const response = await handleGetThreadState({
798
+ runtime,
799
+ request: new Request("https://example.com/threads/thread-1/state"),
800
+ threadId: "thread-1",
801
+ });
802
+
803
+ expect(response.status).toBe(200);
804
+ const body = await response.json();
805
+ expect(body.state).toEqual(snapshot);
806
+ });
807
+
808
+ it("returns state:null when the runner has no snapshot for the thread", async () => {
809
+ const runtime = new CopilotRuntime({ agents: {} });
810
+
811
+ const response = await handleGetThreadState({
812
+ runtime,
813
+ request: new Request("https://example.com/threads/nonexistent/state"),
814
+ threadId: "nonexistent",
815
+ });
816
+
817
+ expect(response.status).toBe(200);
818
+ const body = await response.json();
819
+ expect(body.state).toBeNull();
820
+ });
821
+
822
+ it("delegates to intelligence.getThreadState and returns the snapshot when intelligence is configured", async () => {
823
+ // Platform returns a discriminated `ThreadStateResult` (Intelligence
824
+ // PR #144). The `snapshot` arm carries the folded current state; the
825
+ // handler flattens it to `{ state }` so the inspector consumes the
826
+ // same shape as the in-memory branch.
827
+ const snapshot = { counter: 7, label: "intel" };
828
+ const intelligence = {
829
+ getThreadState: vi.fn().mockResolvedValue({
830
+ kind: "snapshot",
831
+ state: snapshot,
832
+ skippedDeltas: 0,
833
+ }),
834
+ };
835
+ const identifyUser = createIdentifyUser();
836
+ const runtime = createIntelligenceRuntime({ intelligence, identifyUser });
837
+
838
+ const response = await handleGetThreadState({
839
+ runtime,
840
+ request: new Request("https://example.com/threads/thread-1/state"),
841
+ threadId: "thread-1",
842
+ });
843
+
844
+ expect(response.status).toBe(200);
845
+ expect(intelligence.getThreadState).toHaveBeenCalledWith({
846
+ threadId: "thread-1",
847
+ });
848
+ expect(identifyUser).toHaveBeenCalledTimes(1);
849
+ const body = await response.json();
850
+ expect(body.state).toEqual(snapshot);
851
+ });
852
+
853
+ it("returns state:null for the no-snapshot kind from intelligence", async () => {
854
+ const intelligence = {
855
+ getThreadState: vi.fn().mockResolvedValue({ kind: "no-snapshot" }),
856
+ };
857
+ const runtime = createIntelligenceRuntime({ intelligence });
858
+
859
+ const response = await handleGetThreadState({
860
+ runtime,
861
+ request: new Request("https://example.com/threads/thread-1/state"),
862
+ threadId: "thread-1",
863
+ });
864
+
865
+ expect(response.status).toBe(200);
866
+ const body = await response.json();
867
+ expect(body.state).toBeNull();
868
+ });
869
+
870
+ it("returns state:null for the snapshot-decode-error kind from intelligence", async () => {
871
+ // The platform logs the underlying decode failure server-side; from
872
+ // the inspector's perspective, "no readable state" is the same UX as
873
+ // "no snapshot yet."
874
+ const intelligence = {
875
+ getThreadState: vi
876
+ .fn()
877
+ .mockResolvedValue({ kind: "snapshot-decode-error" }),
878
+ };
879
+ const runtime = createIntelligenceRuntime({ intelligence });
880
+
881
+ const response = await handleGetThreadState({
882
+ runtime,
883
+ request: new Request("https://example.com/threads/thread-1/state"),
884
+ threadId: "thread-1",
885
+ });
886
+
887
+ expect(response.status).toBe(200);
888
+ const body = await response.json();
889
+ expect(body.state).toBeNull();
890
+ });
891
+
892
+ it("returns 500 when intelligence.getThreadState throws", async () => {
893
+ const intelligence = {
894
+ getThreadState: vi
895
+ .fn()
896
+ .mockRejectedValue(new Error("platform unavailable")),
897
+ };
898
+ const runtime = createIntelligenceRuntime({ intelligence });
899
+
900
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
901
+ try {
902
+ const response = await handleGetThreadState({
903
+ runtime,
904
+ request: new Request("https://example.com/threads/thread-1/state"),
905
+ threadId: "thread-1",
906
+ });
907
+
908
+ expect(response.status).toBe(500);
909
+ } finally {
910
+ errorSpy.mockRestore();
911
+ }
912
+ });
913
+
914
+ it("returns 500 when the runner throws", async () => {
915
+ const runner = new InMemoryAgentRunner();
916
+ vi.spyOn(runner, "getThreadState").mockImplementation(() => {
917
+ throw new Error("boom");
918
+ });
919
+ const runtime = new CopilotRuntime({ agents: {}, runner });
920
+
921
+ const response = await handleGetThreadState({
922
+ runtime,
923
+ request: new Request("https://example.com/threads/thread-1/state"),
924
+ threadId: "thread-1",
925
+ });
926
+
927
+ expect(response.status).toBe(500);
928
+ });
929
+ });
450
930
  });