@copilotkit/runtime 1.56.5 → 1.57.0

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 (62) hide show
  1. package/dist/package.cjs +1 -1
  2. package/dist/package.mjs +1 -1
  3. package/dist/v2/index.d.cts +2 -2
  4. package/dist/v2/index.d.mts +2 -2
  5. package/dist/v2/runtime/core/fetch-handler.cjs +16 -0
  6. package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
  7. package/dist/v2/runtime/core/fetch-handler.d.cts.map +1 -1
  8. package/dist/v2/runtime/core/fetch-handler.d.mts.map +1 -1
  9. package/dist/v2/runtime/core/fetch-handler.mjs +17 -1
  10. package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
  11. package/dist/v2/runtime/core/fetch-router.cjs +18 -1
  12. package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
  13. package/dist/v2/runtime/core/fetch-router.mjs +18 -1
  14. package/dist/v2/runtime/core/fetch-router.mjs.map +1 -1
  15. package/dist/v2/runtime/core/hooks.cjs.map +1 -1
  16. package/dist/v2/runtime/core/hooks.d.cts +8 -0
  17. package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
  18. package/dist/v2/runtime/core/hooks.d.mts +8 -0
  19. package/dist/v2/runtime/core/hooks.d.mts.map +1 -1
  20. package/dist/v2/runtime/core/hooks.mjs.map +1 -1
  21. package/dist/v2/runtime/handlers/handle-run.cjs +1 -0
  22. package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
  23. package/dist/v2/runtime/handlers/handle-run.mjs +1 -0
  24. package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
  25. package/dist/v2/runtime/handlers/intelligence/threads.cjs +124 -12
  26. package/dist/v2/runtime/handlers/intelligence/threads.cjs.map +1 -1
  27. package/dist/v2/runtime/handlers/intelligence/threads.mjs +122 -13
  28. package/dist/v2/runtime/handlers/intelligence/threads.mjs.map +1 -1
  29. package/dist/v2/runtime/index.d.cts +1 -1
  30. package/dist/v2/runtime/index.d.mts +1 -1
  31. package/dist/v2/runtime/intelligence-platform/client.cjs +30 -0
  32. package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
  33. package/dist/v2/runtime/intelligence-platform/client.d.cts +66 -0
  34. package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
  35. package/dist/v2/runtime/intelligence-platform/client.d.mts +66 -0
  36. package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
  37. package/dist/v2/runtime/intelligence-platform/client.mjs +30 -0
  38. package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
  39. package/dist/v2/runtime/runner/in-memory.cjs +94 -22
  40. package/dist/v2/runtime/runner/in-memory.cjs.map +1 -1
  41. package/dist/v2/runtime/runner/in-memory.d.cts +65 -2
  42. package/dist/v2/runtime/runner/in-memory.d.cts.map +1 -1
  43. package/dist/v2/runtime/runner/in-memory.d.mts +65 -2
  44. package/dist/v2/runtime/runner/in-memory.d.mts.map +1 -1
  45. package/dist/v2/runtime/runner/in-memory.mjs +94 -22
  46. package/dist/v2/runtime/runner/in-memory.mjs.map +1 -1
  47. package/dist/v2/runtime/runner/index.d.cts +1 -1
  48. package/dist/v2/runtime/runner/index.d.mts +1 -1
  49. package/package.json +2 -2
  50. package/src/v2/runtime/__tests__/fetch-handler-validation.test.ts +68 -0
  51. package/src/v2/runtime/__tests__/fetch-router.test.ts +46 -0
  52. package/src/v2/runtime/__tests__/handle-run.test.ts +97 -1
  53. package/src/v2/runtime/__tests__/handle-threads.test.ts +493 -13
  54. package/src/v2/runtime/core/fetch-handler.ts +19 -0
  55. package/src/v2/runtime/core/fetch-router.ts +33 -1
  56. package/src/v2/runtime/core/hooks.ts +3 -0
  57. package/src/v2/runtime/handlers/handle-run.ts +4 -0
  58. package/src/v2/runtime/handlers/handle-threads.ts +3 -0
  59. package/src/v2/runtime/handlers/intelligence/threads.ts +200 -41
  60. package/src/v2/runtime/intelligence-platform/client.ts +76 -0
  61. package/src/v2/runtime/runner/__tests__/in-memory-runner.test.ts +417 -3
  62. package/src/v2/runtime/runner/in-memory.ts +137 -51
@@ -1,9 +1,8 @@
1
1
  import { describe, it, expect, beforeEach } from "vitest";
2
2
  import { InMemoryAgentRunner } from "../in-memory";
3
- import {
4
- AbstractAgent,
3
+ import type { InMemoryThread } from "../in-memory";
4
+ import type {
5
5
  BaseEvent,
6
- EventType,
7
6
  Message,
8
7
  RunAgentInput,
9
8
  RunErrorEvent,
@@ -13,6 +12,7 @@ import {
13
12
  TextMessageStartEvent,
14
13
  ToolCallResultEvent,
15
14
  } from "@ag-ui/client";
15
+ import { AbstractAgent, EventType } from "@ag-ui/client";
16
16
  import { EMPTY, firstValueFrom } from "rxjs";
17
17
  import { toArray } from "rxjs/operators";
18
18
 
@@ -94,6 +94,7 @@ describe("InMemoryAgentRunner", () => {
94
94
 
95
95
  beforeEach(() => {
96
96
  runner = new InMemoryAgentRunner();
97
+ runner.clearThreads();
97
98
  });
98
99
 
99
100
  describe("RunStarted payload", () => {
@@ -336,6 +337,12 @@ describe("InMemoryAgentRunner", () => {
336
337
 
337
338
  expect(errorEvent).toBeDefined();
338
339
  expect(errorEvent!.message).toBe("HTTP 401: Unauthorized");
340
+ // RUN_ERROR must be the terminal event — the runner must not also emit
341
+ // RUN_FINISHED on the failure path, and nothing should follow the error.
342
+ expect(events[events.length - 1].type).toBe(EventType.RUN_ERROR);
343
+ expect(
344
+ events.filter((e) => e.type === EventType.RUN_FINISHED),
345
+ ).toHaveLength(0);
339
346
  });
340
347
 
341
348
  it("propagates non-HTTP error messages into the RUN_ERROR event", async () => {
@@ -361,3 +368,410 @@ describe("InMemoryAgentRunner", () => {
361
368
  });
362
369
  });
363
370
  });
371
+
372
+ // ---------------------------------------------------------------------------
373
+ // Agent that populates this.messages after a run — needed to test the
374
+ // listThreads / getThreadMessages fallback which reads agent.messages.
375
+ // ---------------------------------------------------------------------------
376
+ class MessagePopulatingTestAgent extends AbstractAgent {
377
+ constructor(
378
+ // Accept undefined so `clone()` can forward `this.agentId` losslessly.
379
+ // `AbstractAgent.agentId` is optional (`AgentConfig.agentId?: string`),
380
+ // so coercing undefined to "" would silently turn "no agent id" into
381
+ // "empty agent id" — a different state.
382
+ agentId: string | undefined,
383
+ private readonly inputMessages: Message[],
384
+ private readonly generatedMessages: Message[],
385
+ ) {
386
+ super({ agentId });
387
+ }
388
+
389
+ // Override runAgent to simulate what a real agent does: populate this.messages
390
+ // with the full conversation (input + generated) then call the subscriber callbacks.
391
+ // Aligns with TestAgent above — `onEvent` is required so the in-memory runner
392
+ // contract (always supply an event sink) is exercised exactly the same way.
393
+ // `onNewMessage` is declared optional to match TestAgent and the actual
394
+ // runner call site, which always passes it. Without the declaration the
395
+ // mock's options shape silently drifts from production and a regression
396
+ // that starts depending on `onNewMessage` here would compile cleanly.
397
+ async runAgent(
398
+ input: RunAgentInput,
399
+ options: {
400
+ onEvent: (params: { event: BaseEvent }) => void;
401
+ onNewMessage?: (args: { message: Message }) => void;
402
+ onRunStartedEvent?: () => void;
403
+ },
404
+ ): Promise<{ result: unknown; newMessages: Message[] }> {
405
+ const runStarted: RunStartedEvent = {
406
+ type: EventType.RUN_STARTED,
407
+ threadId: input.threadId,
408
+ runId: input.runId,
409
+ };
410
+ options.onEvent({ event: runStarted });
411
+ options.onRunStartedEvent?.();
412
+
413
+ for (const msg of this.generatedMessages) {
414
+ const start = {
415
+ type: EventType.TEXT_MESSAGE_START,
416
+ messageId: msg.id,
417
+ role: (msg as { role: string }).role,
418
+ } as TextMessageStartEvent;
419
+ const content = {
420
+ type: EventType.TEXT_MESSAGE_CONTENT,
421
+ messageId: msg.id,
422
+ delta: (msg as { content?: string }).content ?? "",
423
+ } as TextMessageContentEvent;
424
+ const end = {
425
+ type: EventType.TEXT_MESSAGE_END,
426
+ messageId: msg.id,
427
+ } as TextMessageEndEvent;
428
+ options.onEvent({ event: start });
429
+ options.onEvent({ event: content });
430
+ options.onEvent({ event: end });
431
+ }
432
+
433
+ // Populate this.messages — this is what real AbstractAgent.runAgent does
434
+ this.messages = [...this.inputMessages, ...this.generatedMessages];
435
+ return { result: undefined, newMessages: this.generatedMessages };
436
+ }
437
+
438
+ clone(): AbstractAgent {
439
+ return new MessagePopulatingTestAgent(
440
+ this.agentId,
441
+ this.inputMessages,
442
+ this.generatedMessages,
443
+ );
444
+ }
445
+
446
+ protected run(): ReturnType<AbstractAgent["run"]> {
447
+ return EMPTY;
448
+ }
449
+
450
+ // Mirror `TestAgent` and `ThrowingAgent` — `AbstractAgent.connect()` would
451
+ // otherwise inherit production behavior that may try to open a transport.
452
+ // Returning EMPTY keeps clones inert in tests.
453
+ protected connect(): ReturnType<AbstractAgent["connect"]> {
454
+ return EMPTY;
455
+ }
456
+ }
457
+
458
+ describe("InMemoryAgentRunner — listThreads / getThreadMessages", () => {
459
+ let runner: InMemoryAgentRunner;
460
+
461
+ const userMessage: Message = { id: "u1", role: "user", content: "Hello" };
462
+ const assistantMessage: Message = {
463
+ id: "a1",
464
+ role: "assistant",
465
+ content: "Hi there!",
466
+ };
467
+
468
+ beforeEach(async () => {
469
+ runner = new InMemoryAgentRunner();
470
+ // Reset the module-level GLOBAL_STORE singleton so tests don't leak into each other
471
+ runner.clearThreads();
472
+
473
+ // Run a single turn on a unique thread so each test starts fresh
474
+ const agent = new MessagePopulatingTestAgent(
475
+ "test-agent",
476
+ [userMessage],
477
+ [assistantMessage],
478
+ );
479
+ await firstValueFrom(
480
+ runner
481
+ .run({
482
+ threadId: "list-threads-thread-1",
483
+ agent,
484
+ input: {
485
+ threadId: "list-threads-thread-1",
486
+ runId: "run-lt-1",
487
+ messages: [userMessage],
488
+ state: {},
489
+ tools: [],
490
+ context: [],
491
+ },
492
+ })
493
+ .pipe(toArray()),
494
+ );
495
+ });
496
+
497
+ describe("listThreads", () => {
498
+ it("returns a summary for each completed thread", () => {
499
+ const threads = runner.listThreads();
500
+ const thread = threads.find(
501
+ (t: InMemoryThread) => t.id === "list-threads-thread-1",
502
+ );
503
+ expect(thread).toBeDefined();
504
+ expect(thread!.agentId).toBe("test-agent");
505
+ expect(thread!.name).toBeNull();
506
+ expect(thread!.archived).toBe(false);
507
+ expect(thread!.createdAt).toBeTruthy();
508
+ expect(thread!.updatedAt).toBeTruthy();
509
+ });
510
+
511
+ it("returns threads sorted most-recently-updated first", async () => {
512
+ // Run a second thread after a delay long enough that timer-resolution
513
+ // jitter on slow CI runners cannot collapse the two timestamps. 20ms
514
+ // sits comfortably above typical setTimeout granularity (~4ms in Node)
515
+ // and the file-system timestamp resolution we observed flakes around.
516
+ await new Promise((r) => setTimeout(r, 20));
517
+ const agent2 = new MessagePopulatingTestAgent(
518
+ "test-agent",
519
+ [userMessage],
520
+ [assistantMessage],
521
+ );
522
+ await firstValueFrom(
523
+ runner
524
+ .run({
525
+ threadId: "list-threads-thread-2",
526
+ agent: agent2,
527
+ input: {
528
+ threadId: "list-threads-thread-2",
529
+ runId: "run-lt-2",
530
+ messages: [userMessage],
531
+ state: {},
532
+ tools: [],
533
+ context: [],
534
+ },
535
+ })
536
+ .pipe(toArray()),
537
+ );
538
+
539
+ const threads = runner.listThreads();
540
+ const ids = threads.map((t: InMemoryThread) => t.id);
541
+ const idx1 = ids.indexOf("list-threads-thread-1");
542
+ const idx2 = ids.indexOf("list-threads-thread-2");
543
+ // thread-2 is more recent, should appear before thread-1
544
+ expect(idx2).toBeLessThan(idx1);
545
+ });
546
+
547
+ it("returns an empty array when no threads have been run", () => {
548
+ const freshRunner = new InMemoryAgentRunner();
549
+ freshRunner.clearThreads();
550
+ expect(freshRunner.listThreads()).toEqual([]);
551
+ });
552
+ });
553
+
554
+ describe("getThreadMessages", () => {
555
+ it("returns all messages for a completed thread", () => {
556
+ const messages = runner.getThreadMessages("list-threads-thread-1");
557
+ expect(messages).toHaveLength(2);
558
+ const roles = messages.map((m) => (m as { role: string }).role);
559
+ expect(roles).toContain("user");
560
+ expect(roles).toContain("assistant");
561
+ });
562
+
563
+ it("includes message content", () => {
564
+ const messages = runner.getThreadMessages("list-threads-thread-1");
565
+ const user = messages.find(
566
+ (m) => (m as { role: string }).role === "user",
567
+ ) as {
568
+ content?: string;
569
+ };
570
+ const assistant = messages.find(
571
+ (m) => (m as { role: string }).role === "assistant",
572
+ ) as { content?: string };
573
+ expect(user?.content).toBe("Hello");
574
+ expect(assistant?.content).toBe("Hi there!");
575
+ });
576
+
577
+ it("returns an empty array for an unknown threadId", () => {
578
+ const messages = runner.getThreadMessages("nonexistent-thread-xyz");
579
+ expect(messages).toEqual([]);
580
+ });
581
+
582
+ it("reflects the most recent run's full message history", async () => {
583
+ const followUp: Message = {
584
+ id: "u2",
585
+ role: "user",
586
+ content: "Follow up",
587
+ };
588
+ const followUpReply: Message = {
589
+ id: "a2",
590
+ role: "assistant",
591
+ content: "Sure!",
592
+ };
593
+ const agent2 = new MessagePopulatingTestAgent(
594
+ "test-agent",
595
+ [userMessage, assistantMessage, followUp],
596
+ [followUpReply],
597
+ );
598
+ await firstValueFrom(
599
+ runner
600
+ .run({
601
+ threadId: "list-threads-thread-1",
602
+ agent: agent2,
603
+ input: {
604
+ threadId: "list-threads-thread-1",
605
+ runId: "run-lt-turn-2",
606
+ messages: [userMessage, assistantMessage, followUp],
607
+ state: {},
608
+ tools: [],
609
+ context: [],
610
+ },
611
+ })
612
+ .pipe(toArray()),
613
+ );
614
+
615
+ const messages = runner.getThreadMessages("list-threads-thread-1");
616
+ // Should have all 4 messages from the second run's snapshot
617
+ expect(messages).toHaveLength(4);
618
+ });
619
+ });
620
+
621
+ describe("getThreadEvents", () => {
622
+ it("returns stored events for a completed thread", () => {
623
+ const events = runner.getThreadEvents("list-threads-thread-1");
624
+ // The beforeEach runs a single turn. The MessagePopulatingTestAgent
625
+ // emits RUN_STARTED + a TEXT_MESSAGE triple for the assistant reply
626
+ // and never emits a terminal event itself.
627
+ expect(events.length).toBeGreaterThan(0);
628
+ const types = events.map((e) => e.type);
629
+ expect(types).toContain(EventType.RUN_STARTED);
630
+ // Content events must be present so the inspector can replay full
631
+ // thread history — guard against a regression that strips them
632
+ // during compaction.
633
+ expect(types).toContain(EventType.TEXT_MESSAGE_START);
634
+ expect(types).toContain(EventType.TEXT_MESSAGE_CONTENT);
635
+ expect(types).toContain(EventType.TEXT_MESSAGE_END);
636
+ // finalizeRunEvents mutates the events array to append a synthetic
637
+ // terminal event when the agent does not emit one itself: a
638
+ // RUN_ERROR with code INCOMPLETE_STREAM. Asserting this explicitly
639
+ // guards against a regression where the synthetic event is dropped
640
+ // (the inspector would render an in-progress thread forever) or
641
+ // where the code is silently changed to something inspectors don't
642
+ // recognise.
643
+ const terminal = events.find(
644
+ (e): e is BaseEvent & { code?: string } =>
645
+ e.type === EventType.RUN_ERROR,
646
+ );
647
+ expect(terminal).toBeDefined();
648
+ expect((terminal as { code?: string }).code).toBe("INCOMPLETE_STREAM");
649
+ });
650
+
651
+ it("returns an empty array for an unknown threadId", () => {
652
+ expect(runner.getThreadEvents("nonexistent-thread-xyz")).toEqual([]);
653
+ });
654
+
655
+ it("flattens events across multiple historic runs", async () => {
656
+ const followUp: Message = {
657
+ id: "u2",
658
+ role: "user",
659
+ content: "Follow up",
660
+ };
661
+ const agent2 = new MessagePopulatingTestAgent(
662
+ "test-agent",
663
+ [userMessage, assistantMessage, followUp],
664
+ [{ id: "a2", role: "assistant", content: "Sure!" }],
665
+ );
666
+ await firstValueFrom(
667
+ runner
668
+ .run({
669
+ threadId: "list-threads-thread-1",
670
+ agent: agent2,
671
+ input: {
672
+ threadId: "list-threads-thread-1",
673
+ runId: "run-lt-turn-2",
674
+ messages: [userMessage, assistantMessage, followUp],
675
+ state: {},
676
+ tools: [],
677
+ context: [],
678
+ },
679
+ })
680
+ .pipe(toArray()),
681
+ );
682
+
683
+ const events = runner.getThreadEvents("list-threads-thread-1");
684
+ const runStartedCount = events.filter(
685
+ (e) => e.type === EventType.RUN_STARTED,
686
+ ).length;
687
+ // Two runs means two RUN_STARTED events survive compaction.
688
+ expect(runStartedCount).toBe(2);
689
+ });
690
+ });
691
+
692
+ describe("getThreadState", () => {
693
+ it("returns null when the thread has never emitted a state snapshot", () => {
694
+ // The beforeEach agent doesn't emit STATE_SNAPSHOT events.
695
+ expect(runner.getThreadState("list-threads-thread-1")).toBeNull();
696
+ });
697
+
698
+ it("returns null for an unknown threadId", () => {
699
+ expect(runner.getThreadState("nonexistent-thread-xyz")).toBeNull();
700
+ });
701
+
702
+ it("returns the last STATE_SNAPSHOT payload after a run", async () => {
703
+ const snapshot = { counter: 7, name: "alpha" };
704
+ const stateAgent = new TestAgent(
705
+ [
706
+ {
707
+ type: EventType.STATE_SNAPSHOT,
708
+ snapshot,
709
+ } as BaseEvent,
710
+ ],
711
+ true,
712
+ );
713
+ await firstValueFrom(
714
+ runner
715
+ .run({
716
+ threadId: "thread-with-state",
717
+ agent: stateAgent,
718
+ input: {
719
+ threadId: "thread-with-state",
720
+ runId: "run-state-1",
721
+ messages: [],
722
+ state: {},
723
+ tools: [],
724
+ context: [],
725
+ },
726
+ })
727
+ .pipe(toArray()),
728
+ );
729
+
730
+ expect(runner.getThreadState("thread-with-state")).toEqual(snapshot);
731
+ });
732
+
733
+ it("returns the most recent snapshot across multiple runs", async () => {
734
+ const first = { step: 1 };
735
+ const second = { step: 2 };
736
+
737
+ const run = async (threadId: string, runId: string, snapshot: object) => {
738
+ const agent = new TestAgent(
739
+ [{ type: EventType.STATE_SNAPSHOT, snapshot } as BaseEvent],
740
+ true,
741
+ );
742
+ await firstValueFrom(
743
+ runner
744
+ .run({
745
+ threadId,
746
+ agent,
747
+ input: {
748
+ threadId,
749
+ runId,
750
+ messages: [],
751
+ state: {},
752
+ tools: [],
753
+ context: [],
754
+ },
755
+ })
756
+ .pipe(toArray()),
757
+ );
758
+ };
759
+
760
+ await run("thread-multi-state", "run-a", first);
761
+ await run("thread-multi-state", "run-b", second);
762
+
763
+ expect(runner.getThreadState("thread-multi-state")).toEqual(second);
764
+
765
+ // Cross-thread isolation: a snapshot on a different thread must not
766
+ // bleed into the original thread's state. This guards against any
767
+ // accidental "last-write-wins" leak in the per-thread state store.
768
+ const otherThreadSnapshot = { step: 999 };
769
+ await run("thread-other", "run-other", otherThreadSnapshot);
770
+
771
+ expect(runner.getThreadState("thread-other")).toEqual(
772
+ otherThreadSnapshot,
773
+ );
774
+ expect(runner.getThreadState("thread-multi-state")).toEqual(second);
775
+ });
776
+ });
777
+ });
@@ -10,8 +10,9 @@ import {
10
10
  AbstractAgent,
11
11
  BaseEvent,
12
12
  EventType,
13
- MessagesSnapshotEvent,
13
+ Message,
14
14
  RunStartedEvent,
15
+ StateSnapshotEvent,
15
16
  compactEvents,
16
17
  } from "@ag-ui/client";
17
18
  import { finalizeRunEvents } from "@copilotkit/shared";
@@ -19,11 +20,34 @@ import { finalizeRunEvents } from "@copilotkit/shared";
19
20
  interface HistoricRun {
20
21
  threadId: string;
21
22
  runId: string;
23
+ /** ID of the agent that executed this run. */
24
+ agentId: string;
22
25
  parentRunId: string | null;
23
26
  events: BaseEvent[];
27
+ /**
28
+ * Snapshot of all messages (input + generated) at the end of this run.
29
+ * Used by the local thread-messages fallback endpoint.
30
+ */
31
+ messages: Message[];
24
32
  createdAt: number;
25
33
  }
26
34
 
35
+ /**
36
+ * Lightweight thread summary returned by {@link InMemoryAgentRunner.listThreads}.
37
+ * Shape matches the Intelligence platform's ThreadRecord so the same HTTP
38
+ * response envelope can be used for both backends.
39
+ */
40
+ export interface InMemoryThread {
41
+ id: string;
42
+ name: string | null;
43
+ agentId: string;
44
+ organizationId: ""; // always empty in in-memory mode
45
+ createdById: ""; // always empty in in-memory mode
46
+ archived: false; // always false in in-memory mode
47
+ createdAt: string;
48
+ updatedAt: string;
49
+ }
50
+
27
51
  class InMemoryEventStore {
28
52
  constructor(public threadId: string) {}
29
53
 
@@ -52,50 +76,7 @@ class InMemoryEventStore {
52
76
  currentEvents: BaseEvent[] | null = null;
53
77
  }
54
78
 
55
- // Use a symbol key on globalThis to survive hot reloads in development
56
- const GLOBAL_STORE_KEY = Symbol.for("@copilotkit/runtime/in-memory-store");
57
-
58
- interface GlobalStoreData {
59
- stores: Map<string, InMemoryEventStore>;
60
- historicRunsBackup: Map<string, HistoricRun[]>;
61
- }
62
-
63
- function getGlobalStore(): Map<string, InMemoryEventStore> {
64
- const globalAny = globalThis as unknown as Record<symbol, GlobalStoreData>;
65
-
66
- if (!globalAny[GLOBAL_STORE_KEY]) {
67
- globalAny[GLOBAL_STORE_KEY] = {
68
- stores: new Map<string, InMemoryEventStore>(),
69
- historicRunsBackup: new Map<string, HistoricRun[]>(),
70
- };
71
- }
72
-
73
- const data = globalAny[GLOBAL_STORE_KEY];
74
-
75
- // Restore historic runs from backup after hot reload
76
- // (when stores map is empty but backup has data)
77
- if (data.stores.size === 0 && data.historicRunsBackup.size > 0) {
78
- for (const [threadId, historicRuns] of data.historicRunsBackup) {
79
- const store = new InMemoryEventStore(threadId);
80
- store.historicRuns = historicRuns;
81
- data.stores.set(threadId, store);
82
- }
83
- }
84
-
85
- return data.stores;
86
- }
87
-
88
- function backupHistoricRuns(
89
- threadId: string,
90
- historicRuns: HistoricRun[],
91
- ): void {
92
- const globalAny = globalThis as unknown as Record<symbol, GlobalStoreData>;
93
- if (globalAny[GLOBAL_STORE_KEY]) {
94
- globalAny[GLOBAL_STORE_KEY].historicRunsBackup.set(threadId, historicRuns);
95
- }
96
- }
97
-
98
- const GLOBAL_STORE = getGlobalStore();
79
+ const GLOBAL_STORE = new Map<string, InMemoryEventStore>();
99
80
 
100
81
  export class InMemoryAgentRunner extends AgentRunner {
101
82
  run(request: AgentRunnerRunRequest): Observable<BaseEvent> {
@@ -215,13 +196,15 @@ export class InMemoryAgentRunner extends AgentRunner {
215
196
  store.historicRuns.push({
216
197
  threadId: request.threadId,
217
198
  runId: store.currentRunId,
199
+ agentId: request.agent.agentId ?? "default",
218
200
  parentRunId,
219
201
  events: compactedEvents,
202
+ // Snapshot all messages (input + generated) for the thread-messages endpoint
203
+ messages: Array.isArray(request.agent.messages)
204
+ ? [...request.agent.messages]
205
+ : [],
220
206
  createdAt: Date.now(),
221
207
  });
222
-
223
- // Backup for hot reload survival
224
- backupHistoricRuns(request.threadId, store.historicRuns);
225
208
  }
226
209
 
227
210
  // Complete the run
@@ -252,13 +235,14 @@ export class InMemoryAgentRunner extends AgentRunner {
252
235
  store.historicRuns.push({
253
236
  threadId: request.threadId,
254
237
  runId: store.currentRunId,
238
+ agentId: request.agent.agentId ?? "default",
255
239
  parentRunId,
256
240
  events: compactedEvents,
241
+ messages: Array.isArray(request.agent.messages)
242
+ ? [...request.agent.messages]
243
+ : [],
257
244
  createdAt: Date.now(),
258
245
  });
259
-
260
- // Backup for hot reload survival
261
- backupHistoricRuns(request.threadId, store.historicRuns);
262
246
  }
263
247
 
264
248
  // Complete the run
@@ -378,4 +362,106 @@ export class InMemoryAgentRunner extends AgentRunner {
378
362
  return Promise.resolve(false);
379
363
  }
380
364
  }
365
+
366
+ /**
367
+ * Returns a summary of every thread that has been run through this runner.
368
+ *
369
+ * This powers the local-dev fallback for `GET /threads` when the Intelligence
370
+ * platform is not configured. Each entry mirrors the shape of a platform
371
+ * `ThreadRecord` so the HTTP handler can use the same response envelope.
372
+ */
373
+ listThreads(): InMemoryThread[] {
374
+ const threads: InMemoryThread[] = [];
375
+ for (const [threadId, store] of GLOBAL_STORE) {
376
+ if (store.historicRuns.length === 0) continue;
377
+ const firstRun = store.historicRuns[0]!;
378
+ const lastRun = store.historicRuns[store.historicRuns.length - 1]!;
379
+ threads.push({
380
+ id: threadId,
381
+ name: null,
382
+ agentId: lastRun.agentId,
383
+ organizationId: "",
384
+ createdById: "",
385
+ archived: false,
386
+ createdAt: new Date(firstRun.createdAt).toISOString(),
387
+ updatedAt: new Date(lastRun.createdAt).toISOString(),
388
+ });
389
+ }
390
+ // Most recently updated first
391
+ return threads.sort(
392
+ (a, b) =>
393
+ new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
394
+ );
395
+ }
396
+
397
+ /**
398
+ * Returns all messages for a thread, using the snapshot captured at the end
399
+ * of the most recent run.
400
+ *
401
+ * This powers the local-dev fallback for `GET /threads/:threadId/messages`
402
+ * when the Intelligence platform is not configured. The returned `Message[]`
403
+ * objects come directly from the ag-ui agent, so their shape is compatible
404
+ * with the Intelligence platform's `ThreadMessage` type.
405
+ */
406
+ getThreadMessages(threadId: string): Message[] {
407
+ const store = GLOBAL_STORE.get(threadId);
408
+ if (!store || store.historicRuns.length === 0) return [];
409
+ // The last run's snapshot has the complete conversation history
410
+ return store.historicRuns[store.historicRuns.length - 1]!.messages;
411
+ }
412
+
413
+ /**
414
+ * Returns all AG-UI events for a thread, compacted across historic runs.
415
+ *
416
+ * Powers the local-dev fallback for `GET /threads/:threadId/events` when the
417
+ * Intelligence platform is not configured. The compaction logic matches
418
+ * the connection-replay path in {@link connect}, so the stream a
419
+ * late-joining inspector sees matches what this method returns.
420
+ */
421
+ getThreadEvents(threadId: string): BaseEvent[] {
422
+ const store = GLOBAL_STORE.get(threadId);
423
+ if (!store || store.historicRuns.length === 0) return [];
424
+ const all: BaseEvent[] = [];
425
+ for (const run of store.historicRuns) all.push(...run.events);
426
+ return compactEvents(all);
427
+ }
428
+
429
+ /**
430
+ * Returns the agent state snapshot for a thread.
431
+ *
432
+ * Derived from the last `STATE_SNAPSHOT` in the compacted event stream. The
433
+ * AG-UI `compactEvents` helper consolidates STATE_DELTA events and produces
434
+ * a single trailing STATE_SNAPSHOT when state changes exist, so this is a
435
+ * faithful view of state at the end of the most recent run.
436
+ *
437
+ * Returns `null` when the thread has never emitted a STATE_SNAPSHOT.
438
+ */
439
+ getThreadState(threadId: string): Record<string, unknown> | null {
440
+ const events = this.getThreadEvents(threadId);
441
+ // Walk backwards — the last snapshot wins.
442
+ for (let i = events.length - 1; i >= 0; i--) {
443
+ const event = events[i]!;
444
+ if (event.type === EventType.STATE_SNAPSHOT) {
445
+ const snapshot = (event as StateSnapshotEvent).snapshot;
446
+ if (snapshot && typeof snapshot === "object") {
447
+ return snapshot as Record<string, unknown>;
448
+ }
449
+ return null;
450
+ }
451
+ }
452
+ return null;
453
+ }
454
+
455
+ /**
456
+ * Clears all in-memory thread history.
457
+ *
458
+ * Powers the local-dev fallback for `POST /threads/clear`, letting consumers
459
+ * (e.g. the demo's Clear button) reset to an empty thread list without
460
+ * restarting the runtime. Intentionally not exposed on the Intelligence
461
+ * platform path: there, thread history lives in a real database and must
462
+ * not be wiped this way.
463
+ */
464
+ clearThreads(): void {
465
+ GLOBAL_STORE.clear();
466
+ }
381
467
  }