@truefoundry/assistant-ui-runtime 0.1.5 → 0.1.6-rc.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 (58) hide show
  1. package/README.md +374 -190
  2. package/dist/index.d.ts +32 -29
  3. package/dist/index.js +330 -236
  4. package/dist/index.js.map +1 -1
  5. package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +30 -0
  6. package/dist/plugins/truefoundry-agent-server-adapter/index.js +198 -0
  7. package/dist/plugins/truefoundry-agent-server-adapter/index.js.map +1 -0
  8. package/dist/types-VUBzoJT2.d.ts +462 -0
  9. package/package.json +10 -2
  10. package/src/askUserQuestion.ts +3 -3
  11. package/src/buildEditedUserMessageContent.test.ts +2 -2
  12. package/src/collectPending.ts +1 -1
  13. package/src/convertTurnMessages.test.ts +141 -196
  14. package/src/convertTurnMessages.ts +130 -76
  15. package/src/createSubAgent.ts +1 -1
  16. package/src/draftAgentConfig.test.ts +26 -29
  17. package/src/extractTurnUserText.ts +1 -1
  18. package/src/foldPeerThreads.test.ts +1 -1
  19. package/src/foldPeerThreads.ts +3 -2
  20. package/src/index.ts +39 -4
  21. package/src/listPages.ts +21 -0
  22. package/src/loadSessionSnapshot.test.ts +9 -8
  23. package/src/loadSessionSnapshot.ts +9 -14
  24. package/src/mcpAuth.ts +6 -3
  25. package/src/messageCustomMetadata.ts +1 -1
  26. package/src/modelMessageContent.ts +1 -1
  27. package/src/modelMessageImageContent.test.ts +1 -1
  28. package/src/modelMessageImageContent.ts +7 -6
  29. package/src/plugins/truefoundry-agent-server-adapter/index.ts +285 -0
  30. package/src/private/agentSpec.ts +8 -3
  31. package/src/private/draftSessionBridge.ts +14 -13
  32. package/src/private/truefoundryDraftThreadListAdapter.test.ts +44 -49
  33. package/src/private/truefoundryDraftThreadListAdapter.ts +22 -16
  34. package/src/requiredActionInputs.ts +1 -1
  35. package/src/requiredActionsFromActiveUpdate.test.ts +1 -1
  36. package/src/server/eventUtils.ts +120 -0
  37. package/src/server/events.ts +246 -0
  38. package/src/server/index.ts +66 -0
  39. package/src/server/types.ts +313 -0
  40. package/src/sessionSnapshot.ts +1 -1
  41. package/src/sessions.ts +5 -21
  42. package/src/streamTurn.test.ts +172 -155
  43. package/src/streamTurn.ts +51 -48
  44. package/src/toolApproval.ts +4 -4
  45. package/src/toolResponse.ts +4 -4
  46. package/src/truefoundryExtras.ts +1 -1
  47. package/src/truefoundryOwnedSessionsThreadListAdapter.test.ts +26 -29
  48. package/src/truefoundryOwnedSessionsThreadListAdapter.ts +18 -23
  49. package/src/truefoundryThreadListAdapter.test.ts +16 -18
  50. package/src/truefoundryThreadListAdapter.ts +7 -7
  51. package/src/turnEventHelpers.ts +1 -1
  52. package/src/types.ts +2 -16
  53. package/src/useTrueFoundryAgentMessages.test.tsx +38 -70
  54. package/src/useTrueFoundryAgentMessages.ts +32 -44
  55. package/src/useTrueFoundryAgentRuntime.ts +11 -28
  56. package/src/private/bindDraftAgentSession.test.ts +0 -54
  57. package/src/private/bindDraftAgentSession.ts +0 -28
  58. package/src/private/getGatewayFromPrivateClient.ts +0 -13
@@ -1,9 +1,10 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import type { AppendMessage } from "@assistant-ui/core";
3
3
  import type {
4
- AgentSession,
4
+ AgentChatServer,
5
5
  ModelMessageEvent,
6
6
  SandboxCreatedEvent,
7
+ SessionEventItem,
7
8
  ThreadCreatedEvent,
8
9
  ToolApprovalRequiredEvent,
9
10
  ToolResponseRequiredEvent,
@@ -12,7 +13,7 @@ import type {
12
13
  TurnDoneEvent,
13
14
  TurnEvent,
14
15
  TurnStreamData,
15
- } from "truefoundry-gateway-sdk/agents";
16
+ } from "./server/index.js";
16
17
 
17
18
  import { ROOT_THREAD_ID } from "./constants.js";
18
19
  import { collectPendingToolResponses } from "./collectPending.js";
@@ -45,6 +46,9 @@ import {
45
46
  import type { TurnStreamUpdate } from "./turnStreamUpdate.js";
46
47
 
47
48
  const createdAt = new Date().toISOString();
49
+ const SESSION_ID = "session-1";
50
+
51
+ type TurnFixture = Turn & { events: TurnEvent[] };
48
52
 
49
53
  function modelMessage(
50
54
  event: Omit<ModelMessageEvent, "type" | "createdAt">,
@@ -65,9 +69,15 @@ function threadCreated(
65
69
  }
66
70
 
67
71
  function sandboxCreated(
68
- event: Omit<SandboxCreatedEvent, "type" | "createdAt">,
72
+ event: { id: string; sandboxId: string; threadId?: string | null },
69
73
  ): SandboxCreatedEvent {
70
- return { type: "sandbox.created", createdAt, ...event };
74
+ return {
75
+ type: "sandbox.created",
76
+ createdAt,
77
+ threadId: event.threadId ?? null,
78
+ id: event.id,
79
+ sandboxId: event.sandboxId,
80
+ };
71
81
  }
72
82
 
73
83
  function responseRequired(
@@ -84,32 +94,6 @@ async function* streamFrom(
84
94
  }
85
95
  }
86
96
 
87
- function eventsPage(events: TurnEvent[]) {
88
- return async () => ({
89
- async *[Symbol.asyncIterator]() {
90
- for (const event of events) {
91
- yield event;
92
- }
93
- },
94
- });
95
- }
96
-
97
- function turnsPage(turns: Turn[]) {
98
- return async () => ({
99
- data: turns,
100
- response: {
101
- data: turns,
102
- pagination: { limit: turns.length || 1 },
103
- },
104
- hasNextPage: () => false,
105
- async *[Symbol.asyncIterator]() {
106
- for (const turn of turns) {
107
- yield turn;
108
- }
109
- },
110
- });
111
- }
112
-
113
97
  function appendUserMessage(
114
98
  content: AppendMessage["content"],
115
99
  attachments: AppendMessage["attachments"] = [],
@@ -127,48 +111,45 @@ function appendUserMessage(
127
111
  }
128
112
 
129
113
  function mockTurn(
130
- overrides: Partial<Turn> & Pick<Turn, "id" | "createdAt">,
131
- ): Pick<Turn, "id" | "createdAt" | "input" | "state" | "listEvents"> {
132
- return {
133
- input: [{ type: "user.message", content: "hello" }],
134
- state: { status: "done", requiredActions: [], completedAt: createdAt },
135
- listEvents: eventsPage([
114
+ overrides: Partial<Turn> &
115
+ Pick<Turn, "id" | "createdAt"> & {
116
+ events?: TurnEvent[];
117
+ },
118
+ ): TurnFixture {
119
+ const {
120
+ events = [
136
121
  modelMessage({
137
122
  id: "m1",
138
123
  threadId: ROOT_THREAD_ID,
139
124
  content: "assistant reply",
140
125
  }),
141
- ]) as unknown as Turn["listEvents"],
142
- ...overrides,
126
+ ],
127
+ ...rest
128
+ } = overrides;
129
+ return {
130
+ sessionId: SESSION_ID,
131
+ input: [{ type: "user.message", content: "hello" }],
132
+ state: { status: "done", requiredActions: [], completedAt: createdAt },
133
+ ...rest,
134
+ events,
143
135
  };
144
136
  }
145
137
 
146
- async function collectTurnListEvents(
147
- turn: Pick<Turn, "listEvents">,
148
- ): Promise<TurnEvent[]> {
149
- const events: TurnEvent[] = [];
150
- for await (const event of await turn.listEvents()) {
151
- events.push(event as TurnEvent);
152
- }
153
- return events;
154
- }
155
-
156
138
  /**
157
139
  * Builds session-level event items from per-turn mocks — newest-first turns,
158
140
  * running turns excluded (matches session.listEvents API contract).
159
141
  */
160
- async function sessionEventItemsFromTurns(
161
- turnsNewestFirst: ReturnType<typeof mockTurn>[],
142
+ function sessionEventItemsFromTurns(
143
+ turnsNewestFirst: TurnFixture[],
162
144
  lastTurnId?: string,
163
- ): Promise<{ turnId: string; event: TurnCreatedEvent | TurnDoneEvent | TurnEvent }[]> {
145
+ ): SessionEventItem[] {
164
146
  let chain = turnsNewestFirst.filter((turn) => turn.state.status !== "running");
165
147
  if (lastTurnId != null) {
166
148
  const anchorIndex = chain.findIndex((turn) => turn.id === lastTurnId);
167
149
  chain = anchorIndex === -1 ? [] : chain.slice(anchorIndex);
168
150
  }
169
151
 
170
- const items: { turnId: string; event: TurnCreatedEvent | TurnDoneEvent | TurnEvent }[] =
171
- [];
152
+ const items: SessionEventItem[] = [];
172
153
  for (const turn of [...chain].reverse()) {
173
154
  items.push({
174
155
  turnId: turn.id,
@@ -178,11 +159,10 @@ async function sessionEventItemsFromTurns(
178
159
  turnId: turn.id,
179
160
  input: turn.input,
180
161
  state: { status: "running" },
181
- createdBy: { subjectId: "u1", subjectType: "user", subjectSlug: "u1" },
182
162
  createdAt: turn.createdAt,
183
163
  },
184
164
  });
185
- for (const event of await collectTurnListEvents(turn as Turn)) {
165
+ for (const event of turn.events) {
186
166
  items.push({ turnId: turn.id, event });
187
167
  }
188
168
  items.push({
@@ -198,27 +178,21 @@ async function sessionEventItemsFromTurns(
198
178
  return items;
199
179
  }
200
180
 
201
- function mockSession(turns: ReturnType<typeof mockTurn>[]): AgentSession {
181
+ function mockServerWithTurns(turns: TurnFixture[]): AgentChatServer {
182
+ const eventsByTurnId = new Map(turns.map((turn) => [turn.id, turn.events]));
202
183
  return {
203
- listTurns: turnsPage(turns as Turn[]) as unknown as AgentSession["listTurns"],
204
- listEvents: async (opts?: { lastTurnId?: string; pageToken?: string; limit?: number }) => {
205
- const items = await sessionEventItemsFromTurns(turns, opts?.lastTurnId);
184
+ listTurns: async ({ limit }: { limit?: number } = {}) => ({
185
+ data: limit != null ? turns.slice(0, limit) : turns,
186
+ }),
187
+ listEvents: async (opts: { lastTurnId?: string } = {}) => {
188
+ const items = sessionEventItemsFromTurns(turns, opts.lastTurnId);
206
189
  const newestFirst = [...items].reverse();
207
- return {
208
- data: newestFirst,
209
- response: {
210
- data: newestFirst,
211
- pagination: { limit: newestFirst.length || 1 },
212
- },
213
- hasNextPage: () => false,
214
- async *[Symbol.asyncIterator]() {
215
- for (const item of newestFirst) {
216
- yield item;
217
- }
218
- },
219
- };
190
+ return { data: newestFirst };
220
191
  },
221
- } as unknown as AgentSession;
192
+ listTurnEvents: async ({ turnId }: { turnId: string }) => ({
193
+ data: eventsByTurnId.get(turnId) ?? [],
194
+ }),
195
+ } as unknown as AgentChatServer;
222
196
  }
223
197
 
224
198
  async function collectStream<T>(stream: AsyncGenerator<T>): Promise<T[]> {
@@ -513,11 +487,14 @@ describe("convertTurnMessages", () => {
513
487
 
514
488
  describe("buildTurnAssistantContent", () => {
515
489
  it("aggregates turn listEvents into root assistant content", async () => {
490
+ const turn = mockTurn({
491
+ id: "turn-1",
492
+ createdAt,
493
+ });
516
494
  const content = await buildTurnAssistantContent(
517
- mockTurn({
518
- id: "turn-1",
519
- createdAt,
520
- }),
495
+ mockServerWithTurns([turn]),
496
+ SESSION_ID,
497
+ turn,
521
498
  );
522
499
  expect(content).toEqual([{ type: "text", text: "assistant reply" }]);
523
500
  });
@@ -526,14 +503,13 @@ describe("convertTurnMessages", () => {
526
503
  describe("convertTurnsToThreadMessages", () => {
527
504
  it("builds user and assistant messages from completed turns", async () => {
528
505
  const result = await convertTurnsToThreadMessages(
529
- mockSession([
506
+ mockServerWithTurns([
530
507
  mockTurn({
531
508
  id: "turn-1",
532
509
  createdAt,
533
510
  input: [{ type: "user.message", content: "hello" }],
534
511
  }),
535
- ]),
536
- );
512
+ ]), SESSION_ID);
537
513
 
538
514
  expect(result.messages).toHaveLength(2);
539
515
  expect(result.messages[0]).toMatchObject({
@@ -552,22 +528,21 @@ describe("convertTurnMessages", () => {
552
528
 
553
529
  it("carries sandboxId from a historical sandbox.created event onto the assistant message", async () => {
554
530
  const result = await convertTurnsToThreadMessages(
555
- mockSession([
531
+ mockServerWithTurns([
556
532
  mockTurn({
557
533
  id: "turn-1",
558
534
  createdAt,
559
535
  input: [{ type: "user.message", content: "hello" }],
560
- listEvents: eventsPage([
536
+ events: [
561
537
  sandboxCreated({ id: "sandbox-evt", sandboxId: "sbx-123" }),
562
538
  modelMessage({
563
539
  id: "m1",
564
540
  threadId: ROOT_THREAD_ID,
565
541
  content: "assistant reply",
566
542
  }),
567
- ]) as unknown as Turn["listEvents"],
543
+ ],
568
544
  }),
569
- ]),
570
- );
545
+ ]), SESSION_ID);
571
546
 
572
547
  expect(result.messages[1]).toMatchObject({
573
548
  role: "assistant",
@@ -581,7 +556,7 @@ describe("convertTurnMessages", () => {
581
556
  createdAt,
582
557
  state: { status: "running" },
583
558
  });
584
- const result = await convertTurnsToThreadMessages(mockSession([runningTurn]));
559
+ const result = await convertTurnsToThreadMessages(mockServerWithTurns([runningTurn]), SESSION_ID);
585
560
 
586
561
  expect(result.runningTurn).toBe(runningTurn);
587
562
  expect(result.unstable_resume).toBe(true);
@@ -592,30 +567,29 @@ describe("convertTurnMessages", () => {
592
567
  const firstTurn = mockTurn({
593
568
  id: "turn-1",
594
569
  createdAt,
595
- listEvents: eventsPage([
570
+ events: [
596
571
  modelMessage({
597
572
  id: "m1",
598
573
  threadId: ROOT_THREAD_ID,
599
574
  content: "first chunk",
600
575
  }),
601
- ]) as unknown as Turn["listEvents"],
576
+ ],
602
577
  });
603
578
  const continuationTurn = mockTurn({
604
579
  id: "turn-2",
605
580
  createdAt,
606
581
  input: [{ type: "user.tool_approval", threadId: ROOT_THREAD_ID, toolCallId: "tc-1", approval: { status: "allow" } }],
607
- listEvents: eventsPage([
582
+ events: [
608
583
  modelMessage({
609
584
  id: "m2",
610
585
  threadId: ROOT_THREAD_ID,
611
586
  content: " after approval",
612
587
  }),
613
- ]) as unknown as Turn["listEvents"],
588
+ ],
614
589
  });
615
590
 
616
591
  const result = await convertTurnsToThreadMessages(
617
- mockSession([continuationTurn, firstTurn]),
618
- );
592
+ mockServerWithTurns([continuationTurn, firstTurn]), SESSION_ID);
619
593
 
620
594
  expect(result.messages).toHaveLength(2);
621
595
  const assistant = result.messages[1];
@@ -643,10 +617,10 @@ describe("convertTurnMessages", () => {
643
617
  threadId: ROOT_THREAD_ID,
644
618
  toolCalls: [{ id: "approval-1", sourceEventId: "m1" }],
645
619
  }),
646
- ],
620
+ ] as never,
647
621
  completedAt: createdAt,
648
622
  },
649
- listEvents: eventsPage([
623
+ events: [
650
624
  modelMessage({
651
625
  id: "m1",
652
626
  threadId: ROOT_THREAD_ID,
@@ -670,10 +644,10 @@ describe("convertTurnMessages", () => {
670
644
  threadId: ROOT_THREAD_ID,
671
645
  toolCalls: [{ id: "approval-1", sourceEventId: "m1" }],
672
646
  }),
673
- ]) as unknown as Turn["listEvents"],
647
+ ],
674
648
  });
675
649
 
676
- const result = await convertTurnsToThreadMessages(mockSession([pausedTurn]));
650
+ const result = await convertTurnsToThreadMessages(mockServerWithTurns([pausedTurn]), SESSION_ID);
677
651
 
678
652
  expect(result.messages).toHaveLength(2);
679
653
  const assistant = result.messages[1];
@@ -710,10 +684,10 @@ describe("convertTurnMessages", () => {
710
684
  threadId: ROOT_THREAD_ID,
711
685
  toolCalls: [{ id: "approval-1", sourceEventId: "m1" }],
712
686
  }),
713
- ],
687
+ ] as never,
714
688
  completedAt: createdAt,
715
689
  },
716
- listEvents: eventsPage([
690
+ events: [
717
691
  modelMessage({
718
692
  id: "m1",
719
693
  threadId: ROOT_THREAD_ID,
@@ -737,7 +711,7 @@ describe("convertTurnMessages", () => {
737
711
  threadId: ROOT_THREAD_ID,
738
712
  toolCalls: [{ id: "approval-1", sourceEventId: "m1" }],
739
713
  }),
740
- ]) as unknown as Turn["listEvents"],
714
+ ],
741
715
  });
742
716
  const continuationTurn = mockTurn({
743
717
  id: "turn-approval-resume",
@@ -750,18 +724,17 @@ describe("convertTurnMessages", () => {
750
724
  approval: { status: "allow" },
751
725
  },
752
726
  ],
753
- listEvents: eventsPage([
727
+ events: [
754
728
  modelMessage({
755
729
  id: "m2",
756
730
  threadId: ROOT_THREAD_ID,
757
731
  content: "done",
758
732
  }),
759
- ]) as unknown as Turn["listEvents"],
733
+ ],
760
734
  });
761
735
 
762
736
  const result = await convertTurnsToThreadMessages(
763
- mockSession([continuationTurn, pausedTurn]),
764
- );
737
+ mockServerWithTurns([continuationTurn, pausedTurn]), SESSION_ID);
765
738
 
766
739
  const assistant = result.messages[1];
767
740
  expect(assistant?.role).toBe("assistant");
@@ -794,10 +767,10 @@ describe("convertTurnMessages", () => {
794
767
  threadId: ROOT_THREAD_ID,
795
768
  toolCalls: [{ id: "question-1", sourceEventId: "model-1" }],
796
769
  }),
797
- ],
770
+ ] as never,
798
771
  completedAt: createdAt,
799
772
  },
800
- listEvents: eventsPage([
773
+ events: [
801
774
  modelMessage({
802
775
  id: "model-1",
803
776
  threadId: ROOT_THREAD_ID,
@@ -824,10 +797,10 @@ describe("convertTurnMessages", () => {
824
797
  threadId: ROOT_THREAD_ID,
825
798
  toolCalls: [{ id: "question-1", sourceEventId: "model-1" }],
826
799
  }),
827
- ]) as unknown as Turn["listEvents"],
800
+ ],
828
801
  });
829
802
 
830
- const result = await convertTurnsToThreadMessages(mockSession([pausedTurn]));
803
+ const result = await convertTurnsToThreadMessages(mockServerWithTurns([pausedTurn]), SESSION_ID);
831
804
 
832
805
  const assistant = result.messages[1];
833
806
  expect(assistant?.role).toBe("assistant");
@@ -866,10 +839,10 @@ describe("convertTurnMessages", () => {
866
839
  threadId: ROOT_THREAD_ID,
867
840
  toolCalls: [{ id: "question-1", sourceEventId: "model-1" }],
868
841
  }),
869
- ],
842
+ ] as never,
870
843
  completedAt: createdAt,
871
844
  },
872
- listEvents: eventsPage([
845
+ events: [
873
846
  modelMessage({
874
847
  id: "model-1",
875
848
  threadId: ROOT_THREAD_ID,
@@ -896,7 +869,7 @@ describe("convertTurnMessages", () => {
896
869
  threadId: ROOT_THREAD_ID,
897
870
  toolCalls: [{ id: "question-1", sourceEventId: "model-1" }],
898
871
  }),
899
- ]) as unknown as Turn["listEvents"],
872
+ ],
900
873
  });
901
874
  const continuationTurn = mockTurn({
902
875
  id: "turn-response-resume",
@@ -909,18 +882,17 @@ describe("convertTurnMessages", () => {
909
882
  content: "A",
910
883
  },
911
884
  ],
912
- listEvents: eventsPage([
885
+ events: [
913
886
  modelMessage({
914
887
  id: "model-2",
915
888
  threadId: ROOT_THREAD_ID,
916
889
  content: "Thanks",
917
890
  }),
918
- ]) as unknown as Turn["listEvents"],
891
+ ],
919
892
  });
920
893
 
921
894
  const result = await convertTurnsToThreadMessages(
922
- mockSession([continuationTurn, pausedTurn]),
923
- );
895
+ mockServerWithTurns([continuationTurn, pausedTurn]), SESSION_ID);
924
896
 
925
897
  const assistant = result.messages[1];
926
898
  expect(assistant?.role).toBe("assistant");
@@ -944,25 +916,25 @@ describe("convertTurnMessages", () => {
944
916
  id: "turn-hello",
945
917
  createdAt,
946
918
  input: [{ type: "user.message", content: "hello" }],
947
- listEvents: eventsPage([
919
+ events: [
948
920
  modelMessage({
949
921
  id: "m-hello",
950
922
  threadId: ROOT_THREAD_ID,
951
923
  content: "Hello! How can I help?",
952
924
  }),
953
- ]) as unknown as Turn["listEvents"],
925
+ ],
954
926
  });
955
927
  const analyzeTurn = mockTurn({
956
928
  id: "turn-analyze",
957
929
  createdAt,
958
930
  input: [{ type: "user.message", content: "analyze" }],
959
- listEvents: eventsPage([
931
+ events: [
960
932
  modelMessage({
961
933
  id: "m-analyze",
962
934
  threadId: ROOT_THREAD_ID,
963
935
  content: "Let me look at the file.",
964
936
  }),
965
- ]) as unknown as Turn["listEvents"],
937
+ ],
966
938
  });
967
939
  const ticketsTurn = mockTurn({
968
940
  id: "turn-tickets",
@@ -973,18 +945,17 @@ describe("convertTurnMessages", () => {
973
945
  content: "create linear tickets",
974
946
  },
975
947
  ],
976
- listEvents: eventsPage([
948
+ events: [
977
949
  modelMessage({
978
950
  id: "m-tickets",
979
951
  threadId: ROOT_THREAD_ID,
980
952
  content: "Creating tickets now.",
981
953
  }),
982
- ]) as unknown as Turn["listEvents"],
954
+ ],
983
955
  });
984
956
 
985
957
  const result = await convertTurnsToThreadMessages(
986
- mockSession([ticketsTurn, analyzeTurn, helloTurn]),
987
- );
958
+ mockServerWithTurns([ticketsTurn, analyzeTurn, helloTurn]), SESSION_ID);
988
959
 
989
960
  expect(result.messages).toHaveLength(6);
990
961
  expect(result.messages[1]).toMatchObject({
@@ -1018,16 +989,16 @@ describe("convertTurnMessages", () => {
1018
989
  ],
1019
990
  },
1020
991
  ],
1021
- listEvents: eventsPage([
992
+ events: [
1022
993
  modelMessage({
1023
994
  id: "m-analyze",
1024
995
  threadId: ROOT_THREAD_ID,
1025
996
  content: "File analyzed.",
1026
997
  }),
1027
- ]) as unknown as Turn["listEvents"],
998
+ ],
1028
999
  });
1029
1000
 
1030
- const result = await convertTurnsToThreadMessages(mockSession([analyzeTurn]));
1001
+ const result = await convertTurnsToThreadMessages(mockServerWithTurns([analyzeTurn]), SESSION_ID);
1031
1002
 
1032
1003
  expect(result.messages).toHaveLength(2);
1033
1004
  expect(result.messages[0]).toMatchObject({
@@ -1060,7 +1031,7 @@ describe("convertTurnMessages", () => {
1060
1031
  id: "turn-spawn",
1061
1032
  createdAt,
1062
1033
  input: [{ type: "user.message", content: "spawn agent" }],
1063
- listEvents: eventsPage([
1034
+ events: [
1064
1035
  modelMessage({
1065
1036
  id: "root-spawn",
1066
1037
  threadId: ROOT_THREAD_ID,
@@ -1092,7 +1063,7 @@ describe("convertTurnMessages", () => {
1092
1063
  threadId: "child-1",
1093
1064
  content: "initial child work",
1094
1065
  }),
1095
- ]) as unknown as Turn["listEvents"],
1066
+ ],
1096
1067
  });
1097
1068
  const continuationTurn = mockTurn({
1098
1069
  id: "turn-continuation",
@@ -1105,30 +1076,29 @@ describe("convertTurnMessages", () => {
1105
1076
  approval: { status: "allow" },
1106
1077
  },
1107
1078
  ],
1108
- listEvents: eventsPage([
1079
+ events: [
1109
1080
  modelMessage({
1110
1081
  id: "child-msg-2",
1111
1082
  threadId: "child-1",
1112
1083
  content: "more child work",
1113
1084
  }),
1114
- ]) as unknown as Turn["listEvents"],
1085
+ ],
1115
1086
  });
1116
1087
  const nextUserTurn = mockTurn({
1117
1088
  id: "turn-next",
1118
1089
  createdAt,
1119
1090
  input: [{ type: "user.message", content: "thanks" }],
1120
- listEvents: eventsPage([
1091
+ events: [
1121
1092
  modelMessage({
1122
1093
  id: "m-thanks",
1123
1094
  threadId: ROOT_THREAD_ID,
1124
1095
  content: "You're welcome!",
1125
1096
  }),
1126
- ]) as unknown as Turn["listEvents"],
1097
+ ],
1127
1098
  });
1128
1099
 
1129
1100
  const result = await convertTurnsToThreadMessages(
1130
- mockSession([nextUserTurn, continuationTurn, spawnTurn]),
1131
- );
1101
+ mockServerWithTurns([nextUserTurn, continuationTurn, spawnTurn]), SESSION_ID);
1132
1102
 
1133
1103
  expect(result.messages).toHaveLength(4);
1134
1104
  const groupAssistant = result.messages[1];
@@ -1830,7 +1800,7 @@ describe("convertTurnMessages", () => {
1830
1800
  threadId: ROOT_THREAD_ID,
1831
1801
  toolCalls: [{ id: "question-1", sourceEventId: "model-1" }],
1832
1802
  }),
1833
- ],
1803
+ ] as never,
1834
1804
  completedAt: createdAt,
1835
1805
  },
1836
1806
  input: [{ type: "user.message", content: "ask me a question" }],
@@ -2115,14 +2085,13 @@ describe("convertTurnMessages", () => {
2115
2085
  // buildSnapshotFromSessionEvents
2116
2086
  // ---------------------------------------------------------------------------
2117
2087
 
2118
- type SessionEventItem = { turnId: string; event: TurnCreatedEvent | TurnDoneEvent | TurnEvent };
2119
-
2120
2088
  /** Simulates session.listEvents — returns items in desc order (newest first). */
2121
2089
  function sessionEventsPage(
2122
2090
  items: SessionEventItem[],
2123
2091
  options?: { pageSize?: number },
2124
2092
  ) {
2125
2093
  return async (opts?: {
2094
+ sessionId?: string;
2126
2095
  lastTurnId?: string;
2127
2096
  pageToken?: string;
2128
2097
  limit?: number;
@@ -2146,33 +2115,23 @@ function sessionEventsPage(
2146
2115
  const nextPageToken = hasMore ? String(pageIndex + 1) : undefined;
2147
2116
  return {
2148
2117
  data: slice,
2149
- response: {
2150
- data: slice,
2151
- pagination: {
2152
- limit: pageSize,
2153
- ...(nextPageToken != null ? { nextPageToken } : {}),
2154
- },
2155
- },
2156
- hasNextPage: () => hasMore,
2157
- async *[Symbol.asyncIterator]() {
2158
- // Full drain for rewind paths that still iterate all pages.
2159
- for (const item of newestFirst) {
2160
- yield item;
2161
- }
2162
- },
2118
+ ...(nextPageToken != null ? { nextPageToken } : {}),
2163
2119
  };
2164
2120
  };
2165
2121
  }
2166
2122
 
2167
- /** Builds a mock AgentSession with listTurns and listEvents. */
2168
- function mockSessionWithEvents(
2123
+ /** Builds a mock AgentChatServer with listTurns and listEvents. */
2124
+ function mockServerWithEvents(
2169
2125
  turns: Turn[],
2170
2126
  eventItems: SessionEventItem[],
2171
- ): AgentSession {
2127
+ ): AgentChatServer {
2172
2128
  return {
2173
- listTurns: turnsPage(turns),
2129
+ listTurns: async ({ limit }: { limit?: number } = {}) => ({
2130
+ data: limit != null ? turns.slice(0, limit) : turns,
2131
+ }),
2174
2132
  listEvents: sessionEventsPage(eventItems),
2175
- } as unknown as AgentSession;
2133
+ listTurnEvents: async () => ({ data: [] }),
2134
+ } as unknown as AgentChatServer;
2176
2135
  }
2177
2136
 
2178
2137
  describe("buildSnapshotFromSessionEvents", () => {
@@ -2186,7 +2145,6 @@ describe("buildSnapshotFromSessionEvents", () => {
2186
2145
  turnId: "t1",
2187
2146
  input: [{ type: "user.message", content: "hello" }],
2188
2147
  state: { status: "running" },
2189
- createdBy: { subjectId: "u1", subjectType: "user", subjectSlug: "u1" },
2190
2148
  createdAt,
2191
2149
  },
2192
2150
  },
@@ -2205,7 +2163,7 @@ describe("buildSnapshotFromSessionEvents", () => {
2205
2163
  },
2206
2164
  ];
2207
2165
 
2208
- const snapshot = await buildSnapshotFromSessionEvents(mockSessionWithEvents([], items));
2166
+ const snapshot = await buildSnapshotFromSessionEvents(mockServerWithEvents([], items), SESSION_ID);
2209
2167
 
2210
2168
  expect(snapshot.turns).toHaveLength(1);
2211
2169
  expect(snapshot.turns[0]?.id).toBe("t1");
@@ -2241,7 +2199,6 @@ describe("buildSnapshotFromSessionEvents", () => {
2241
2199
  turnId: "t1",
2242
2200
  input: [{ type: "user.message", content: "first" }],
2243
2201
  state: { status: "running" },
2244
- createdBy: { subjectId: "u1", subjectType: "user", subjectSlug: "u1" },
2245
2202
  createdAt,
2246
2203
  },
2247
2204
  },
@@ -2261,9 +2218,7 @@ describe("buildSnapshotFromSessionEvents", () => {
2261
2218
  // t2 (running) has no events — it is excluded from session-level listEvents
2262
2219
  ];
2263
2220
 
2264
- const snapshot = await buildSnapshotFromSessionEvents(
2265
- mockSessionWithEvents([runningTurn], items),
2266
- );
2221
+ const snapshot = await buildSnapshotFromSessionEvents(mockServerWithEvents([runningTurn], items), SESSION_ID);
2267
2222
 
2268
2223
  expect(snapshot.turns).toHaveLength(1);
2269
2224
  expect(snapshot.turns[0]?.id).toBe("t1");
@@ -2286,7 +2241,6 @@ describe("buildSnapshotFromSessionEvents", () => {
2286
2241
  turnId: "t1",
2287
2242
  input: [{ type: "user.message", content: "first" }],
2288
2243
  state: { status: "running" },
2289
- createdBy: { subjectId: "u1", subjectType: "user", subjectSlug: "u1" },
2290
2244
  createdAt,
2291
2245
  },
2292
2246
  },
@@ -2311,7 +2265,6 @@ describe("buildSnapshotFromSessionEvents", () => {
2311
2265
  turnId: "t2",
2312
2266
  input: [{ type: "user.message", content: "second" }],
2313
2267
  state: { status: "running" },
2314
- createdBy: { subjectId: "u1", subjectType: "user", subjectSlug: "u1" },
2315
2268
  createdAt,
2316
2269
  },
2317
2270
  },
@@ -2331,10 +2284,7 @@ describe("buildSnapshotFromSessionEvents", () => {
2331
2284
  ];
2332
2285
 
2333
2286
  const progressSnapshots: number[] = [];
2334
- await buildSnapshotFromSessionEvents(
2335
- mockSessionWithEvents([], items),
2336
- (snap) => progressSnapshots.push(snap.turns.length),
2337
- );
2287
+ await buildSnapshotFromSessionEvents(mockServerWithEvents([], items), SESSION_ID, (snap) => progressSnapshots.push(snap.turns.length));
2338
2288
 
2339
2289
  expect(progressSnapshots).toEqual([1, 2]);
2340
2290
  });
@@ -2344,28 +2294,23 @@ describe("buildSnapshotFromSessionEvents", () => {
2344
2294
  data: [
2345
2295
  {
2346
2296
  id: "t-running",
2297
+ sessionId: SESSION_ID,
2347
2298
  state: { status: "running" },
2348
2299
  input: [{ type: "user.message", content: "now" }],
2349
2300
  createdAt,
2350
2301
  },
2351
2302
  ],
2352
- response: { data: [], pagination: { limit: 1, nextPageToken: "more" } },
2353
- hasNextPage: () => true,
2354
- getNextPage: async () => {
2355
- throw new Error("listTurns must not paginate on initial load");
2356
- },
2357
- async *[Symbol.asyncIterator]() {
2358
- throw new Error("listTurns must not be fully iterated on initial load");
2359
- },
2303
+ nextPageToken: "more",
2360
2304
  }));
2361
2305
 
2362
- const session = {
2306
+ const server = {
2363
2307
  listTurns,
2364
2308
  listEvents: sessionEventsPage([]),
2365
- } as unknown as AgentSession;
2309
+ listTurnEvents: async () => ({ data: [] }),
2310
+ } as unknown as AgentChatServer;
2366
2311
 
2367
- const snapshot = await buildSnapshotFromSessionEvents(session);
2368
- expect(listTurns).toHaveBeenCalledWith({ limit: 1 });
2312
+ const snapshot = await buildSnapshotFromSessionEvents(server, SESSION_ID);
2313
+ expect(listTurns).toHaveBeenCalledWith({ sessionId: SESSION_ID, limit: 1 });
2369
2314
  expect(snapshot.runningTurn?.id).toBe("t-running");
2370
2315
  expect(snapshot.pendingUser?.content).toBe("now");
2371
2316
  });
@@ -2380,7 +2325,6 @@ describe("buildSnapshotFromSessionEvents", () => {
2380
2325
  turnId: id,
2381
2326
  input: [{ type: "user.message", content: text }],
2382
2327
  state: { status: "running" },
2383
- createdBy: { subjectId: "u1", subjectType: "user", subjectSlug: "u1" },
2384
2328
  createdAt,
2385
2329
  },
2386
2330
  },
@@ -2411,12 +2355,13 @@ describe("buildSnapshotFromSessionEvents", () => {
2411
2355
  ];
2412
2356
  const listEvents = sessionEventsPage(items, { pageSize: 3 });
2413
2357
  const listEventsSpy = vi.fn(listEvents);
2414
- const session = {
2415
- listTurns: turnsPage([]),
2358
+ const server = {
2359
+ listTurns: async () => ({ data: [] }),
2416
2360
  listEvents: listEventsSpy,
2417
- } as unknown as AgentSession;
2361
+ listTurnEvents: async () => ({ data: [] }),
2362
+ } as unknown as AgentChatServer;
2418
2363
 
2419
- const snapshot = await buildSnapshotFromSessionEvents(session);
2364
+ const snapshot = await buildSnapshotFromSessionEvents(server, SESSION_ID);
2420
2365
  expect(listEventsSpy).toHaveBeenCalledTimes(1);
2421
2366
  expect(snapshot.turns.map((t) => t.id)).toEqual(["t3"]);
2422
2367
  expect(snapshot.historyPagination).toEqual({
@@ -2424,7 +2369,7 @@ describe("buildSnapshotFromSessionEvents", () => {
2424
2369
  olderPageToken: "1",
2425
2370
  });
2426
2371
 
2427
- const withOlder = await prependOlderSessionHistory(session, snapshot);
2372
+ const withOlder = await prependOlderSessionHistory(server, SESSION_ID, snapshot);
2428
2373
  expect(withOlder.turns.map((t) => t.id)).toEqual(["t2", "t3"]);
2429
2374
  expect(projectSessionMessages(withOlder).map((m) => m.role)).toEqual([
2430
2375
  "user",
@@ -2434,7 +2379,7 @@ describe("buildSnapshotFromSessionEvents", () => {
2434
2379
  ]);
2435
2380
  expect(withOlder.historyPagination?.hasOlder).toBe(true);
2436
2381
 
2437
- const withAll = await prependOlderSessionHistory(session, withOlder);
2382
+ const withAll = await prependOlderSessionHistory(server, SESSION_ID, withOlder);
2438
2383
  expect(withAll.turns.map((t) => t.id)).toEqual(["t1", "t2", "t3"]);
2439
2384
  expect(withAll.historyPagination?.hasOlder).toBe(false);
2440
2385
  });
@@ -2446,30 +2391,30 @@ describe("buildSnapshotBeforeTurnIndex", () => {
2446
2391
  id: "t1",
2447
2392
  createdAt,
2448
2393
  input: [{ type: "user.message", content: "first" }],
2449
- listEvents: eventsPage([
2394
+ events: [
2450
2395
  modelMessage({ id: "m1", threadId: ROOT_THREAD_ID, content: "reply 1" }),
2451
- ]) as unknown as Turn["listEvents"],
2396
+ ],
2452
2397
  });
2453
2398
  const t2 = mockTurn({
2454
2399
  id: "t2",
2455
2400
  createdAt,
2456
2401
  input: [{ type: "user.message", content: "second" }],
2457
- listEvents: eventsPage([
2402
+ events: [
2458
2403
  modelMessage({ id: "m2", threadId: ROOT_THREAD_ID, content: "reply 2" }),
2459
- ]) as unknown as Turn["listEvents"],
2404
+ ],
2460
2405
  });
2461
2406
  const t3 = mockTurn({
2462
2407
  id: "t3",
2463
2408
  createdAt,
2464
2409
  input: [{ type: "user.message", content: "third" }],
2465
- listEvents: eventsPage([
2410
+ events: [
2466
2411
  modelMessage({ id: "m3", threadId: ROOT_THREAD_ID, content: "reply 3" }),
2467
- ]) as unknown as Turn["listEvents"],
2412
+ ],
2468
2413
  });
2469
2414
 
2470
2415
  // listTurns is newest-first.
2471
- const session = mockSession([t3, t2, t1]);
2472
- const snapshot = await buildSnapshotBeforeTurnIndex(session, 2);
2416
+ const server = mockServerWithTurns([t3, t2, t1]);
2417
+ const snapshot = await buildSnapshotBeforeTurnIndex(server, SESSION_ID, 2);
2473
2418
 
2474
2419
  expect(snapshot.turns.map((turn) => turn.id)).toEqual(["t1", "t2"]);
2475
2420
  const messages = projectSessionMessages(snapshot);
@@ -2482,10 +2427,10 @@ describe("buildSnapshotBeforeTurnIndex", () => {
2482
2427
  });
2483
2428
 
2484
2429
  it("returns an empty snapshot when branching from the first turn", async () => {
2485
- const session = mockSession([
2430
+ const server = mockServerWithTurns([
2486
2431
  mockTurn({ id: "t1", createdAt }),
2487
2432
  ]);
2488
- const snapshot = await buildSnapshotBeforeTurnIndex(session, 0);
2433
+ const snapshot = await buildSnapshotBeforeTurnIndex(server, SESSION_ID, 0);
2489
2434
  expect(snapshot.turns).toHaveLength(0);
2490
2435
  });
2491
2436
  });