@truefoundry/assistant-ui-runtime 0.1.1 → 0.1.2
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 +31 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +470 -112
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/convertTurnMessages.test.ts +146 -7
- package/src/convertTurnMessages.ts +442 -164
- package/src/hooks.ts +18 -0
- package/src/index.ts +1 -0
- package/src/private/bindDraftAgentSession.test.ts +3 -2
- package/src/private/draftSessionBridge.ts +8 -2
- package/src/private/useDraftAgentSpec.test.tsx +153 -0
- package/src/private/useDraftAgentSpec.ts +80 -3
- package/src/sessionSnapshot.ts +21 -1
- package/src/streamTurn.test.ts +34 -0
- package/src/streamTurn.ts +9 -1
- package/src/truefoundryExtras.ts +3 -0
- package/src/useTrueFoundryAgentMessages.test.tsx +50 -0
- package/src/useTrueFoundryAgentMessages.ts +85 -10
- package/src/useTrueFoundryAgentRuntime.ts +25 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
2109
|
-
|
|
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
|
|
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", () => {
|