openbot 0.3.0 → 0.3.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/dist/app/cli.js +1 -1
- package/dist/app/server.js +1 -4
- package/dist/bus/services.js +222 -15
- package/dist/harness/context.js +205 -26
- package/dist/harness/queue-processor.js +44 -110
- package/dist/harness/runtime-factory.js +11 -7
- package/dist/harness/todo-advance.js +93 -0
- package/dist/plugins/ai-sdk/index.js +0 -3
- package/dist/plugins/ai-sdk/runtime.js +78 -13
- package/dist/plugins/ai-sdk/system-prompt.js +18 -3
- package/dist/plugins/delegation/index.js +7 -46
- package/dist/plugins/memory/index.js +71 -0
- package/dist/plugins/storage-tools/index.js +2 -11
- package/dist/plugins/todo/index.js +54 -0
- package/dist/plugins/workflow/index.js +65 -0
- package/dist/registry/plugins.js +4 -2
- package/dist/services/memory.js +152 -0
- package/dist/services/storage.js +9 -31
- package/dist/workflow/service.js +106 -0
- package/dist/workflow/types.js +3 -0
- package/docs/agents.md +15 -1
- package/docs/plugins.md +0 -1
- package/package.json +1 -1
- package/src/app/cli.ts +1 -1
- package/src/app/server.ts +3 -4
- package/src/app/types.ts +140 -45
- package/src/bus/plugin.ts +0 -2
- package/src/bus/services.ts +258 -17
- package/src/bus/types.ts +13 -4
- package/src/harness/context.ts +233 -37
- package/src/harness/queue-processor.ts +54 -143
- package/src/harness/runtime-factory.ts +11 -7
- package/src/harness/todo-advance.ts +128 -0
- package/src/plugins/ai-sdk/index.ts +0 -3
- package/src/plugins/ai-sdk/runtime.ts +356 -298
- package/src/plugins/ai-sdk/system-prompt.ts +18 -4
- package/src/plugins/delegation/index.ts +7 -50
- package/src/plugins/memory/index.ts +85 -0
- package/src/plugins/storage-tools/index.ts +8 -19
- package/src/plugins/todo/index.ts +64 -0
- package/src/registry/plugins.ts +4 -3
- package/src/services/memory.ts +213 -0
- package/src/services/storage.ts +9 -49
|
@@ -11,8 +11,6 @@ import { saveConfig } from '../../app/config.js';
|
|
|
11
11
|
export interface AiSdkRuntimeOptions {
|
|
12
12
|
/** Provider model string (e.g. `openai/gpt-4o-mini`, `anthropic/claude-3-5-sonnet-20240620`). */
|
|
13
13
|
model?: string;
|
|
14
|
-
/** Static or dynamic system prompt. */
|
|
15
|
-
system?: string | ((context: RuntimeContext) => string | Promise<string>);
|
|
16
14
|
storage?: Storage;
|
|
17
15
|
contextEngine?: {
|
|
18
16
|
buildContext: (state: OpenBotState, storage?: Storage) => Promise<string>;
|
|
@@ -42,6 +40,78 @@ const asRecord = (value: unknown): Record<string, unknown> =>
|
|
|
42
40
|
? (value as Record<string, unknown>)
|
|
43
41
|
: {};
|
|
44
42
|
|
|
43
|
+
/** Per-message hard cap (in characters) on tool-result payloads we feed back
|
|
44
|
+
* to the model. Prevents one huge tool output from eating the context window;
|
|
45
|
+
* the original event remains intact in storage. */
|
|
46
|
+
const TOOL_RESULT_MAX_CHARS = 8000;
|
|
47
|
+
|
|
48
|
+
/** Sliding window: max number of messages we replay to the model on each
|
|
49
|
+
* invocation. Older turns stay on disk but are not sent. Keeps both the
|
|
50
|
+
* recent prompts and the prompt token budget bounded. */
|
|
51
|
+
const MAX_WINDOW_MESSAGES = 80;
|
|
52
|
+
|
|
53
|
+
const truncateToolPayload = (raw: unknown): string => {
|
|
54
|
+
const serialized = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
|
55
|
+
if (serialized.length <= TOOL_RESULT_MAX_CHARS) return serialized;
|
|
56
|
+
const dropped = serialized.length - TOOL_RESULT_MAX_CHARS;
|
|
57
|
+
return `${serialized.slice(0, TOOL_RESULT_MAX_CHARS)}\n…[truncated ${dropped} chars]`;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Trim the message history to a sliding window while preserving tool-call
|
|
62
|
+
* integrity. Drops any leading orphan `tool` messages whose matching
|
|
63
|
+
* assistant call was sliced off, since most providers reject that.
|
|
64
|
+
*/
|
|
65
|
+
const buildMessageWindow = (messages: ShortTermMessage[]): ShortTermMessage[] => {
|
|
66
|
+
if (messages.length <= MAX_WINDOW_MESSAGES) return messages;
|
|
67
|
+
const tail = messages.slice(-MAX_WINDOW_MESSAGES);
|
|
68
|
+
const knownAssistantCallIds = new Set<string>();
|
|
69
|
+
for (const m of tail) {
|
|
70
|
+
if (m.role === 'assistant' && m.toolCalls) {
|
|
71
|
+
for (const tc of m.toolCalls) knownAssistantCallIds.add(tc.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return tail.filter((m) => m.role !== 'tool' || knownAssistantCallIds.has(m.toolCallId));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Self-healing pass: every assistant tool_call must have a matching tool
|
|
79
|
+
* result before the next user/assistant turn, or providers (OpenAI in
|
|
80
|
+
* particular) reject the request with "Tool result is missing for tool call".
|
|
81
|
+
*
|
|
82
|
+
* This can happen when a handler emits a `:result` event without `meta`
|
|
83
|
+
* (orphaning the call), the process restarts mid-run, or a tool handler
|
|
84
|
+
* crashes. Rather than refuse to continue, we inject synthetic tool messages
|
|
85
|
+
* with a clear error payload — the LLM can then explain the failure to the
|
|
86
|
+
* user and proceed.
|
|
87
|
+
*/
|
|
88
|
+
const repairOpenToolCalls = (messages: ShortTermMessage[]): ShortTermMessage[] => {
|
|
89
|
+
const fulfilled = new Set<string>();
|
|
90
|
+
for (const m of messages) {
|
|
91
|
+
if (m.role === 'tool') fulfilled.add(m.toolCallId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const repaired: ShortTermMessage[] = [];
|
|
95
|
+
for (const m of messages) {
|
|
96
|
+
repaired.push(m);
|
|
97
|
+
if (m.role !== 'assistant' || !m.toolCalls) continue;
|
|
98
|
+
for (const tc of m.toolCalls) {
|
|
99
|
+
if (fulfilled.has(tc.id)) continue;
|
|
100
|
+
repaired.push({
|
|
101
|
+
role: 'tool',
|
|
102
|
+
toolCallId: tc.id,
|
|
103
|
+
toolName: tc.function.name,
|
|
104
|
+
content: JSON.stringify({
|
|
105
|
+
success: false,
|
|
106
|
+
error: 'Tool result was lost (handler did not emit a matching :result event).',
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
fulfilled.add(tc.id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return repaired;
|
|
113
|
+
};
|
|
114
|
+
|
|
45
115
|
const readPersistedShortTermMessages = (state: OpenBotState): ShortTermMessage[] => {
|
|
46
116
|
const source = state.threadDetails?.state ?? state.channelDetails?.state;
|
|
47
117
|
const record = asRecord(source);
|
|
@@ -71,18 +141,12 @@ const persistShortTermMessages = async (
|
|
|
71
141
|
|
|
72
142
|
async function buildSystemPrompt(
|
|
73
143
|
state: OpenBotState,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
storage?: Storage,
|
|
77
|
-
contextEngine?: {
|
|
144
|
+
storage: Storage | undefined,
|
|
145
|
+
contextEngine: {
|
|
78
146
|
buildContext: (state: OpenBotState, storage?: Storage) => Promise<string>;
|
|
79
147
|
},
|
|
80
148
|
): Promise<string> {
|
|
81
|
-
|
|
82
|
-
if (system && typeof system === 'string') sections.push(system);
|
|
83
|
-
if (system && typeof system === 'function' && context) sections.push(await system(context));
|
|
84
|
-
if (contextEngine) sections.push(await contextEngine.buildContext(state, storage));
|
|
85
|
-
return sections.join('\n\n');
|
|
149
|
+
return contextEngine.buildContext(state, storage);
|
|
86
150
|
}
|
|
87
151
|
|
|
88
152
|
/**
|
|
@@ -94,317 +158,311 @@ async function buildSystemPrompt(
|
|
|
94
158
|
*/
|
|
95
159
|
export const aiSdkRuntime =
|
|
96
160
|
(options: AiSdkRuntimeOptions): MelonyPlugin<OpenBotState, OpenBotEvent> =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
system,
|
|
101
|
-
storage,
|
|
102
|
-
contextEngine = createDefaultContextEngine(),
|
|
103
|
-
toolDefinitions = {},
|
|
104
|
-
} = options;
|
|
105
|
-
|
|
106
|
-
let currentModelString = modelString;
|
|
107
|
-
let model = resolveModel(currentModelString);
|
|
108
|
-
|
|
109
|
-
const ensureShortTermMessages = (state: OpenBotState) => {
|
|
110
|
-
if (!state.shortTermMessages || state.shortTermMessages.length === 0) {
|
|
111
|
-
state.shortTermMessages = readPersistedShortTermMessages(state);
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const mapToCoreMessages = (messages: ShortTermMessage[]): ModelMessage[] => {
|
|
116
|
-
return messages.map((m): ModelMessage => {
|
|
117
|
-
if (m.role === 'assistant' && m.toolCalls) {
|
|
118
|
-
return {
|
|
119
|
-
role: 'assistant',
|
|
120
|
-
content: [
|
|
121
|
-
{ type: 'text', text: m.content || '' },
|
|
122
|
-
...m.toolCalls.map((tc) => ({
|
|
123
|
-
type: 'tool-call' as const,
|
|
124
|
-
toolCallId: tc.id,
|
|
125
|
-
toolName: tc.function.name,
|
|
126
|
-
input: JSON.parse(tc.function.arguments),
|
|
127
|
-
})),
|
|
128
|
-
],
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
if (m.role === 'assistant') {
|
|
132
|
-
return { role: 'assistant', content: m.content || '' };
|
|
133
|
-
}
|
|
134
|
-
if (m.role === 'tool') {
|
|
135
|
-
return {
|
|
136
|
-
role: 'tool',
|
|
137
|
-
content: [
|
|
138
|
-
{
|
|
139
|
-
type: 'tool-result',
|
|
140
|
-
toolCallId: m.toolCallId,
|
|
141
|
-
toolName: m.toolName,
|
|
142
|
-
output: { type: 'text', value: JSON.stringify(m.content) },
|
|
143
|
-
},
|
|
144
|
-
],
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
return m;
|
|
148
|
-
});
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const runLLM = async function* (
|
|
152
|
-
context: RuntimeContext<OpenBotState, OpenBotEvent>,
|
|
153
|
-
threadId?: string,
|
|
154
|
-
): AsyncGenerator<OpenBotEvent> {
|
|
155
|
-
ensureShortTermMessages(context.state);
|
|
156
|
-
const systemPrompt = await buildSystemPrompt(
|
|
157
|
-
context.state,
|
|
158
|
-
system,
|
|
159
|
-
context,
|
|
161
|
+
(builder) => {
|
|
162
|
+
const {
|
|
163
|
+
model: modelString = 'openai/gpt-4o-mini',
|
|
160
164
|
storage,
|
|
161
|
-
contextEngine,
|
|
162
|
-
|
|
165
|
+
contextEngine = createDefaultContextEngine(),
|
|
166
|
+
toolDefinitions = {},
|
|
167
|
+
} = options;
|
|
163
168
|
|
|
164
|
-
|
|
169
|
+
let currentModelString = modelString;
|
|
170
|
+
let model = resolveModel(currentModelString);
|
|
165
171
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
tools: toolDefinitions as Record<string, { description: string; inputSchema: any }>,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
const toolCalls = result.toolCalls ?? [];
|
|
172
|
+
const ensureShortTermMessages = (state: OpenBotState) => {
|
|
173
|
+
if (!state.shortTermMessages || state.shortTermMessages.length === 0) {
|
|
174
|
+
state.shortTermMessages = readPersistedShortTermMessages(state);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
175
177
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{
|
|
178
|
+
const mapToCoreMessages = (messages: ShortTermMessage[]): ModelMessage[] => {
|
|
179
|
+
return messages.map((m): ModelMessage => {
|
|
180
|
+
if (m.role === 'assistant' && m.toolCalls) {
|
|
181
|
+
return {
|
|
180
182
|
role: 'assistant',
|
|
181
|
-
content:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
content: [
|
|
184
|
+
{ type: 'text', text: m.content || '' },
|
|
185
|
+
...m.toolCalls.map((tc) => ({
|
|
186
|
+
type: 'tool-call' as const,
|
|
187
|
+
toolCallId: tc.id,
|
|
188
|
+
toolName: tc.function.name,
|
|
189
|
+
input: JSON.parse(tc.function.arguments),
|
|
190
|
+
})),
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (m.role === 'assistant') {
|
|
195
|
+
return { role: 'assistant', content: m.content || '' };
|
|
196
|
+
}
|
|
197
|
+
if (m.role === 'tool') {
|
|
198
|
+
return {
|
|
199
|
+
role: 'tool',
|
|
200
|
+
content: [
|
|
201
|
+
{
|
|
202
|
+
type: 'tool-result',
|
|
203
|
+
toolCallId: m.toolCallId,
|
|
204
|
+
toolName: m.toolName,
|
|
205
|
+
output: { type: 'text', value: JSON.stringify(m.content) },
|
|
188
206
|
},
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
];
|
|
192
|
-
await persistShortTermMessages(context.state, storage);
|
|
193
|
-
|
|
194
|
-
for (const toolCall of toolCalls) {
|
|
195
|
-
yield {
|
|
196
|
-
type: `action:${toolCall.toolName}` as OpenBotEvent['type'],
|
|
197
|
-
data: toolCall.input,
|
|
198
|
-
meta: {
|
|
199
|
-
toolCallId: toolCall.toolCallId,
|
|
200
|
-
agentId: context.state.agentId,
|
|
201
|
-
threadId,
|
|
202
|
-
},
|
|
203
|
-
} as unknown as OpenBotEvent;
|
|
207
|
+
],
|
|
208
|
+
};
|
|
204
209
|
}
|
|
205
|
-
|
|
210
|
+
return m;
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const runLLM = async function* (
|
|
215
|
+
context: RuntimeContext<OpenBotState, OpenBotEvent>,
|
|
216
|
+
threadId?: string,
|
|
217
|
+
): AsyncGenerator<OpenBotEvent> {
|
|
218
|
+
ensureShortTermMessages(context.state);
|
|
219
|
+
const systemPrompt = await buildSystemPrompt(context.state, storage, contextEngine);
|
|
206
220
|
|
|
207
|
-
|
|
208
|
-
|
|
221
|
+
const coreMessages = mapToCoreMessages(
|
|
222
|
+
buildMessageWindow(repairOpenToolCalls(context.state.shortTermMessages || [])),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const result = await generateText({
|
|
227
|
+
model,
|
|
228
|
+
system: systemPrompt,
|
|
229
|
+
messages: coreMessages,
|
|
230
|
+
tools: toolDefinitions as Record<string, { description: string; inputSchema: any }>,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const toolCalls = result.toolCalls ?? [];
|
|
234
|
+
|
|
235
|
+
if (toolCalls.length > 0) {
|
|
209
236
|
context.state.shortTermMessages = [
|
|
210
237
|
...(context.state.shortTermMessages ?? []),
|
|
211
|
-
{
|
|
238
|
+
{
|
|
239
|
+
role: 'assistant',
|
|
240
|
+
content: result.text || '',
|
|
241
|
+
toolCalls: toolCalls.map((tc) => ({
|
|
242
|
+
id: tc.toolCallId,
|
|
243
|
+
type: 'function',
|
|
244
|
+
function: {
|
|
245
|
+
name: tc.toolName,
|
|
246
|
+
arguments: JSON.stringify(tc.input),
|
|
247
|
+
},
|
|
248
|
+
})),
|
|
249
|
+
},
|
|
212
250
|
];
|
|
213
251
|
await persistShortTermMessages(context.state, storage);
|
|
214
|
-
}
|
|
215
252
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const isApiKeyError =
|
|
225
|
-
errorMessage.includes('API key') ||
|
|
226
|
-
errorMessage.includes('401') ||
|
|
227
|
-
errorMessage.includes('Unauthorized') ||
|
|
228
|
-
errorMessage.includes('authentication');
|
|
229
|
-
|
|
230
|
-
if (isApiKeyError) {
|
|
231
|
-
const [currentProvider, ...rest] = currentModelString.split('/');
|
|
232
|
-
const currentModelId = rest.join('/');
|
|
233
|
-
yield {
|
|
234
|
-
type: 'client:ui:widget',
|
|
235
|
-
data: {
|
|
236
|
-
kind: 'form',
|
|
237
|
-
widgetId: `api_key_request_${Date.now()}`,
|
|
238
|
-
title: `AI Provider API Key Required`,
|
|
239
|
-
description: `The AI provider returned an authentication error. Select your provider, model, and provide a valid API key to continue. The key never leaves your local runtime.`,
|
|
240
|
-
fields: [
|
|
241
|
-
{
|
|
242
|
-
id: 'provider',
|
|
243
|
-
label: 'Provider',
|
|
244
|
-
type: 'select',
|
|
245
|
-
required: true,
|
|
246
|
-
options: [
|
|
247
|
-
{ label: 'OpenAI', value: 'openai' },
|
|
248
|
-
{ label: 'Anthropic', value: 'anthropic' },
|
|
249
|
-
],
|
|
250
|
-
defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
id: 'model',
|
|
254
|
-
label: 'Model',
|
|
255
|
-
type: 'text',
|
|
256
|
-
description:
|
|
257
|
-
'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
|
|
258
|
-
placeholder: 'gpt-4o-mini',
|
|
259
|
-
required: true,
|
|
260
|
-
defaultValue: currentModelId,
|
|
253
|
+
for (const toolCall of toolCalls) {
|
|
254
|
+
yield {
|
|
255
|
+
type: `action:${toolCall.toolName}` as OpenBotEvent['type'],
|
|
256
|
+
data: toolCall.input,
|
|
257
|
+
meta: {
|
|
258
|
+
toolCallId: toolCall.toolCallId,
|
|
259
|
+
agentId: context.state.agentId,
|
|
260
|
+
threadId,
|
|
261
261
|
},
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
262
|
+
} as unknown as OpenBotEvent;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (result.text) {
|
|
267
|
+
if (toolCalls.length === 0) {
|
|
268
|
+
context.state.shortTermMessages = [
|
|
269
|
+
...(context.state.shortTermMessages ?? []),
|
|
270
|
+
{ role: 'assistant', content: result.text },
|
|
271
|
+
];
|
|
272
|
+
await persistShortTermMessages(context.state, storage);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
yield {
|
|
276
|
+
type: 'agent:output',
|
|
277
|
+
data: { content: result.text },
|
|
278
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
} catch (error: unknown) {
|
|
282
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
283
|
+
const isApiKeyError =
|
|
284
|
+
errorMessage.includes('API key') ||
|
|
285
|
+
errorMessage.includes('401') ||
|
|
286
|
+
errorMessage.includes('Unauthorized') ||
|
|
287
|
+
errorMessage.includes('authentication');
|
|
288
|
+
|
|
289
|
+
if (isApiKeyError) {
|
|
290
|
+
const [currentProvider, ...rest] = currentModelString.split('/');
|
|
291
|
+
const currentModelId = rest.join('/');
|
|
292
|
+
yield {
|
|
293
|
+
type: 'client:ui:widget',
|
|
294
|
+
data: {
|
|
295
|
+
kind: 'form',
|
|
296
|
+
widgetId: `api_key_request_${Date.now()}`,
|
|
297
|
+
title: `AI Provider API Key Required`,
|
|
298
|
+
description: `The AI provider returned an authentication error. Select your provider, model, and provide a valid API key to continue. The key never leaves your local runtime.`,
|
|
299
|
+
fields: [
|
|
300
|
+
{
|
|
301
|
+
id: 'provider',
|
|
302
|
+
label: 'Provider',
|
|
303
|
+
type: 'select',
|
|
304
|
+
required: true,
|
|
305
|
+
options: [
|
|
306
|
+
{ label: 'OpenAI', value: 'openai' },
|
|
307
|
+
{ label: 'Anthropic', value: 'anthropic' },
|
|
308
|
+
],
|
|
309
|
+
defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: 'model',
|
|
313
|
+
label: 'Model',
|
|
314
|
+
type: 'text',
|
|
315
|
+
description:
|
|
316
|
+
'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
|
|
317
|
+
placeholder: 'gpt-4o-mini',
|
|
318
|
+
required: true,
|
|
319
|
+
defaultValue: currentModelId,
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
id: 'apiKey',
|
|
323
|
+
label: 'API Key',
|
|
324
|
+
type: 'text',
|
|
325
|
+
placeholder: `sk-...`,
|
|
326
|
+
required: true,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
submitLabel: 'Save & Continue',
|
|
330
|
+
metadata: {
|
|
331
|
+
type: 'api_key_request',
|
|
268
332
|
},
|
|
269
|
-
],
|
|
270
|
-
submitLabel: 'Save & Continue',
|
|
271
|
-
metadata: {
|
|
272
|
-
type: 'api_key_request',
|
|
273
333
|
},
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
334
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
335
|
+
} as OpenBotEvent;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
builder.on('agent:invoke', async function* (event, context) {
|
|
344
|
+
const routedTo = (event as { data?: { agentId?: string } }).data?.agentId;
|
|
345
|
+
if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
|
|
277
346
|
return;
|
|
278
347
|
}
|
|
279
348
|
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
};
|
|
349
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
283
350
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
context.state.shortTermMessages = [
|
|
294
|
-
...(context.state.shortTermMessages ?? []),
|
|
295
|
-
{
|
|
296
|
-
role: event.data?.role || 'user',
|
|
297
|
-
content: event?.data?.content || '',
|
|
298
|
-
},
|
|
299
|
-
];
|
|
300
|
-
await persistShortTermMessages(context.state, storage);
|
|
301
|
-
|
|
302
|
-
yield* runLLM(context, threadId);
|
|
303
|
-
});
|
|
351
|
+
ensureShortTermMessages(context.state);
|
|
352
|
+
context.state.shortTermMessages = [
|
|
353
|
+
...(context.state.shortTermMessages ?? []),
|
|
354
|
+
{
|
|
355
|
+
role: event.data?.role || 'user',
|
|
356
|
+
content: event?.data?.content || '',
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
await persistShortTermMessages(context.state, storage);
|
|
304
360
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (event.meta?.agentId !== context.state.agentId) return;
|
|
308
|
-
const toolCallId = event.meta?.toolCallId;
|
|
309
|
-
if (!toolCallId) return;
|
|
310
|
-
ensureShortTermMessages(context.state);
|
|
311
|
-
|
|
312
|
-
const toolName = event.type.replace(/^action:/, '').replace(/:result$/, '');
|
|
313
|
-
const resultData = (event as { data?: unknown }).data;
|
|
314
|
-
const content = typeof resultData === 'string' ? resultData : JSON.stringify(resultData);
|
|
315
|
-
|
|
316
|
-
context.state.shortTermMessages = [
|
|
317
|
-
...(context.state.shortTermMessages ?? []),
|
|
318
|
-
{ role: 'tool', content, toolCallId, toolName },
|
|
319
|
-
];
|
|
320
|
-
await persistShortTermMessages(context.state, storage);
|
|
321
|
-
|
|
322
|
-
const lastAssistant = [...(context.state.shortTermMessages ?? [])]
|
|
323
|
-
.reverse()
|
|
324
|
-
.find(
|
|
325
|
-
(m): m is Extract<ShortTermMessage, { role: 'assistant' }> =>
|
|
326
|
-
m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0,
|
|
327
|
-
);
|
|
361
|
+
yield* runLLM(context, threadId);
|
|
362
|
+
});
|
|
328
363
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
);
|
|
364
|
+
builder.on('*', async function* (event, context) {
|
|
365
|
+
if (!event.type.endsWith(':result')) return;
|
|
366
|
+
if (event.meta?.agentId !== context.state.agentId) return;
|
|
367
|
+
const toolCallId = event.meta?.toolCallId;
|
|
368
|
+
if (!toolCallId) return;
|
|
369
|
+
ensureShortTermMessages(context.state);
|
|
370
|
+
|
|
371
|
+
const toolName = event.type.replace(/^action:/, '').replace(/:result$/, '');
|
|
372
|
+
const resultData = (event as { data?: unknown }).data;
|
|
373
|
+
const content = truncateToolPayload(resultData);
|
|
374
|
+
|
|
375
|
+
context.state.shortTermMessages = [
|
|
376
|
+
...(context.state.shortTermMessages ?? []),
|
|
377
|
+
{ role: 'tool', content, toolCallId, toolName },
|
|
378
|
+
];
|
|
379
|
+
await persistShortTermMessages(context.state, storage);
|
|
380
|
+
|
|
381
|
+
const lastAssistant = [...(context.state.shortTermMessages ?? [])]
|
|
382
|
+
.reverse()
|
|
383
|
+
.find(
|
|
384
|
+
(m): m is Extract<ShortTermMessage, { role: 'assistant' }> =>
|
|
385
|
+
m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
if (lastAssistant && lastAssistant.toolCalls) {
|
|
389
|
+
const allFulfilled = lastAssistant.toolCalls.every((tc) =>
|
|
390
|
+
context.state.shortTermMessages?.some(
|
|
391
|
+
(m) => m.role === 'tool' && m.toolCallId === tc.id,
|
|
392
|
+
),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
if (allFulfilled) {
|
|
396
|
+
if (toolName === 'handoff') return;
|
|
397
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
398
|
+
yield* runLLM(context, threadId);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
404
|
+
const { metadata, values } = event.data;
|
|
405
|
+
if (metadata?.type !== 'api_key_request') return;
|
|
406
|
+
if (!values?.apiKey || !values?.provider || !values?.model) return;
|
|
407
|
+
|
|
408
|
+
const provider = String(values.provider);
|
|
409
|
+
const modelId = String(values.model).trim();
|
|
410
|
+
const apiKey = String(values.apiKey);
|
|
335
411
|
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
412
|
+
if (provider !== 'openai' && provider !== 'anthropic') {
|
|
413
|
+
yield {
|
|
414
|
+
type: 'agent:output',
|
|
415
|
+
data: { content: `Unsupported provider: ${provider}` },
|
|
416
|
+
meta: { agentId: context.state.agentId },
|
|
417
|
+
};
|
|
418
|
+
return;
|
|
340
419
|
}
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
420
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const provider = String(values.provider);
|
|
350
|
-
const modelId = String(values.model).trim();
|
|
351
|
-
const apiKey = String(values.apiKey);
|
|
352
|
-
|
|
353
|
-
if (provider !== 'openai' && provider !== 'anthropic') {
|
|
354
|
-
yield {
|
|
355
|
-
type: 'agent:output',
|
|
356
|
-
data: { content: `Unsupported provider: ${provider}` },
|
|
357
|
-
meta: { agentId: context.state.agentId },
|
|
358
|
-
};
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
|
|
363
|
-
const newModelString = `${provider}/${modelId}`;
|
|
364
|
-
|
|
365
|
-
if (!storage) return;
|
|
366
|
-
try {
|
|
367
|
-
await storage.createVariable({ key: envVar, value: apiKey, secret: true });
|
|
368
|
-
process.env[envVar] = apiKey;
|
|
369
|
-
|
|
370
|
-
currentModelString = newModelString;
|
|
371
|
-
model = resolveModel(currentModelString);
|
|
421
|
+
const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
|
|
422
|
+
const newModelString = `${provider}/${modelId}`;
|
|
423
|
+
|
|
424
|
+
if (!storage) return;
|
|
372
425
|
try {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
426
|
+
await storage.createVariable({ key: envVar, value: apiKey, secret: true });
|
|
427
|
+
process.env[envVar] = apiKey;
|
|
428
|
+
|
|
429
|
+
currentModelString = newModelString;
|
|
430
|
+
model = resolveModel(currentModelString);
|
|
431
|
+
try {
|
|
432
|
+
saveConfig({ model: currentModelString });
|
|
433
|
+
} catch {
|
|
434
|
+
// best-effort: config persistence failure shouldn't block the conversation
|
|
435
|
+
}
|
|
377
436
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
};
|
|
437
|
+
yield {
|
|
438
|
+
type: 'agent:output',
|
|
439
|
+
data: {
|
|
440
|
+
content: `Saved ${provider} API key and set model to \`${newModelString}\`.`,
|
|
441
|
+
},
|
|
442
|
+
meta: { agentId: context.state.agentId },
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
yield {
|
|
446
|
+
type: 'client:ui:widget',
|
|
447
|
+
data: {
|
|
448
|
+
widgetId: event.data.widgetId,
|
|
449
|
+
kind: 'message',
|
|
450
|
+
title: 'API Key Saved',
|
|
451
|
+
body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
|
|
452
|
+
state: 'submitted',
|
|
453
|
+
actions: [{ id: 'ok', label: 'Got it', variant: 'primary' }],
|
|
454
|
+
},
|
|
455
|
+
meta: { agentId: context.state.agentId },
|
|
456
|
+
};
|
|
457
|
+
} catch (error) {
|
|
458
|
+
yield {
|
|
459
|
+
type: 'agent:output',
|
|
460
|
+
data: {
|
|
461
|
+
content: `Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'
|
|
462
|
+
}`,
|
|
463
|
+
},
|
|
464
|
+
meta: { agentId: context.state.agentId },
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
};
|