@truefoundry/assistant-ui-runtime 0.1.4 → 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.
- package/README.md +374 -190
- package/dist/index.d.ts +32 -29
- package/dist/index.js +334 -241
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +30 -0
- package/dist/plugins/truefoundry-agent-server-adapter/index.js +198 -0
- package/dist/plugins/truefoundry-agent-server-adapter/index.js.map +1 -0
- package/dist/types-VUBzoJT2.d.ts +462 -0
- package/package.json +12 -4
- package/src/askUserQuestion.ts +3 -3
- package/src/collectPending.ts +1 -1
- package/src/convertTurnMessages.test.ts +141 -196
- package/src/convertTurnMessages.ts +131 -77
- package/src/createSubAgent.ts +1 -1
- package/src/draftAgentConfig.test.ts +26 -29
- package/src/extractTurnUserText.ts +1 -1
- package/src/foldPeerThreads.test.ts +1 -1
- package/src/foldPeerThreads.ts +3 -2
- package/src/index.ts +39 -4
- package/src/listPages.ts +21 -0
- package/src/loadSessionSnapshot.test.ts +9 -8
- package/src/loadSessionSnapshot.ts +9 -14
- package/src/mcpAuth.ts +6 -3
- package/src/messageCustomMetadata.ts +1 -1
- package/src/modelMessageContent.ts +1 -1
- package/src/modelMessageImageContent.test.ts +1 -1
- package/src/modelMessageImageContent.ts +7 -6
- package/src/plugins/truefoundry-agent-server-adapter/index.ts +285 -0
- package/src/private/agentSpec.ts +8 -3
- package/src/private/draftSessionBridge.ts +14 -13
- package/src/private/truefoundryDraftThreadListAdapter.test.ts +44 -49
- package/src/private/truefoundryDraftThreadListAdapter.ts +22 -16
- package/src/requiredActionInputs.ts +1 -1
- package/src/requiredActionsFromActiveUpdate.test.ts +1 -1
- package/src/server/eventUtils.ts +120 -0
- package/src/server/events.ts +246 -0
- package/src/server/index.ts +66 -0
- package/src/server/types.ts +313 -0
- package/src/sessionSnapshot.ts +1 -1
- package/src/sessions.ts +5 -21
- package/src/streamTurn.test.ts +175 -158
- package/src/streamTurn.ts +51 -57
- package/src/toolApproval.ts +4 -4
- package/src/toolResponse.ts +4 -4
- package/src/truefoundryExtras.ts +1 -1
- package/src/truefoundryOwnedSessionsThreadListAdapter.test.ts +26 -29
- package/src/truefoundryOwnedSessionsThreadListAdapter.ts +18 -23
- package/src/truefoundryThreadListAdapter.test.ts +16 -18
- package/src/truefoundryThreadListAdapter.ts +9 -9
- package/src/turnEventHelpers.ts +1 -1
- package/src/types.ts +2 -16
- package/src/useTrueFoundryAgentMessages.test.tsx +38 -70
- package/src/useTrueFoundryAgentMessages.ts +33 -45
- package/src/useTrueFoundryAgentRuntime.ts +11 -28
- package/src/private/bindDraftAgentSession.test.ts +0 -54
- package/src/private/bindDraftAgentSession.ts +0 -28
- 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
|
-
|
|
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 "
|
|
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:
|
|
72
|
+
event: { id: string; sandboxId: string; threadId?: string | null },
|
|
69
73
|
): SandboxCreatedEvent {
|
|
70
|
-
return {
|
|
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> &
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
]
|
|
142
|
-
...
|
|
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
|
-
|
|
161
|
-
turnsNewestFirst:
|
|
142
|
+
function sessionEventItemsFromTurns(
|
|
143
|
+
turnsNewestFirst: TurnFixture[],
|
|
162
144
|
lastTurnId?: string,
|
|
163
|
-
):
|
|
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:
|
|
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
|
|
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
|
|
181
|
+
function mockServerWithTurns(turns: TurnFixture[]): AgentChatServer {
|
|
182
|
+
const eventsByTurnId = new Map(turns.map((turn) => [turn.id, turn.events]));
|
|
202
183
|
return {
|
|
203
|
-
listTurns:
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
531
|
+
mockServerWithTurns([
|
|
556
532
|
mockTurn({
|
|
557
533
|
id: "turn-1",
|
|
558
534
|
createdAt,
|
|
559
535
|
input: [{ type: "user.message", content: "hello" }],
|
|
560
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
570
|
+
events: [
|
|
596
571
|
modelMessage({
|
|
597
572
|
id: "m1",
|
|
598
573
|
threadId: ROOT_THREAD_ID,
|
|
599
574
|
content: "first chunk",
|
|
600
575
|
}),
|
|
601
|
-
]
|
|
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
|
-
|
|
582
|
+
events: [
|
|
608
583
|
modelMessage({
|
|
609
584
|
id: "m2",
|
|
610
585
|
threadId: ROOT_THREAD_ID,
|
|
611
586
|
content: " after approval",
|
|
612
587
|
}),
|
|
613
|
-
]
|
|
588
|
+
],
|
|
614
589
|
});
|
|
615
590
|
|
|
616
591
|
const result = await convertTurnsToThreadMessages(
|
|
617
|
-
|
|
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
|
-
|
|
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
|
-
]
|
|
647
|
+
],
|
|
674
648
|
});
|
|
675
649
|
|
|
676
|
-
const result = await convertTurnsToThreadMessages(
|
|
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
|
-
|
|
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
|
-
]
|
|
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
|
-
|
|
727
|
+
events: [
|
|
754
728
|
modelMessage({
|
|
755
729
|
id: "m2",
|
|
756
730
|
threadId: ROOT_THREAD_ID,
|
|
757
731
|
content: "done",
|
|
758
732
|
}),
|
|
759
|
-
]
|
|
733
|
+
],
|
|
760
734
|
});
|
|
761
735
|
|
|
762
736
|
const result = await convertTurnsToThreadMessages(
|
|
763
|
-
|
|
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
|
-
|
|
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
|
-
]
|
|
800
|
+
],
|
|
828
801
|
});
|
|
829
802
|
|
|
830
|
-
const result = await convertTurnsToThreadMessages(
|
|
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
|
-
|
|
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
|
-
]
|
|
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
|
-
|
|
885
|
+
events: [
|
|
913
886
|
modelMessage({
|
|
914
887
|
id: "model-2",
|
|
915
888
|
threadId: ROOT_THREAD_ID,
|
|
916
889
|
content: "Thanks",
|
|
917
890
|
}),
|
|
918
|
-
]
|
|
891
|
+
],
|
|
919
892
|
});
|
|
920
893
|
|
|
921
894
|
const result = await convertTurnsToThreadMessages(
|
|
922
|
-
|
|
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
|
-
|
|
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
|
-
]
|
|
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
|
-
|
|
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
|
-
]
|
|
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
|
-
|
|
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
|
-
]
|
|
954
|
+
],
|
|
983
955
|
});
|
|
984
956
|
|
|
985
957
|
const result = await convertTurnsToThreadMessages(
|
|
986
|
-
|
|
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
|
-
|
|
992
|
+
events: [
|
|
1022
993
|
modelMessage({
|
|
1023
994
|
id: "m-analyze",
|
|
1024
995
|
threadId: ROOT_THREAD_ID,
|
|
1025
996
|
content: "File analyzed.",
|
|
1026
997
|
}),
|
|
1027
|
-
]
|
|
998
|
+
],
|
|
1028
999
|
});
|
|
1029
1000
|
|
|
1030
|
-
const result = await convertTurnsToThreadMessages(
|
|
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
|
-
|
|
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
|
-
]
|
|
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
|
-
|
|
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
|
-
]
|
|
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
|
-
|
|
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
|
-
]
|
|
1097
|
+
],
|
|
1127
1098
|
});
|
|
1128
1099
|
|
|
1129
1100
|
const result = await convertTurnsToThreadMessages(
|
|
1130
|
-
|
|
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
|
-
|
|
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
|
|
2168
|
-
function
|
|
2123
|
+
/** Builds a mock AgentChatServer with listTurns and listEvents. */
|
|
2124
|
+
function mockServerWithEvents(
|
|
2169
2125
|
turns: Turn[],
|
|
2170
2126
|
eventItems: SessionEventItem[],
|
|
2171
|
-
):
|
|
2127
|
+
): AgentChatServer {
|
|
2172
2128
|
return {
|
|
2173
|
-
listTurns:
|
|
2129
|
+
listTurns: async ({ limit }: { limit?: number } = {}) => ({
|
|
2130
|
+
data: limit != null ? turns.slice(0, limit) : turns,
|
|
2131
|
+
}),
|
|
2174
2132
|
listEvents: sessionEventsPage(eventItems),
|
|
2175
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
2306
|
+
const server = {
|
|
2363
2307
|
listTurns,
|
|
2364
2308
|
listEvents: sessionEventsPage([]),
|
|
2365
|
-
|
|
2309
|
+
listTurnEvents: async () => ({ data: [] }),
|
|
2310
|
+
} as unknown as AgentChatServer;
|
|
2366
2311
|
|
|
2367
|
-
const snapshot = await buildSnapshotFromSessionEvents(
|
|
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
|
|
2415
|
-
listTurns:
|
|
2358
|
+
const server = {
|
|
2359
|
+
listTurns: async () => ({ data: [] }),
|
|
2416
2360
|
listEvents: listEventsSpy,
|
|
2417
|
-
|
|
2361
|
+
listTurnEvents: async () => ({ data: [] }),
|
|
2362
|
+
} as unknown as AgentChatServer;
|
|
2418
2363
|
|
|
2419
|
-
const snapshot = await buildSnapshotFromSessionEvents(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2394
|
+
events: [
|
|
2450
2395
|
modelMessage({ id: "m1", threadId: ROOT_THREAD_ID, content: "reply 1" }),
|
|
2451
|
-
|
|
2396
|
+
],
|
|
2452
2397
|
});
|
|
2453
2398
|
const t2 = mockTurn({
|
|
2454
2399
|
id: "t2",
|
|
2455
2400
|
createdAt,
|
|
2456
2401
|
input: [{ type: "user.message", content: "second" }],
|
|
2457
|
-
|
|
2402
|
+
events: [
|
|
2458
2403
|
modelMessage({ id: "m2", threadId: ROOT_THREAD_ID, content: "reply 2" }),
|
|
2459
|
-
|
|
2404
|
+
],
|
|
2460
2405
|
});
|
|
2461
2406
|
const t3 = mockTurn({
|
|
2462
2407
|
id: "t3",
|
|
2463
2408
|
createdAt,
|
|
2464
2409
|
input: [{ type: "user.message", content: "third" }],
|
|
2465
|
-
|
|
2410
|
+
events: [
|
|
2466
2411
|
modelMessage({ id: "m3", threadId: ROOT_THREAD_ID, content: "reply 3" }),
|
|
2467
|
-
|
|
2412
|
+
],
|
|
2468
2413
|
});
|
|
2469
2414
|
|
|
2470
2415
|
// listTurns is newest-first.
|
|
2471
|
-
const
|
|
2472
|
-
const snapshot = await buildSnapshotBeforeTurnIndex(
|
|
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
|
|
2430
|
+
const server = mockServerWithTurns([
|
|
2486
2431
|
mockTurn({ id: "t1", createdAt }),
|
|
2487
2432
|
]);
|
|
2488
|
-
const snapshot = await buildSnapshotBeforeTurnIndex(
|
|
2433
|
+
const snapshot = await buildSnapshotBeforeTurnIndex(server, SESSION_ID, 0);
|
|
2489
2434
|
expect(snapshot.turns).toHaveLength(0);
|
|
2490
2435
|
});
|
|
2491
2436
|
});
|