@truefoundry/assistant-ui-runtime 0.1.3-rc.2 → 0.1.3-rc.3
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/dist/index.js +14 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/streamTurn.test.ts +85 -0
- package/src/streamTurn.ts +25 -12
- package/src/useTrueFoundryAgentMessages.ts +10 -1
package/package.json
CHANGED
package/src/streamTurn.test.ts
CHANGED
|
@@ -227,6 +227,91 @@ describe("streamTurn", () => {
|
|
|
227
227
|
},
|
|
228
228
|
);
|
|
229
229
|
});
|
|
230
|
+
|
|
231
|
+
it("notifies gateway turn id when turn.done errors with no content yields", async () => {
|
|
232
|
+
const gatewayTurnId = "01ky6mqzmczwt6ssyd5r02gjjc";
|
|
233
|
+
const turn = {
|
|
234
|
+
id: undefined as string | undefined,
|
|
235
|
+
execute: vi.fn(() =>
|
|
236
|
+
(async function* () {
|
|
237
|
+
// SDK sets turn.id when turn.created is observed.
|
|
238
|
+
turn.id = gatewayTurnId;
|
|
239
|
+
yield streamData(1, {
|
|
240
|
+
type: "turn.created",
|
|
241
|
+
createdAt,
|
|
242
|
+
input: [{ type: "user.message", content: "hello" }],
|
|
243
|
+
});
|
|
244
|
+
yield streamData(2, {
|
|
245
|
+
type: "turn.done",
|
|
246
|
+
createdAt,
|
|
247
|
+
state: {
|
|
248
|
+
status: "error",
|
|
249
|
+
message:
|
|
250
|
+
"Publisher Model is not servable in region us-central1.",
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
})(),
|
|
254
|
+
),
|
|
255
|
+
};
|
|
256
|
+
const prepareTurn = vi.fn(() => turn);
|
|
257
|
+
const session = {
|
|
258
|
+
prepareTurn,
|
|
259
|
+
cancel: vi.fn().mockResolvedValue(undefined),
|
|
260
|
+
} as unknown as AgentSession;
|
|
261
|
+
const onTurnIdAvailable = vi.fn();
|
|
262
|
+
|
|
263
|
+
await expect(
|
|
264
|
+
collectUpdates(
|
|
265
|
+
streamTurnContent(
|
|
266
|
+
session,
|
|
267
|
+
new PeerThreadFoldState(),
|
|
268
|
+
{ userMessage: "hello" },
|
|
269
|
+
new AbortController().signal,
|
|
270
|
+
undefined,
|
|
271
|
+
onTurnIdAvailable,
|
|
272
|
+
),
|
|
273
|
+
),
|
|
274
|
+
).rejects.toThrow("Publisher Model is not servable in region us-central1.");
|
|
275
|
+
|
|
276
|
+
expect(onTurnIdAvailable).toHaveBeenCalledTimes(1);
|
|
277
|
+
expect(onTurnIdAvailable).toHaveBeenCalledWith(gatewayTurnId);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("does not notify when an error stream never assigns turn.id", async () => {
|
|
281
|
+
const execute = vi.fn(() =>
|
|
282
|
+
(async function* () {
|
|
283
|
+
yield streamData(1, {
|
|
284
|
+
type: "turn.done",
|
|
285
|
+
createdAt,
|
|
286
|
+
state: {
|
|
287
|
+
status: "error",
|
|
288
|
+
message: "boom",
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
})(),
|
|
292
|
+
);
|
|
293
|
+
const prepareTurn = vi.fn(() => ({ id: undefined, execute }));
|
|
294
|
+
const session = {
|
|
295
|
+
prepareTurn,
|
|
296
|
+
cancel: vi.fn().mockResolvedValue(undefined),
|
|
297
|
+
} as unknown as AgentSession;
|
|
298
|
+
const onTurnIdAvailable = vi.fn();
|
|
299
|
+
|
|
300
|
+
await expect(
|
|
301
|
+
collectUpdates(
|
|
302
|
+
streamTurnContent(
|
|
303
|
+
session,
|
|
304
|
+
new PeerThreadFoldState(),
|
|
305
|
+
{ userMessage: "hello" },
|
|
306
|
+
new AbortController().signal,
|
|
307
|
+
undefined,
|
|
308
|
+
onTurnIdAvailable,
|
|
309
|
+
),
|
|
310
|
+
),
|
|
311
|
+
).rejects.toThrow("boom");
|
|
312
|
+
|
|
313
|
+
expect(onTurnIdAvailable).not.toHaveBeenCalled();
|
|
314
|
+
});
|
|
230
315
|
});
|
|
231
316
|
|
|
232
317
|
describe("resumeTurnStream", () => {
|
package/src/streamTurn.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
type AgentSession,
|
|
3
3
|
type Turn,
|
|
4
4
|
type TurnInputItem,
|
|
5
|
+
TrueFoundryGatewayApi,
|
|
5
6
|
} from "truefoundry-gateway-sdk/agents";
|
|
6
7
|
|
|
7
8
|
import {
|
|
@@ -18,7 +19,7 @@ export type StreamTurnOptions = {
|
|
|
18
19
|
inputs?: RequiredActionInput[];
|
|
19
20
|
/**
|
|
20
21
|
* Branch anchor for `prepareTurn`. Omit for `"auto"`. Pass `null` for a fresh
|
|
21
|
-
* root turn
|
|
22
|
+
* root turn — sent as `previous_turn_id: null` on the wire.
|
|
22
23
|
*/
|
|
23
24
|
previousTurnId?: string | null;
|
|
24
25
|
/** Extra headers for the createTurn request (`execute` request options). */
|
|
@@ -61,13 +62,18 @@ export async function* streamTurnContent(
|
|
|
61
62
|
*/
|
|
62
63
|
onTurnIdAvailable?: (turnId: string) => void,
|
|
63
64
|
): AsyncGenerator<TurnStreamUpdate> {
|
|
64
|
-
|
|
65
|
+
// When previousTurnId is explicitly null, pass it through to prepareTurn so the
|
|
66
|
+
// SDK serializer emits `previous_turn_id: null` on the wire (first turn in session).
|
|
67
|
+
// The SDK type doesn't admit null, but the Fern serializer handles it correctly.
|
|
68
|
+
const previousTurnId: TrueFoundryGatewayApi.PreviousTurnIdInput | null | undefined =
|
|
65
69
|
options.previousTurnId === null
|
|
66
|
-
?
|
|
70
|
+
? null
|
|
67
71
|
: (options.previousTurnId ?? "auto");
|
|
68
72
|
const turn = session.prepareTurn({
|
|
69
73
|
input: buildTurnInput(options),
|
|
70
|
-
...(previousTurnId
|
|
74
|
+
...(previousTurnId !== undefined
|
|
75
|
+
? { previousTurnId: previousTurnId as TrueFoundryGatewayApi.PreviousTurnIdInput }
|
|
76
|
+
: {}),
|
|
71
77
|
});
|
|
72
78
|
|
|
73
79
|
const onAbort = bindAbort(session, abortSignal);
|
|
@@ -75,8 +81,15 @@ export async function* streamTurnContent(
|
|
|
75
81
|
return;
|
|
76
82
|
}
|
|
77
83
|
|
|
84
|
+
let turnIdNotified = false;
|
|
85
|
+
const notifyTurnIdIfAvailable = () => {
|
|
86
|
+
if (!turnIdNotified && turn.id != null) {
|
|
87
|
+
onTurnIdAvailable?.(turn.id);
|
|
88
|
+
turnIdNotified = true;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
78
92
|
try {
|
|
79
|
-
let turnIdNotified = false;
|
|
80
93
|
for await (const update of streamTurnEvents(
|
|
81
94
|
turn.execute(
|
|
82
95
|
{ stream: true },
|
|
@@ -91,17 +104,17 @@ export async function* streamTurnContent(
|
|
|
91
104
|
// After the first `turn.created` event, `turn.id` is set.
|
|
92
105
|
// Notify BEFORE yielding so the caller can update its tracking
|
|
93
106
|
// before the snapshot is written with the stream update.
|
|
94
|
-
|
|
95
|
-
onTurnIdAvailable?.(turn.id);
|
|
96
|
-
turnIdNotified = true;
|
|
97
|
-
}
|
|
107
|
+
notifyTurnIdIfAvailable();
|
|
98
108
|
yield update;
|
|
99
109
|
}
|
|
100
110
|
// Handle streams that complete without yielding any content.
|
|
101
|
-
|
|
102
|
-
onTurnIdAvailable?.(turn.id);
|
|
103
|
-
}
|
|
111
|
+
notifyTurnIdIfAvailable();
|
|
104
112
|
} catch (error) {
|
|
113
|
+
// Error streams often throw on `turn.done` (status=error) without ever
|
|
114
|
+
// yielding content (e.g. model not servable). `turn.id` is still set
|
|
115
|
+
// after `turn.created` — notify so edit/retry can resolve the turn.
|
|
116
|
+
// Same for AbortError after create: keep local ids aligned with gateway.
|
|
117
|
+
notifyTurnIdIfAvailable();
|
|
105
118
|
if (error instanceof Error && error.name === "AbortError") {
|
|
106
119
|
return;
|
|
107
120
|
}
|
|
@@ -596,6 +596,13 @@ export function useTrueFoundryAgentMessages({
|
|
|
596
596
|
isContinuation && continuationTurnId != null
|
|
597
597
|
? continuationTurnId
|
|
598
598
|
: generateId();
|
|
599
|
+
// First turns must send previousTurnId: null.
|
|
600
|
+
const isFirstTurnInSession =
|
|
601
|
+
"userMessage" in options &&
|
|
602
|
+
options.previousTurnId === undefined &&
|
|
603
|
+
snapshotRef.current.turns.length === 0 &&
|
|
604
|
+
snapshotRef.current.pendingUser == null &&
|
|
605
|
+
snapshotRef.current.activeStream == null;
|
|
599
606
|
|
|
600
607
|
// Mutable ref so runStream always reads the latest ID. For new
|
|
601
608
|
// user-message turns the local `generateId()` value is replaced
|
|
@@ -689,7 +696,9 @@ export function useTrueFoundryAgentMessages({
|
|
|
689
696
|
userMessage: options.userMessage,
|
|
690
697
|
...(options.previousTurnId !== undefined
|
|
691
698
|
? { previousTurnId: options.previousTurnId }
|
|
692
|
-
:
|
|
699
|
+
: isFirstTurnInSession
|
|
700
|
+
? { previousTurnId: null }
|
|
701
|
+
: {}),
|
|
693
702
|
...streamHeaders,
|
|
694
703
|
},
|
|
695
704
|
signal,
|