@truefoundry/assistant-ui-runtime 0.1.0-rc.1 → 0.1.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/README.md +14 -77
- package/dist/index.d.ts +4 -1
- package/dist/index.js +314 -122
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agentSessionModule.d.ts +14 -2
- package/src/convertTurnMessages.test.ts +362 -0
- package/src/convertTurnMessages.ts +193 -28
- package/src/createSubAgent.ts +13 -5
- package/src/draftAgentConfig.test.ts +1 -1
- package/src/foldPeerThreads.test.ts +56 -0
- package/src/foldPeerThreads.ts +7 -1
- package/src/hooks.ts +6 -0
- package/src/index.ts +6 -5
- package/src/loadSessionSnapshot.test.ts +21 -6
- package/src/loadSessionSnapshot.ts +9 -5
- package/src/{truefoundryDraftThreadListAdapter.ts → private/truefoundryDraftThreadListAdapter.ts} +1 -1
- package/src/sessionSnapshot.ts +27 -1
- package/src/sessions.ts +1 -1
- package/src/truefoundryExtras.ts +2 -1
- package/src/types.ts +1 -1
- package/src/useTrueFoundryAgentMessages.test.tsx +225 -1
- package/src/useTrueFoundryAgentMessages.ts +178 -60
- package/src/useTrueFoundryAgentRuntime.ts +27 -13
- /package/src/{agentSpec.ts → private/agentSpec.ts} +0 -0
- /package/src/{bindDraftAgentSession.test.ts → private/bindDraftAgentSession.test.ts} +0 -0
- /package/src/{bindDraftAgentSession.ts → private/bindDraftAgentSession.ts} +0 -0
- /package/src/{draftSessionBridge.ts → private/draftSessionBridge.ts} +0 -0
- /package/src/{truefoundryDraftThreadListAdapter.test.ts → private/truefoundryDraftThreadListAdapter.test.ts} +0 -0
- /package/src/{useDraftAgentSpec.ts → private/useDraftAgentSpec.ts} +0 -0
|
@@ -10,12 +10,15 @@ import type {
|
|
|
10
10
|
AgentSession,
|
|
11
11
|
McpAuthRequiredEvent,
|
|
12
12
|
Turn,
|
|
13
|
+
TurnCreatedEvent,
|
|
14
|
+
TurnDoneEvent,
|
|
13
15
|
TurnEvent,
|
|
14
16
|
TurnInputItem,
|
|
15
17
|
TurnStreamData,
|
|
16
18
|
} from "truefoundry-gateway-sdk/agents";
|
|
17
19
|
|
|
18
20
|
import { ROOT_THREAD_ID } from "./constants.js";
|
|
21
|
+
import { extractTurnUserText } from "./extractTurnUserText.js";
|
|
19
22
|
import {
|
|
20
23
|
buildRootAssistantContent,
|
|
21
24
|
buildRootAssistantContentForIds,
|
|
@@ -42,6 +45,7 @@ import {
|
|
|
42
45
|
createEmptySessionSnapshot,
|
|
43
46
|
emptyRequiredActionsOverlay,
|
|
44
47
|
replaceSessionSnapshot,
|
|
48
|
+
sessionEventsToSessionRecord,
|
|
45
49
|
turnToSessionRecord,
|
|
46
50
|
} from "./sessionSnapshot.js";
|
|
47
51
|
import {
|
|
@@ -186,12 +190,23 @@ import {
|
|
|
186
190
|
* `user.message`.
|
|
187
191
|
* - Tool-call identity is stable via `toolCallId` across events, fold state, and
|
|
188
192
|
* assistant-ui parts.
|
|
189
|
-
* - Reload (`
|
|
193
|
+
* - Reload (`buildSnapshotFromSessionEvents`) and live paths must produce the same
|
|
190
194
|
* per-group scoping; history uses cumulative `groupRootIds` per turn index,
|
|
191
195
|
* live uses `groupRootBaseline`.
|
|
192
196
|
*/
|
|
193
197
|
|
|
194
198
|
const TURN_EVENTS_PAGE_SIZE = 25;
|
|
199
|
+
const SESSION_EVENTS_PAGE_SIZE = 100;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Session-level event item — structurally equivalent to
|
|
203
|
+
* `TrueFoundryGateway.SessionEventItem` from
|
|
204
|
+
* `AgentSession.listEvents` (not re-exported from agents subpath).
|
|
205
|
+
*/
|
|
206
|
+
type GatewaySessionEventItem = {
|
|
207
|
+
turnId: string;
|
|
208
|
+
event: TurnCreatedEvent | TurnDoneEvent | TurnEvent;
|
|
209
|
+
};
|
|
195
210
|
|
|
196
211
|
export type ConvertTurnsResult = {
|
|
197
212
|
messages: ThreadMessage[];
|
|
@@ -731,6 +746,163 @@ export function projectSessionMessages(
|
|
|
731
746
|
|
|
732
747
|
const DEFAULT_LIST_EVENTS_CONCURRENCY = 5;
|
|
733
748
|
|
|
749
|
+
type FetchSessionEventsOptions = {
|
|
750
|
+
/**
|
|
751
|
+
* Newest turn in the listing window (initial load only). Lists that turn and
|
|
752
|
+
* its ancestors. Omit to use the session last turn. Running-turn events are
|
|
753
|
+
* excluded by the API — subscribe to that turn for live events.
|
|
754
|
+
*/
|
|
755
|
+
lastTurnId?: string;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Fetches session-level events via `session.listEvents()`. The API returns pages
|
|
760
|
+
* in desc order (newest first); the collected array is reversed before returning
|
|
761
|
+
* so callers receive events in chronological (asc) order.
|
|
762
|
+
*/
|
|
763
|
+
async function fetchAllSessionEvents(
|
|
764
|
+
session: AgentSession,
|
|
765
|
+
options?: FetchSessionEventsOptions,
|
|
766
|
+
): Promise<GatewaySessionEventItem[]> {
|
|
767
|
+
const items: GatewaySessionEventItem[] = [];
|
|
768
|
+
for await (const item of await session.listEvents({
|
|
769
|
+
limit: SESSION_EVENTS_PAGE_SIZE,
|
|
770
|
+
...(options?.lastTurnId != null ? { lastTurnId: options.lastTurnId } : {}),
|
|
771
|
+
})) {
|
|
772
|
+
items.push(item as GatewaySessionEventItem);
|
|
773
|
+
}
|
|
774
|
+
items.reverse();
|
|
775
|
+
return items;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Ingests a chronologically ordered list of session-level event items into the
|
|
780
|
+
* snapshot. `turn.created` marks the start of a turn (provides user input),
|
|
781
|
+
* content events are folded as usual, and `turn.done` finalises the turn record
|
|
782
|
+
* and pushes it to `snapshot.turns`.
|
|
783
|
+
*
|
|
784
|
+
* `onTurnComplete` is called after each `turn.done` with the partially-built
|
|
785
|
+
* snapshot so callers can update UI progressively. The fold state is correct at
|
|
786
|
+
* that point for every turn up to and including the just-completed one.
|
|
787
|
+
*/
|
|
788
|
+
function ingestSessionEventsIntoSnapshot(
|
|
789
|
+
snapshot: SessionSnapshot,
|
|
790
|
+
items: GatewaySessionEventItem[],
|
|
791
|
+
onTurnComplete?: (snap: SessionSnapshot) => void,
|
|
792
|
+
): void {
|
|
793
|
+
let currentTurnId: string | null = null;
|
|
794
|
+
let currentCreatedEvent: TurnCreatedEvent | null = null;
|
|
795
|
+
let currentContentEvents: TurnEvent[] = [];
|
|
796
|
+
let beforeCount = 0;
|
|
797
|
+
|
|
798
|
+
for (const item of items) {
|
|
799
|
+
const { turnId, event } = item;
|
|
800
|
+
|
|
801
|
+
if (event.type === "turn.created") {
|
|
802
|
+
currentTurnId = turnId;
|
|
803
|
+
currentCreatedEvent = event;
|
|
804
|
+
currentContentEvents = [];
|
|
805
|
+
beforeCount =
|
|
806
|
+
snapshot.fold.threads.get(ROOT_THREAD_ID)?.modelMessageIds.length ?? 0;
|
|
807
|
+
} else if (event.type === "turn.done") {
|
|
808
|
+
if (currentTurnId == null || currentCreatedEvent == null) {
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const afterBucket = snapshot.fold.threads.get(ROOT_THREAD_ID);
|
|
813
|
+
const rootModelMessageIds = (afterBucket?.modelMessageIds ?? []).slice(
|
|
814
|
+
beforeCount,
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
const sandboxEvent = currentContentEvents.find(
|
|
818
|
+
(ev): ev is Extract<TurnEvent, { type: "sandbox.created" }> =>
|
|
819
|
+
ev.type === "sandbox.created",
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
applyUserToolResponsesToFold(
|
|
823
|
+
snapshot.fold,
|
|
824
|
+
currentCreatedEvent.input ?? [],
|
|
825
|
+
);
|
|
826
|
+
snapshot.turns.push(
|
|
827
|
+
sessionEventsToSessionRecord(
|
|
828
|
+
currentTurnId,
|
|
829
|
+
currentCreatedEvent,
|
|
830
|
+
event,
|
|
831
|
+
rootModelMessageIds,
|
|
832
|
+
sandboxEvent?.sandboxId,
|
|
833
|
+
),
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
// Pass a new object reference so React's Object.is check in
|
|
837
|
+
// useState sees a changed value and schedules a re-render.
|
|
838
|
+
// The snapshot is mutated in place throughout this loop, so
|
|
839
|
+
// passing `snapshot` directly would make every tick look identical
|
|
840
|
+
// to React after the first setSnapshot call.
|
|
841
|
+
onTurnComplete?.(replaceSessionSnapshot(snapshot, {}));
|
|
842
|
+
|
|
843
|
+
currentTurnId = null;
|
|
844
|
+
currentCreatedEvent = null;
|
|
845
|
+
currentContentEvents = [];
|
|
846
|
+
} else {
|
|
847
|
+
if (currentTurnId != null) {
|
|
848
|
+
ingestTurnEvent(snapshot.fold, event as TurnEvent);
|
|
849
|
+
currentContentEvents.push(event as TurnEvent);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Builds a session snapshot using the session-level `listEvents` API.
|
|
857
|
+
*
|
|
858
|
+
* Compared to `buildSnapshotFromSession`, this makes a single paginated request
|
|
859
|
+
* for all history events instead of one per turn. Only `listTurns({ limit: 1 })`
|
|
860
|
+
* is called first to detect a currently-running turn (the session-level API does
|
|
861
|
+
* not return events for the running turn).
|
|
862
|
+
*
|
|
863
|
+
* `onProgress` is called after each complete turn is ingested so callers can
|
|
864
|
+
* update the UI progressively while the processing loop runs.
|
|
865
|
+
*/
|
|
866
|
+
export async function buildSnapshotFromSessionEvents(
|
|
867
|
+
session: AgentSession,
|
|
868
|
+
onProgress?: (snap: SessionSnapshot) => void,
|
|
869
|
+
): Promise<SessionSnapshot> {
|
|
870
|
+
// Detect a running turn with a single minimal listTurns call.
|
|
871
|
+
let runningTurn: Turn | undefined;
|
|
872
|
+
for await (const turn of await session.listTurns({ limit: 1 })) {
|
|
873
|
+
if ((turn as Turn).state?.status === "running") {
|
|
874
|
+
runningTurn = turn as Turn;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const items = await fetchAllSessionEvents(session);
|
|
879
|
+
|
|
880
|
+
const snapshot = createEmptySessionSnapshot();
|
|
881
|
+
ingestSessionEventsIntoSnapshot(snapshot, items, onProgress);
|
|
882
|
+
|
|
883
|
+
if (runningTurn == null) {
|
|
884
|
+
return snapshot;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Session listEvents excludes the running turn. Seed its user message so
|
|
888
|
+
// reconnect UI can show the in-flight bubble before subscribe yields.
|
|
889
|
+
const pendingUserText = extractTurnUserText(runningTurn.input);
|
|
890
|
+
return replaceSessionSnapshot(snapshot, {
|
|
891
|
+
runningTurn,
|
|
892
|
+
unstable_resume: true as const,
|
|
893
|
+
groupRootBaseline: computeGroupRootBaseline(snapshot.turns),
|
|
894
|
+
...(pendingUserText
|
|
895
|
+
? {
|
|
896
|
+
pendingUser: {
|
|
897
|
+
turnId: runningTurn.id,
|
|
898
|
+
content: extractTurnUserMessageContent(runningTurn.input),
|
|
899
|
+
createdAt: new Date(runningTurn.createdAt),
|
|
900
|
+
},
|
|
901
|
+
}
|
|
902
|
+
: {}),
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
734
906
|
function ingestTurnsIntoSnapshot(
|
|
735
907
|
snapshot: SessionSnapshot,
|
|
736
908
|
turns: Turn[],
|
|
@@ -775,25 +947,22 @@ export async function buildSnapshotFromSession(
|
|
|
775
947
|
session: AgentSession,
|
|
776
948
|
concurrency: number = DEFAULT_LIST_EVENTS_CONCURRENCY,
|
|
777
949
|
): Promise<SessionSnapshot> {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
950
|
+
// Completed history comes from session-level listEvents. The session API
|
|
951
|
+
// excludes the running turn — hydrate that turn via turn.listEvents so
|
|
952
|
+
// convertTurnsToThreadMessages still surfaces in-flight content.
|
|
953
|
+
const snapshot = await buildSnapshotFromSessionEvents(session);
|
|
954
|
+
if (snapshot.runningTurn == null) {
|
|
955
|
+
return snapshot;
|
|
781
956
|
}
|
|
782
|
-
turns.reverse();
|
|
783
957
|
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const runningTurn = ingestTurnsIntoSnapshot(snapshot, turns, eventArrays);
|
|
958
|
+
const turn = snapshot.runningTurn;
|
|
959
|
+
const eventArrays = await fetchAllTurnEventsWithConcurrency([turn], concurrency);
|
|
960
|
+
ingestTurnsIntoSnapshot(snapshot, [turn], eventArrays);
|
|
788
961
|
|
|
789
962
|
return replaceSessionSnapshot(snapshot, {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
unstable_resume: true as const,
|
|
794
|
-
groupRootBaseline: computeGroupRootBaseline(snapshot.turns),
|
|
795
|
-
}
|
|
796
|
-
: {}),
|
|
963
|
+
runningTurn: turn,
|
|
964
|
+
unstable_resume: true as const,
|
|
965
|
+
groupRootBaseline: computeGroupRootBaseline(snapshot.turns),
|
|
797
966
|
});
|
|
798
967
|
}
|
|
799
968
|
|
|
@@ -803,11 +972,7 @@ export async function buildSnapshotBeforeTurn(
|
|
|
803
972
|
beforeTurnId: string,
|
|
804
973
|
concurrency: number = DEFAULT_LIST_EVENTS_CONCURRENCY,
|
|
805
974
|
): Promise<SessionSnapshot> {
|
|
806
|
-
const turns
|
|
807
|
-
for await (const turn of await session.listTurns()) {
|
|
808
|
-
turns.push(turn);
|
|
809
|
-
}
|
|
810
|
-
turns.reverse();
|
|
975
|
+
const turns = await listSessionTurnsOrdered(session);
|
|
811
976
|
|
|
812
977
|
const beforeIndex = turns.findIndex((turn) => turn.id === beforeTurnId);
|
|
813
978
|
if (beforeIndex === -1) {
|
|
@@ -821,7 +986,7 @@ export async function buildSnapshotBeforeTurn(
|
|
|
821
986
|
export async function buildSnapshotBeforeTurnIndex(
|
|
822
987
|
session: AgentSession,
|
|
823
988
|
turnIndex: number,
|
|
824
|
-
|
|
989
|
+
_concurrency: number = DEFAULT_LIST_EVENTS_CONCURRENCY,
|
|
825
990
|
orderedTurns?: Turn[],
|
|
826
991
|
): Promise<SessionSnapshot> {
|
|
827
992
|
if (turnIndex <= 0) {
|
|
@@ -830,16 +995,16 @@ export async function buildSnapshotBeforeTurnIndex(
|
|
|
830
995
|
|
|
831
996
|
const turns = orderedTurns ?? (await listSessionTurnsOrdered(session));
|
|
832
997
|
const turnsToInclude = turns.slice(0, turnIndex);
|
|
833
|
-
|
|
998
|
+
const lastTurnId = turnsToInclude.at(-1)?.id;
|
|
999
|
+
if (lastTurnId == null) {
|
|
834
1000
|
return createEmptySessionSnapshot();
|
|
835
1001
|
}
|
|
836
1002
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
);
|
|
1003
|
+
// Anchor the session events window at the newest included turn so the
|
|
1004
|
+
// ancestor chain matches `[turns[0], …, turns[turnIndex - 1]]`.
|
|
1005
|
+
const items = await fetchAllSessionEvents(session, { lastTurnId });
|
|
841
1006
|
const snapshot = createEmptySessionSnapshot();
|
|
842
|
-
|
|
1007
|
+
ingestSessionEventsIntoSnapshot(snapshot, items);
|
|
843
1008
|
return snapshot;
|
|
844
1009
|
}
|
|
845
1010
|
|
package/src/createSubAgent.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { ToolCall } from "truefoundry-gateway-sdk/agents";
|
|
2
2
|
|
|
3
|
-
export function isCreateSubAgentToolCall(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
export function isCreateSubAgentToolCall(
|
|
4
|
+
toolCall: Pick<ToolCall, "toolInfo" | "function">,
|
|
5
|
+
): boolean {
|
|
6
|
+
// Gateway may attach truefoundry-system toolInfo on persisted turns, but streamed
|
|
7
|
+
// model.message tool calls often only carry function.name. Without the fallback,
|
|
8
|
+
// foldPeerThreads never nests child threads under the spawning tool call.
|
|
9
|
+
if (
|
|
10
|
+
toolCall.toolInfo?.type === "truefoundry-system" &&
|
|
11
|
+
toolCall.toolInfo?.name === "create_sub_agent"
|
|
12
|
+
) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return toolCall.function?.name === "create_sub_agent";
|
|
8
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { mergeAgentSpec } from "./agentSpec.js";
|
|
3
|
+
import { mergeAgentSpec } from "./private/agentSpec.js";
|
|
4
4
|
import { resolveTrueFoundryAgentConfig } from "./types.js";
|
|
5
5
|
|
|
6
6
|
describe("resolveTrueFoundryAgentConfig", () => {
|
|
@@ -129,6 +129,62 @@ describe("foldPeerThreads", () => {
|
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
it("attaches sub-agent artifact from thread.created before child model messages arrive", () => {
|
|
133
|
+
const state = new PeerThreadFoldState();
|
|
134
|
+
|
|
135
|
+
ingestTurnEvent(
|
|
136
|
+
state,
|
|
137
|
+
modelMessage({
|
|
138
|
+
id: "root-msg",
|
|
139
|
+
threadId: ROOT_THREAD_ID,
|
|
140
|
+
toolCalls: [
|
|
141
|
+
{
|
|
142
|
+
id: "spawn-1",
|
|
143
|
+
type: "function",
|
|
144
|
+
function: { name: "create_sub_agent", arguments: "{}" },
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
ingestTurnEvent(
|
|
151
|
+
state,
|
|
152
|
+
threadCreated({
|
|
153
|
+
id: "t-created",
|
|
154
|
+
threadId: "child-1",
|
|
155
|
+
title: "gateway-usage-7d",
|
|
156
|
+
agentInfo: {
|
|
157
|
+
type: "dynamic",
|
|
158
|
+
name: "gateway-usage-7d",
|
|
159
|
+
input: "run queries",
|
|
160
|
+
},
|
|
161
|
+
parent: { threadId: ROOT_THREAD_ID, toolCallId: "spawn-1" },
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const parts = buildRootAssistantContent(state);
|
|
166
|
+
const spawn = parts.find((part) => part.type === "tool-call");
|
|
167
|
+
expect(spawn?.type).toBe("tool-call");
|
|
168
|
+
if (spawn?.type !== "tool-call") {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
expect(spawn.messages).toBeUndefined();
|
|
173
|
+
expect(spawn.artifact).toEqual({
|
|
174
|
+
subAgents: [
|
|
175
|
+
{
|
|
176
|
+
threadId: "child-1",
|
|
177
|
+
title: "gateway-usage-7d",
|
|
178
|
+
agentInfo: {
|
|
179
|
+
type: "dynamic",
|
|
180
|
+
name: "gateway-usage-7d",
|
|
181
|
+
input: "run queries",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
132
188
|
it("classifies stream events by thread id", () => {
|
|
133
189
|
const state = new PeerThreadFoldState();
|
|
134
190
|
expect(
|
package/src/foldPeerThreads.ts
CHANGED
|
@@ -389,11 +389,17 @@ function attachSubAgentMessages(
|
|
|
389
389
|
});
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
|
-
if (messages.length === 0) {
|
|
392
|
+
if (messages.length === 0 && subAgents.length === 0) {
|
|
393
393
|
return part;
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
const artifact: SubAgentArtifact = { subAgents };
|
|
397
|
+
// thread.created (title, agentInfo) arrives before the child's first model.message.
|
|
398
|
+
// Attach artifact-only so UI can render the sub-agent header immediately; see README
|
|
399
|
+
// SubAgentArtifact / MessagePartPrimitive.Messages in agent-ui ToolCallContainer.
|
|
400
|
+
if (messages.length === 0) {
|
|
401
|
+
return { ...part, artifact };
|
|
402
|
+
}
|
|
397
403
|
return { ...part, messages, artifact };
|
|
398
404
|
});
|
|
399
405
|
}
|
package/src/hooks.ts
CHANGED
|
@@ -98,6 +98,12 @@ export const useTrueFoundryCancel = () => {
|
|
|
98
98
|
return () => trueFoundryExtras.get(aui).cancel();
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
+
/** Returns a function to reload (retry) the current session from any render context. */
|
|
102
|
+
export const useTrueFoundryReload = () => {
|
|
103
|
+
const aui = useAui();
|
|
104
|
+
return () => trueFoundryExtras.get(aui).reload();
|
|
105
|
+
};
|
|
106
|
+
|
|
101
107
|
/** Returns a function to reset (re-submit) a user turn from any render context. */
|
|
102
108
|
export const useTrueFoundryResetFromTurn = () => {
|
|
103
109
|
const aui = useAui();
|
package/src/index.ts
CHANGED
|
@@ -13,11 +13,11 @@ export {
|
|
|
13
13
|
export type { ConvertTurnsResult, UserMessageContent } from "./convertTurnMessages.js";
|
|
14
14
|
export { ROOT_THREAD_ID } from "./constants.js";
|
|
15
15
|
export type { UseTrueFoundryAgentRuntimeOptions, NamedAgentConfig, DraftAgentConfig, TrueFoundryAgentConfig } from "./types.js";
|
|
16
|
-
export type { AgentSpec, AgentSpecUpdate, DraftSession } from "./agentSpec.js";
|
|
17
|
-
export { mergeAgentSpec, draftSessionTitle } from "./agentSpec.js";
|
|
18
|
-
export { createTrueFoundryDraftThreadListAdapter } from "./truefoundryDraftThreadListAdapter.js";
|
|
19
|
-
export { createDraftSessionBridge } from "./draftSessionBridge.js";
|
|
20
|
-
export type { DraftSessionBridge } from "./draftSessionBridge.js";
|
|
16
|
+
export type { AgentSpec, AgentSpecUpdate, DraftSession } from "./private/agentSpec.js";
|
|
17
|
+
export { mergeAgentSpec, draftSessionTitle } from "./private/agentSpec.js";
|
|
18
|
+
export { createTrueFoundryDraftThreadListAdapter } from "./private/truefoundryDraftThreadListAdapter.js";
|
|
19
|
+
export { createDraftSessionBridge } from "./private/draftSessionBridge.js";
|
|
20
|
+
export type { DraftSessionBridge } from "./private/draftSessionBridge.js";
|
|
21
21
|
export {
|
|
22
22
|
useTrueFoundryAgentSpec,
|
|
23
23
|
useTrueFoundryUpdateAgentSpec,
|
|
@@ -47,6 +47,7 @@ export {
|
|
|
47
47
|
useTrueFoundryDownloadSandboxFile,
|
|
48
48
|
useTrueFoundryCancel,
|
|
49
49
|
useTrueFoundryResetFromTurn,
|
|
50
|
+
useTrueFoundryReload,
|
|
50
51
|
} from "./hooks.js";
|
|
51
52
|
export {
|
|
52
53
|
collectApprovalInputs,
|
|
@@ -11,11 +11,11 @@ vi.mock("./convertTurnMessages.js", async (importOriginal) => {
|
|
|
11
11
|
const actual = await importOriginal<typeof import("./convertTurnMessages.js")>();
|
|
12
12
|
return {
|
|
13
13
|
...actual,
|
|
14
|
-
|
|
14
|
+
buildSnapshotFromSessionEvents: vi.fn(),
|
|
15
15
|
};
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { buildSnapshotFromSessionEvents } from "./convertTurnMessages.js";
|
|
19
19
|
import { loadSessionSnapshot } from "./loadSessionSnapshot.js";
|
|
20
20
|
import { getSession } from "./sessions.js";
|
|
21
21
|
|
|
@@ -28,7 +28,7 @@ describe("loadSessionSnapshot", () => {
|
|
|
28
28
|
|
|
29
29
|
it("deduplicates concurrent loads for the same session", async () => {
|
|
30
30
|
vi.mocked(getSession).mockResolvedValue({} as never);
|
|
31
|
-
vi.mocked(
|
|
31
|
+
vi.mocked(buildSnapshotFromSessionEvents).mockResolvedValue(
|
|
32
32
|
createEmptySessionSnapshot(),
|
|
33
33
|
);
|
|
34
34
|
|
|
@@ -39,12 +39,12 @@ describe("loadSessionSnapshot", () => {
|
|
|
39
39
|
|
|
40
40
|
expect(first).toBe(second);
|
|
41
41
|
expect(getSession).toHaveBeenCalledTimes(1);
|
|
42
|
-
expect(
|
|
42
|
+
expect(buildSnapshotFromSessionEvents).toHaveBeenCalledTimes(1);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
it("allows a new load after the previous one settles", async () => {
|
|
46
46
|
vi.mocked(getSession).mockResolvedValue({} as never);
|
|
47
|
-
vi.mocked(
|
|
47
|
+
vi.mocked(buildSnapshotFromSessionEvents).mockResolvedValue(
|
|
48
48
|
createEmptySessionSnapshot(),
|
|
49
49
|
);
|
|
50
50
|
|
|
@@ -52,6 +52,21 @@ describe("loadSessionSnapshot", () => {
|
|
|
52
52
|
await loadSessionSnapshot(mockClient, "session-1");
|
|
53
53
|
|
|
54
54
|
expect(getSession).toHaveBeenCalledTimes(2);
|
|
55
|
-
expect(
|
|
55
|
+
expect(buildSnapshotFromSessionEvents).toHaveBeenCalledTimes(2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("forwards onProgress to buildSnapshotFromSessionEvents", async () => {
|
|
59
|
+
vi.mocked(getSession).mockResolvedValue({} as never);
|
|
60
|
+
vi.mocked(buildSnapshotFromSessionEvents).mockResolvedValue(
|
|
61
|
+
createEmptySessionSnapshot(),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const onProgress = vi.fn();
|
|
65
|
+
await loadSessionSnapshot(mockClient, "session-1", undefined, onProgress);
|
|
66
|
+
|
|
67
|
+
expect(buildSnapshotFromSessionEvents).toHaveBeenCalledWith(
|
|
68
|
+
{},
|
|
69
|
+
onProgress,
|
|
70
|
+
);
|
|
56
71
|
});
|
|
57
72
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AgentSessionClient } from "truefoundry-gateway-sdk/agents";
|
|
2
|
-
import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { buildSnapshotFromSessionEvents } from "./convertTurnMessages.js";
|
|
5
4
|
import { getSession, type GetSessionOptions } from "./sessions.js";
|
|
6
5
|
import type { SessionSnapshot } from "./sessionSnapshot.js";
|
|
7
6
|
|
|
@@ -10,20 +9,25 @@ const inflightBySessionId = new Map<string, Promise<SessionSnapshot>>();
|
|
|
10
9
|
/**
|
|
11
10
|
* Loads a session snapshot once per concurrent burst for a given session id.
|
|
12
11
|
* React Strict Mode and overlapping fetch/load paths share the same in-flight
|
|
13
|
-
* request instead of duplicating getSession +
|
|
12
|
+
* request instead of duplicating getSession + listEvents calls.
|
|
13
|
+
*
|
|
14
|
+
* `onProgress` is called after each complete turn is ingested so the caller
|
|
15
|
+
* can update the UI progressively while history is being processed.
|
|
14
16
|
*/
|
|
15
17
|
export function loadSessionSnapshot(
|
|
16
18
|
client: AgentSessionClient,
|
|
17
19
|
sessionId: string,
|
|
18
|
-
concurrency?: number,
|
|
19
20
|
sessionOptions?: GetSessionOptions,
|
|
21
|
+
onProgress?: (snap: SessionSnapshot) => void,
|
|
20
22
|
): Promise<SessionSnapshot> {
|
|
21
23
|
const cacheKey =
|
|
22
24
|
sessionOptions?.draftGateway != null ? `draft:${sessionId}` : sessionId;
|
|
23
25
|
let inflight = inflightBySessionId.get(cacheKey);
|
|
24
26
|
if (inflight == null) {
|
|
25
27
|
inflight = getSession(client, sessionId, sessionOptions)
|
|
26
|
-
.then((session) =>
|
|
28
|
+
.then((session) =>
|
|
29
|
+
buildSnapshotFromSessionEvents(session, onProgress),
|
|
30
|
+
)
|
|
27
31
|
.finally(() => {
|
|
28
32
|
if (inflightBySessionId.get(cacheKey) === inflight) {
|
|
29
33
|
inflightBySessionId.delete(cacheKey);
|
package/src/{truefoundryDraftThreadListAdapter.ts → private/truefoundryDraftThreadListAdapter.ts}
RENAMED
|
@@ -2,7 +2,7 @@ import type { RemoteThreadListAdapter } from "@assistant-ui/core";
|
|
|
2
2
|
import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
|
|
3
3
|
|
|
4
4
|
import { draftSessionTitle, type AgentSpec } from "./agentSpec.js";
|
|
5
|
-
import { sessionListStartTimestamp } from "
|
|
5
|
+
import { sessionListStartTimestamp } from "../sessionListStartTimestamp.js";
|
|
6
6
|
|
|
7
7
|
const THREAD_LIST_PAGE_SIZE = 20;
|
|
8
8
|
|
package/src/sessionSnapshot.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
McpAuthRequiredEvent,
|
|
3
|
+
Turn,
|
|
4
|
+
TurnCreatedEvent,
|
|
5
|
+
TurnDoneEvent,
|
|
6
|
+
TurnInputItem,
|
|
7
|
+
} from "truefoundry-gateway-sdk/agents";
|
|
2
8
|
|
|
3
9
|
import { extractTurnUserText } from "./extractTurnUserText.js";
|
|
4
10
|
import { PeerThreadFoldState } from "./foldPeerThreads.js";
|
|
@@ -81,6 +87,26 @@ export function turnToSessionRecord(turn: Turn): SessionTurnRecord {
|
|
|
81
87
|
};
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
/** Builds a SessionTurnRecord from session-level TurnCreatedEvent + TurnDoneEvent data. */
|
|
91
|
+
export function sessionEventsToSessionRecord(
|
|
92
|
+
turnId: string,
|
|
93
|
+
createdEvent: TurnCreatedEvent,
|
|
94
|
+
doneEvent: TurnDoneEvent,
|
|
95
|
+
rootModelMessageIds: readonly string[],
|
|
96
|
+
sandboxId?: string,
|
|
97
|
+
): SessionTurnRecord {
|
|
98
|
+
const userText = extractTurnUserText(createdEvent.input);
|
|
99
|
+
return {
|
|
100
|
+
id: turnId,
|
|
101
|
+
...(userText ? { userText } : {}),
|
|
102
|
+
createdAt: createdEvent.createdAt,
|
|
103
|
+
state: doneEvent.state,
|
|
104
|
+
input: createdEvent.input,
|
|
105
|
+
rootModelMessageIds,
|
|
106
|
+
...(sandboxId != null ? { sandboxId } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
84
110
|
/** Returns a new snapshot wrapper; fold maps may be mutated in place before calling. */
|
|
85
111
|
export function replaceSessionSnapshot(
|
|
86
112
|
snapshot: SessionSnapshot,
|
package/src/sessions.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
} from "truefoundry-gateway-sdk/agents";
|
|
5
5
|
import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
|
|
6
6
|
|
|
7
|
-
import { bindDraftAgentSession } from "./bindDraftAgentSession.js";
|
|
7
|
+
import { bindDraftAgentSession } from "./private/bindDraftAgentSession.js";
|
|
8
8
|
|
|
9
9
|
const inflightBySessionId = new Map<string, Promise<AgentSession>>();
|
|
10
10
|
|
package/src/truefoundryExtras.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRuntimeExtras } from "@assistant-ui/core/internal";
|
|
2
2
|
import type { McpAuthRequiredEvent } from "truefoundry-gateway-sdk/agents";
|
|
3
3
|
|
|
4
|
-
import type { AgentSpec, AgentSpecUpdate } from "./agentSpec.js";
|
|
4
|
+
import type { AgentSpec, AgentSpecUpdate } from "./private/agentSpec.js";
|
|
5
5
|
import type { PendingApproval, PendingToolResponse } from "./collectPending.js";
|
|
6
6
|
import type { RespondToToolApprovalOptions } from "./toolApproval.js";
|
|
7
7
|
import type { RespondToToolResponseOptions } from "./toolResponse.js";
|
|
@@ -27,6 +27,7 @@ export type TrueFoundryRuntimeExtras = {
|
|
|
27
27
|
downloadSandboxFile: (path: string) => Promise<Blob>;
|
|
28
28
|
cancel: () => Promise<void>;
|
|
29
29
|
resetFromTurn: (turnId: string) => Promise<void>;
|
|
30
|
+
reload: () => void;
|
|
30
31
|
draft: TrueFoundryDraftRuntimeExtras | null;
|
|
31
32
|
};
|
|
32
33
|
|
package/src/types.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
import type { AgentSessionClient } from "truefoundry-gateway-sdk/agents";
|
|
10
10
|
import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
|
|
11
11
|
|
|
12
|
-
import type { AgentSpec } from "./agentSpec.js";
|
|
12
|
+
import type { AgentSpec } from "./private/agentSpec.js";
|
|
13
13
|
|
|
14
14
|
export type NamedAgentConfig = {
|
|
15
15
|
mode: "named";
|