@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +22 -17
  3. package/dist/chunk-CXBZ6WLZ.js +636 -0
  4. package/dist/chunk-CXBZ6WLZ.js.map +1 -0
  5. package/dist/index.d.ts +20 -24
  6. package/dist/index.js +269 -166
  7. package/dist/index.js.map +1 -1
  8. package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +82 -39
  9. package/dist/plugins/truefoundry-agent-server-adapter/index.js +3 -1
  10. package/dist/server/index.d.ts +2 -2
  11. package/dist/{types-BfiFf8O1.d.ts → types-B_z-FsDS.d.ts} +208 -10
  12. package/package.json +1 -1
  13. package/src/convertTurnMessages.ts +4 -0
  14. package/src/{private → draft}/agentSpec.ts +14 -17
  15. package/src/{private → draft}/draftSessionBridge.ts +1 -2
  16. package/src/{private → draft}/truefoundryDraftThreadListAdapter.test.ts +1 -1
  17. package/src/{private → draft}/truefoundryDraftThreadListAdapter.ts +6 -2
  18. package/src/{private → draft}/useDraftAgentSpec.ts +16 -5
  19. package/src/draftAgentConfig.test.ts +2 -1
  20. package/src/harness.temp.ts +85 -0
  21. package/src/index.ts +43 -7
  22. package/src/plugins/truefoundry-agent-server-adapter/README.md +83 -44
  23. package/src/plugins/truefoundry-agent-server-adapter/chatServer.ts +365 -0
  24. package/src/plugins/truefoundry-agent-server-adapter/cp.test.ts +444 -0
  25. package/src/plugins/truefoundry-agent-server-adapter/cp.ts +482 -0
  26. package/src/plugins/truefoundry-agent-server-adapter/createTrueFoundryAgentUIServer.ts +94 -0
  27. package/src/plugins/truefoundry-agent-server-adapter/guards.ts +1 -1
  28. package/src/plugins/truefoundry-agent-server-adapter/index.ts +20 -351
  29. package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.test.ts +85 -0
  30. package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.ts +84 -0
  31. package/src/plugins/truefoundry-agent-server-adapter/types.ts +7 -5
  32. package/src/server/index.ts +29 -0
  33. package/src/server/types.ts +264 -12
  34. package/src/streamTurn.test.ts +27 -27
  35. package/src/streamTurn.ts +2 -2
  36. package/src/truefoundryExtras.ts +4 -1
  37. package/src/truefoundryOwnedSessionsThreadListAdapter.ts +5 -2
  38. package/src/truefoundryThreadListAdapter.test.ts +22 -0
  39. package/src/truefoundryThreadListAdapter.ts +4 -1
  40. package/src/types.ts +1 -2
  41. package/src/useTrueFoundryAgentMessages.test.tsx +262 -2
  42. package/src/useTrueFoundryAgentMessages.ts +284 -176
  43. package/src/useTrueFoundryAgentRuntime.ts +31 -21
  44. package/dist/chunk-Q2SHKMLM.js +0 -270
  45. package/dist/chunk-Q2SHKMLM.js.map +0 -1
  46. /package/src/{private → draft}/useDraftAgentSpec.test.tsx +0 -0
@@ -0,0 +1,365 @@
1
+ import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";
2
+ import type { AgentSession } from "truefoundry-gateway-sdk/agents";
3
+ import { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
4
+ import type { AgentDraftSession } from "truefoundry-gateway-sdk/agents/private";
5
+ import type {
6
+ AgentChatServer,
7
+ ListResult,
8
+ TurnInputItem,
9
+ PreviousTurnIdInput,
10
+ UpdateSessionRequest,
11
+ } from "../../server/types.js";
12
+ import type {
13
+ SessionEventItem,
14
+ TurnEvent,
15
+ TurnStreamData,
16
+ } from "../../server/events.js";
17
+ import { normalizeAgentSpecForGateway } from "./normalizeAgentSpec.js";
18
+ import type {
19
+ TfyAgentSpec,
20
+ TfyCreateSessionRequest,
21
+ TfyListSessionsParams,
22
+ TfySession,
23
+ TfyTurn,
24
+ TfyTurnState,
25
+ } from "./types.js";
26
+
27
+ type GwSession = AgentSession | AgentDraftSession;
28
+
29
+ export type CreateTrueFoundryChatServerOptions = {
30
+ apiKey: string;
31
+ baseUrl: string;
32
+ /** Optional override — otherwise constructed from apiKey/baseUrl. */
33
+ client?: AgentSessionClient;
34
+ privateClient?: PrivateAgentSessionClient;
35
+ deleteSession?: (req: { sessionId: string }) => Promise<void>;
36
+ };
37
+
38
+ /**
39
+ * Only the spec is generic. Session/Turn/list-params are the concrete Tfy*
40
+ * types because the adapter builds them as fixed object literals — a generic
41
+ * there would type fields that nothing ever populates. The spec is safe: the
42
+ * gateway SDK serializes with `unrecognizedObjectKeys: "passthrough"`, so
43
+ * host-added spec fields survive the round trip.
44
+ */
45
+ export type TrueFoundryChatServer<TSpec extends TfyAgentSpec = TfyAgentSpec> =
46
+ AgentChatServer<
47
+ TSpec,
48
+ TfySession<TSpec>,
49
+ TfyCreateSessionRequest<TSpec>,
50
+ TfyListSessionsParams,
51
+ UpdateSessionRequest<TSpec>,
52
+ TfyTurn
53
+ > & {
54
+ /** Escape hatch for hosts that still need raw gateway clients. */
55
+ getGatewayClients(): {
56
+ client: AgentSessionClient;
57
+ privateClient: PrivateAgentSessionClient;
58
+ };
59
+ };
60
+
61
+ /** Gateway SDK errors carry the HTTP status as `statusCode`. */
62
+ function isNotFound(error: unknown): boolean {
63
+ return (
64
+ typeof error === "object" &&
65
+ error !== null &&
66
+ (error as { statusCode?: unknown }).statusCode === 404
67
+ );
68
+ }
69
+
70
+ function isDraft(session: GwSession): session is AgentDraftSession {
71
+ return (session as AgentDraftSession).type === "session/draft";
72
+ }
73
+
74
+ function toSession<TSpec extends TfyAgentSpec>(raw: GwSession): TfySession<TSpec> {
75
+ const mutable = isDraft(raw);
76
+ return {
77
+ id: raw.id,
78
+ title: raw.title,
79
+ agentName: raw.agentName,
80
+ ...(mutable ? { agentSpec: raw.agentSpec as TSpec } : {}),
81
+ isMutable: mutable,
82
+ createdBySubject: raw.createdBySubject,
83
+ createdAt: raw.createdAt,
84
+ updatedAt: raw.updatedAt,
85
+ };
86
+ }
87
+
88
+ function toTurn(raw: {
89
+ id: string;
90
+ sessionId: string;
91
+ previousTurnId?: string | null;
92
+ input?: TurnInputItem[];
93
+ state: unknown;
94
+ createdBySubject: TfyTurn["createdBySubject"];
95
+ createdAt: string;
96
+ }): TfyTurn {
97
+ return {
98
+ id: raw.id,
99
+ sessionId: raw.sessionId,
100
+ previousTurnId: raw.previousTurnId,
101
+ input: raw.input,
102
+ state: raw.state as TfyTurnState,
103
+ createdBySubject: raw.createdBySubject,
104
+ createdAt: raw.createdAt,
105
+ };
106
+ }
107
+
108
+ async function toListResult<TIn, TOut>(
109
+ page: {
110
+ data: TIn[];
111
+ response?: { pagination?: { nextPageToken?: string } };
112
+ hasNextPage?: () => boolean;
113
+ },
114
+ map: (item: TIn) => TOut,
115
+ ): Promise<ListResult<TOut>> {
116
+ const nextPageToken = page.response?.pagination?.nextPageToken;
117
+ return {
118
+ data: page.data.map(map),
119
+ ...(nextPageToken != null && nextPageToken !== ""
120
+ ? { nextPageToken }
121
+ : {}),
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Wraps TrueFoundry gateway clients into a flat `AgentChatServer`.
127
+ * Named vs draft routing is fully internal — an in-memory session-type cache
128
+ * (populated by createSession/listSessions) determines which gateway client
129
+ * to call, falling back to a one-time probe for ids seen only in a URL.
130
+ */
131
+ export function createTrueFoundryChatServer<
132
+ TSpec extends TfyAgentSpec = TfyAgentSpec,
133
+ >(
134
+ opts: CreateTrueFoundryChatServerOptions,
135
+ ): TrueFoundryChatServer<TSpec> {
136
+ const gatewayOpts = { apiKey: opts.apiKey, baseUrl: opts.baseUrl };
137
+ const client = opts.client ?? new AgentSessionClient(gatewayOpts);
138
+ const privateClient =
139
+ opts.privateClient ?? new PrivateAgentSessionClient(gatewayOpts);
140
+
141
+ const sessionTypeCache = new Map<string, boolean>();
142
+ const sessionTypeProbes = new Map<string, Promise<boolean>>();
143
+
144
+ function cacheSessionType(session: {
145
+ id: string;
146
+ isMutable: boolean;
147
+ }): void {
148
+ sessionTypeCache.set(session.id, session.isMutable);
149
+ }
150
+
151
+ /**
152
+ * Resolving a session id straight from a URL (page reload / shared link)
153
+ * reaches the gateway before createSession or listSessions has cached the
154
+ * type, so discover it by probing the draft endpoint and falling back to
155
+ * the conversation endpoint. Concurrent callers share one probe.
156
+ */
157
+ async function probeSessionType(sessionId: string): Promise<boolean> {
158
+ const inflight = sessionTypeProbes.get(sessionId);
159
+ if (inflight != null) {
160
+ return inflight;
161
+ }
162
+ const probe = (async () => {
163
+ try {
164
+ await privateClient.getDraftSession({ draftSessionId: sessionId });
165
+ return true;
166
+ } catch (error) {
167
+ // Only a miss proves the id isn't a draft. Auth or transient
168
+ // failures would otherwise be retried as a conversation session
169
+ // and mislabel a real draft as named.
170
+ if (!isNotFound(error)) {
171
+ throw error;
172
+ }
173
+ await client.getSession({ sessionId });
174
+ return false;
175
+ }
176
+ })();
177
+ sessionTypeProbes.set(sessionId, probe);
178
+ try {
179
+ const isMutable = await probe;
180
+ sessionTypeCache.set(sessionId, isMutable);
181
+ return isMutable;
182
+ } finally {
183
+ sessionTypeProbes.delete(sessionId);
184
+ }
185
+ }
186
+
187
+ async function getSessionObj(sessionId: string): Promise<GwSession> {
188
+ const isMutable =
189
+ sessionTypeCache.get(sessionId) ?? (await probeSessionType(sessionId));
190
+ return isMutable
191
+ ? privateClient.getDraftSession({ draftSessionId: sessionId })
192
+ : client.getSession({ sessionId });
193
+ }
194
+
195
+ const server: TrueFoundryChatServer<TSpec> = {
196
+ async createSession(req) {
197
+ if (req.agentSpec != null) {
198
+ const draft = await privateClient.createDraftSession({
199
+ agentSpec: normalizeAgentSpecForGateway(req.agentSpec),
200
+ ...(req.agentName != null ? { agentName: req.agentName } : {}),
201
+ ...(req.tfyMetadata != null
202
+ ? { tfyMetadata: req.tfyMetadata }
203
+ : {}),
204
+ });
205
+ const session = toSession<TSpec>(draft);
206
+ cacheSessionType(session);
207
+ return session;
208
+ }
209
+ if (req.agentName != null) {
210
+ const named = await client.createSession({
211
+ agentName: req.agentName,
212
+ ...(req.tfyMetadata != null
213
+ ? { tfyMetadata: req.tfyMetadata }
214
+ : {}),
215
+ });
216
+ const session = toSession<TSpec>(named);
217
+ cacheSessionType(session);
218
+ return session;
219
+ }
220
+ throw new Error("createSession requires agentName and/or agentSpec");
221
+ },
222
+
223
+ async listSessions(req) {
224
+ const page = await privateClient.listOwnedSessions({
225
+ limit: req?.limit,
226
+ order: req?.order,
227
+ pageToken: req?.pageToken,
228
+ startTimestamp: req?.startTimestamp,
229
+ endTimestamp: req?.endTimestamp,
230
+ ...(req?.agentName != null ? { agentName: req.agentName } : {}),
231
+ });
232
+ const result = await toListResult(page, (s) => toSession<TSpec>(s));
233
+ for (const session of result.data) {
234
+ cacheSessionType(session);
235
+ }
236
+ return result;
237
+ },
238
+
239
+ async getSession({ sessionId }) {
240
+ const raw = await getSessionObj(sessionId);
241
+ const session = toSession<TSpec>(raw);
242
+ cacheSessionType(session);
243
+ return session;
244
+ },
245
+
246
+ async updateSession(req) {
247
+ const raw = await getSessionObj(req.sessionId);
248
+ if (!isDraft(raw)) {
249
+ throw new Error(
250
+ "updateSession: session is not mutable (isMutable=false)",
251
+ );
252
+ }
253
+ if (req.agentSpec != null) {
254
+ await raw.update({
255
+ agentSpec: normalizeAgentSpecForGateway(req.agentSpec),
256
+ });
257
+ }
258
+ return toSession<TSpec>(raw);
259
+ },
260
+
261
+ createTurn(req: {
262
+ sessionId: string;
263
+ input?: TurnInputItem[];
264
+ previousTurnId?: PreviousTurnIdInput;
265
+ abortSignal?: AbortSignal;
266
+ headers?: Record<string, string>;
267
+ }): AsyncIterable<TurnStreamData> {
268
+ return (async function* () {
269
+ const session = await getSessionObj(req.sessionId);
270
+ const prepared = session.prepareTurn({
271
+ input: req.input,
272
+ previousTurnId: req.previousTurnId ?? "auto",
273
+ });
274
+ yield* prepared.execute(
275
+ { stream: true },
276
+ {
277
+ ...(req.abortSignal != null
278
+ ? { abortSignal: req.abortSignal }
279
+ : {}),
280
+ ...(req.headers != null ? { headers: req.headers } : {}),
281
+ },
282
+ ) as AsyncIterable<TurnStreamData>;
283
+ })();
284
+ },
285
+
286
+ async cancelSession({ sessionId }) {
287
+ await (await getSessionObj(sessionId)).cancel();
288
+ },
289
+
290
+ async deleteSession({ sessionId }) {
291
+ if (opts.deleteSession == null) {
292
+ throw new Error(
293
+ "deleteSession is not on the gateway SDK. Pass deleteSession to createTrueFoundryChatServer.",
294
+ );
295
+ }
296
+ await opts.deleteSession({ sessionId });
297
+ },
298
+
299
+ // The runtime's signature offers `order`, but the gateway's listTurns
300
+ // takes no such param — forwarding it silently did nothing.
301
+ async listTurns({ sessionId, limit, pageToken }) {
302
+ const raw = await getSessionObj(sessionId);
303
+ const page = await raw.listTurns({
304
+ ...(limit != null ? { limit } : {}),
305
+ ...(pageToken != null ? { pageToken } : {}),
306
+ });
307
+ return toListResult(page, (turn) => toTurn(turn));
308
+ },
309
+
310
+ async getTurn({ sessionId, turnId }) {
311
+ const raw = await getSessionObj(sessionId);
312
+ return toTurn(await raw.getTurn({ turnId }));
313
+ },
314
+
315
+ async listEvents({ sessionId, pageToken, lastTurnId, limit }) {
316
+ const raw = await getSessionObj(sessionId);
317
+ const page = await raw.listEvents({
318
+ ...(limit != null ? { limit } : {}),
319
+ ...(pageToken != null ? { pageToken } : {}),
320
+ ...(lastTurnId != null ? { lastTurnId } : {}),
321
+ });
322
+ return toListResult(
323
+ page,
324
+ (item) => item as SessionEventItem,
325
+ );
326
+ },
327
+
328
+ async listTurnEvents({ sessionId, turnId, limit, pageToken, order }) {
329
+ const raw = await getSessionObj(sessionId);
330
+ const turn = await raw.getTurn({ turnId });
331
+ const page = await turn.listEvents({
332
+ ...(limit != null ? { limit } : {}),
333
+ ...(pageToken != null ? { pageToken } : {}),
334
+ ...(order != null ? { order } : {}),
335
+ });
336
+ return toListResult(page, (event) => event as TurnEvent);
337
+ },
338
+
339
+ async *subscribeToTurn({
340
+ sessionId,
341
+ turnId,
342
+ afterSequenceNumber,
343
+ abortSignal,
344
+ }) {
345
+ const raw = await getSessionObj(sessionId);
346
+ const turn = await raw.getTurn({ turnId });
347
+ yield* turn.stream(
348
+ afterSequenceNumber != null ? { afterSequenceNumber } : {},
349
+ abortSignal != null ? { abortSignal } : {},
350
+ ) as AsyncIterable<TurnStreamData>;
351
+ },
352
+
353
+ async downloadSandboxFile(sandboxId, req) {
354
+ const response = await privateClient.downloadSandboxFile(
355
+ sandboxId,
356
+ req,
357
+ );
358
+ return await response.blob();
359
+ },
360
+
361
+ getGatewayClients: () => ({ client, privateClient }),
362
+ };
363
+
364
+ return server;
365
+ }