@truefoundry/trueforge-assistant-ui-runtime 0.0.0 → 0.2.0-rc.0

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 (55) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +201 -0
  3. package/README.md +146 -4
  4. package/dist/chunk-2SQK6TIO.js +104 -0
  5. package/dist/chunk-2SQK6TIO.js.map +1 -0
  6. package/dist/index.d.ts +378 -0
  7. package/dist/index.js +4391 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/server/index.d.ts +1210 -0
  10. package/dist/server/index.js +9 -0
  11. package/dist/server/index.js.map +1 -0
  12. package/package.json +79 -16
  13. package/src/askUserQuestion.ts +38 -0
  14. package/src/attachmentAdapter.ts +63 -0
  15. package/src/collectPending.ts +167 -0
  16. package/src/constants.ts +2 -0
  17. package/src/convertTurnMessages.ts +1679 -0
  18. package/src/createSubAgent.ts +11 -0
  19. package/src/draft/agentSpec.ts +34 -0
  20. package/src/draft/draftSessionBridge.ts +28 -0
  21. package/src/draft/trueforgeDraftThreadListAdapter.ts +73 -0
  22. package/src/draft/useDraftAgentSpec.ts +289 -0
  23. package/src/extractTurnUserText.ts +23 -0
  24. package/src/foldPeerThreads.ts +553 -0
  25. package/src/hooks.ts +176 -0
  26. package/src/index.ts +227 -0
  27. package/src/lastUserMessageText.ts +19 -0
  28. package/src/listPages.ts +19 -0
  29. package/src/loadSessionSnapshot.ts +34 -0
  30. package/src/mcpAuth.ts +35 -0
  31. package/src/messageCustomMetadata.ts +50 -0
  32. package/src/modelMessageContent.ts +149 -0
  33. package/src/modelMessageImageContent.ts +154 -0
  34. package/src/requiredActionInputs.ts +38 -0
  35. package/src/sandboxDownload.ts +33 -0
  36. package/src/server/eventUtils.ts +125 -0
  37. package/src/server/events.ts +232 -0
  38. package/src/server/index.ts +178 -0
  39. package/src/server/types.ts +1191 -0
  40. package/src/sessionListStartTimestamp.ts +6 -0
  41. package/src/sessionSnapshot.ts +146 -0
  42. package/src/sessionThreadMetadata.ts +36 -0
  43. package/src/sessions.ts +17 -0
  44. package/src/streamTurn.ts +118 -0
  45. package/src/toolApproval.ts +413 -0
  46. package/src/toolResponse.ts +346 -0
  47. package/src/trueforgeExtras.ts +223 -0
  48. package/src/trueforgeOwnedSessionsThreadListAdapter.ts +71 -0
  49. package/src/trueforgeThreadListAdapter.ts +69 -0
  50. package/src/turnEventHelpers.ts +71 -0
  51. package/src/turnStreamUpdate.ts +11 -0
  52. package/src/types.ts +84 -0
  53. package/src/useTrueForgeAgentMessages.ts +1138 -0
  54. package/src/useTrueForgeAgentRuntime.ts +308 -0
  55. package/index.js +0 -6
@@ -0,0 +1,308 @@
1
+ 'use client';
2
+
3
+ import {
4
+ pickExternalStoreSharedOptions,
5
+ type AppendMessage,
6
+ type RemoteThreadListAdapter,
7
+ type ToolExecutionStatus,
8
+ } from '@assistant-ui/core';
9
+ import { useExternalStoreRuntime, useRemoteThreadListRuntime, useRuntimeAdapters } from '@assistant-ui/core/react';
10
+ import { useAui, useAuiState } from '@assistant-ui/store';
11
+ import type { RefObject } from 'react';
12
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
+
14
+ import {
15
+ collectPendingApprovals,
16
+ collectPendingToolResponses,
17
+ derivePendingMcpAuth,
18
+ deriveSandboxId,
19
+ } from './collectPending.js';
20
+ import {
21
+ buildUserMessageContent,
22
+ extractEditedText,
23
+ parseTurnIdFromMessageId,
24
+ userMessageContentToText,
25
+ } from './convertTurnMessages.js';
26
+ import { createDraftSessionBridge, DRAFT_SESSION_LAST_UPDATED_AT_HEADER } from './draft/draftSessionBridge.js';
27
+ import { createTrueForgeDraftThreadListAdapter } from './draft/trueforgeDraftThreadListAdapter.js';
28
+ import { useDraftAgentSpec } from './draft/useDraftAgentSpec.js';
29
+ import { MCP_AUTH_RESUME_RUN_CUSTOM_KEY } from './mcpAuth.js';
30
+ import { buildSandboxDownloadRequest } from './sandboxDownload.js';
31
+ import type { AgentSpec } from './server/types.js';
32
+ import { trueForgeExtras } from './trueforgeExtras.js';
33
+ import { createTrueForgeThreadListAdapter } from './trueforgeThreadListAdapter.js';
34
+ import type { UseTrueForgeAgentRuntimeOptions } from './types.js';
35
+ import { resolveTrueForgeAgentRuntimeOptions } from './types.js';
36
+ import { useTrueForgeAgentMessages } from './useTrueForgeAgentMessages.js';
37
+
38
+ /**
39
+ * Wraps the mode-specific adapter behind a stable object so assistant-ui does
40
+ * not treat a draft/named mode switch as an adapter change (which would reset
41
+ * loaded thread-list pages). Each call reads the ref, so behavior always
42
+ * follows the current mode.
43
+ */
44
+ function createDelegatingThreadListAdapter(adapterRef: RefObject<RemoteThreadListAdapter>): RemoteThreadListAdapter {
45
+ return {
46
+ list: params => adapterRef.current.list(params),
47
+ initialize: threadId => adapterRef.current.initialize(threadId),
48
+ fetch: threadId => adapterRef.current.fetch(threadId),
49
+ rename: (remoteId, newTitle) => adapterRef.current.rename(remoteId, newTitle),
50
+ archive: remoteId => adapterRef.current.archive(remoteId),
51
+ unarchive: remoteId => adapterRef.current.unarchive(remoteId),
52
+ delete: remoteId => adapterRef.current.delete(remoteId),
53
+ generateTitle: (remoteId, messages) => adapterRef.current.generateTitle(remoteId, messages),
54
+ };
55
+ }
56
+
57
+ function useTrueForgeAgentRuntimeImpl(
58
+ options: ReturnType<typeof resolveTrueForgeAgentRuntimeOptions>,
59
+ pendingAgentSpecRef: RefObject<AgentSpec | undefined>,
60
+ ) {
61
+ const { server, agent, adapters, onError, ...sharedOptions } = options;
62
+
63
+ const draftBridgeRef = useRef(agent.mode === 'draft' ? createDraftSessionBridge(server) : null);
64
+
65
+ const draftSessionId = useAuiState(state =>
66
+ agent.mode === 'draft' ? (state.threadListItem.remoteId ?? undefined) : undefined,
67
+ );
68
+ const sessionId = useAuiState(state => state.threadListItem.remoteId ?? undefined);
69
+ const isMain = useAuiState(state => state.threads.mainThreadId === state.threadListItem.id);
70
+ // On a hard refresh, the URL session runtime mounts before assistant-ui
71
+ // promotes it to the main thread. Allow that one session to hydrate early.
72
+ const isInitialSession = sessionId != null && sessionId === options.initialSessionId;
73
+
74
+ const draftSpec = useDraftAgentSpec({
75
+ draftSessionId,
76
+ draftBridge: draftBridgeRef.current,
77
+ defaultAgentSpec: agent.mode === 'draft' ? agent.defaultAgentSpec : { model: { name: '' } },
78
+ onAgentSpecChange: agent.mode === 'draft' ? agent.onAgentSpecChange : undefined,
79
+ onError,
80
+ });
81
+
82
+ const takeTurnHeaderTimestampRef = useRef(draftSpec.takeTurnHeaderTimestamp);
83
+ takeTurnHeaderTimestampRef.current = draftSpec.takeTurnHeaderTimestamp;
84
+
85
+ const getTurnHeaders = useCallback(async () => {
86
+ if (agent.mode !== 'draft') {
87
+ return undefined;
88
+ }
89
+ const updatedAt = await takeTurnHeaderTimestampRef.current();
90
+ if (updatedAt == null) {
91
+ return undefined;
92
+ }
93
+ return { [DRAFT_SESSION_LAST_UPDATED_AT_HEADER]: updatedAt };
94
+ }, [agent.mode]);
95
+
96
+ const aui = useAui();
97
+ const initializeSession = useCallback(() => aui.threadListItem().initialize(), [aui]);
98
+ const runtimeAdapters = useRuntimeAdapters();
99
+ const [, setToolStatuses] = useState<Record<string, ToolExecutionStatus>>({});
100
+
101
+ const {
102
+ messages,
103
+ isRunning,
104
+ resumeUnavailable,
105
+ isLoading,
106
+ isLoadingOlderHistory,
107
+ hasOlderHistory,
108
+ loadOlderHistory,
109
+ sendTurn,
110
+ cancel,
111
+ respondToToolApproval,
112
+ respondToToolResponse,
113
+ resumeRun,
114
+ editFromTurn,
115
+ resetFromTurn,
116
+ resolveSandboxIdForTurn,
117
+ retryLoad,
118
+ } = useTrueForgeAgentMessages({
119
+ server,
120
+ sessionId,
121
+ isMain,
122
+ isInitialSession,
123
+ onError,
124
+ initializeSession,
125
+ ...(agent.mode === 'draft' ? { getTurnHeaders } : {}),
126
+ });
127
+
128
+ if (agent.mode === 'draft' && draftSpec.agentSpec != null) {
129
+ pendingAgentSpecRef.current = draftSpec.agentSpec;
130
+ }
131
+
132
+ const pendingApprovals = useMemo(() => collectPendingApprovals(messages), [messages]);
133
+ const pendingToolResponses = useMemo(() => collectPendingToolResponses(messages), [messages]);
134
+ const pendingMcpAuth = useMemo(() => derivePendingMcpAuth(messages), [messages]);
135
+ const sandboxId = useMemo(() => deriveSandboxId(messages), [messages]);
136
+
137
+ const resumeMcpAuth = useMemo(() => () => sendTurn({ resumeMcpAuth: true }), [sendTurn]);
138
+
139
+ const downloadSandboxFile = useCallback(
140
+ async ({ turnId, path }: { turnId: string; path: string }) => {
141
+ if (server.downloadSandboxFile == null) {
142
+ throw new Error('Downloading a sandbox file requires AgentChatServer.downloadSandboxFile.');
143
+ }
144
+ // Prefer the sandbox that was current as of this turn (paging in
145
+ // older history if its reference predates the loaded window); the
146
+ // session-wide latest covers turns projected without a record.
147
+ // sandboxId stays best-effort — turn-scoped hosts resolve the
148
+ // sandbox from turnId and need no sandboxId at all.
149
+ const turnSandboxId = (await resolveSandboxIdForTurn(turnId)) ?? sandboxId;
150
+ return await server.downloadSandboxFile(
151
+ buildSandboxDownloadRequest({
152
+ sessionId,
153
+ turnId,
154
+ path,
155
+ ...(turnSandboxId != null ? { sandboxId: turnSandboxId } : {}),
156
+ }),
157
+ );
158
+ },
159
+ [server, sessionId, sandboxId, resolveSandboxIdForTurn],
160
+ );
161
+
162
+ const draftExtras = useMemo(() => {
163
+ if (agent.mode !== 'draft') {
164
+ return null;
165
+ }
166
+ return {
167
+ agentSpec: draftSpec.agentSpec,
168
+ draftSessionId: draftSpec.draftSessionId,
169
+ isSpecLoading: draftSpec.isSpecLoading,
170
+ isSpecSyncing: draftSpec.isSpecSyncing,
171
+ specError: draftSpec.specError,
172
+ updateAgentSpec: draftSpec.updateAgentSpec,
173
+ flushAgentSpec: draftSpec.flushAgentSpec,
174
+ adoptAgentSpec: draftSpec.adoptAgentSpec,
175
+ };
176
+ }, [agent.mode, draftSpec]);
177
+
178
+ return useExternalStoreRuntime({
179
+ ...pickExternalStoreSharedOptions(sharedOptions),
180
+ messages,
181
+ isRunning,
182
+ isLoading,
183
+ extras: trueForgeExtras.provide({
184
+ pendingApprovals,
185
+ pendingToolResponses,
186
+ pendingMcpAuth,
187
+ resumeUnavailable,
188
+ sandboxId,
189
+ respondToToolApproval,
190
+ respondToToolResponse,
191
+ resumeMcpAuth,
192
+ downloadSandboxFile,
193
+ cancel,
194
+ // resetFromTurn/branchFromTurn/sendTurn already report via onError.
195
+ resetFromTurn: (turnId: string) => resetFromTurn(turnId).catch(() => undefined),
196
+ reload: retryLoad,
197
+ hasOlderHistory,
198
+ isLoadingOlderHistory,
199
+ loadOlderHistory,
200
+ draft: draftExtras,
201
+ }),
202
+ unstable_enableToolInvocations: true,
203
+ setToolStatuses,
204
+ adapters: {
205
+ attachments: adapters?.attachments ?? runtimeAdapters?.attachments,
206
+ speech: adapters?.speech,
207
+ dictation: adapters?.dictation,
208
+ voice: adapters?.voice,
209
+ feedback: adapters?.feedback,
210
+ },
211
+ onNew: async (message: AppendMessage) => {
212
+ if (!(message.startRun ?? message.role === 'user')) {
213
+ return;
214
+ }
215
+
216
+ const resumeMcpAuthFlag = message.runConfig?.custom?.[MCP_AUTH_RESUME_RUN_CUSTOM_KEY] === true;
217
+
218
+ if (resumeMcpAuthFlag) {
219
+ await sendTurn({ resumeMcpAuth: true });
220
+ return;
221
+ }
222
+
223
+ const userMessage = buildUserMessageContent(message);
224
+ await sendTurn({
225
+ userMessage,
226
+ // The composer clears before onNew runs. Restore its text only when
227
+ // the turn failed before turn.created registered it in the backend.
228
+ onPreTurnFailure: () => {
229
+ const text = userMessageContentToText(userMessage);
230
+ const composer = aui.thread().composer();
231
+ if (text && !composer.getState().text.trim()) {
232
+ composer.setText(text);
233
+ }
234
+ },
235
+ });
236
+ },
237
+ onCancel: async () => {
238
+ await cancel();
239
+ },
240
+ onRespondToToolApproval: response => {
241
+ respondToToolApproval(response);
242
+ return Promise.resolve();
243
+ },
244
+ onResume: async () => {
245
+ await resumeRun();
246
+ },
247
+ onEdit: async (message: AppendMessage) => {
248
+ const sourceId = message.sourceId;
249
+ if (sourceId == null) {
250
+ throw new Error('Could not resolve edited user message.');
251
+ }
252
+ const turnId = parseTurnIdFromMessageId(sourceId);
253
+ const editedText = extractEditedText(message);
254
+ // editFromTurn/branchFromTurn/sendTurn already report via onError.
255
+ await editFromTurn(turnId, editedText);
256
+ },
257
+ });
258
+ }
259
+
260
+ export function useTrueForgeAgentRuntime(options: UseTrueForgeAgentRuntimeOptions) {
261
+ const resolved = resolveTrueForgeAgentRuntimeOptions(options);
262
+ const { server, agent } = resolved;
263
+
264
+ const pendingAgentSpecRef = useRef<AgentSpec | undefined>(
265
+ agent.mode === 'draft' ? agent.defaultAgentSpec : undefined,
266
+ );
267
+
268
+ const listSessionsAgentId = resolved.listSessionsAgentId;
269
+ const listSessionsCreatedByMe = resolved.listSessionsCreatedByMe;
270
+ // Mode-specific adapter: rebuilt on draft/named switches, but never handed
271
+ // to assistant-ui directly — it is reached through the delegating adapter below.
272
+ const modeThreadListAdapter = useMemo(() => {
273
+ if (agent.mode === 'draft') {
274
+ return createTrueForgeDraftThreadListAdapter({
275
+ server,
276
+ defaultAgentSpec: agent.defaultAgentSpec,
277
+ getAgentSpec: () => pendingAgentSpecRef.current ?? agent.defaultAgentSpec,
278
+ ...(listSessionsAgentId == null ? {} : { listSessionsAgentId }),
279
+ ...(listSessionsCreatedByMe == null ? {} : { listSessionsCreatedByMe }),
280
+ });
281
+ }
282
+ return createTrueForgeThreadListAdapter({
283
+ server,
284
+ agentName: agent.agentName,
285
+ ...(listSessionsAgentId == null ? {} : { listSessionsAgentId }),
286
+ ...(listSessionsCreatedByMe == null ? {} : { listSessionsCreatedByMe }),
287
+ });
288
+ }, [agent, listSessionsAgentId, listSessionsCreatedByMe, server]);
289
+ const modeThreadListAdapterRef = useRef(modeThreadListAdapter);
290
+ useEffect(() => {
291
+ modeThreadListAdapterRef.current = modeThreadListAdapter;
292
+ }, [modeThreadListAdapter]);
293
+ // Identity stays stable across mode switches; a new server or session
294
+ // filter is a genuinely different list, so those do reset it.
295
+ const threadListAdapter = useMemo(
296
+ () => createDelegatingThreadListAdapter(modeThreadListAdapterRef),
297
+ [listSessionsAgentId, listSessionsCreatedByMe, server],
298
+ );
299
+
300
+ return useRemoteThreadListRuntime({
301
+ allowNesting: true,
302
+ adapter: threadListAdapter,
303
+ initialThreadId: resolved.initialSessionId,
304
+ threadId: resolved.threadId,
305
+ onThreadIdChange: resolved.onThreadIdChange,
306
+ runtimeHook: () => useTrueForgeAgentRuntimeImpl(resolved, pendingAgentSpecRef),
307
+ });
308
+ }
package/index.js DELETED
@@ -1,6 +0,0 @@
1
- "use strict";
2
-
3
- module.exports = {
4
- name: "trueforge-assistant-ui-runtime",
5
- version: "0.0.0",
6
- };