@4djs/assistant 0.0.0 → 0.0.1

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 (188) hide show
  1. package/dist/core/chat-activity.d.ts +19 -0
  2. package/dist/core/chat-activity.d.ts.map +1 -0
  3. package/dist/core/chat-commands.d.ts +33 -0
  4. package/dist/core/chat-commands.d.ts.map +1 -0
  5. package/dist/core/chat-history.d.ts +14 -0
  6. package/dist/core/chat-history.d.ts.map +1 -0
  7. package/dist/core/chat-reply-suggestions-parse.d.ts +20 -0
  8. package/dist/core/chat-reply-suggestions-parse.d.ts.map +1 -0
  9. package/dist/core/code-highlight.d.ts +3 -0
  10. package/dist/core/code-highlight.d.ts.map +1 -0
  11. package/dist/core/create-assistant-store.d.ts +33 -0
  12. package/dist/core/create-assistant-store.d.ts.map +1 -0
  13. package/dist/core/fetch-suggested-prompts.d.ts +11 -0
  14. package/dist/core/fetch-suggested-prompts.d.ts.map +1 -0
  15. package/dist/core/index.d.ts +19 -0
  16. package/dist/core/index.d.ts.map +1 -0
  17. package/dist/core/index.js +2876 -0
  18. package/dist/core/interactive-tools/choices.d.ts +22 -0
  19. package/dist/core/interactive-tools/choices.d.ts.map +1 -0
  20. package/dist/core/interactive-tools/confirmation.d.ts +15 -0
  21. package/dist/core/interactive-tools/confirmation.d.ts.map +1 -0
  22. package/dist/core/interactive-tools/constants.d.ts +6 -0
  23. package/dist/core/interactive-tools/constants.d.ts.map +1 -0
  24. package/dist/core/interactive-tools/execute.d.ts +11 -0
  25. package/dist/core/interactive-tools/execute.d.ts.map +1 -0
  26. package/dist/core/interactive-tools/index.d.ts +7 -0
  27. package/dist/core/interactive-tools/index.d.ts.map +1 -0
  28. package/dist/core/interactive-tools/suggestions.d.ts +13 -0
  29. package/dist/core/interactive-tools/suggestions.d.ts.map +1 -0
  30. package/dist/core/interactive-tools/waiters.d.ts +4 -0
  31. package/dist/core/interactive-tools/waiters.d.ts.map +1 -0
  32. package/dist/core/llm-chat.d.ts +96 -0
  33. package/dist/core/llm-chat.d.ts.map +1 -0
  34. package/dist/core/llm-config.d.ts +24 -0
  35. package/dist/core/llm-config.d.ts.map +1 -0
  36. package/dist/core/llm-models.d.ts +14 -0
  37. package/dist/core/llm-models.d.ts.map +1 -0
  38. package/dist/core/llm-provider.d.ts +13 -0
  39. package/dist/core/llm-provider.d.ts.map +1 -0
  40. package/dist/core/llm-settings-storage.d.ts +47 -0
  41. package/dist/core/llm-settings-storage.d.ts.map +1 -0
  42. package/dist/core/llm-sse.d.ts +13 -0
  43. package/dist/core/llm-sse.d.ts.map +1 -0
  44. package/dist/core/llm-types.d.ts +49 -0
  45. package/dist/core/llm-types.d.ts.map +1 -0
  46. package/dist/core/markdown-utils.d.ts +3 -0
  47. package/dist/core/markdown-utils.d.ts.map +1 -0
  48. package/dist/core/prepare-markdown.d.ts +7 -0
  49. package/dist/core/prepare-markdown.d.ts.map +1 -0
  50. package/dist/core/types.d.ts +74 -0
  51. package/dist/core/types.d.ts.map +1 -0
  52. package/dist/index.css +1195 -0
  53. package/dist/index.js +184948 -0
  54. package/dist/react/Assistant.d.ts +10 -0
  55. package/dist/react/Assistant.d.ts.map +1 -0
  56. package/dist/react/components/HighlightedJsonCode.d.ts +6 -0
  57. package/dist/react/components/HighlightedJsonCode.d.ts.map +1 -0
  58. package/dist/react/components/MarkdownContent.d.ts +10 -0
  59. package/dist/react/components/MarkdownContent.d.ts.map +1 -0
  60. package/dist/react/components/MarkdownEditor.d.ts +11 -0
  61. package/dist/react/components/MarkdownEditor.d.ts.map +1 -0
  62. package/dist/react/components/MermaidDiagram.d.ts +8 -0
  63. package/dist/react/components/MermaidDiagram.d.ts.map +1 -0
  64. package/dist/react/components/ModelSelector.d.ts +8 -0
  65. package/dist/react/components/ModelSelector.d.ts.map +1 -0
  66. package/dist/react/components/chat/AssistantErrorCallout.d.ts +11 -0
  67. package/dist/react/components/chat/AssistantErrorCallout.d.ts.map +1 -0
  68. package/dist/react/components/chat/ChatActivity.d.ts +8 -0
  69. package/dist/react/components/chat/ChatActivity.d.ts.map +1 -0
  70. package/dist/react/components/chat/ChatComposer.d.ts +36 -0
  71. package/dist/react/components/chat/ChatComposer.d.ts.map +1 -0
  72. package/dist/react/components/chat/ChatEmptyState.d.ts +10 -0
  73. package/dist/react/components/chat/ChatEmptyState.d.ts.map +1 -0
  74. package/dist/react/components/chat/ChatInteractivePrompt/choices-prompt.d.ts +7 -0
  75. package/dist/react/components/chat/ChatInteractivePrompt/choices-prompt.d.ts.map +1 -0
  76. package/dist/react/components/chat/ChatInteractivePrompt/confirmation-prompt.d.ts +7 -0
  77. package/dist/react/components/chat/ChatInteractivePrompt/confirmation-prompt.d.ts.map +1 -0
  78. package/dist/react/components/chat/ChatInteractivePrompt/index.d.ts +7 -0
  79. package/dist/react/components/chat/ChatInteractivePrompt/index.d.ts.map +1 -0
  80. package/dist/react/components/chat/ChatInteractivePrompt/shell.d.ts +13 -0
  81. package/dist/react/components/chat/ChatInteractivePrompt/shell.d.ts.map +1 -0
  82. package/dist/react/components/chat/ChatInteractivePrompt/utils.d.ts +4 -0
  83. package/dist/react/components/chat/ChatInteractivePrompt/utils.d.ts.map +1 -0
  84. package/dist/react/components/chat/ChatMessage.d.ts +11 -0
  85. package/dist/react/components/chat/ChatMessage.d.ts.map +1 -0
  86. package/dist/react/components/chat/ChatMessageScroll.d.ts +8 -0
  87. package/dist/react/components/chat/ChatMessageScroll.d.ts.map +1 -0
  88. package/dist/react/components/chat/ChatReplySuggestions.d.ts +9 -0
  89. package/dist/react/components/chat/ChatReplySuggestions.d.ts.map +1 -0
  90. package/dist/react/components/chat/ComposerCommandMenu.d.ts +10 -0
  91. package/dist/react/components/chat/ComposerCommandMenu.d.ts.map +1 -0
  92. package/dist/react/components/chat/LlmSettingsStrip.d.ts +7 -0
  93. package/dist/react/components/chat/LlmSettingsStrip.d.ts.map +1 -0
  94. package/dist/react/components/chat/LlmSetupPrompt.d.ts +7 -0
  95. package/dist/react/components/chat/LlmSetupPrompt.d.ts.map +1 -0
  96. package/dist/react/components/chat/LlmUnavailableBanner.d.ts +6 -0
  97. package/dist/react/components/chat/LlmUnavailableBanner.d.ts.map +1 -0
  98. package/dist/react/components/chat/SuggestedPromptsList.d.ts +14 -0
  99. package/dist/react/components/chat/SuggestedPromptsList.d.ts.map +1 -0
  100. package/dist/react/components/chat/SuggestedPromptsStrip.d.ts +11 -0
  101. package/dist/react/components/chat/SuggestedPromptsStrip.d.ts.map +1 -0
  102. package/dist/react/components/chat/SystemPromptField.d.ts +10 -0
  103. package/dist/react/components/chat/SystemPromptField.d.ts.map +1 -0
  104. package/dist/react/components/highlighted-code.d.ts +8 -0
  105. package/dist/react/components/highlighted-code.d.ts.map +1 -0
  106. package/dist/react/context.d.ts +11 -0
  107. package/dist/react/context.d.ts.map +1 -0
  108. package/dist/react/hooks/use-composer-commands.d.ts +21 -0
  109. package/dist/react/hooks/use-composer-commands.d.ts.map +1 -0
  110. package/dist/react/hooks/use-suggested-prompts.d.ts +29 -0
  111. package/dist/react/hooks/use-suggested-prompts.d.ts.map +1 -0
  112. package/dist/react/index.d.ts +17 -0
  113. package/dist/react/index.d.ts.map +1 -0
  114. package/dist/react/lib/parse-assistant-error.d.ts +9 -0
  115. package/dist/react/lib/parse-assistant-error.d.ts.map +1 -0
  116. package/dist/react/lib/prompt-icons.d.ts +5 -0
  117. package/dist/react/lib/prompt-icons.d.ts.map +1 -0
  118. package/dist/react/types.d.ts +69 -0
  119. package/dist/react/types.d.ts.map +1 -0
  120. package/dist/react/utils/cn.d.ts +2 -0
  121. package/dist/react/utils/cn.d.ts.map +1 -0
  122. package/package.json +16 -5
  123. package/src/core/chat-activity.ts +0 -107
  124. package/src/core/chat-commands.ts +0 -173
  125. package/src/core/chat-history.ts +0 -113
  126. package/src/core/chat-reply-suggestions-parse.ts +0 -119
  127. package/src/core/code-highlight.ts +0 -20
  128. package/src/core/create-assistant-store.ts +0 -639
  129. package/src/core/fetch-suggested-prompts.ts +0 -53
  130. package/src/core/index.ts +0 -125
  131. package/src/core/interactive-tools/choices.ts +0 -155
  132. package/src/core/interactive-tools/confirmation.ts +0 -63
  133. package/src/core/interactive-tools/constants.ts +0 -22
  134. package/src/core/interactive-tools/execute.ts +0 -70
  135. package/src/core/interactive-tools/index.ts +0 -41
  136. package/src/core/interactive-tools/suggestions.ts +0 -87
  137. package/src/core/interactive-tools/waiters.ts +0 -55
  138. package/src/core/llm-chat.ts +0 -686
  139. package/src/core/llm-config.ts +0 -101
  140. package/src/core/llm-models.ts +0 -96
  141. package/src/core/llm-provider.ts +0 -99
  142. package/src/core/llm-settings-storage.ts +0 -331
  143. package/src/core/llm-sse.ts +0 -166
  144. package/src/core/llm-types.ts +0 -52
  145. package/src/core/markdown-utils.ts +0 -11
  146. package/src/core/prepare-markdown.ts +0 -38
  147. package/src/core/types.ts +0 -86
  148. package/src/css.d.ts +0 -1
  149. package/src/react/Assistant.tsx +0 -358
  150. package/src/react/components/HighlightedJsonCode.tsx +0 -24
  151. package/src/react/components/MarkdownContent.tsx +0 -98
  152. package/src/react/components/MarkdownEditor.tsx +0 -60
  153. package/src/react/components/MermaidDiagram.tsx +0 -139
  154. package/src/react/components/ModelSelector.tsx +0 -243
  155. package/src/react/components/chat/AssistantErrorCallout.tsx +0 -79
  156. package/src/react/components/chat/ChatActivity.tsx +0 -274
  157. package/src/react/components/chat/ChatComposer.tsx +0 -189
  158. package/src/react/components/chat/ChatEmptyState.tsx +0 -145
  159. package/src/react/components/chat/ChatInteractivePrompt/choices-prompt.tsx +0 -262
  160. package/src/react/components/chat/ChatInteractivePrompt/confirmation-prompt.tsx +0 -97
  161. package/src/react/components/chat/ChatInteractivePrompt/index.tsx +0 -60
  162. package/src/react/components/chat/ChatInteractivePrompt/shell.tsx +0 -60
  163. package/src/react/components/chat/ChatInteractivePrompt/utils.ts +0 -14
  164. package/src/react/components/chat/ChatMessage.tsx +0 -150
  165. package/src/react/components/chat/ChatMessageScroll.tsx +0 -116
  166. package/src/react/components/chat/ChatReplySuggestions.tsx +0 -231
  167. package/src/react/components/chat/ComposerCommandMenu.tsx +0 -69
  168. package/src/react/components/chat/LlmSettingsStrip.tsx +0 -348
  169. package/src/react/components/chat/LlmSetupPrompt.tsx +0 -58
  170. package/src/react/components/chat/LlmUnavailableBanner.tsx +0 -11
  171. package/src/react/components/chat/SuggestedPromptsList.tsx +0 -121
  172. package/src/react/components/chat/SuggestedPromptsStrip.tsx +0 -72
  173. package/src/react/components/chat/SystemPromptField.tsx +0 -107
  174. package/src/react/components/highlighted-code.tsx +0 -107
  175. package/src/react/context.tsx +0 -72
  176. package/src/react/hooks/use-composer-commands.ts +0 -129
  177. package/src/react/hooks/use-suggested-prompts.ts +0 -128
  178. package/src/react/index.ts +0 -39
  179. package/src/react/lib/parse-assistant-error.ts +0 -96
  180. package/src/react/lib/prompt-icons.ts +0 -40
  181. package/src/react/types.ts +0 -83
  182. package/src/react/utils/cn.ts +0 -5
  183. package/test/buildLlmHistory.test.ts +0 -95
  184. package/test/llm-config.test.ts +0 -72
  185. package/test/llmSettingsStorage.test.ts +0 -121
  186. package/test/parse-assistant-error.test.ts +0 -24
  187. package/tsconfig.json +0 -8
  188. /package/{src/styles/assistant.css → dist/styles.css} +0 -0
@@ -1,686 +0,0 @@
1
- import type { ReplySuggestions } from "./interactive-tools/index.ts";
2
- import {
3
- executeChatInteractiveTool,
4
- getChatInteractiveTools,
5
- isChatUiTool,
6
- shouldTerminateTurnAfterTool,
7
- } from "./interactive-tools/index.ts";
8
- import {
9
- buildCompletionsUrl,
10
- buildLlmRequestHeaders,
11
- isLlmConfigured,
12
- type ResolvedLlmSettings,
13
- resolveRequestModel,
14
- } from "./llm-config.ts";
15
- import { fetchProviderModels } from "./llm-models.ts";
16
- import { resolveAssistantLlmSettings } from "./llm-provider.ts";
17
- import { readSseStream } from "./llm-sse.ts";
18
- import type {
19
- ClientToolDefinition,
20
- LlmChatMessage,
21
- OpenAiToolCall,
22
- } from "./llm-types.ts";
23
- import type { AssistantToolDefinition, AssistantToolResult } from "./types.ts";
24
-
25
- const MAX_AGENT_STEPS = 8;
26
-
27
- const DEFAULT_SYSTEM_PROMPT =
28
- "You are a helpful assistant with access to tools. Use tools when they help answer the user.";
29
-
30
- export class LlmNotConfiguredError extends Error {
31
- readonly code = "LLM_DISABLED";
32
-
33
- constructor(
34
- message = "LLM is not configured. Provide llm settings with a valid apiKey.",
35
- ) {
36
- super(message);
37
- this.name = "LlmNotConfiguredError";
38
- }
39
- }
40
-
41
- export class LlmUpstreamError extends Error {
42
- readonly status: number;
43
- readonly code: string;
44
-
45
- constructor(message: string, status: number, code = "LLM_UPSTREAM_ERROR") {
46
- super(message);
47
- this.name = "LlmUpstreamError";
48
- this.status = status;
49
- this.code = code;
50
- }
51
- }
52
-
53
- export interface LlmChatStatus {
54
- enabled: boolean;
55
- model: string | null;
56
- models: string[];
57
- }
58
-
59
- export class ChatAbortedError extends Error {
60
- constructor() {
61
- super("Chat aborted");
62
- this.name = "ChatAbortedError";
63
- }
64
- }
65
-
66
- export interface StreamHandlers {
67
- turnId: string;
68
- onUpdate: (content: string) => void;
69
- }
70
-
71
- export interface ToolHandlers {
72
- onStart: (step: {
73
- id: string;
74
- name: string;
75
- args: Record<string, unknown>;
76
- callId: string;
77
- }) => void;
78
- onFinish: (
79
- stepId: string,
80
- update: {
81
- status: "done" | "error";
82
- result?: unknown;
83
- error?: string;
84
- },
85
- ) => void;
86
- }
87
-
88
- export interface AgentStepResult {
89
- assistantMessages: Array<{
90
- id?: string;
91
- content: string;
92
- toolCall?: { name: string; args: Record<string, unknown> };
93
- isError?: boolean;
94
- streaming?: boolean;
95
- }>;
96
- apiMessages: LlmChatMessage[];
97
- lastToolResult?: AssistantToolResult;
98
- }
99
-
100
- export async function fetchLlmStatus(): Promise<LlmChatStatus> {
101
- try {
102
- const config = await resolveAssistantLlmSettings();
103
- if (!isLlmConfigured(config)) {
104
- return { enabled: false, model: null, models: [] };
105
- }
106
-
107
- const models = config.models?.length
108
- ? config.models
109
- : await fetchProviderModels(config);
110
- return {
111
- enabled: true,
112
- model: config.model,
113
- models,
114
- };
115
- } catch {
116
- return { enabled: false, model: null, models: [] };
117
- }
118
- }
119
-
120
- export async function requestLlmCompletion(input: {
121
- messages: LlmChatMessage[];
122
- tools?: ClientToolDefinition[];
123
- model?: string;
124
- systemPrompt?: string;
125
- responseFormat?: { type: "json_object" };
126
- stream?: StreamHandlers;
127
- signal?: AbortSignal;
128
- }): Promise<{
129
- content: string | null;
130
- toolCalls: OpenAiToolCall[];
131
- model: string;
132
- }> {
133
- if (input.stream) {
134
- return requestLlmCompletionStream(input, input.stream);
135
- }
136
-
137
- const settings = await requireLlmSettings();
138
- const payload = buildChatPayload(input, settings);
139
-
140
- const res = await fetch(buildCompletionsUrl(settings.baseUrl), {
141
- method: "POST",
142
- headers: buildLlmRequestHeaders(settings.apiKey),
143
- body: JSON.stringify(payload),
144
- signal: input.signal,
145
- });
146
-
147
- const raw = await res.text();
148
- let data: Record<string, unknown> = {};
149
- try {
150
- data = raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
151
- } catch {
152
- throw new LlmUpstreamError(
153
- `LLM returned non-JSON response (${res.status})`,
154
- 502,
155
- "LLM_BAD_RESPONSE",
156
- );
157
- }
158
-
159
- if (!res.ok) {
160
- throw upstreamError(data, res.status);
161
- }
162
-
163
- const choice = (
164
- data.choices as Array<Record<string, unknown>> | undefined
165
- )?.[0];
166
- const message = choice?.message as Record<string, unknown> | undefined;
167
- if (!message) {
168
- throw new LlmUpstreamError(
169
- "LLM response missing message",
170
- 502,
171
- "LLM_BAD_RESPONSE",
172
- );
173
- }
174
-
175
- return {
176
- content: (message.content as string | null) ?? null,
177
- toolCalls: (message.tool_calls as OpenAiToolCall[] | undefined) ?? [],
178
- model: typeof data.model === "string" ? data.model : settings.model,
179
- };
180
- }
181
-
182
- async function requestLlmCompletionStream(
183
- input: {
184
- messages: LlmChatMessage[];
185
- tools?: ClientToolDefinition[];
186
- model?: string;
187
- systemPrompt?: string;
188
- responseFormat?: { type: "json_object" };
189
- signal?: AbortSignal;
190
- },
191
- handlers: StreamHandlers,
192
- ): Promise<{
193
- content: string | null;
194
- toolCalls: OpenAiToolCall[];
195
- model: string;
196
- }> {
197
- const settings = await requireLlmSettings();
198
- const payload = buildChatPayload(input, settings);
199
-
200
- try {
201
- const res = await fetch(buildCompletionsUrl(settings.baseUrl), {
202
- method: "POST",
203
- headers: buildLlmRequestHeaders(settings.apiKey),
204
- body: JSON.stringify({ ...payload, stream: true }),
205
- signal: input.signal,
206
- });
207
-
208
- if (!res.ok) {
209
- const raw = await res.text();
210
- let data: Record<string, unknown> = {};
211
- try {
212
- data = raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
213
- } catch {
214
- // ignore
215
- }
216
- throw upstreamError(data, res.status);
217
- }
218
-
219
- if (!res.body) {
220
- throw new LlmUpstreamError(
221
- "LLM stream missing body",
222
- 502,
223
- "LLM_BAD_RESPONSE",
224
- );
225
- }
226
-
227
- const final = await readSseStream(
228
- res.body,
229
- (chunk) => {
230
- if (chunk.contentDelta) {
231
- handlers.onUpdate(chunk.content);
232
- }
233
- },
234
- input.signal,
235
- );
236
-
237
- return {
238
- content: final.content || null,
239
- toolCalls: final.toolCalls,
240
- model: final.model ?? settings.model,
241
- };
242
- } catch (error) {
243
- if (
244
- input.signal?.aborted ||
245
- (error instanceof DOMException && error.name === "AbortError")
246
- ) {
247
- throw new ChatAbortedError();
248
- }
249
- throw error;
250
- }
251
- }
252
-
253
- export async function runLlmAgent(input: {
254
- userMessage: string;
255
- history: LlmChatMessage[];
256
- tools: AssistantToolDefinition[];
257
- model?: string;
258
- signal?: AbortSignal;
259
- stream?: StreamHandlers;
260
- toolHandlers?: ToolHandlers;
261
- invokeTool: (
262
- name: string,
263
- args: Record<string, unknown>,
264
- ) => Promise<AssistantToolResult>;
265
- onPostResponseTool?: (suggestions: ReplySuggestions) => void;
266
- }): Promise<AgentStepResult> {
267
- const apiMessages: LlmChatMessage[] = [
268
- ...input.history,
269
- { role: "user", content: input.userMessage },
270
- ];
271
-
272
- let lastToolResult: AssistantToolResult | undefined;
273
- const clientTools = [
274
- ...input.tools.map(toClientTool),
275
- ...getChatInteractiveTools(),
276
- ];
277
- let finalContent = "";
278
- let endedWithPostResponseTool = false;
279
-
280
- for (let step = 0; step < MAX_AGENT_STEPS; step++) {
281
- if (input.signal?.aborted) {
282
- throw new ChatAbortedError();
283
- }
284
-
285
- const completion = await requestLlmCompletion({
286
- messages: apiMessages,
287
- tools: clientTools,
288
- model: input.model,
289
- signal: input.signal,
290
- stream: input.stream
291
- ? {
292
- turnId: input.stream.turnId,
293
- onUpdate: (content) => {
294
- finalContent = content;
295
- input.stream?.onUpdate(content);
296
- },
297
- }
298
- : undefined,
299
- });
300
-
301
- if (completion.toolCalls.length === 0) {
302
- finalContent =
303
- completion.content?.trim() ||
304
- "I couldn't produce a response. Try rephrasing your request.";
305
- input.stream?.onUpdate(finalContent);
306
- apiMessages.push({
307
- role: "assistant",
308
- content: completion.content,
309
- });
310
- break;
311
- }
312
-
313
- apiMessages.push({
314
- role: "assistant",
315
- content: completion.content,
316
- tool_calls: completion.toolCalls,
317
- });
318
-
319
- let terminatesTurn = false;
320
-
321
- for (const call of completion.toolCalls) {
322
- const args = parseToolArguments(call.function.arguments);
323
- const stepId = crypto.randomUUID();
324
- input.toolHandlers?.onStart({
325
- id: stepId,
326
- name: call.function.name,
327
- args,
328
- callId: call.id,
329
- });
330
-
331
- let toolContent: string;
332
- let finishUpdate: {
333
- status: "done" | "error";
334
- result?: unknown;
335
- error?: string;
336
- };
337
-
338
- if (isChatUiTool(call.function.name)) {
339
- try {
340
- const interactiveResult = await executeChatInteractiveTool(
341
- call.function.name,
342
- args,
343
- call.id,
344
- input.signal,
345
- );
346
- toolContent = interactiveResult.content;
347
- finishUpdate = {
348
- status: interactiveResult.isError ? "error" : "done",
349
- result: JSON.parse(interactiveResult.content),
350
- };
351
- if (interactiveResult.postResponse) {
352
- input.onPostResponseTool?.(interactiveResult.postResponse);
353
- terminatesTurn = true;
354
- }
355
- } catch (error) {
356
- if (
357
- input.signal?.aborted ||
358
- (error instanceof DOMException && error.name === "AbortError")
359
- ) {
360
- throw new ChatAbortedError();
361
- }
362
- const message =
363
- error instanceof Error ? error.message : String(error);
364
- toolContent = message;
365
- finishUpdate = { status: "error", error: message };
366
- }
367
- } else {
368
- const result = await input.invokeTool(call.function.name, args);
369
- lastToolResult = result;
370
- toolContent = formatToolResult(result);
371
- finishUpdate = {
372
- status: result.isError ? "error" : "done",
373
- result: result.structuredContent ?? result,
374
- error: result.isError ? result.content[0]?.text : undefined,
375
- };
376
- }
377
-
378
- input.toolHandlers?.onFinish(stepId, finishUpdate);
379
-
380
- apiMessages.push({
381
- role: "tool",
382
- tool_call_id: call.id,
383
- content: toolContent,
384
- });
385
-
386
- if (shouldTerminateTurnAfterTool(call.function.name)) {
387
- terminatesTurn = true;
388
- }
389
- }
390
-
391
- if (terminatesTurn) {
392
- finalContent =
393
- completion.content?.trim() ||
394
- finalContent.trim() ||
395
- "Choose an option below or type your reply.";
396
- input.stream?.onUpdate(finalContent);
397
- endedWithPostResponseTool = true;
398
- break;
399
- }
400
- }
401
-
402
- if (!endedWithPostResponseTool && apiMessages.at(-1)?.role === "tool") {
403
- const completion = await requestLlmCompletion({
404
- messages: apiMessages,
405
- tools: clientTools,
406
- model: input.model,
407
- signal: input.signal,
408
- stream: input.stream
409
- ? {
410
- turnId: input.stream.turnId,
411
- onUpdate: (content) => {
412
- finalContent = content;
413
- input.stream?.onUpdate(content);
414
- },
415
- }
416
- : undefined,
417
- });
418
- finalContent =
419
- completion.content?.trim() || "Done — see the trace below for details.";
420
- input.stream?.onUpdate(finalContent);
421
- apiMessages.push({
422
- role: "assistant",
423
- content: completion.content,
424
- });
425
- }
426
-
427
- return {
428
- assistantMessages: finalContent
429
- ? [{ id: input.stream?.turnId, content: finalContent }]
430
- : [],
431
- apiMessages,
432
- lastToolResult,
433
- };
434
- }
435
-
436
- function resolveAssistantContentForApi(message: {
437
- content: string;
438
- replySuggestions?: ReplySuggestions;
439
- }): string {
440
- const content = message.content.trim();
441
- if (content) return message.content;
442
- if (message.replySuggestions?.question.trim()) {
443
- return message.replySuggestions.question;
444
- }
445
- return message.content;
446
- }
447
-
448
- /** Keep recent turns at UI-message boundaries so tool-call groups stay intact. */
449
- const MAX_HISTORY_USER_TURNS = 12;
450
-
451
- function trimMessagesForHistory<T extends { role: string }>(
452
- messages: T[],
453
- maxUserTurns: number,
454
- ): T[] {
455
- if (messages.length === 0 || maxUserTurns <= 0) return messages;
456
-
457
- let userCount = 0;
458
- let start = 0;
459
- for (let i = messages.length - 1; i >= 0; i--) {
460
- if (messages[i]?.role === "user") {
461
- userCount++;
462
- if (userCount >= maxUserTurns) {
463
- start = i;
464
- break;
465
- }
466
- }
467
- }
468
- return messages.slice(start);
469
- }
470
-
471
- /** Drop tool messages that are not preceded by a matching assistant tool_calls entry. */
472
- export function sanitizeLlmMessages(
473
- messages: LlmChatMessage[],
474
- ): LlmChatMessage[] {
475
- const out: LlmChatMessage[] = [];
476
- let pendingToolCallIds: Set<string> | null = null;
477
-
478
- for (const msg of messages) {
479
- if (msg.role === "assistant" && msg.tool_calls?.length) {
480
- out.push(msg);
481
- pendingToolCallIds = new Set(
482
- msg.tool_calls.map((toolCall) => toolCall.id),
483
- );
484
- continue;
485
- }
486
-
487
- if (msg.role === "tool") {
488
- const callId = msg.tool_call_id;
489
- if (callId && pendingToolCallIds?.has(callId)) {
490
- out.push(msg);
491
- pendingToolCallIds.delete(callId);
492
- if (pendingToolCallIds.size === 0) {
493
- pendingToolCallIds = null;
494
- }
495
- }
496
- continue;
497
- }
498
-
499
- pendingToolCallIds = null;
500
- out.push(msg);
501
- }
502
-
503
- while (out[0]?.role === "tool") {
504
- out.shift();
505
- }
506
-
507
- return out;
508
- }
509
-
510
- export function buildLlmHistory(
511
- messages: Array<{
512
- role: string;
513
- content: string;
514
- replySuggestions?: ReplySuggestions;
515
- activity?: Array<{
516
- name: string;
517
- args: Record<string, unknown>;
518
- error?: string;
519
- result?: unknown;
520
- }>;
521
- toolCall?: { name: string; args: Record<string, unknown> };
522
- }>,
523
- ): LlmChatMessage[] {
524
- const history: LlmChatMessage[] = [];
525
-
526
- for (const msg of trimMessagesForHistory(messages, MAX_HISTORY_USER_TURNS)) {
527
- if (msg.role === "user") {
528
- history.push({ role: "user", content: msg.content });
529
- continue;
530
- }
531
- if (msg.role !== "assistant") continue;
532
-
533
- if (msg.activity?.length) {
534
- const baseId = history.length;
535
- const toolCalls = msg.activity.map((step, index) => ({
536
- id: `call_${baseId}_${index}`,
537
- type: "function" as const,
538
- function: {
539
- name: step.name,
540
- arguments: JSON.stringify(step.args),
541
- },
542
- }));
543
- history.push({
544
- role: "assistant",
545
- content: resolveAssistantContentForApi(msg) || null,
546
- tool_calls: toolCalls,
547
- });
548
- for (const [index, step] of msg.activity.entries()) {
549
- history.push({
550
- role: "tool",
551
- tool_call_id: `call_${baseId}_${index}`,
552
- content:
553
- step.error ??
554
- (typeof step.result === "string"
555
- ? step.result
556
- : JSON.stringify(step.result ?? "Result shown in UI.")),
557
- });
558
- }
559
- continue;
560
- }
561
-
562
- if (msg.toolCall) {
563
- const id = `call_${history.length}`;
564
- history.push({
565
- role: "assistant",
566
- content: msg.content || null,
567
- tool_calls: [
568
- {
569
- id,
570
- type: "function",
571
- function: {
572
- name: msg.toolCall.name,
573
- arguments: JSON.stringify(msg.toolCall.args),
574
- },
575
- },
576
- ],
577
- });
578
- history.push({
579
- role: "tool",
580
- tool_call_id: id,
581
- content: "Result shown in UI.",
582
- });
583
- continue;
584
- }
585
-
586
- history.push({
587
- role: "assistant",
588
- content: resolveAssistantContentForApi(msg),
589
- });
590
- }
591
-
592
- return sanitizeLlmMessages(history);
593
- }
594
-
595
- async function requireLlmSettings(): Promise<ResolvedLlmSettings> {
596
- const settings = await resolveAssistantLlmSettings();
597
- if (!isLlmConfigured(settings)) {
598
- throw new LlmNotConfiguredError();
599
- }
600
- return settings;
601
- }
602
-
603
- function buildChatPayload(
604
- body: {
605
- messages: LlmChatMessage[];
606
- tools?: ClientToolDefinition[];
607
- model?: string;
608
- systemPrompt?: string;
609
- responseFormat?: { type: "json_object" };
610
- },
611
- settings: ResolvedLlmSettings,
612
- ) {
613
- const messages: LlmChatMessage[] = [
614
- {
615
- role: "system",
616
- content:
617
- body.systemPrompt ?? settings.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
618
- },
619
- ...body.messages.filter((message) => message.role !== "system"),
620
- ];
621
-
622
- const payload: Record<string, unknown> = {
623
- model: resolveRequestModel(body.model, settings),
624
- messages,
625
- };
626
-
627
- if (body.responseFormat) {
628
- payload.response_format = body.responseFormat;
629
- }
630
-
631
- if (body.tools?.length) {
632
- payload.tools = body.tools.map((tool) => ({
633
- type: "function",
634
- function: {
635
- name: tool.name,
636
- description: tool.description,
637
- parameters: tool.inputSchema,
638
- },
639
- }));
640
- payload.tool_choice = "auto";
641
- }
642
-
643
- return payload;
644
- }
645
-
646
- function upstreamError(body: Record<string, unknown>, status: number): Error {
647
- const errorRecord = body.error as Record<string, unknown> | undefined;
648
- const message =
649
- (typeof errorRecord?.message === "string" && errorRecord.message) ||
650
- (typeof body.message === "string" && body.message) ||
651
- (typeof body.error === "string" && body.error) ||
652
- `LLM request failed (${status})`;
653
- const code =
654
- (typeof body.code === "string" && body.code) || "LLM_UPSTREAM_ERROR";
655
- return new LlmUpstreamError(message, status, code);
656
- }
657
-
658
- function toClientTool(tool: AssistantToolDefinition): ClientToolDefinition {
659
- return {
660
- name: tool.name,
661
- description: tool.description,
662
- inputSchema: tool.inputSchema,
663
- };
664
- }
665
-
666
- function parseToolArguments(raw: string): Record<string, unknown> {
667
- try {
668
- const parsed = JSON.parse(raw) as unknown;
669
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
670
- return parsed as Record<string, unknown>;
671
- }
672
- return { params: parsed };
673
- } catch {
674
- return { params: [raw] };
675
- }
676
- }
677
-
678
- function formatToolResult(result: AssistantToolResult): string {
679
- if (result.isError) {
680
- return result.content.map((c) => c.text).join("\n");
681
- }
682
- if (result.structuredContent !== undefined) {
683
- return JSON.stringify(result.structuredContent);
684
- }
685
- return result.content.map((c) => c.text).join("\n");
686
- }