@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,413 @@
1
+ import type {
2
+ MessageStatus,
3
+ ThreadAssistantMessage,
4
+ ThreadAssistantMessagePart,
5
+ ThreadMessage,
6
+ } from '@assistant-ui/core';
7
+ import type { ToolApprovalRequiredEvent, Turn, UserToolApprovalEvent } from './server/index.js';
8
+
9
+ import { ROOT_THREAD_ID } from './constants.js';
10
+ import type { ToolApprovalMessageCustomMetadata } from './messageCustomMetadata.js';
11
+ import type { TurnStreamUpdate } from './turnStreamUpdate.js';
12
+
13
+ export { ROOT_THREAD_ID } from './constants.js';
14
+
15
+ export interface StoredApprovalDecision {
16
+ approved: boolean;
17
+ reason?: string;
18
+ }
19
+
20
+ export const TOOL_APPROVAL_THREAD_ID_CUSTOM_KEY = 'toolApprovalThreadId';
21
+
22
+ export interface RespondToToolApprovalOptions {
23
+ approvalId: string;
24
+ approved: boolean;
25
+ optionId?: string;
26
+ reason?: string;
27
+ }
28
+
29
+ type ApprovalDecision = UserToolApprovalEvent['approval'];
30
+
31
+ type ToolCallPart = Extract<ThreadMessage['content'][number], { type: 'tool-call' }>;
32
+
33
+ type AssistantToolCallPart = Extract<ThreadAssistantMessagePart, { type: 'tool-call' }>;
34
+
35
+ export function hasPendingToolApproval(approval: ToolCallPart['approval'] | undefined): boolean {
36
+ return approval != null && approval.approved === undefined && approval.resolution === undefined;
37
+ }
38
+
39
+ function applyApprovalDecisionToToolCall(
40
+ part: AssistantToolCallPart,
41
+ options: RespondToToolApprovalOptions,
42
+ ): AssistantToolCallPart {
43
+ const { approved, optionId, reason } = options;
44
+ const targetApproval = part.approval;
45
+ if (targetApproval == null) {
46
+ return part;
47
+ }
48
+ const approval = {
49
+ ...targetApproval,
50
+ approved,
51
+ ...(optionId != null ? { optionId } : {}),
52
+ ...(reason != null ? { reason } : {}),
53
+ };
54
+ if (approved) {
55
+ return { ...part, approval };
56
+ }
57
+ return {
58
+ ...part,
59
+ approval,
60
+ result: { error: reason ?? 'Tool approval denied' },
61
+ isError: true,
62
+ };
63
+ }
64
+
65
+ function updateToolApprovalInContent(
66
+ content: readonly ThreadAssistantMessagePart[],
67
+ options: RespondToToolApprovalOptions,
68
+ ): { content: readonly ThreadAssistantMessagePart[]; found: boolean } {
69
+ let found = false;
70
+ const newContent = content.map(part => {
71
+ if (part.type !== 'tool-call') {
72
+ return part;
73
+ }
74
+
75
+ if (part.approval?.id === options.approvalId) {
76
+ found = true;
77
+ return applyApprovalDecisionToToolCall(part, options);
78
+ }
79
+
80
+ if (part.messages == null) {
81
+ return part;
82
+ }
83
+
84
+ const messages = part.messages.map(message => {
85
+ if (message.role !== 'assistant') {
86
+ return message;
87
+ }
88
+ const nested = updateToolApprovalInContent(message.content, options);
89
+ if (!nested.found) {
90
+ return message;
91
+ }
92
+ found = true;
93
+ return { ...message, content: nested.content };
94
+ });
95
+ return { ...part, messages };
96
+ });
97
+
98
+ return { content: newContent, found };
99
+ }
100
+
101
+ export function applyApprovalDecisionsToMessage(
102
+ message: ThreadAssistantMessage,
103
+ options: RespondToToolApprovalOptions,
104
+ ): ThreadAssistantMessage {
105
+ const { content } = updateToolApprovalInContent(message.content, options);
106
+ return { ...message, content: [...content] };
107
+ }
108
+
109
+ export function toolApprovalStatus(): MessageStatus {
110
+ return { type: 'requires-action', reason: 'tool-calls' };
111
+ }
112
+
113
+ export function toolApprovalMessageCustom(threadId: string): ToolApprovalMessageCustomMetadata {
114
+ return {
115
+ [TOOL_APPROVAL_THREAD_ID_CUSTOM_KEY]: threadId === ROOT_THREAD_ID ? ROOT_THREAD_ID : threadId,
116
+ };
117
+ }
118
+
119
+ export function getToolApprovalThreadId(message: ThreadMessage | undefined): string | undefined {
120
+ if (message?.role !== 'assistant') {
121
+ return undefined;
122
+ }
123
+ const threadId = message.metadata.custom[TOOL_APPROVAL_THREAD_ID_CUSTOM_KEY];
124
+ return typeof threadId === 'string' ? threadId : undefined;
125
+ }
126
+
127
+ export function findApprovalRequiredInTurn(turn: Pick<Turn, 'state'>): ToolApprovalRequiredEvent | undefined {
128
+ if (turn.state.status !== 'done') {
129
+ return undefined;
130
+ }
131
+ const found = turn.state.requiredActions?.find(action => action.type === 'tool.approval_required');
132
+ return found?.type === 'tool.approval_required' ? found : undefined;
133
+ }
134
+
135
+ function toolCallPartHasPendingApproval(part: ToolCallPart): boolean {
136
+ return hasPendingToolApproval(part.approval);
137
+ }
138
+
139
+ function nestedMessagesHavePendingApprovals(messages: readonly ThreadMessage[]): boolean {
140
+ for (const message of messages) {
141
+ if (messageHasPendingApprovals(message)) {
142
+ return true;
143
+ }
144
+ }
145
+ return false;
146
+ }
147
+
148
+ export function messageHasPendingApprovals(message: ThreadMessage | undefined): boolean {
149
+ if (message?.role !== 'assistant') {
150
+ return false;
151
+ }
152
+ for (const part of message.content) {
153
+ if (part.type !== 'tool-call') {
154
+ continue;
155
+ }
156
+ if (toolCallPartHasPendingApproval(part)) {
157
+ return true;
158
+ }
159
+ if (part.messages != null && nestedMessagesHavePendingApprovals(part.messages)) {
160
+ return true;
161
+ }
162
+ }
163
+ return false;
164
+ }
165
+
166
+ function walkAssistantToolCallParts(
167
+ content: readonly ThreadAssistantMessagePart[],
168
+ visit: (part: AssistantToolCallPart) => void,
169
+ ): void {
170
+ for (const part of content) {
171
+ if (part.type !== 'tool-call') {
172
+ continue;
173
+ }
174
+ visit(part);
175
+ if (part.messages == null) {
176
+ continue;
177
+ }
178
+ for (const message of part.messages) {
179
+ if (message.role === 'assistant') {
180
+ walkAssistantToolCallParts(message.content, visit);
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ export function collectDecidedApprovalsFromContent(
187
+ content: readonly ThreadAssistantMessagePart[],
188
+ ): Map<string, StoredApprovalDecision> {
189
+ const decisions = new Map<string, StoredApprovalDecision>();
190
+ walkAssistantToolCallParts(content, part => {
191
+ const { approval } = part;
192
+ if (approval?.id == null || approval.approved === undefined) {
193
+ return;
194
+ }
195
+ decisions.set(approval.id, {
196
+ approved: approval.approved,
197
+ ...(approval.reason != null ? { reason: approval.reason } : {}),
198
+ });
199
+ });
200
+ return decisions;
201
+ }
202
+
203
+ function applyApprovalDecisionsToMessages(
204
+ messages: readonly ThreadMessage[],
205
+ decisions: ReadonlyMap<string, StoredApprovalDecision>,
206
+ ): ThreadMessage[] {
207
+ return messages.map(message => {
208
+ if (message.role !== 'assistant') {
209
+ return message;
210
+ }
211
+ return {
212
+ ...message,
213
+ content: applyApprovalDecisionsToContent(message.content, decisions),
214
+ };
215
+ });
216
+ }
217
+
218
+ export function applyApprovalDecisionsToContent(
219
+ content: readonly ThreadAssistantMessagePart[],
220
+ decisions: ReadonlyMap<string, StoredApprovalDecision>,
221
+ ): ThreadAssistantMessagePart[] {
222
+ return content.map(part => {
223
+ if (part.type !== 'tool-call') {
224
+ return part;
225
+ }
226
+
227
+ const decision = part.approval?.id != null ? decisions.get(part.approval.id) : undefined;
228
+ let nextPart: AssistantToolCallPart = part;
229
+
230
+ if (decision != null && part.approval != null && part.approval.approved === undefined) {
231
+ nextPart = applyApprovalDecisionToToolCall(part, {
232
+ approvalId: part.approval.id,
233
+ approved: decision.approved,
234
+ ...(decision.reason == null ? {} : { reason: decision.reason }),
235
+ });
236
+ }
237
+
238
+ if (nextPart.messages == null) {
239
+ return nextPart;
240
+ }
241
+
242
+ return {
243
+ ...nextPart,
244
+ messages: applyApprovalDecisionsToMessages(nextPart.messages, decisions),
245
+ };
246
+ });
247
+ }
248
+
249
+ export function mergeDecidedApprovalsIntoContent(
250
+ incoming: readonly ThreadAssistantMessagePart[],
251
+ existing: readonly ThreadAssistantMessagePart[],
252
+ ): ThreadAssistantMessagePart[] {
253
+ const decided = collectDecidedApprovalsFromContent(existing);
254
+ if (decided.size === 0) {
255
+ return [...incoming];
256
+ }
257
+ return applyApprovalDecisionsToContent(incoming, decided);
258
+ }
259
+
260
+ export function extractToolApprovalsFromTurnInput(input: Turn['input'] | undefined): UserToolApprovalEvent[] {
261
+ const events: UserToolApprovalEvent[] = [];
262
+ for (const item of input ?? []) {
263
+ if (item.type === 'user.tool_approval') {
264
+ events.push(item);
265
+ }
266
+ }
267
+ return events;
268
+ }
269
+
270
+ export function collectSubsequentApprovalDecisions(
271
+ turns: readonly Pick<Turn, 'input'>[],
272
+ fromIndex: number,
273
+ ): Map<string, StoredApprovalDecision> {
274
+ const decisions = new Map<string, StoredApprovalDecision>();
275
+
276
+ for (let index = fromIndex + 1; index < turns.length; index++) {
277
+ const input = turns[index]?.input ?? [];
278
+ if (input.some(item => item.type === 'user.message')) {
279
+ break;
280
+ }
281
+
282
+ for (const event of extractToolApprovalsFromTurnInput(input)) {
283
+ decisions.set(event.toolCallId, {
284
+ approved: event.approval.status === 'allow',
285
+ ...(event.approval.status === 'deny' && event.approval.reason != null ? { reason: event.approval.reason } : {}),
286
+ });
287
+ }
288
+ }
289
+
290
+ return decisions;
291
+ }
292
+
293
+ export function collectApprovalDecisionsFromTurnInput(
294
+ input: Turn['input'] | undefined,
295
+ ): Map<string, StoredApprovalDecision> {
296
+ const decisions = new Map<string, StoredApprovalDecision>();
297
+ for (const event of extractToolApprovalsFromTurnInput(input)) {
298
+ decisions.set(event.toolCallId, {
299
+ approved: event.approval.status === 'allow',
300
+ ...(event.approval.status === 'deny' && event.approval.reason != null ? { reason: event.approval.reason } : {}),
301
+ });
302
+ }
303
+ return decisions;
304
+ }
305
+
306
+ function contentHasPendingApprovals(content: readonly ThreadAssistantMessagePart[]): boolean {
307
+ return messageHasPendingApprovals({
308
+ id: 'pending-check',
309
+ role: 'assistant',
310
+ content,
311
+ status: { type: 'complete', reason: 'stop' },
312
+ createdAt: new Date(),
313
+ metadata: {
314
+ unstable_state: null,
315
+ unstable_annotations: [],
316
+ unstable_data: [],
317
+ steps: [],
318
+ custom: {},
319
+ },
320
+ });
321
+ }
322
+
323
+ export function resolveToolApprovalUpdate(
324
+ update: TurnStreamUpdate,
325
+ priorDecisions?: ReadonlyMap<string, StoredApprovalDecision>,
326
+ ): TurnStreamUpdate {
327
+ let { content } = update;
328
+ if (priorDecisions != null && priorDecisions.size > 0) {
329
+ content = applyApprovalDecisionsToContent(content, priorDecisions);
330
+ }
331
+
332
+ if (
333
+ contentHasPendingApprovals(content) ||
334
+ update.status?.type !== 'requires-action' ||
335
+ update.status.reason !== 'tool-calls'
336
+ ) {
337
+ return { ...update, content };
338
+ }
339
+
340
+ return { content };
341
+ }
342
+
343
+ export function mapApprovalDecision(approved: boolean, reason?: string): ApprovalDecision {
344
+ if (approved) {
345
+ return { status: 'allow' };
346
+ }
347
+ return { status: 'deny', ...(reason != null ? { reason } : {}) };
348
+ }
349
+
350
+ function isDecidedApprovalAwaitingSdk(part: ToolCallPart): boolean {
351
+ const { approval, result, isError } = part;
352
+ if (approval?.id == null || approval.approved === undefined) {
353
+ return false;
354
+ }
355
+ if (approval.approved) {
356
+ return result === undefined;
357
+ }
358
+ return isError === true;
359
+ }
360
+
361
+ function collectApprovalInputsFromMessages(
362
+ messages: readonly ThreadMessage[],
363
+ defaultThreadId: string,
364
+ ): UserToolApprovalEvent[] {
365
+ const events: UserToolApprovalEvent[] = [];
366
+ for (const message of messages) {
367
+ events.push(...collectApprovalInputs(message, defaultThreadId));
368
+ }
369
+ return events;
370
+ }
371
+
372
+ export function collectApprovalInputs(message: ThreadMessage, threadId: string): UserToolApprovalEvent[] {
373
+ if (message.role !== 'assistant' || !threadId) {
374
+ return [];
375
+ }
376
+ if (messageHasPendingApprovals(message)) {
377
+ return [];
378
+ }
379
+
380
+ const scopedThreadId = getToolApprovalThreadId(message) ?? threadId;
381
+ const events: UserToolApprovalEvent[] = [];
382
+
383
+ for (const part of message.content) {
384
+ if (part.type !== 'tool-call') {
385
+ continue;
386
+ }
387
+ if (isDecidedApprovalAwaitingSdk(part)) {
388
+ const { approval } = part;
389
+ if (approval?.approved === undefined) {
390
+ continue;
391
+ }
392
+ events.push({
393
+ type: 'user.tool_approval',
394
+ threadId: scopedThreadId,
395
+ toolCallId: approval.id,
396
+ approval: mapApprovalDecision(approval.approved, approval.reason),
397
+ });
398
+ }
399
+ if (part.messages != null) {
400
+ events.push(...collectApprovalInputsFromMessages(part.messages, scopedThreadId));
401
+ }
402
+ }
403
+ return events;
404
+ }
405
+
406
+ export function toTrueForgeApprovalInputs(
407
+ message: Extract<ThreadMessage, { role: 'assistant' }>,
408
+ response: RespondToToolApprovalOptions,
409
+ defaultThreadId: string = ROOT_THREAD_ID,
410
+ ): UserToolApprovalEvent[] {
411
+ const updated = applyApprovalDecisionsToMessage(message, response);
412
+ return collectApprovalInputs(updated, defaultThreadId);
413
+ }