@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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +201 -0
- package/README.md +146 -4
- package/dist/chunk-2SQK6TIO.js +104 -0
- package/dist/chunk-2SQK6TIO.js.map +1 -0
- package/dist/index.d.ts +378 -0
- package/dist/index.js +4391 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +1210 -0
- package/dist/server/index.js +9 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +79 -16
- package/src/askUserQuestion.ts +38 -0
- package/src/attachmentAdapter.ts +63 -0
- package/src/collectPending.ts +167 -0
- package/src/constants.ts +2 -0
- package/src/convertTurnMessages.ts +1679 -0
- package/src/createSubAgent.ts +11 -0
- package/src/draft/agentSpec.ts +34 -0
- package/src/draft/draftSessionBridge.ts +28 -0
- package/src/draft/trueforgeDraftThreadListAdapter.ts +73 -0
- package/src/draft/useDraftAgentSpec.ts +289 -0
- package/src/extractTurnUserText.ts +23 -0
- package/src/foldPeerThreads.ts +553 -0
- package/src/hooks.ts +176 -0
- package/src/index.ts +227 -0
- package/src/lastUserMessageText.ts +19 -0
- package/src/listPages.ts +19 -0
- package/src/loadSessionSnapshot.ts +34 -0
- package/src/mcpAuth.ts +35 -0
- package/src/messageCustomMetadata.ts +50 -0
- package/src/modelMessageContent.ts +149 -0
- package/src/modelMessageImageContent.ts +154 -0
- package/src/requiredActionInputs.ts +38 -0
- package/src/sandboxDownload.ts +33 -0
- package/src/server/eventUtils.ts +125 -0
- package/src/server/events.ts +232 -0
- package/src/server/index.ts +178 -0
- package/src/server/types.ts +1191 -0
- package/src/sessionListStartTimestamp.ts +6 -0
- package/src/sessionSnapshot.ts +146 -0
- package/src/sessionThreadMetadata.ts +36 -0
- package/src/sessions.ts +17 -0
- package/src/streamTurn.ts +118 -0
- package/src/toolApproval.ts +413 -0
- package/src/toolResponse.ts +346 -0
- package/src/trueforgeExtras.ts +223 -0
- package/src/trueforgeOwnedSessionsThreadListAdapter.ts +71 -0
- package/src/trueforgeThreadListAdapter.ts +69 -0
- package/src/turnEventHelpers.ts +71 -0
- package/src/turnStreamUpdate.ts +11 -0
- package/src/types.ts +84 -0
- package/src/useTrueForgeAgentMessages.ts +1138 -0
- package/src/useTrueForgeAgentRuntime.ts +308 -0
- 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
|
+
}
|