@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.
Files changed (57) hide show
  1. package/README.md +374 -190
  2. package/dist/index.d.ts +32 -29
  3. package/dist/index.js +334 -241
  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 +12 -4
  10. package/src/askUserQuestion.ts +3 -3
  11. package/src/collectPending.ts +1 -1
  12. package/src/convertTurnMessages.test.ts +141 -196
  13. package/src/convertTurnMessages.ts +131 -77
  14. package/src/createSubAgent.ts +1 -1
  15. package/src/draftAgentConfig.test.ts +26 -29
  16. package/src/extractTurnUserText.ts +1 -1
  17. package/src/foldPeerThreads.test.ts +1 -1
  18. package/src/foldPeerThreads.ts +3 -2
  19. package/src/index.ts +39 -4
  20. package/src/listPages.ts +21 -0
  21. package/src/loadSessionSnapshot.test.ts +9 -8
  22. package/src/loadSessionSnapshot.ts +9 -14
  23. package/src/mcpAuth.ts +6 -3
  24. package/src/messageCustomMetadata.ts +1 -1
  25. package/src/modelMessageContent.ts +1 -1
  26. package/src/modelMessageImageContent.test.ts +1 -1
  27. package/src/modelMessageImageContent.ts +7 -6
  28. package/src/plugins/truefoundry-agent-server-adapter/index.ts +285 -0
  29. package/src/private/agentSpec.ts +8 -3
  30. package/src/private/draftSessionBridge.ts +14 -13
  31. package/src/private/truefoundryDraftThreadListAdapter.test.ts +44 -49
  32. package/src/private/truefoundryDraftThreadListAdapter.ts +22 -16
  33. package/src/requiredActionInputs.ts +1 -1
  34. package/src/requiredActionsFromActiveUpdate.test.ts +1 -1
  35. package/src/server/eventUtils.ts +120 -0
  36. package/src/server/events.ts +246 -0
  37. package/src/server/index.ts +66 -0
  38. package/src/server/types.ts +313 -0
  39. package/src/sessionSnapshot.ts +1 -1
  40. package/src/sessions.ts +5 -21
  41. package/src/streamTurn.test.ts +175 -158
  42. package/src/streamTurn.ts +51 -57
  43. package/src/toolApproval.ts +4 -4
  44. package/src/toolResponse.ts +4 -4
  45. package/src/truefoundryExtras.ts +1 -1
  46. package/src/truefoundryOwnedSessionsThreadListAdapter.test.ts +26 -29
  47. package/src/truefoundryOwnedSessionsThreadListAdapter.ts +18 -23
  48. package/src/truefoundryThreadListAdapter.test.ts +16 -18
  49. package/src/truefoundryThreadListAdapter.ts +9 -9
  50. package/src/turnEventHelpers.ts +1 -1
  51. package/src/types.ts +2 -16
  52. package/src/useTrueFoundryAgentMessages.test.tsx +38 -70
  53. package/src/useTrueFoundryAgentMessages.ts +33 -45
  54. package/src/useTrueFoundryAgentRuntime.ts +11 -28
  55. package/src/private/bindDraftAgentSession.test.ts +0 -54
  56. package/src/private/bindDraftAgentSession.ts +0 -28
  57. package/src/private/getGatewayFromPrivateClient.ts +0 -13
@@ -1,15 +1,12 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
- import type {
3
- AgentSession,
4
- Turn,
5
- TurnStreamData,
6
- } from "truefoundry-gateway-sdk/agents";
2
+ import type { AgentChatServer, TurnStreamData } from "./server/index.js";
7
3
 
8
4
  import { ROOT_THREAD_ID } from "./constants.js";
9
5
  import { PeerThreadFoldState } from "./foldPeerThreads.js";
10
6
  import { resumeTurnStream, streamTurnContent } from "./streamTurn.js";
11
7
 
12
8
  const createdAt = new Date().toISOString();
9
+ const SESSION_ID = "session-1";
13
10
 
14
11
  function streamData(
15
12
  sequenceNumber: number,
@@ -18,6 +15,10 @@ function streamData(
18
15
  return { sequenceNumber, event: event as TurnStreamData["event"] };
19
16
  }
20
17
 
18
+ function mockServer(partial: Record<string, unknown>): AgentChatServer {
19
+ return partial as unknown as AgentChatServer;
20
+ }
21
+
21
22
  async function collectUpdates(
22
23
  generator: AsyncGenerator<{ content: unknown[] }>,
23
24
  ): Promise<{ content: unknown[] }[]> {
@@ -32,42 +33,42 @@ describe("streamTurn", () => {
32
33
  describe("streamTurnContent", () => {
33
34
  it("prepares a user turn and yields folded stream updates", async () => {
34
35
  const foldState = new PeerThreadFoldState();
35
- const execute = vi.fn(() =>
36
- (async function* () {
37
- yield streamData(1, {
38
- type: "model.message",
39
- createdAt,
40
- id: "m1",
41
- threadId: ROOT_THREAD_ID,
42
- content: "hello from stream",
43
- });
44
- })(),
45
- );
46
- const prepareTurn = vi.fn(() => ({ execute }));
47
- const session = {
48
- prepareTurn,
49
- cancel: vi.fn().mockResolvedValue(undefined),
50
- } as unknown as AgentSession;
36
+ const prepareAndExecuteTurn = vi.fn(async function* () {
37
+ yield streamData(1, {
38
+ type: "model.message",
39
+ createdAt,
40
+ id: "m1",
41
+ threadId: ROOT_THREAD_ID,
42
+ content: "hello from stream",
43
+ });
44
+ });
45
+ const server = mockServer({
46
+ prepareAndExecuteTurn,
47
+ cancelSession: vi.fn().mockResolvedValue(undefined),
48
+ });
51
49
 
52
50
  const updates = await collectUpdates(
53
51
  streamTurnContent(
54
- session,
52
+ server,
53
+ SESSION_ID,
55
54
  foldState,
56
55
  { userMessage: "hello" },
57
56
  new AbortController().signal,
58
57
  ),
59
58
  );
60
59
 
61
- expect(prepareTurn).toHaveBeenCalledWith({
60
+ expect(prepareAndExecuteTurn).toHaveBeenCalledWith({
61
+ sessionId: SESSION_ID,
62
62
  input: [{ type: "user.message", content: "hello" }],
63
63
  previousTurnId: "auto",
64
+ abortSignal: expect.any(AbortSignal),
64
65
  });
65
66
  expect(updates).toEqual([
66
67
  { content: [{ type: "text", text: "hello from stream" }] },
67
68
  ]);
68
69
  });
69
70
 
70
- it("passes required-action inputs through prepareTurn", async () => {
71
+ it("passes required-action inputs through prepareAndExecuteTurn", async () => {
71
72
  const inputs = [
72
73
  {
73
74
  type: "user.tool_approval" as const,
@@ -82,131 +83,139 @@ describe("streamTurn", () => {
82
83
  content: "A",
83
84
  },
84
85
  ];
85
- const execute = vi.fn(() => (async function* () {})());
86
- const prepareTurn = vi.fn(() => ({ execute }));
87
- const session = {
88
- prepareTurn,
89
- cancel: vi.fn().mockResolvedValue(undefined),
90
- } as unknown as AgentSession;
86
+ const prepareAndExecuteTurn = vi.fn(async function* () {});
87
+ const server = mockServer({
88
+ prepareAndExecuteTurn,
89
+ cancelSession: vi.fn().mockResolvedValue(undefined),
90
+ });
91
91
 
92
92
  await collectUpdates(
93
93
  streamTurnContent(
94
- session,
94
+ server,
95
+ SESSION_ID,
95
96
  new PeerThreadFoldState(),
96
97
  { inputs },
97
98
  new AbortController().signal,
98
99
  ),
99
100
  );
100
101
 
101
- expect(prepareTurn).toHaveBeenCalledWith({
102
+ expect(prepareAndExecuteTurn).toHaveBeenCalledWith({
103
+ sessionId: SESSION_ID,
102
104
  input: inputs,
103
105
  previousTurnId: "auto",
106
+ abortSignal: expect.any(AbortSignal),
104
107
  });
105
108
  });
106
109
 
107
110
  it("uses empty input when resuming after MCP auth", async () => {
108
- const execute = vi.fn(() => (async function* () {})());
109
- const prepareTurn = vi.fn(() => ({ execute }));
110
- const session = {
111
- prepareTurn,
112
- cancel: vi.fn().mockResolvedValue(undefined),
113
- } as unknown as AgentSession;
111
+ const prepareAndExecuteTurn = vi.fn(async function* () {});
112
+ const server = mockServer({
113
+ prepareAndExecuteTurn,
114
+ cancelSession: vi.fn().mockResolvedValue(undefined),
115
+ });
114
116
 
115
117
  await collectUpdates(
116
118
  streamTurnContent(
117
- session,
119
+ server,
120
+ SESSION_ID,
118
121
  new PeerThreadFoldState(),
119
122
  { resumeMcpAuth: true },
120
123
  new AbortController().signal,
121
124
  ),
122
125
  );
123
126
 
124
- expect(prepareTurn).toHaveBeenCalledWith({
127
+ expect(prepareAndExecuteTurn).toHaveBeenCalledWith({
128
+ sessionId: SESSION_ID,
125
129
  input: [],
126
130
  previousTurnId: "auto",
131
+ abortSignal: expect.any(AbortSignal),
127
132
  });
128
133
  });
129
134
 
130
135
  it("forwards an explicit previousTurnId when branching", async () => {
131
- const execute = vi.fn(() => (async function* () {})());
132
- const prepareTurn = vi.fn(() => ({ execute }));
133
- const session = {
134
- prepareTurn,
135
- cancel: vi.fn().mockResolvedValue(undefined),
136
- } as unknown as AgentSession;
136
+ const prepareAndExecuteTurn = vi.fn(async function* () {});
137
+ const server = mockServer({
138
+ prepareAndExecuteTurn,
139
+ cancelSession: vi.fn().mockResolvedValue(undefined),
140
+ });
137
141
 
138
142
  await collectUpdates(
139
143
  streamTurnContent(
140
- session,
144
+ server,
145
+ SESSION_ID,
141
146
  new PeerThreadFoldState(),
142
147
  { userMessage: "edited", previousTurnId: "turn-a" },
143
148
  new AbortController().signal,
144
149
  ),
145
150
  );
146
151
 
147
- expect(prepareTurn).toHaveBeenCalledWith({
152
+ expect(prepareAndExecuteTurn).toHaveBeenCalledWith({
153
+ sessionId: SESSION_ID,
148
154
  input: [{ type: "user.message", content: "edited" }],
149
155
  previousTurnId: "turn-a",
156
+ abortSignal: expect.any(AbortSignal),
150
157
  });
151
158
  });
152
159
 
153
- it("forwards previousTurnId null when branching from root", async () => {
154
- const execute = vi.fn(() => (async function* () {})());
155
- const prepareTurn = vi.fn(() => ({ execute }));
156
- const session = {
157
- prepareTurn,
158
- cancel: vi.fn().mockResolvedValue(undefined),
159
- } as unknown as AgentSession;
160
+ it("forwards previousTurnId \"none\" when branching from root", async () => {
161
+ const prepareAndExecuteTurn = vi.fn(async function* () {});
162
+ const server = mockServer({
163
+ prepareAndExecuteTurn,
164
+ cancelSession: vi.fn().mockResolvedValue(undefined),
165
+ });
160
166
 
161
167
  await collectUpdates(
162
168
  streamTurnContent(
163
- session,
169
+ server,
170
+ SESSION_ID,
164
171
  new PeerThreadFoldState(),
165
- { userMessage: "first", previousTurnId: null },
172
+ { userMessage: "first", previousTurnId: "none" },
166
173
  new AbortController().signal,
167
174
  ),
168
175
  );
169
176
 
170
- expect(prepareTurn).toHaveBeenCalledWith({
177
+ expect(prepareAndExecuteTurn).toHaveBeenCalledWith({
178
+ sessionId: SESSION_ID,
171
179
  input: [{ type: "user.message", content: "first" }],
172
- previousTurnId: null,
180
+ previousTurnId: "none",
181
+ abortSignal: expect.any(AbortSignal),
173
182
  });
174
183
  });
175
184
 
176
185
  it("returns early and cancels the session when already aborted", async () => {
177
- const execute = vi.fn(() => (async function* () {})());
178
- const prepareTurn = vi.fn(() => ({ execute }));
179
- const cancel = vi.fn().mockResolvedValue(undefined);
180
- const session = { prepareTurn, cancel } as unknown as AgentSession;
186
+ const prepareAndExecuteTurn = vi.fn(async function* () {});
187
+ const cancelSession = vi.fn().mockResolvedValue(undefined);
188
+ const server = mockServer({ prepareAndExecuteTurn, cancelSession });
181
189
  const abortController = new AbortController();
182
190
  abortController.abort();
183
191
 
184
192
  const updates = await collectUpdates(
185
193
  streamTurnContent(
186
- session,
194
+ server,
195
+ SESSION_ID,
187
196
  new PeerThreadFoldState(),
188
197
  { userMessage: "hello" },
189
198
  abortController.signal,
190
199
  ),
191
200
  );
192
201
 
193
- expect(cancel).toHaveBeenCalled();
194
- expect(execute).not.toHaveBeenCalled();
202
+ expect(cancelSession).toHaveBeenCalledWith({ sessionId: SESSION_ID });
203
+ expect(prepareAndExecuteTurn).not.toHaveBeenCalled();
195
204
  expect(updates).toEqual([]);
196
205
  });
197
206
 
198
- it("forwards headers to turn.execute request options", async () => {
199
- const execute = vi.fn(() => (async function* () {})());
200
- const prepareTurn = vi.fn(() => ({ execute }));
201
- const session = {
202
- prepareTurn,
203
- cancel: vi.fn().mockResolvedValue(undefined),
204
- } as unknown as AgentSession;
207
+ it("forwards headers to prepareAndExecuteTurn", async () => {
208
+ const prepareAndExecuteTurn = vi.fn(async function* () {});
209
+ const server = mockServer({
210
+ prepareAndExecuteTurn,
211
+ cancelSession: vi.fn().mockResolvedValue(undefined),
212
+ });
205
213
  const abortSignal = new AbortController().signal;
206
214
 
207
215
  await collectUpdates(
208
216
  streamTurnContent(
209
- session,
217
+ server,
218
+ SESSION_ID,
210
219
  new PeerThreadFoldState(),
211
220
  {
212
221
  userMessage: "hello",
@@ -218,54 +227,50 @@ describe("streamTurn", () => {
218
227
  ),
219
228
  );
220
229
 
221
- expect(execute).toHaveBeenCalledWith(
222
- { stream: true },
223
- {
224
- abortSignal,
225
- headers: {
226
- "x-tfy-session-last-updated-at": "2026-06-30T10:00:00.000Z",
227
- },
230
+ expect(prepareAndExecuteTurn).toHaveBeenCalledWith({
231
+ sessionId: SESSION_ID,
232
+ input: [{ type: "user.message", content: "hello" }],
233
+ previousTurnId: "auto",
234
+ abortSignal,
235
+ headers: {
236
+ "x-tfy-session-last-updated-at": "2026-06-30T10:00:00.000Z",
228
237
  },
229
- );
238
+ });
230
239
  });
231
240
 
232
241
  it("notifies gateway turn id when turn.done errors with no content yields", async () => {
233
242
  const gatewayTurnId = "01ky6mqzmczwt6ssyd5r02gjjc";
234
- const turn = {
235
- id: undefined as string | undefined,
236
- execute: vi.fn(() =>
237
- (async function* () {
238
- // SDK sets turn.id when turn.created is observed.
239
- turn.id = gatewayTurnId;
240
- yield streamData(1, {
241
- type: "turn.created",
242
- createdAt,
243
- input: [{ type: "user.message", content: "hello" }],
244
- });
245
- yield streamData(2, {
246
- type: "turn.done",
247
- createdAt,
248
- state: {
249
- status: "error",
250
- message:
251
- "Publisher Model is not servable in region us-central1.",
252
- completedAt: createdAt,
253
- },
254
- });
255
- })(),
256
- ),
257
- };
258
- const prepareTurn = vi.fn(() => turn);
259
- const session = {
260
- prepareTurn,
261
- cancel: vi.fn().mockResolvedValue(undefined),
262
- } as unknown as AgentSession;
243
+ const prepareAndExecuteTurn = vi.fn(async function* () {
244
+ yield streamData(1, {
245
+ type: "turn.created",
246
+ createdAt,
247
+ id: "created-1",
248
+ turnId: gatewayTurnId,
249
+ input: [{ type: "user.message", content: "hello" }],
250
+ });
251
+ yield streamData(2, {
252
+ type: "turn.done",
253
+ createdAt,
254
+ id: "done-1",
255
+ state: {
256
+ status: "error",
257
+ message:
258
+ "Publisher Model is not servable in region us-central1.",
259
+ completedAt: createdAt,
260
+ },
261
+ });
262
+ });
263
+ const server = mockServer({
264
+ prepareAndExecuteTurn,
265
+ cancelSession: vi.fn().mockResolvedValue(undefined),
266
+ });
263
267
  const onTurnIdAvailable = vi.fn();
264
268
 
265
269
  await expect(
266
270
  collectUpdates(
267
271
  streamTurnContent(
268
- session,
272
+ server,
273
+ SESSION_ID,
269
274
  new PeerThreadFoldState(),
270
275
  { userMessage: "hello" },
271
276
  new AbortController().signal,
@@ -279,31 +284,30 @@ describe("streamTurn", () => {
279
284
  expect(onTurnIdAvailable).toHaveBeenCalledWith(gatewayTurnId);
280
285
  });
281
286
 
282
- it("does not notify when an error stream never assigns turn.id", async () => {
283
- const execute = vi.fn(() =>
284
- (async function* () {
285
- yield streamData(1, {
286
- type: "turn.done",
287
- createdAt,
288
- state: {
289
- status: "error",
290
- completedAt: createdAt,
291
- message: "boom",
292
- },
293
- });
294
- })(),
295
- );
296
- const prepareTurn = vi.fn(() => ({ id: undefined, execute }));
297
- const session = {
298
- prepareTurn,
299
- cancel: vi.fn().mockResolvedValue(undefined),
300
- } as unknown as AgentSession;
287
+ it("does not notify when an error stream never emits turn.created", async () => {
288
+ const prepareAndExecuteTurn = vi.fn(async function* () {
289
+ yield streamData(1, {
290
+ type: "turn.done",
291
+ createdAt,
292
+ id: "done-1",
293
+ state: {
294
+ status: "error",
295
+ completedAt: createdAt,
296
+ message: "boom",
297
+ },
298
+ });
299
+ });
300
+ const server = mockServer({
301
+ prepareAndExecuteTurn,
302
+ cancelSession: vi.fn().mockResolvedValue(undefined),
303
+ });
301
304
  const onTurnIdAvailable = vi.fn();
302
305
 
303
306
  await expect(
304
307
  collectUpdates(
305
308
  streamTurnContent(
306
- session,
309
+ server,
310
+ SESSION_ID,
307
311
  new PeerThreadFoldState(),
308
312
  { userMessage: "hello" },
309
313
  new AbortController().signal,
@@ -320,45 +324,58 @@ describe("streamTurn", () => {
320
324
  describe("resumeTurnStream", () => {
321
325
  it("reconnects with afterSequenceNumber and yields updates", async () => {
322
326
  const foldState = new PeerThreadFoldState();
323
- const stream = vi.fn(() =>
324
- (async function* () {
325
- yield streamData(2, {
326
- type: "model.message",
327
- createdAt,
328
- id: "m2",
329
- threadId: ROOT_THREAD_ID,
330
- content: "resumed",
331
- });
332
- })(),
333
- );
334
- const turn = {
335
- stream,
336
- session: { cancel: vi.fn().mockResolvedValue(undefined) },
337
- } as unknown as Turn;
327
+ const subscribeToTurn = vi.fn(async function* () {
328
+ yield streamData(2, {
329
+ type: "model.message",
330
+ createdAt,
331
+ id: "m2",
332
+ threadId: ROOT_THREAD_ID,
333
+ content: "resumed",
334
+ });
335
+ });
336
+ const server = mockServer({
337
+ subscribeToTurn,
338
+ cancelSession: vi.fn().mockResolvedValue(undefined),
339
+ });
338
340
 
339
341
  const updates = await collectUpdates(
340
- resumeTurnStream(turn, foldState, new AbortController().signal, 1),
342
+ resumeTurnStream(
343
+ server,
344
+ SESSION_ID,
345
+ "turn-1",
346
+ foldState,
347
+ new AbortController().signal,
348
+ 1,
349
+ ),
341
350
  );
342
- expect(stream).toHaveBeenCalledWith({ afterSequenceNumber: 1 }, expect.any(Object));
351
+ expect(subscribeToTurn).toHaveBeenCalledWith({
352
+ sessionId: SESSION_ID,
353
+ turnId: "turn-1",
354
+ afterSequenceNumber: 1,
355
+ abortSignal: expect.any(AbortSignal),
356
+ });
343
357
  expect(updates).toEqual([{ content: [{ type: "text", text: "resumed" }] }]);
344
358
  });
345
359
 
346
360
  it("returns early when aborted before streaming starts", async () => {
347
- const stream = vi.fn(() => (async function* () {})());
348
- const cancel = vi.fn().mockResolvedValue(undefined);
349
- const turn = {
350
- stream,
351
- session: { cancel },
352
- } as unknown as Turn;
361
+ const subscribeToTurn = vi.fn(async function* () {});
362
+ const cancelSession = vi.fn().mockResolvedValue(undefined);
363
+ const server = mockServer({ subscribeToTurn, cancelSession });
353
364
  const abortController = new AbortController();
354
365
  abortController.abort();
355
366
 
356
367
  const updates = await collectUpdates(
357
- resumeTurnStream(turn, new PeerThreadFoldState(), abortController.signal),
368
+ resumeTurnStream(
369
+ server,
370
+ SESSION_ID,
371
+ "turn-1",
372
+ new PeerThreadFoldState(),
373
+ abortController.signal,
374
+ ),
358
375
  );
359
376
 
360
- expect(cancel).toHaveBeenCalled();
361
- expect(stream).not.toHaveBeenCalled();
377
+ expect(cancelSession).toHaveBeenCalledWith({ sessionId: SESSION_ID });
378
+ expect(subscribeToTurn).not.toHaveBeenCalled();
362
379
  expect(updates).toEqual([]);
363
380
  });
364
381
  });