@truefoundry/assistant-ui-runtime 0.1.1 → 0.1.3-rc.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truefoundry/assistant-ui-runtime",
3
- "version": "0.1.1",
3
+ "version": "0.1.3-rc.1",
4
4
  "description": "TrueFoundry Gateway agent runtime adapter for assistant-ui",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -50,7 +50,7 @@
50
50
  "peerDependencies": {
51
51
  "@types/react": "*",
52
52
  "react": "^18 || ^19",
53
- "truefoundry-gateway-sdk": "^0.3.1"
53
+ "truefoundry-gateway-sdk": "^0.3.2-rc.1"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "@types/react": {
@@ -63,8 +63,9 @@
63
63
  "@types/react": "^19.2.17",
64
64
  "jsdom": "^29.1.1",
65
65
  "react": "^19.2.4",
66
+ "truefoundry-gateway-sdk": "0.3.2-rc.1",
66
67
  "tsup": "^8.5.0",
67
68
  "typescript": "^5.9.3",
68
69
  "vitest": "^4.1.9"
69
70
  }
70
- }
71
+ }
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, it, vi } from "vitest";
2
2
  import type { AppendMessage } from "@assistant-ui/core";
3
3
  import type {
4
4
  AgentSession,
@@ -24,6 +24,7 @@ import {
24
24
  buildUserMessageFromTurnInput,
25
25
  convertTurnsToThreadMessages,
26
26
  getTurnMessageContent,
27
+ prependOlderSessionHistory,
27
28
  projectSessionMessages,
28
29
  repositoryItemsFromMessages,
29
30
  streamTurnEvents,
@@ -95,6 +96,12 @@ function eventsPage(events: TurnEvent[]) {
95
96
 
96
97
  function turnsPage(turns: Turn[]) {
97
98
  return async () => ({
99
+ data: turns,
100
+ response: {
101
+ data: turns,
102
+ pagination: { limit: turns.length || 1 },
103
+ },
104
+ hasNextPage: () => false,
98
105
  async *[Symbol.asyncIterator]() {
99
106
  for (const turn of turns) {
100
107
  yield turn;
@@ -194,12 +201,18 @@ async function sessionEventItemsFromTurns(
194
201
  function mockSession(turns: ReturnType<typeof mockTurn>[]): AgentSession {
195
202
  return {
196
203
  listTurns: turnsPage(turns as Turn[]) as unknown as AgentSession["listTurns"],
197
- listEvents: async (opts?: { lastTurnId?: string }) => {
204
+ listEvents: async (opts?: { lastTurnId?: string; pageToken?: string; limit?: number }) => {
198
205
  const items = await sessionEventItemsFromTurns(turns, opts?.lastTurnId);
206
+ const newestFirst = [...items].reverse();
199
207
  return {
208
+ data: newestFirst,
209
+ response: {
210
+ data: newestFirst,
211
+ pagination: { limit: newestFirst.length || 1 },
212
+ },
213
+ hasNextPage: () => false,
200
214
  async *[Symbol.asyncIterator]() {
201
- // session.listEvents yields newest-first.
202
- for (const item of [...items].reverse()) {
215
+ for (const item of newestFirst) {
203
216
  yield item;
204
217
  }
205
218
  },
@@ -2105,8 +2118,15 @@ describe("convertTurnMessages", () => {
2105
2118
  type SessionEventItem = { turnId: string; event: TurnCreatedEvent | TurnDoneEvent | TurnEvent };
2106
2119
 
2107
2120
  /** Simulates session.listEvents — returns items in desc order (newest first). */
2108
- function sessionEventsPage(items: SessionEventItem[]) {
2109
- return async (opts?: { lastTurnId?: string }) => {
2121
+ function sessionEventsPage(
2122
+ items: SessionEventItem[],
2123
+ options?: { pageSize?: number },
2124
+ ) {
2125
+ return async (opts?: {
2126
+ lastTurnId?: string;
2127
+ pageToken?: string;
2128
+ limit?: number;
2129
+ }) => {
2110
2130
  let filtered = items;
2111
2131
  if (opts?.lastTurnId != null) {
2112
2132
  // Chronological items: keep through lastTurnId, drop later turns.
@@ -2115,9 +2135,28 @@ function sessionEventsPage(items: SessionEventItem[]) {
2115
2135
  );
2116
2136
  filtered = lastIndex === -1 ? [] : filtered.slice(0, lastIndex + 1);
2117
2137
  }
2138
+ const newestFirst = [...filtered].reverse();
2139
+ // Prefer fixture pageSize so pagination tests are not forced to the API default limit.
2140
+ const pageSize =
2141
+ options?.pageSize ?? opts?.limit ?? (newestFirst.length || 1);
2142
+ const pageIndex = opts?.pageToken != null ? Number(opts.pageToken) : 0;
2143
+ const start = pageIndex * pageSize;
2144
+ const slice = newestFirst.slice(start, start + pageSize);
2145
+ const hasMore = start + pageSize < newestFirst.length;
2146
+ const nextPageToken = hasMore ? String(pageIndex + 1) : undefined;
2118
2147
  return {
2148
+ data: slice,
2149
+ response: {
2150
+ data: slice,
2151
+ pagination: {
2152
+ limit: pageSize,
2153
+ ...(nextPageToken != null ? { nextPageToken } : {}),
2154
+ },
2155
+ },
2156
+ hasNextPage: () => hasMore,
2119
2157
  async *[Symbol.asyncIterator]() {
2120
- for (const item of [...filtered].reverse()) {
2158
+ // Full drain for rewind paths that still iterate all pages.
2159
+ for (const item of newestFirst) {
2121
2160
  yield item;
2122
2161
  }
2123
2162
  },
@@ -2299,6 +2338,106 @@ describe("buildSnapshotFromSessionEvents", () => {
2299
2338
 
2300
2339
  expect(progressSnapshots).toEqual([1, 2]);
2301
2340
  });
2341
+
2342
+ it("only inspects the newest listTurns page for a running turn", async () => {
2343
+ const listTurns = vi.fn(async () => ({
2344
+ data: [
2345
+ {
2346
+ id: "t-running",
2347
+ state: { status: "running" },
2348
+ input: [{ type: "user.message", content: "now" }],
2349
+ createdAt,
2350
+ },
2351
+ ],
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
+ },
2360
+ }));
2361
+
2362
+ const session = {
2363
+ listTurns,
2364
+ listEvents: sessionEventsPage([]),
2365
+ } as unknown as AgentSession;
2366
+
2367
+ const snapshot = await buildSnapshotFromSessionEvents(session);
2368
+ expect(listTurns).toHaveBeenCalledWith({ limit: 1 });
2369
+ expect(snapshot.runningTurn?.id).toBe("t-running");
2370
+ expect(snapshot.pendingUser?.content).toBe("now");
2371
+ });
2372
+
2373
+ it("loads only the newest event page and exposes an older-history cursor", async () => {
2374
+ const makeTurnItems = (id: string, text: string): SessionEventItem[] => [
2375
+ {
2376
+ turnId: id,
2377
+ event: {
2378
+ type: "turn.created",
2379
+ id: `evt-c-${id}`,
2380
+ turnId: id,
2381
+ input: [{ type: "user.message", content: text }],
2382
+ state: { status: "running" },
2383
+ createdBy: { subjectId: "u1", subjectType: "user" },
2384
+ createdAt,
2385
+ },
2386
+ },
2387
+ {
2388
+ turnId: id,
2389
+ event: modelMessage({
2390
+ id: `m-${id}`,
2391
+ threadId: ROOT_THREAD_ID,
2392
+ content: `reply ${text}`,
2393
+ }),
2394
+ },
2395
+ {
2396
+ turnId: id,
2397
+ event: {
2398
+ type: "turn.done",
2399
+ id: `evt-d-${id}`,
2400
+ state: { status: "done", requiredActions: [], completedAt: createdAt },
2401
+ createdAt,
2402
+ } as TurnDoneEvent,
2403
+ },
2404
+ ];
2405
+
2406
+ // 3 events per turn; pageSize 3 → one turn per page (newest first).
2407
+ const items = [
2408
+ ...makeTurnItems("t1", "first"),
2409
+ ...makeTurnItems("t2", "second"),
2410
+ ...makeTurnItems("t3", "third"),
2411
+ ];
2412
+ const listEvents = sessionEventsPage(items, { pageSize: 3 });
2413
+ const listEventsSpy = vi.fn(listEvents);
2414
+ const session = {
2415
+ listTurns: turnsPage([]),
2416
+ listEvents: listEventsSpy,
2417
+ } as unknown as AgentSession;
2418
+
2419
+ const snapshot = await buildSnapshotFromSessionEvents(session);
2420
+ expect(listEventsSpy).toHaveBeenCalledTimes(1);
2421
+ expect(snapshot.turns.map((t) => t.id)).toEqual(["t3"]);
2422
+ expect(snapshot.historyPagination).toEqual({
2423
+ hasOlder: true,
2424
+ olderPageToken: "1",
2425
+ });
2426
+
2427
+ const withOlder = await prependOlderSessionHistory(session, snapshot);
2428
+ expect(withOlder.turns.map((t) => t.id)).toEqual(["t2", "t3"]);
2429
+ expect(projectSessionMessages(withOlder).map((m) => m.role)).toEqual([
2430
+ "user",
2431
+ "assistant",
2432
+ "user",
2433
+ "assistant",
2434
+ ]);
2435
+ expect(withOlder.historyPagination?.hasOlder).toBe(true);
2436
+
2437
+ const withAll = await prependOlderSessionHistory(session, withOlder);
2438
+ expect(withAll.turns.map((t) => t.id)).toEqual(["t1", "t2", "t3"]);
2439
+ expect(withAll.historyPagination?.hasOlder).toBe(false);
2440
+ });
2302
2441
  });
2303
2442
 
2304
2443
  describe("buildSnapshotBeforeTurnIndex", () => {