@truefoundry/assistant-ui-runtime 0.1.6 → 0.1.8
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/CHANGELOG.md +23 -0
- package/README.md +22 -17
- package/dist/chunk-CXBZ6WLZ.js +636 -0
- package/dist/chunk-CXBZ6WLZ.js.map +1 -0
- package/dist/index.d.ts +20 -24
- package/dist/index.js +269 -166
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +82 -39
- package/dist/plugins/truefoundry-agent-server-adapter/index.js +3 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/{types-BfiFf8O1.d.ts → types-B_z-FsDS.d.ts} +208 -10
- package/package.json +1 -1
- package/src/convertTurnMessages.ts +4 -0
- package/src/{private → draft}/agentSpec.ts +14 -17
- package/src/{private → draft}/draftSessionBridge.ts +1 -2
- package/src/{private → draft}/truefoundryDraftThreadListAdapter.test.ts +1 -1
- package/src/{private → draft}/truefoundryDraftThreadListAdapter.ts +6 -2
- package/src/{private → draft}/useDraftAgentSpec.ts +16 -5
- package/src/draftAgentConfig.test.ts +2 -1
- package/src/harness.temp.ts +85 -0
- package/src/index.ts +43 -7
- package/src/plugins/truefoundry-agent-server-adapter/README.md +83 -44
- package/src/plugins/truefoundry-agent-server-adapter/chatServer.ts +365 -0
- package/src/plugins/truefoundry-agent-server-adapter/cp.test.ts +444 -0
- package/src/plugins/truefoundry-agent-server-adapter/cp.ts +482 -0
- package/src/plugins/truefoundry-agent-server-adapter/createTrueFoundryAgentUIServer.ts +94 -0
- package/src/plugins/truefoundry-agent-server-adapter/guards.ts +1 -1
- package/src/plugins/truefoundry-agent-server-adapter/index.ts +20 -351
- package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.test.ts +85 -0
- package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.ts +84 -0
- package/src/plugins/truefoundry-agent-server-adapter/types.ts +7 -5
- package/src/server/index.ts +29 -0
- package/src/server/types.ts +264 -12
- package/src/streamTurn.test.ts +27 -27
- package/src/streamTurn.ts +2 -2
- package/src/truefoundryExtras.ts +4 -1
- package/src/truefoundryOwnedSessionsThreadListAdapter.ts +5 -2
- package/src/truefoundryThreadListAdapter.test.ts +22 -0
- package/src/truefoundryThreadListAdapter.ts +4 -1
- package/src/types.ts +1 -2
- package/src/useTrueFoundryAgentMessages.test.tsx +262 -2
- package/src/useTrueFoundryAgentMessages.ts +284 -176
- package/src/useTrueFoundryAgentRuntime.ts +31 -21
- package/dist/chunk-Q2SHKMLM.js +0 -270
- package/dist/chunk-Q2SHKMLM.js.map +0 -1
- /package/src/{private → draft}/useDraftAgentSpec.test.tsx +0 -0
|
@@ -46,6 +46,7 @@ vi.mock("./convertTurnMessages.js", async (importOriginal) => {
|
|
|
46
46
|
|
|
47
47
|
const mockServer = {
|
|
48
48
|
cancelSession: vi.fn().mockResolvedValue(undefined),
|
|
49
|
+
listTurns: vi.fn(),
|
|
49
50
|
} as unknown as AgentChatServer;
|
|
50
51
|
|
|
51
52
|
function snapshotWithAssistantMessage(
|
|
@@ -279,6 +280,7 @@ describe("useTrueFoundryAgentMessages", () => {
|
|
|
279
280
|
beforeEach(() => {
|
|
280
281
|
vi.clearAllMocks();
|
|
281
282
|
vi.mocked(mockServer.cancelSession).mockResolvedValue(undefined);
|
|
283
|
+
vi.mocked(mockServer.listTurns).mockResolvedValue({ data: [] });
|
|
282
284
|
vi.mocked(loadSessionSnapshot).mockResolvedValue(createEmptySessionSnapshot());
|
|
283
285
|
vi.mocked(streamTurnContent).mockReturnValue(singleUpdateStream());
|
|
284
286
|
vi.mocked(resumeTurnStream).mockReturnValue(singleUpdateStream());
|
|
@@ -298,6 +300,79 @@ describe("useTrueFoundryAgentMessages", () => {
|
|
|
298
300
|
expect(loadSessionSnapshot).not.toHaveBeenCalled();
|
|
299
301
|
});
|
|
300
302
|
|
|
303
|
+
it("loads the initial URL session before it is marked as the main thread", async () => {
|
|
304
|
+
const { rerender } = renderHook(
|
|
305
|
+
({ isMain }: { isMain: boolean }) =>
|
|
306
|
+
useTrueFoundryAgentMessages({
|
|
307
|
+
server: mockServer,
|
|
308
|
+
sessionId: "session-from-url",
|
|
309
|
+
isMain,
|
|
310
|
+
isInitialSession: true,
|
|
311
|
+
}),
|
|
312
|
+
{ initialProps: { isMain: false } },
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
await waitFor(() =>
|
|
316
|
+
expect(loadSessionSnapshot).toHaveBeenCalledWith(
|
|
317
|
+
mockServer,
|
|
318
|
+
"session-from-url",
|
|
319
|
+
expect.any(Function),
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
rerender({ isMain: true });
|
|
324
|
+
await act(async () => Promise.resolve());
|
|
325
|
+
expect(loadSessionSnapshot).toHaveBeenCalledTimes(1);
|
|
326
|
+
|
|
327
|
+
rerender({ isMain: false });
|
|
328
|
+
rerender({ isMain: true });
|
|
329
|
+
await waitFor(() => expect(loadSessionSnapshot).toHaveBeenCalledTimes(2));
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("does not load an inactive background thread", async () => {
|
|
333
|
+
const { result } = renderHook(() =>
|
|
334
|
+
useTrueFoundryAgentMessages({
|
|
335
|
+
server: mockServer,
|
|
336
|
+
sessionId: "background-session",
|
|
337
|
+
isMain: false,
|
|
338
|
+
isInitialSession: false,
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
343
|
+
expect(loadSessionSnapshot).not.toHaveBeenCalled();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("retries a failed URL early load before the thread is promoted to main", async () => {
|
|
347
|
+
vi.mocked(loadSessionSnapshot)
|
|
348
|
+
.mockRejectedValueOnce(new Error("load failed"))
|
|
349
|
+
.mockResolvedValueOnce(snapshotWithUserTurn("Hello"));
|
|
350
|
+
|
|
351
|
+
const { result } = renderHook(() =>
|
|
352
|
+
useTrueFoundryAgentMessages({
|
|
353
|
+
server: mockServer,
|
|
354
|
+
sessionId: "session-from-url",
|
|
355
|
+
isMain: false,
|
|
356
|
+
isInitialSession: true,
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
await waitFor(() => expect(loadSessionSnapshot).toHaveBeenCalledTimes(1));
|
|
361
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
362
|
+
|
|
363
|
+
await act(async () => {
|
|
364
|
+
result.current.retryLoad();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await waitFor(() => expect(loadSessionSnapshot).toHaveBeenCalledTimes(2));
|
|
368
|
+
await waitFor(() =>
|
|
369
|
+
expect(result.current.messages[0]).toMatchObject({
|
|
370
|
+
role: "user",
|
|
371
|
+
content: [{ type: "text", text: "Hello" }],
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
301
376
|
it("sendTurn lazily initializes a session when sessionId is undefined", async () => {
|
|
302
377
|
const initializeSession = vi.fn().mockResolvedValue({
|
|
303
378
|
remoteId: "session-new",
|
|
@@ -358,6 +433,7 @@ describe("useTrueFoundryAgentMessages", () => {
|
|
|
358
433
|
},
|
|
359
434
|
expect.any(AbortSignal),
|
|
360
435
|
expect.any(Array),
|
|
436
|
+
expect.any(Function),
|
|
361
437
|
);
|
|
362
438
|
|
|
363
439
|
await act(async () => {
|
|
@@ -372,6 +448,7 @@ describe("useTrueFoundryAgentMessages", () => {
|
|
|
372
448
|
{ userMessage: "second" },
|
|
373
449
|
expect.any(AbortSignal),
|
|
374
450
|
expect.any(Array),
|
|
451
|
+
expect.any(Function),
|
|
375
452
|
);
|
|
376
453
|
});
|
|
377
454
|
|
|
@@ -385,7 +462,11 @@ describe("useTrueFoundryAgentMessages", () => {
|
|
|
385
462
|
);
|
|
386
463
|
|
|
387
464
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
388
|
-
expect(loadSessionSnapshot).toHaveBeenCalledWith(
|
|
465
|
+
expect(loadSessionSnapshot).toHaveBeenCalledWith(
|
|
466
|
+
mockServer,
|
|
467
|
+
"session-1",
|
|
468
|
+
expect.any(Function),
|
|
469
|
+
);
|
|
389
470
|
expect(result.current.messages).toHaveLength(1);
|
|
390
471
|
expect(result.current.messages[0]?.role).toBe("user");
|
|
391
472
|
});
|
|
@@ -493,6 +574,21 @@ describe("useTrueFoundryAgentMessages", () => {
|
|
|
493
574
|
|
|
494
575
|
it("editFromTurn drops prior turns before showing the edited user message", async () => {
|
|
495
576
|
const createdAt = new Date().toISOString();
|
|
577
|
+
vi.mocked(mockServer.listTurns).mockResolvedValue({
|
|
578
|
+
data: [
|
|
579
|
+
{
|
|
580
|
+
id: "turn-1",
|
|
581
|
+
sessionId: "session-1",
|
|
582
|
+
createdAt,
|
|
583
|
+
state: {
|
|
584
|
+
status: "done",
|
|
585
|
+
requiredActions: [],
|
|
586
|
+
completedAt: createdAt,
|
|
587
|
+
},
|
|
588
|
+
input: [{ type: "user.message", content: "Hello" }],
|
|
589
|
+
} as Turn,
|
|
590
|
+
],
|
|
591
|
+
});
|
|
496
592
|
const fold = new PeerThreadFoldState();
|
|
497
593
|
ingestTurnEvent(fold, {
|
|
498
594
|
type: "model.message",
|
|
@@ -589,6 +685,54 @@ describe("useTrueFoundryAgentMessages", () => {
|
|
|
589
685
|
});
|
|
590
686
|
});
|
|
591
687
|
|
|
688
|
+
it("restores history when edit fails before turn.created", async () => {
|
|
689
|
+
const createdAt = new Date().toISOString();
|
|
690
|
+
const onError = vi.fn();
|
|
691
|
+
const original = snapshotWithUserTurn("Hello");
|
|
692
|
+
vi.mocked(loadSessionSnapshot).mockResolvedValue(original);
|
|
693
|
+
vi.mocked(mockServer.listTurns).mockResolvedValue({
|
|
694
|
+
data: [
|
|
695
|
+
{
|
|
696
|
+
id: "turn-1",
|
|
697
|
+
sessionId: "session-1",
|
|
698
|
+
createdAt,
|
|
699
|
+
state: {
|
|
700
|
+
status: "done",
|
|
701
|
+
requiredActions: [],
|
|
702
|
+
completedAt: createdAt,
|
|
703
|
+
},
|
|
704
|
+
input: [{ type: "user.message", content: "Hello" }],
|
|
705
|
+
} as Turn,
|
|
706
|
+
],
|
|
707
|
+
});
|
|
708
|
+
vi.mocked(streamTurnContent).mockImplementation(async function* () {
|
|
709
|
+
throw new Error("Turn preparation failed");
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const { result } = renderHook(() =>
|
|
713
|
+
useTrueFoundryAgentMessages({
|
|
714
|
+
server: mockServer,
|
|
715
|
+
sessionId: "session-1",
|
|
716
|
+
onError,
|
|
717
|
+
}),
|
|
718
|
+
);
|
|
719
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
720
|
+
|
|
721
|
+
await act(async () => {
|
|
722
|
+
await expect(
|
|
723
|
+
result.current.editFromTurn("turn-1", "Edited"),
|
|
724
|
+
).rejects.toThrow("Turn preparation failed");
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
expect(result.current.messages).toHaveLength(1);
|
|
728
|
+
expect(result.current.messages[0]).toMatchObject({
|
|
729
|
+
role: "user",
|
|
730
|
+
content: [{ type: "text", text: "Hello" }],
|
|
731
|
+
});
|
|
732
|
+
// runStream reports once; callers must not double-toast.
|
|
733
|
+
expect(onError).toHaveBeenCalledOnce();
|
|
734
|
+
});
|
|
735
|
+
|
|
592
736
|
it("does not let a superseded stream complete the current stream", async () => {
|
|
593
737
|
let releaseFirstStream: (() => void) | undefined;
|
|
594
738
|
let releaseSecondStream: (() => void) | undefined;
|
|
@@ -850,7 +994,7 @@ describe("useTrueFoundryAgentMessages", () => {
|
|
|
850
994
|
});
|
|
851
995
|
|
|
852
996
|
describe("batched resume invariant", () => {
|
|
853
|
-
it("issues exactly one
|
|
997
|
+
it("issues exactly one createTurn input batch across root and sub-agent threads", async () => {
|
|
854
998
|
vi.mocked(loadSessionSnapshot).mockResolvedValue(
|
|
855
999
|
snapshotWithAssistantMessage(assistantMessageWithMultiThreadPendingActions()),
|
|
856
1000
|
);
|
|
@@ -962,4 +1106,120 @@ describe("useTrueFoundryAgentMessages", () => {
|
|
|
962
1106
|
// on mount and reconciles against the event log on the next page load.
|
|
963
1107
|
expect(loadSessionSnapshot).toHaveBeenCalledTimes(1);
|
|
964
1108
|
});
|
|
1109
|
+
|
|
1110
|
+
describe("pre-turn failure rollback", () => {
|
|
1111
|
+
it("reports and restores a user message when initializeSession fails", async () => {
|
|
1112
|
+
const onError = vi.fn();
|
|
1113
|
+
const onPreTurnFailure = vi.fn();
|
|
1114
|
+
const initializeSession = vi
|
|
1115
|
+
.fn()
|
|
1116
|
+
.mockRejectedValue(new Error("Draft session creation failed"));
|
|
1117
|
+
|
|
1118
|
+
const { result } = renderHook(() =>
|
|
1119
|
+
useTrueFoundryAgentMessages({
|
|
1120
|
+
server: mockServer,
|
|
1121
|
+
sessionId: undefined,
|
|
1122
|
+
initializeSession,
|
|
1123
|
+
onError,
|
|
1124
|
+
}),
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
1128
|
+
expect(result.current.messages).toEqual([]);
|
|
1129
|
+
|
|
1130
|
+
await act(async () => {
|
|
1131
|
+
await expect(
|
|
1132
|
+
result.current.sendTurn({
|
|
1133
|
+
userMessage: "test message",
|
|
1134
|
+
onPreTurnFailure,
|
|
1135
|
+
}),
|
|
1136
|
+
).rejects.toThrow("Draft session creation failed");
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
expect(result.current.messages).toEqual([]);
|
|
1140
|
+
expect(onPreTurnFailure).toHaveBeenCalledOnce();
|
|
1141
|
+
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
|
1142
|
+
expect(initializeSession).toHaveBeenCalledOnce();
|
|
1143
|
+
expect(streamTurnContent).not.toHaveBeenCalled();
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
it("rolls back when the turns stream fails before turn.created", async () => {
|
|
1147
|
+
const onError = vi.fn();
|
|
1148
|
+
const onPreTurnFailure = vi.fn();
|
|
1149
|
+
vi.mocked(streamTurnContent).mockImplementation(async function* () {
|
|
1150
|
+
throw new Error("Turn preparation failed");
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
const { result } = renderHook(() =>
|
|
1154
|
+
useTrueFoundryAgentMessages({
|
|
1155
|
+
server: mockServer,
|
|
1156
|
+
sessionId: "session-1",
|
|
1157
|
+
onError,
|
|
1158
|
+
}),
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
1162
|
+
|
|
1163
|
+
await act(async () => {
|
|
1164
|
+
await expect(
|
|
1165
|
+
result.current.sendTurn({
|
|
1166
|
+
userMessage: "test message",
|
|
1167
|
+
onPreTurnFailure,
|
|
1168
|
+
}),
|
|
1169
|
+
).rejects.toThrow("Turn preparation failed");
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
expect(result.current.messages).toEqual([]);
|
|
1173
|
+
expect(onPreTurnFailure).toHaveBeenCalledOnce();
|
|
1174
|
+
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it("does not roll back after turn.created registers the user message", async () => {
|
|
1178
|
+
const onError = vi.fn();
|
|
1179
|
+
const onPreTurnFailure = vi.fn();
|
|
1180
|
+
vi.mocked(streamTurnContent).mockImplementation(
|
|
1181
|
+
async function* (
|
|
1182
|
+
_server,
|
|
1183
|
+
_sessionId,
|
|
1184
|
+
_fold,
|
|
1185
|
+
_options,
|
|
1186
|
+
_signal,
|
|
1187
|
+
_baseline,
|
|
1188
|
+
onTurnIdAvailable,
|
|
1189
|
+
) {
|
|
1190
|
+
onTurnIdAvailable?.("gateway-turn-123");
|
|
1191
|
+
yield { content: [{ type: "text" as const, text: "partial" }] };
|
|
1192
|
+
throw new Error("Mid-stream error");
|
|
1193
|
+
},
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
const { result } = renderHook(() =>
|
|
1197
|
+
useTrueFoundryAgentMessages({
|
|
1198
|
+
server: mockServer,
|
|
1199
|
+
sessionId: "session-1",
|
|
1200
|
+
onError,
|
|
1201
|
+
}),
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
1205
|
+
|
|
1206
|
+
await act(async () => {
|
|
1207
|
+
await expect(
|
|
1208
|
+
result.current.sendTurn({
|
|
1209
|
+
userMessage: "test message",
|
|
1210
|
+
onPreTurnFailure,
|
|
1211
|
+
}),
|
|
1212
|
+
).rejects.toThrow("Mid-stream error");
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
const userMessages = result.current.messages.filter((m) => m.role === "user");
|
|
1216
|
+
expect(userMessages).toHaveLength(1);
|
|
1217
|
+
expect(userMessages[0]?.content[0]).toMatchObject({
|
|
1218
|
+
type: "text",
|
|
1219
|
+
text: "test message",
|
|
1220
|
+
});
|
|
1221
|
+
expect(onPreTurnFailure).not.toHaveBeenCalled();
|
|
1222
|
+
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
965
1225
|
});
|