@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,9 @@
1
+ import {
2
+ isEventDelta,
3
+ mergeEventDelta
4
+ } from "../chunk-2SQK6TIO.js";
5
+ export {
6
+ isEventDelta,
7
+ mergeEventDelta
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,25 +1,88 @@
1
1
  {
2
2
  "name": "@truefoundry/trueforge-assistant-ui-runtime",
3
- "version": "0.0.0",
4
- "description": "Placeholder package for TrueForge assistant UI runtime.",
5
- "license": "UNLICENSED",
6
- "main": "index.js",
7
- "files": [
8
- "index.js"
9
- ],
10
- "scripts": {
11
- "test": "node --test"
3
+ "version": "0.2.0-rc.0",
4
+ "description": "TrueForge agent runtime adapter for assistant-ui",
5
+ "author": "TrueFoundry",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/truefoundry/trueforge.git",
10
+ "directory": "packages/assistant-ui-runtime"
11
+ },
12
+ "homepage": "https://github.com/truefoundry/trueforge/tree/main/packages/assistant-ui-runtime#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/truefoundry/trueforge/issues"
12
15
  },
13
16
  "keywords": [
14
- "trueforge",
15
17
  "assistant-ui",
16
- "placeholder"
18
+ "truefoundry",
19
+ "ai",
20
+ "agent",
21
+ "react",
22
+ "llm",
23
+ "chat"
17
24
  ],
18
- "engines": {
19
- "node": ">=18"
20
- },
21
25
  "publishConfig": {
22
26
  "access": "public",
23
- "tag": "placeholder"
27
+ "provenance": true
28
+ },
29
+ "engines": {
30
+ "node": ">=22"
31
+ },
32
+ "type": "module",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js",
37
+ "default": "./dist/index.js"
38
+ },
39
+ "./server": {
40
+ "types": "./dist/server/index.d.ts",
41
+ "import": "./dist/server/index.js",
42
+ "default": "./dist/server/index.js"
43
+ }
44
+ },
45
+ "main": "./dist/index.js",
46
+ "types": "./dist/index.d.ts",
47
+ "files": [
48
+ "dist",
49
+ "src",
50
+ "README.md",
51
+ "CHANGELOG.md",
52
+ "LICENSE"
53
+ ],
54
+ "sideEffects": false,
55
+ "dependencies": {
56
+ "@assistant-ui/core": "^0.2.22",
57
+ "@assistant-ui/store": "^0.2.21"
58
+ },
59
+ "peerDependencies": {
60
+ "@types/react": "*",
61
+ "react": "^18 || ^19"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@types/react": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "devDependencies": {
69
+ "@testing-library/react": "^16.3.2",
70
+ "@types/node": "^20",
71
+ "@types/react": "^19.2.17",
72
+ "jsdom": "^29.1.1",
73
+ "react": "^19.2.4",
74
+ "tsup": "^8.5.0",
75
+ "typescript": "^5.9.3",
76
+ "vitest": "^4.1.9"
77
+ },
78
+ "scripts": {
79
+ "build": "tsup",
80
+ "lint": "eslint src --fix --cache --cache-location ../../.eslintcache --cache-strategy content",
81
+ "lint:ci": "eslint src --cache --cache-location ../../.eslintcache",
82
+ "pack:dry": "pnpm build && pnpm pack --dry-run",
83
+ "test": "vitest run",
84
+ "test:file": "vitest run",
85
+ "typecheck": "tsc --noEmit && pnpm run typecheck:test",
86
+ "typecheck:test": "tsc --noEmit --project test/tsconfig.json"
24
87
  }
25
- }
88
+ }
@@ -0,0 +1,38 @@
1
+ import type { ToolCall } from './server/index.js';
2
+
3
+ export function isAskUserQuestionToolCall(toolCall: Pick<ToolCall, 'toolInfo'>): boolean {
4
+ return toolCall.toolInfo?.type === 'trueforge-system' && toolCall.toolInfo.name === 'ask_user_question';
5
+ }
6
+
7
+ export interface AskUserQuestionArgs {
8
+ question?: string;
9
+ options?: string[];
10
+ }
11
+
12
+ export function parseAskUserQuestionArgs(argsText: string | undefined): AskUserQuestionArgs {
13
+ if (!argsText) {
14
+ return {};
15
+ }
16
+ try {
17
+ const parsed: unknown = JSON.parse(argsText);
18
+ if (!isUnknownRecord(parsed)) {
19
+ return {};
20
+ }
21
+ const rawQuestion = parsed['question'];
22
+ const rawOptions = parsed['options'];
23
+ const question = typeof rawQuestion === 'string' ? rawQuestion : undefined;
24
+ const options = Array.isArray(rawOptions)
25
+ ? rawOptions.filter((item): item is string => typeof item === 'string')
26
+ : undefined;
27
+ return {
28
+ ...(question == null ? {} : { question }),
29
+ ...(options == null ? {} : { options }),
30
+ };
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
35
+
36
+ function isUnknownRecord(value: unknown): value is Record<string, unknown> {
37
+ return value != null && typeof value === 'object' && !Array.isArray(value);
38
+ }
@@ -0,0 +1,63 @@
1
+ import type { AttachmentAdapter, PendingAttachment } from '@assistant-ui/core';
2
+
3
+ const bytesToBase64 = (bytes: Uint8Array): string => globalThis.Buffer.from(bytes).toString('base64');
4
+
5
+ const getFileDataURL = async (file: File): Promise<string> => {
6
+ if (typeof FileReader === 'undefined') {
7
+ const buffer = await file.arrayBuffer();
8
+ return `data:${file.type};base64,${bytesToBase64(new Uint8Array(buffer))}`;
9
+ }
10
+ return new Promise((resolve, reject) => {
11
+ const reader = new FileReader();
12
+ reader.onload = () => {
13
+ if (typeof reader.result === 'string') {
14
+ resolve(reader.result);
15
+ } else {
16
+ reject(new Error('Attachment reader returned non-text data.'));
17
+ }
18
+ };
19
+ reader.onerror = () => {
20
+ reject(reader.error ?? new Error('Failed to read attachment.'));
21
+ };
22
+ reader.readAsDataURL(file);
23
+ });
24
+ };
25
+
26
+ /**
27
+ * Reads composer files into `CompleteAttachment` `file` parts for send.
28
+ * Wire via `adapters: { attachments: trueForgeAttachmentAdapter }`.
29
+ */
30
+ export const trueForgeAttachmentAdapter: AttachmentAdapter = {
31
+ accept: '*',
32
+ add({ file }) {
33
+ return Promise.resolve({
34
+ id: globalThis.crypto.randomUUID(),
35
+ type: 'file',
36
+ name: file.name,
37
+ file,
38
+ contentType: file.type,
39
+ content: [],
40
+ status: {
41
+ type: 'requires-action',
42
+ reason: 'composer-send',
43
+ },
44
+ });
45
+ },
46
+ async send(attachment: PendingAttachment) {
47
+ return {
48
+ ...attachment,
49
+ status: { type: 'complete' },
50
+ content: [
51
+ {
52
+ type: 'file',
53
+ mimeType: attachment.contentType ?? '',
54
+ filename: attachment.name,
55
+ data: await getFileDataURL(attachment.file),
56
+ },
57
+ ],
58
+ };
59
+ },
60
+ remove() {
61
+ return Promise.resolve();
62
+ },
63
+ };
@@ -0,0 +1,167 @@
1
+ import type { ThreadAssistantMessagePart, ThreadMessage } from '@assistant-ui/core';
2
+ import type { McpAuthRequiredEvent } from './server/index.js';
3
+
4
+ import { ROOT_THREAD_ID } from './constants.js';
5
+ import { isMcpServerAuthInfoList, isUnknownRecord } from './messageCustomMetadata.js';
6
+ import { getToolApprovalThreadId, hasPendingToolApproval } from './toolApproval.js';
7
+ import {
8
+ getToolResponseThreadId,
9
+ hasPendingToolResponse,
10
+ type AskUserQuestionInterruptPayload,
11
+ } from './toolResponse.js';
12
+
13
+ export interface PendingApproval {
14
+ approvalId: string;
15
+ threadId: string;
16
+ toolName: string;
17
+ args: Record<string, unknown>;
18
+ argsText: string;
19
+ }
20
+
21
+ export interface PendingToolResponse {
22
+ toolCallId: string;
23
+ threadId: string;
24
+ toolName: string;
25
+ args: Record<string, unknown>;
26
+ argsText: string;
27
+ question?: string;
28
+ options?: string[];
29
+ }
30
+
31
+ type ToolCallPart = Extract<ThreadMessage['content'][number], { type: 'tool-call' }>;
32
+
33
+ function walkToolCallParts(
34
+ content: readonly ThreadAssistantMessagePart[],
35
+ visit: (part: ToolCallPart, threadId: string) => void,
36
+ threadId: string,
37
+ ): void {
38
+ for (const part of content) {
39
+ if (part.type !== 'tool-call') {
40
+ continue;
41
+ }
42
+ visit(part, threadId);
43
+ if (part.messages == null) {
44
+ continue;
45
+ }
46
+ for (const message of part.messages) {
47
+ if (message.role !== 'assistant') {
48
+ continue;
49
+ }
50
+ const nestedThreadId = getToolApprovalThreadId(message) ?? getToolResponseThreadId(message) ?? threadId;
51
+ walkToolCallParts(message.content, visit, nestedThreadId);
52
+ }
53
+ }
54
+ }
55
+
56
+ export function collectPendingApprovals(messages: readonly ThreadMessage[]): PendingApproval[] {
57
+ const pending: PendingApproval[] = [];
58
+
59
+ for (const message of messages) {
60
+ if (message.role !== 'assistant') {
61
+ continue;
62
+ }
63
+ const rootThreadId = getToolApprovalThreadId(message) ?? ROOT_THREAD_ID;
64
+ walkToolCallParts(
65
+ message.content,
66
+ (part, threadId) => {
67
+ const approval = part.approval;
68
+ if (approval == null || !hasPendingToolApproval(approval)) {
69
+ return;
70
+ }
71
+ pending.push({
72
+ approvalId: approval.id,
73
+ threadId,
74
+ toolName: part.toolName,
75
+ args: { ...part.args },
76
+ argsText: part.argsText,
77
+ });
78
+ },
79
+ rootThreadId,
80
+ );
81
+ }
82
+
83
+ return pending;
84
+ }
85
+
86
+ export function collectPendingToolResponses(messages: readonly ThreadMessage[]): PendingToolResponse[] {
87
+ const pending: PendingToolResponse[] = [];
88
+
89
+ for (const message of messages) {
90
+ if (message.role !== 'assistant') {
91
+ continue;
92
+ }
93
+ const rootThreadId = getToolResponseThreadId(message) ?? ROOT_THREAD_ID;
94
+ walkToolCallParts(
95
+ message.content,
96
+ (part, threadId) => {
97
+ if (!hasPendingToolResponse(part)) {
98
+ return;
99
+ }
100
+ const payload: AskUserQuestionInterruptPayload | undefined = isUnknownRecord(part.interrupt?.payload)
101
+ ? {
102
+ ...(typeof part.interrupt.payload['question'] === 'string'
103
+ ? { question: part.interrupt.payload['question'] }
104
+ : {}),
105
+ ...(Array.isArray(part.interrupt.payload['options']) &&
106
+ part.interrupt.payload['options'].every(option => typeof option === 'string')
107
+ ? { options: part.interrupt.payload['options'] }
108
+ : {}),
109
+ }
110
+ : undefined;
111
+ pending.push({
112
+ toolCallId: part.toolCallId,
113
+ threadId,
114
+ toolName: part.toolName,
115
+ args: { ...part.args },
116
+ argsText: part.argsText,
117
+ ...(payload?.question != null ? { question: payload.question } : {}),
118
+ ...(payload?.options != null ? { options: payload.options } : {}),
119
+ });
120
+ },
121
+ rootThreadId,
122
+ );
123
+ }
124
+
125
+ return pending;
126
+ }
127
+
128
+ export function derivePendingMcpAuth(
129
+ messages: readonly ThreadMessage[],
130
+ ): { mcpServers: McpAuthRequiredEvent['mcpServers'] } | null {
131
+ for (const message of messages.toReversed()) {
132
+ if (message.role !== 'assistant') {
133
+ continue;
134
+ }
135
+ if (message.status.type !== 'requires-action') {
136
+ continue;
137
+ }
138
+ const custom = message.metadata.custom;
139
+ if (custom['pendingMcpAuth'] !== true) {
140
+ continue;
141
+ }
142
+ const servers = custom['mcpServers'];
143
+ if (!isMcpServerAuthInfoList(servers)) {
144
+ return { mcpServers: [] };
145
+ }
146
+ return { mcpServers: servers };
147
+ }
148
+ return null;
149
+ }
150
+
151
+ /**
152
+ * Most recent `sandboxId` observed anywhere in the conversation, scanning backward.
153
+ * Unlike `derivePendingMcpAuth`, this isn't gated on message status: once a sandbox
154
+ * is created it stays valid for the rest of the session, not just the paused message.
155
+ */
156
+ export function deriveSandboxId(messages: readonly ThreadMessage[]): string | undefined {
157
+ for (const message of messages.toReversed()) {
158
+ if (message.role !== 'assistant') {
159
+ continue;
160
+ }
161
+ const sandboxId = message.metadata.custom['sandboxId'];
162
+ if (typeof sandboxId === 'string') {
163
+ return sandboxId;
164
+ }
165
+ }
166
+ return undefined;
167
+ }
@@ -0,0 +1,2 @@
1
+ /** Gateway root thread id — always the literal `"main"`. */
2
+ export const ROOT_THREAD_ID = 'main';