openbot 0.3.1 → 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/server.js +1 -4
- package/dist/bus/services.js +106 -10
- package/dist/harness/context.js +66 -6
- 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 +4 -11
- package/dist/plugins/ai-sdk/system-prompt.js +18 -3
- package/dist/plugins/delegation/index.js +7 -46
- 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 +2 -2
- package/dist/services/storage.js +3 -31
- package/dist/workflow/service.js +106 -0
- package/dist/workflow/types.js +3 -0
- 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 +80 -45
- package/src/bus/plugin.ts +0 -2
- package/src/bus/services.ts +133 -12
- package/src/bus/types.ts +0 -4
- package/src/harness/context.ts +73 -10
- 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 +284 -300
- package/src/plugins/ai-sdk/system-prompt.ts +18 -4
- package/src/plugins/delegation/index.ts +7 -50
- package/src/plugins/storage-tools/index.ts +8 -19
- package/src/plugins/todo/index.ts +64 -0
- package/src/registry/plugins.ts +2 -3
- package/src/services/storage.ts +2 -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>;
|
|
@@ -143,18 +141,12 @@ const persistShortTermMessages = async (
|
|
|
143
141
|
|
|
144
142
|
async function buildSystemPrompt(
|
|
145
143
|
state: OpenBotState,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
storage?: Storage,
|
|
149
|
-
contextEngine?: {
|
|
144
|
+
storage: Storage | undefined,
|
|
145
|
+
contextEngine: {
|
|
150
146
|
buildContext: (state: OpenBotState, storage?: Storage) => Promise<string>;
|
|
151
147
|
},
|
|
152
148
|
): Promise<string> {
|
|
153
|
-
|
|
154
|
-
if (system && typeof system === 'string') sections.push(system);
|
|
155
|
-
if (system && typeof system === 'function' && context) sections.push(await system(context));
|
|
156
|
-
if (contextEngine) sections.push(await contextEngine.buildContext(state, storage));
|
|
157
|
-
return sections.join('\n\n');
|
|
149
|
+
return contextEngine.buildContext(state, storage);
|
|
158
150
|
}
|
|
159
151
|
|
|
160
152
|
/**
|
|
@@ -166,319 +158,311 @@ async function buildSystemPrompt(
|
|
|
166
158
|
*/
|
|
167
159
|
export const aiSdkRuntime =
|
|
168
160
|
(options: AiSdkRuntimeOptions): MelonyPlugin<OpenBotState, OpenBotEvent> =>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
} = options;
|
|
177
|
-
|
|
178
|
-
let currentModelString = modelString;
|
|
179
|
-
let model = resolveModel(currentModelString);
|
|
180
|
-
|
|
181
|
-
const ensureShortTermMessages = (state: OpenBotState) => {
|
|
182
|
-
if (!state.shortTermMessages || state.shortTermMessages.length === 0) {
|
|
183
|
-
state.shortTermMessages = readPersistedShortTermMessages(state);
|
|
184
|
-
}
|
|
185
|
-
};
|
|
161
|
+
(builder) => {
|
|
162
|
+
const {
|
|
163
|
+
model: modelString = 'openai/gpt-4o-mini',
|
|
164
|
+
storage,
|
|
165
|
+
contextEngine = createDefaultContextEngine(),
|
|
166
|
+
toolDefinitions = {},
|
|
167
|
+
} = options;
|
|
186
168
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
{ type: 'text', text: m.content || '' },
|
|
194
|
-
...m.toolCalls.map((tc) => ({
|
|
195
|
-
type: 'tool-call' as const,
|
|
196
|
-
toolCallId: tc.id,
|
|
197
|
-
toolName: tc.function.name,
|
|
198
|
-
input: JSON.parse(tc.function.arguments),
|
|
199
|
-
})),
|
|
200
|
-
],
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
if (m.role === 'assistant') {
|
|
204
|
-
return { role: 'assistant', content: m.content || '' };
|
|
205
|
-
}
|
|
206
|
-
if (m.role === 'tool') {
|
|
207
|
-
return {
|
|
208
|
-
role: 'tool',
|
|
209
|
-
content: [
|
|
210
|
-
{
|
|
211
|
-
type: 'tool-result',
|
|
212
|
-
toolCallId: m.toolCallId,
|
|
213
|
-
toolName: m.toolName,
|
|
214
|
-
output: { type: 'text', value: JSON.stringify(m.content) },
|
|
215
|
-
},
|
|
216
|
-
],
|
|
217
|
-
};
|
|
169
|
+
let currentModelString = modelString;
|
|
170
|
+
let model = resolveModel(currentModelString);
|
|
171
|
+
|
|
172
|
+
const ensureShortTermMessages = (state: OpenBotState) => {
|
|
173
|
+
if (!state.shortTermMessages || state.shortTermMessages.length === 0) {
|
|
174
|
+
state.shortTermMessages = readPersistedShortTermMessages(state);
|
|
218
175
|
}
|
|
219
|
-
|
|
220
|
-
});
|
|
221
|
-
};
|
|
176
|
+
};
|
|
222
177
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
178
|
+
const mapToCoreMessages = (messages: ShortTermMessage[]): ModelMessage[] => {
|
|
179
|
+
return messages.map((m): ModelMessage => {
|
|
180
|
+
if (m.role === 'assistant' && m.toolCalls) {
|
|
181
|
+
return {
|
|
182
|
+
role: 'assistant',
|
|
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) },
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return m;
|
|
246
211
|
});
|
|
212
|
+
};
|
|
247
213
|
|
|
248
|
-
|
|
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);
|
|
249
220
|
|
|
250
|
-
|
|
251
|
-
context.state.shortTermMessages
|
|
252
|
-
|
|
253
|
-
{
|
|
254
|
-
role: 'assistant',
|
|
255
|
-
content: result.text || '',
|
|
256
|
-
toolCalls: toolCalls.map((tc) => ({
|
|
257
|
-
id: tc.toolCallId,
|
|
258
|
-
type: 'function',
|
|
259
|
-
function: {
|
|
260
|
-
name: tc.toolName,
|
|
261
|
-
arguments: JSON.stringify(tc.input),
|
|
262
|
-
},
|
|
263
|
-
})),
|
|
264
|
-
},
|
|
265
|
-
];
|
|
266
|
-
await persistShortTermMessages(context.state, storage);
|
|
221
|
+
const coreMessages = mapToCoreMessages(
|
|
222
|
+
buildMessageWindow(repairOpenToolCalls(context.state.shortTermMessages || [])),
|
|
223
|
+
);
|
|
267
224
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
} as unknown as OpenBotEvent;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
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 ?? [];
|
|
280
234
|
|
|
281
|
-
|
|
282
|
-
if (toolCalls.length === 0) {
|
|
235
|
+
if (toolCalls.length > 0) {
|
|
283
236
|
context.state.shortTermMessages = [
|
|
284
237
|
...(context.state.shortTermMessages ?? []),
|
|
285
|
-
{
|
|
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
|
+
},
|
|
286
250
|
];
|
|
287
251
|
await persistShortTermMessages(context.state, storage);
|
|
288
|
-
}
|
|
289
252
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const isApiKeyError =
|
|
299
|
-
errorMessage.includes('API key') ||
|
|
300
|
-
errorMessage.includes('401') ||
|
|
301
|
-
errorMessage.includes('Unauthorized') ||
|
|
302
|
-
errorMessage.includes('authentication');
|
|
303
|
-
|
|
304
|
-
if (isApiKeyError) {
|
|
305
|
-
const [currentProvider, ...rest] = currentModelString.split('/');
|
|
306
|
-
const currentModelId = rest.join('/');
|
|
307
|
-
yield {
|
|
308
|
-
type: 'client:ui:widget',
|
|
309
|
-
data: {
|
|
310
|
-
kind: 'form',
|
|
311
|
-
widgetId: `api_key_request_${Date.now()}`,
|
|
312
|
-
title: `AI Provider API Key Required`,
|
|
313
|
-
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.`,
|
|
314
|
-
fields: [
|
|
315
|
-
{
|
|
316
|
-
id: 'provider',
|
|
317
|
-
label: 'Provider',
|
|
318
|
-
type: 'select',
|
|
319
|
-
required: true,
|
|
320
|
-
options: [
|
|
321
|
-
{ label: 'OpenAI', value: 'openai' },
|
|
322
|
-
{ label: 'Anthropic', value: 'anthropic' },
|
|
323
|
-
],
|
|
324
|
-
defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
|
|
325
|
-
},
|
|
326
|
-
{
|
|
327
|
-
id: 'model',
|
|
328
|
-
label: 'Model',
|
|
329
|
-
type: 'text',
|
|
330
|
-
description:
|
|
331
|
-
'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
|
|
332
|
-
placeholder: 'gpt-4o-mini',
|
|
333
|
-
required: true,
|
|
334
|
-
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,
|
|
335
261
|
},
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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',
|
|
342
332
|
},
|
|
343
|
-
],
|
|
344
|
-
submitLabel: 'Save & Continue',
|
|
345
|
-
metadata: {
|
|
346
|
-
type: 'api_key_request',
|
|
347
333
|
},
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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) {
|
|
351
346
|
return;
|
|
352
347
|
}
|
|
353
348
|
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
};
|
|
349
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
357
350
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
context.state.shortTermMessages = [
|
|
368
|
-
...(context.state.shortTermMessages ?? []),
|
|
369
|
-
{
|
|
370
|
-
role: event.data?.role || 'user',
|
|
371
|
-
content: event?.data?.content || '',
|
|
372
|
-
},
|
|
373
|
-
];
|
|
374
|
-
await persistShortTermMessages(context.state, storage);
|
|
375
|
-
|
|
376
|
-
yield* runLLM(context, threadId);
|
|
377
|
-
});
|
|
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);
|
|
378
360
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (event.meta?.agentId !== context.state.agentId) return;
|
|
382
|
-
const toolCallId = event.meta?.toolCallId;
|
|
383
|
-
if (!toolCallId) return;
|
|
384
|
-
ensureShortTermMessages(context.state);
|
|
385
|
-
|
|
386
|
-
const toolName = event.type.replace(/^action:/, '').replace(/:result$/, '');
|
|
387
|
-
const resultData = (event as { data?: unknown }).data;
|
|
388
|
-
const content = truncateToolPayload(resultData);
|
|
389
|
-
|
|
390
|
-
context.state.shortTermMessages = [
|
|
391
|
-
...(context.state.shortTermMessages ?? []),
|
|
392
|
-
{ role: 'tool', content, toolCallId, toolName },
|
|
393
|
-
];
|
|
394
|
-
await persistShortTermMessages(context.state, storage);
|
|
395
|
-
|
|
396
|
-
const lastAssistant = [...(context.state.shortTermMessages ?? [])]
|
|
397
|
-
.reverse()
|
|
398
|
-
.find(
|
|
399
|
-
(m): m is Extract<ShortTermMessage, { role: 'assistant' }> =>
|
|
400
|
-
m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0,
|
|
401
|
-
);
|
|
361
|
+
yield* runLLM(context, threadId);
|
|
362
|
+
});
|
|
402
363
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
);
|
|
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
|
+
});
|
|
409
402
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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);
|
|
411
|
+
|
|
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;
|
|
414
419
|
}
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
420
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const provider = String(values.provider);
|
|
424
|
-
const modelId = String(values.model).trim();
|
|
425
|
-
const apiKey = String(values.apiKey);
|
|
426
|
-
|
|
427
|
-
if (provider !== 'openai' && provider !== 'anthropic') {
|
|
428
|
-
yield {
|
|
429
|
-
type: 'agent:output',
|
|
430
|
-
data: { content: `Unsupported provider: ${provider}` },
|
|
431
|
-
meta: { agentId: context.state.agentId },
|
|
432
|
-
};
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
|
|
437
|
-
const newModelString = `${provider}/${modelId}`;
|
|
438
|
-
|
|
439
|
-
if (!storage) return;
|
|
440
|
-
try {
|
|
441
|
-
await storage.createVariable({ key: envVar, value: apiKey, secret: true });
|
|
442
|
-
process.env[envVar] = apiKey;
|
|
443
|
-
|
|
444
|
-
currentModelString = newModelString;
|
|
445
|
-
model = resolveModel(currentModelString);
|
|
421
|
+
const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
|
|
422
|
+
const newModelString = `${provider}/${modelId}`;
|
|
423
|
+
|
|
424
|
+
if (!storage) return;
|
|
446
425
|
try {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
+
}
|
|
451
436
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
};
|
|
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
|
+
};
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
export const AI_SDK_SYSTEM_PROMPT =
|
|
2
|
-
'You are a helpful AI assistant on the OpenBot platform.
|
|
3
|
-
'Use the tools available to you to help the user.
|
|
4
|
-
'Be concise unless the user asks for depth.'
|
|
1
|
+
export const AI_SDK_SYSTEM_PROMPT = [
|
|
2
|
+
'You are a helpful AI assistant on the OpenBot platform.',
|
|
3
|
+
'Use the tools available to you to help the user.',
|
|
4
|
+
'Be concise unless the user asks for depth.',
|
|
5
|
+
'',
|
|
6
|
+
'## Planning with todos',
|
|
7
|
+
'The current thread has a shared todo list (visible under "Shared todo plan" in context).',
|
|
8
|
+
'It is the single source of truth for multi-step work and is shared across every agent in the thread.',
|
|
9
|
+
'',
|
|
10
|
+
'When planning:',
|
|
11
|
+
'- For any task that needs more than one step, call `todo_write` ONCE with the full ordered plan, then stop. Do not call any other tool in the same turn.',
|
|
12
|
+
'- The platform dispatches assignees automatically and completes their todo when their run ends. You do NOT need to call `handoff` to start the plan or `todo_update` to finish items.',
|
|
13
|
+
'- Each item must be concrete and atomic (one verb, one outcome). Skip the list entirely for trivial single-step requests.',
|
|
14
|
+
'',
|
|
15
|
+
'When you are an assignee (you have an `in_progress` todo addressed to you):',
|
|
16
|
+
'- Just do the work and reply. The platform will mark your todo done and dispatch the next one.',
|
|
17
|
+
'- If you genuinely cannot complete it, call `todo_update(id, status: "cancelled")` with a brief reason in your reply.',
|
|
18
|
+
].join('\n');
|