@arcote.tech/arc-chat 0.5.1 → 0.5.2
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/package.json +6 -6
- package/src/aggregates/message.ts +272 -43
- package/src/chat-builder.ts +243 -83
- package/src/index.ts +4 -22
- package/src/listeners/ai-generation-listener.ts +322 -249
- package/src/react/chat-component.tsx +457 -0
- package/src/react/index.ts +2 -3
- package/src/react/use-chat.ts +1 -260
- package/src/routes/chat-stream-route.ts +4 -10
- package/src/streaming/stream-registry.ts +92 -124
- package/src/tools/ask-questions.tsx +107 -0
- package/src/routes/tool-results-route.ts +0 -49
|
@@ -1,293 +1,366 @@
|
|
|
1
1
|
/// <reference path="../arc.d.ts" />
|
|
2
|
-
import { listener, type ArcContextElement } from "@arcote.tech/arc";
|
|
3
|
-
import type { ArcToolAny, LLMProvider, Message
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
createStreamSession,
|
|
7
|
-
getStreamSession,
|
|
8
|
-
deleteStreamSession,
|
|
9
|
-
} from "../streaming/stream-registry";
|
|
2
|
+
import { listener, type ArcContextElement, type ArcFunction } from "@arcote.tech/arc";
|
|
3
|
+
import type { ArcToolAny, LLMProvider, Message } from "@arcote.tech/arc-ai";
|
|
4
|
+
import { broadcast, endStream } from "../streaming/stream-registry";
|
|
10
5
|
|
|
11
6
|
// ─── Config ─────────────────────────────────────────────────────
|
|
12
7
|
|
|
13
8
|
export interface AiGenerationListenerConfig {
|
|
14
9
|
name: string;
|
|
15
10
|
messageElement: any;
|
|
16
|
-
resolveProvider: (model: string) => LLMProvider | undefined;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
resolveProvider: (model: string, scopeId?: string) => LLMProvider | undefined;
|
|
12
|
+
instruction?: ArcFunction<any>;
|
|
13
|
+
serverTools: ArcToolAny[];
|
|
14
|
+
interactiveTools: ArcToolAny[];
|
|
15
|
+
allQueryElements: ArcContextElement<any>[];
|
|
16
|
+
allMutationElements: ArcContextElement<any>[];
|
|
21
17
|
maxExecutionCount: number;
|
|
18
|
+
toolChoice?: "auto" | "required" | { type: "function"; name: string };
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
// ───
|
|
21
|
+
// ─── Utilities ──────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function buildLlmMessages(
|
|
24
|
+
history: any[],
|
|
25
|
+
systemPrompt?: string,
|
|
26
|
+
skipMessageId?: string,
|
|
27
|
+
): Message[] {
|
|
28
|
+
const messages: Message[] = [];
|
|
29
|
+
if (systemPrompt) {
|
|
30
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
31
|
+
}
|
|
32
|
+
for (const msg of history) {
|
|
33
|
+
if (msg._id === skipMessageId) continue;
|
|
34
|
+
if (msg.role === "user" || msg.role === "assistant") {
|
|
35
|
+
if (msg.isGenerating && !msg.content) continue;
|
|
36
|
+
messages.push({ role: msg.role as Message["role"], content: msg.content });
|
|
37
|
+
} else if (msg.role === "tool_result") {
|
|
38
|
+
messages.push({
|
|
39
|
+
role: "tool",
|
|
40
|
+
content: msg.content,
|
|
41
|
+
toolCallId: msg.toolCallId,
|
|
42
|
+
name: msg.toolName,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return messages;
|
|
47
|
+
}
|
|
25
48
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
async function buildSystemPrompt(
|
|
50
|
+
instruction: ArcFunction<any> | undefined,
|
|
51
|
+
ctx: any,
|
|
52
|
+
): Promise<string> {
|
|
53
|
+
if (!instruction?.handler) return "";
|
|
54
|
+
const instructionCtx = {
|
|
55
|
+
query: (element: ArcContextElement<any>) => ctx.query(element),
|
|
56
|
+
mutate: (element: ArcContextElement<any>) => ctx.mutate(element),
|
|
57
|
+
};
|
|
58
|
+
return (instruction.handler as Function)(instructionCtx);
|
|
59
|
+
}
|
|
37
60
|
|
|
38
|
-
|
|
61
|
+
// ─── AI generation loop ─────────────────────────────────────────
|
|
39
62
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
async function runGenerationLoop(config: {
|
|
64
|
+
ctx: any;
|
|
65
|
+
messageElement: any;
|
|
66
|
+
provider: LLMProvider;
|
|
67
|
+
model: string;
|
|
68
|
+
initialMessages: Message[];
|
|
69
|
+
toolDefs: any[] | undefined;
|
|
70
|
+
serverToolsMap: Map<string, ArcToolAny>;
|
|
71
|
+
interactiveToolNames: Set<string>;
|
|
72
|
+
generationMessageId: string;
|
|
73
|
+
scopeId: string;
|
|
74
|
+
sessionId: string;
|
|
75
|
+
maxExecutionCount: number;
|
|
76
|
+
initialPreviousResponseId?: string;
|
|
77
|
+
toolChoice?: "auto" | "required" | { type: "function"; name: string };
|
|
78
|
+
}) {
|
|
79
|
+
const {
|
|
80
|
+
ctx, messageElement, provider, model, toolDefs,
|
|
81
|
+
serverToolsMap, interactiveToolNames,
|
|
82
|
+
generationMessageId, scopeId, sessionId, maxExecutionCount, toolChoice,
|
|
83
|
+
} = config;
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
let executionCount = 0;
|
|
86
|
+
let fullContent = "";
|
|
87
|
+
let previousResponseId = config.initialPreviousResponseId;
|
|
88
|
+
let currentMessages = config.initialMessages;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
while (executionCount <= maxExecutionCount) {
|
|
92
|
+
const result = await provider.streamComplete(
|
|
93
|
+
{ model, messages: currentMessages, tools: toolDefs, previousResponseId, toolChoice },
|
|
94
|
+
(chunk) => {
|
|
95
|
+
if (chunk.type === "content_delta" && chunk.content) {
|
|
96
|
+
fullContent += chunk.content;
|
|
97
|
+
broadcast(sessionId, {
|
|
98
|
+
type: "content_delta",
|
|
99
|
+
sessionId,
|
|
100
|
+
content: chunk.content,
|
|
101
|
+
});
|
|
102
|
+
} else if (chunk.type === "usage_update") {
|
|
103
|
+
broadcast(sessionId, {
|
|
104
|
+
type: "usage_update",
|
|
105
|
+
sessionId,
|
|
106
|
+
usage: chunk.usage,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (result.content) fullContent = result.content;
|
|
113
|
+
previousResponseId = result.responseId;
|
|
114
|
+
|
|
115
|
+
// No tool calls — done
|
|
116
|
+
if (result.finishReason !== "tool_call" || result.toolCalls.length === 0) {
|
|
117
|
+
if (fullContent) {
|
|
118
|
+
await ctx.mutate(messageElement).saveAssistantMessage({
|
|
119
|
+
scopeId, sessionId, content: fullContent, model,
|
|
120
|
+
previousResponseId,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
await ctx.mutate(messageElement).completeGeneration({
|
|
124
|
+
generationMessageId, sessionId,
|
|
125
|
+
usage: JSON.stringify(result.usage),
|
|
126
|
+
});
|
|
127
|
+
broadcast(sessionId, {
|
|
128
|
+
type: "done",
|
|
66
129
|
sessionId,
|
|
67
|
-
|
|
130
|
+
usage: result.usage,
|
|
131
|
+
finishReason: result.finishReason,
|
|
132
|
+
executionCount,
|
|
68
133
|
});
|
|
69
|
-
|
|
70
|
-
deleteStreamSession(sessionId);
|
|
134
|
+
endStream(sessionId);
|
|
71
135
|
return;
|
|
72
136
|
}
|
|
73
137
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (prepare) {
|
|
80
|
-
const prepareCtx: PrepareContext = {
|
|
81
|
-
query: (element) => ctx.query(element),
|
|
82
|
-
mutate: (element) => ctx.mutate(element),
|
|
83
|
-
};
|
|
84
|
-
const prepareResult = await prepare(prepareCtx, {
|
|
85
|
-
content: userContent,
|
|
86
|
-
identifyBy: scopeId,
|
|
87
|
-
model,
|
|
138
|
+
// Save intermediate text
|
|
139
|
+
if (fullContent) {
|
|
140
|
+
await ctx.mutate(messageElement).saveAssistantMessage({
|
|
141
|
+
scopeId, sessionId, content: fullContent, model,
|
|
142
|
+
previousResponseId,
|
|
88
143
|
});
|
|
89
|
-
|
|
90
|
-
if (prepareResult.tools) serverTools = prepareResult.tools;
|
|
91
|
-
if (prepareResult.clientTools) clientTools = prepareResult.clientTools;
|
|
144
|
+
fullContent = "";
|
|
92
145
|
}
|
|
93
146
|
|
|
94
|
-
//
|
|
95
|
-
const
|
|
96
|
-
const
|
|
147
|
+
// Separate server vs interactive
|
|
148
|
+
const serverCalls = result.toolCalls.filter((tc) => serverToolsMap.has(tc.name));
|
|
149
|
+
const interactiveCalls = result.toolCalls.filter((tc) => interactiveToolNames.has(tc.name));
|
|
150
|
+
|
|
151
|
+
// Execute server tools — collect ONLY new results
|
|
152
|
+
const newToolResults: Message[] = [];
|
|
153
|
+
for (const tc of serverCalls) {
|
|
154
|
+
await ctx.mutate(messageElement).saveToolCall({
|
|
155
|
+
scopeId, sessionId,
|
|
156
|
+
toolName: tc.name, toolCallId: tc.id,
|
|
157
|
+
content: JSON.stringify(tc.arguments),
|
|
158
|
+
previousResponseId,
|
|
159
|
+
});
|
|
97
160
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
161
|
+
broadcast(sessionId, {
|
|
162
|
+
type: "server_tool_start",
|
|
163
|
+
sessionId,
|
|
164
|
+
toolCall: tc,
|
|
165
|
+
executionCount,
|
|
166
|
+
});
|
|
103
167
|
|
|
104
|
-
|
|
105
|
-
|
|
168
|
+
const tool = serverToolsMap.get(tc.name);
|
|
169
|
+
let resultContent: string;
|
|
170
|
+
let isError = false;
|
|
106
171
|
|
|
107
|
-
|
|
108
|
-
|
|
172
|
+
if (tool) {
|
|
173
|
+
try {
|
|
174
|
+
resultContent = await tool.executeWithContext(tc.arguments, ctx, scopeId);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
resultContent = `Tool error: ${err instanceof Error ? err.message : String(err)}`;
|
|
177
|
+
isError = true;
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
resultContent = `Tool "${tc.name}" not found`;
|
|
181
|
+
isError = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await ctx.mutate(messageElement).saveToolResult({
|
|
185
|
+
scopeId, sessionId,
|
|
186
|
+
toolName: tc.name, toolCallId: tc.id,
|
|
187
|
+
content: resultContent, isError,
|
|
188
|
+
});
|
|
109
189
|
|
|
110
|
-
|
|
111
|
-
|
|
190
|
+
broadcast(sessionId, {
|
|
191
|
+
type: "server_tool_result",
|
|
192
|
+
sessionId,
|
|
193
|
+
toolCall: tc,
|
|
194
|
+
toolResult: { toolCallId: tc.id, name: tc.name, content: resultContent, isError },
|
|
195
|
+
executionCount,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
newToolResults.push({
|
|
199
|
+
role: "tool", content: resultContent,
|
|
200
|
+
toolCallId: tc.id, name: tc.name,
|
|
201
|
+
});
|
|
112
202
|
}
|
|
113
203
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
204
|
+
// Interactive tools — save and STOP
|
|
205
|
+
if (interactiveCalls.length > 0) {
|
|
206
|
+
for (const tc of interactiveCalls) {
|
|
207
|
+
await ctx.mutate(messageElement).saveToolCall({
|
|
208
|
+
scopeId, sessionId,
|
|
209
|
+
toolName: tc.name, toolCallId: tc.id,
|
|
210
|
+
content: JSON.stringify(tc.arguments),
|
|
211
|
+
previousResponseId,
|
|
212
|
+
});
|
|
117
213
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
214
|
+
broadcast(sessionId, {
|
|
215
|
+
type: "interactive_tool_request",
|
|
216
|
+
sessionId,
|
|
217
|
+
toolCalls: interactiveCalls,
|
|
218
|
+
executionCount,
|
|
121
219
|
});
|
|
220
|
+
// Don't endStream — client stays connected for possible updates
|
|
221
|
+
// Don't completeGeneration — Listener B will resume
|
|
222
|
+
return;
|
|
122
223
|
}
|
|
123
224
|
|
|
124
|
-
|
|
225
|
+
// Next iteration: ONLY new tool results (provider has rest via previousResponseId)
|
|
226
|
+
currentMessages = newToolResults;
|
|
227
|
+
fullContent = "";
|
|
228
|
+
executionCount++;
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
broadcast(sessionId, {
|
|
232
|
+
type: "error",
|
|
233
|
+
sessionId,
|
|
234
|
+
error: `AI error: ${err instanceof Error ? err.message : String(err)}`,
|
|
235
|
+
executionCount,
|
|
236
|
+
});
|
|
237
|
+
try {
|
|
238
|
+
await ctx.mutate(messageElement).completeGeneration({
|
|
239
|
+
generationMessageId, sessionId,
|
|
240
|
+
});
|
|
241
|
+
} catch {}
|
|
242
|
+
endStream(sessionId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
125
245
|
|
|
126
|
-
|
|
127
|
-
const toolCtx: ToolContext = {
|
|
128
|
-
mutate: (element) => ctx.mutate(element),
|
|
129
|
-
query: (element) => ctx.query(element),
|
|
130
|
-
identifyBy: scopeId,
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
// 7. AI generation loop
|
|
134
|
-
let executionCount = 0;
|
|
135
|
-
let fullContent = "";
|
|
136
|
-
let previousResponseId: string | undefined;
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
while (executionCount <= defaultMaxExecution) {
|
|
140
|
-
const result = await provider.streamComplete(
|
|
141
|
-
{ model, messages, tools: toolDefs, previousResponseId },
|
|
142
|
-
(chunk) => {
|
|
143
|
-
switch (chunk.type) {
|
|
144
|
-
case "content_delta":
|
|
145
|
-
if (chunk.content) {
|
|
146
|
-
fullContent += chunk.content;
|
|
147
|
-
session!.push({
|
|
148
|
-
type: "content_delta",
|
|
149
|
-
sessionId,
|
|
150
|
-
content: chunk.content,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
break;
|
|
154
|
-
case "usage_update":
|
|
155
|
-
session!.push({
|
|
156
|
-
type: "usage_update",
|
|
157
|
-
sessionId,
|
|
158
|
-
usage: chunk.usage,
|
|
159
|
-
});
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
if (result.content) {
|
|
166
|
-
fullContent = result.content;
|
|
167
|
-
}
|
|
168
|
-
previousResponseId = result.responseId;
|
|
169
|
-
|
|
170
|
-
// No tool calls — generation complete
|
|
171
|
-
if (
|
|
172
|
-
result.finishReason !== "tool_call" ||
|
|
173
|
-
result.toolCalls.length === 0
|
|
174
|
-
) {
|
|
175
|
-
await ctx.mutate(messageElement).completeAssistantMessage({
|
|
176
|
-
scopeId,
|
|
177
|
-
sessionId,
|
|
178
|
-
content: fullContent,
|
|
179
|
-
model,
|
|
180
|
-
usage: JSON.stringify(result.usage),
|
|
181
|
-
});
|
|
246
|
+
// ─── Listener A: messageSent → AI generation ────────────────────
|
|
182
247
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
});
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
248
|
+
export function createAiGenerationListener(config: AiGenerationListenerConfig) {
|
|
249
|
+
const {
|
|
250
|
+
name, messageElement, resolveProvider, instruction,
|
|
251
|
+
serverTools, interactiveTools,
|
|
252
|
+
allQueryElements, allMutationElements, maxExecutionCount,
|
|
253
|
+
} = config;
|
|
192
254
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// Execute server tools with aggregate context
|
|
202
|
-
for (const tc of serverCalls) {
|
|
203
|
-
session.push({
|
|
204
|
-
type: "server_tool_start",
|
|
205
|
-
sessionId,
|
|
206
|
-
toolCall: tc,
|
|
207
|
-
executionCount,
|
|
208
|
-
});
|
|
255
|
+
const messageSentEvent = messageElement.getEvent("messageSent");
|
|
256
|
+
const serverToolsMap = new Map(serverTools.map((t) => [t.name, t]));
|
|
257
|
+
const interactiveToolNames = new Set(interactiveTools.map((t) => t.name));
|
|
258
|
+
const allToolsForLLM = [...serverTools, ...interactiveTools];
|
|
259
|
+
const toolDefs = allToolsForLLM.length > 0
|
|
260
|
+
? allToolsForLLM.map((t) => t.toJsonSchema())
|
|
261
|
+
: undefined;
|
|
209
262
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
} catch (err) {
|
|
218
|
-
resultContent = `Tool execution error: ${err instanceof Error ? err.message : String(err)}`;
|
|
219
|
-
isError = true;
|
|
220
|
-
}
|
|
221
|
-
} else {
|
|
222
|
-
resultContent = `Tool "${tc.name}" not found on server`;
|
|
223
|
-
isError = true;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
session.push({
|
|
227
|
-
type: "server_tool_result",
|
|
228
|
-
sessionId,
|
|
229
|
-
toolResult: {
|
|
230
|
-
toolCallId: tc.id,
|
|
231
|
-
name: tc.name,
|
|
232
|
-
content: resultContent,
|
|
233
|
-
isError,
|
|
234
|
-
},
|
|
235
|
-
executionCount,
|
|
236
|
-
});
|
|
263
|
+
return listener(`${name}AiGeneration`)
|
|
264
|
+
.listenTo([messageSentEvent])
|
|
265
|
+
.async()
|
|
266
|
+
.query([messageElement, ...allQueryElements])
|
|
267
|
+
.mutate([messageElement, ...allMutationElements])
|
|
268
|
+
.handle(async (ctx, event) => {
|
|
269
|
+
const { sessionId, scopeId, content: userContent, model: modelName } = event.payload;
|
|
237
270
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
toolCallId: tc.id,
|
|
242
|
-
name: tc.name,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
271
|
+
const model = modelName ?? "gpt-5.4-nano";
|
|
272
|
+
const provider = resolveProvider(model, scopeId);
|
|
273
|
+
if (!provider) return;
|
|
245
274
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
sessionId,
|
|
251
|
-
toolCalls: clientCalls,
|
|
252
|
-
executionCount,
|
|
253
|
-
});
|
|
275
|
+
const systemPrompt = await buildSystemPrompt(instruction, ctx);
|
|
276
|
+
const history = await ctx.query(messageElement).getByScope({ scopeId });
|
|
277
|
+
const messages = buildLlmMessages(history, systemPrompt, event.payload.messageId);
|
|
278
|
+
messages.push({ role: "user", content: userContent });
|
|
254
279
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
280
|
+
const generationResult = await ctx.mutate(messageElement).saveAssistantMessage({
|
|
281
|
+
scopeId, sessionId, content: "", model, isGenerating: true,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await runGenerationLoop({
|
|
285
|
+
ctx, messageElement, provider, model,
|
|
286
|
+
initialMessages: messages,
|
|
287
|
+
toolDefs, serverToolsMap, interactiveToolNames,
|
|
288
|
+
generationMessageId: generationResult.messageId,
|
|
289
|
+
scopeId, sessionId, maxExecutionCount,
|
|
290
|
+
toolChoice: config.toolChoice,
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── Listener B: userResponded → AI resume ──────────────────────
|
|
296
|
+
|
|
297
|
+
export function createAiResumeListener(config: AiGenerationListenerConfig) {
|
|
298
|
+
const {
|
|
299
|
+
name, messageElement, resolveProvider, instruction,
|
|
300
|
+
serverTools, interactiveTools,
|
|
301
|
+
allQueryElements, allMutationElements, maxExecutionCount,
|
|
302
|
+
} = config;
|
|
277
303
|
|
|
278
|
-
|
|
279
|
-
|
|
304
|
+
const userRespondedEvent = messageElement.getEvent("userResponded");
|
|
305
|
+
const serverToolsMap = new Map(serverTools.map((t) => [t.name, t]));
|
|
306
|
+
const interactiveToolNames = new Set(interactiveTools.map((t) => t.name));
|
|
307
|
+
const allToolsForLLM = [...serverTools, ...interactiveTools];
|
|
308
|
+
const toolDefs = allToolsForLLM.length > 0
|
|
309
|
+
? allToolsForLLM.map((t) => t.toJsonSchema())
|
|
310
|
+
: undefined;
|
|
311
|
+
|
|
312
|
+
return listener(`${name}AiResume`)
|
|
313
|
+
.listenTo([userRespondedEvent])
|
|
314
|
+
.async()
|
|
315
|
+
.query([messageElement, ...allQueryElements])
|
|
316
|
+
.mutate([messageElement, ...allMutationElements])
|
|
317
|
+
.handle(async (ctx, event) => {
|
|
318
|
+
const { sessionId, scopeId, toolCallId, toolName, content: toolResult } = event.payload;
|
|
319
|
+
|
|
320
|
+
const history = await ctx.query(messageElement).getByScope({ scopeId });
|
|
321
|
+
|
|
322
|
+
// Find previousResponseId from the tool_call this responds to
|
|
323
|
+
const matchingToolCall = [...history]
|
|
324
|
+
.reverse()
|
|
325
|
+
.find((msg: any) => msg.role === "tool_call" && msg.toolCallId === toolCallId);
|
|
326
|
+
const prevResponseId = matchingToolCall?.previousResponseId;
|
|
327
|
+
|
|
328
|
+
const model = matchingToolCall?.model
|
|
329
|
+
?? history.find((m: any) => m.model)?.model
|
|
330
|
+
?? "gpt-5.4-nano";
|
|
331
|
+
|
|
332
|
+
const provider = resolveProvider(model, scopeId);
|
|
333
|
+
if (!provider) return;
|
|
334
|
+
|
|
335
|
+
// Build initial messages for this iteration
|
|
336
|
+
let initialMessages: Message[];
|
|
337
|
+
if (prevResponseId) {
|
|
338
|
+
// Provider has context — send only the new tool result
|
|
339
|
+
initialMessages = [{
|
|
340
|
+
role: "tool", content: toolResult,
|
|
341
|
+
toolCallId, name: toolName,
|
|
342
|
+
}];
|
|
343
|
+
} else {
|
|
344
|
+
// Fallback: full history
|
|
345
|
+
const systemPrompt = await buildSystemPrompt(instruction, ctx);
|
|
346
|
+
initialMessages = buildLlmMessages(history, systemPrompt);
|
|
347
|
+
if (!history.some((m: any) => m.toolCallId === toolCallId && m.role === "tool_result")) {
|
|
348
|
+
initialMessages.push({ role: "tool", content: toolResult, toolCallId, name: toolName });
|
|
280
349
|
}
|
|
281
|
-
} catch (err) {
|
|
282
|
-
session.push({
|
|
283
|
-
type: "error",
|
|
284
|
-
sessionId,
|
|
285
|
-
error: `AI generation error: ${err instanceof Error ? err.message : String(err)}`,
|
|
286
|
-
executionCount,
|
|
287
|
-
});
|
|
288
|
-
} finally {
|
|
289
|
-
session.close();
|
|
290
|
-
deleteStreamSession(sessionId);
|
|
291
350
|
}
|
|
351
|
+
|
|
352
|
+
const generationResult = await ctx.mutate(messageElement).saveAssistantMessage({
|
|
353
|
+
scopeId, sessionId, content: "", model, isGenerating: true,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
await runGenerationLoop({
|
|
357
|
+
ctx, messageElement, provider, model,
|
|
358
|
+
initialMessages,
|
|
359
|
+
toolDefs, serverToolsMap, interactiveToolNames,
|
|
360
|
+
generationMessageId: generationResult.messageId,
|
|
361
|
+
scopeId, sessionId, maxExecutionCount,
|
|
362
|
+
initialPreviousResponseId: prevResponseId,
|
|
363
|
+
toolChoice: config.toolChoice,
|
|
364
|
+
});
|
|
292
365
|
});
|
|
293
366
|
}
|