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.
Files changed (37) hide show
  1. package/dist/app/server.js +1 -4
  2. package/dist/bus/services.js +106 -10
  3. package/dist/harness/context.js +66 -6
  4. package/dist/harness/queue-processor.js +44 -110
  5. package/dist/harness/runtime-factory.js +11 -7
  6. package/dist/harness/todo-advance.js +93 -0
  7. package/dist/plugins/ai-sdk/index.js +0 -3
  8. package/dist/plugins/ai-sdk/runtime.js +4 -11
  9. package/dist/plugins/ai-sdk/system-prompt.js +18 -3
  10. package/dist/plugins/delegation/index.js +7 -46
  11. package/dist/plugins/storage-tools/index.js +2 -11
  12. package/dist/plugins/todo/index.js +54 -0
  13. package/dist/plugins/workflow/index.js +65 -0
  14. package/dist/registry/plugins.js +2 -2
  15. package/dist/services/storage.js +3 -31
  16. package/dist/workflow/service.js +106 -0
  17. package/dist/workflow/types.js +3 -0
  18. package/docs/plugins.md +0 -1
  19. package/package.json +1 -1
  20. package/src/app/cli.ts +1 -1
  21. package/src/app/server.ts +3 -4
  22. package/src/app/types.ts +80 -45
  23. package/src/bus/plugin.ts +0 -2
  24. package/src/bus/services.ts +133 -12
  25. package/src/bus/types.ts +0 -4
  26. package/src/harness/context.ts +73 -10
  27. package/src/harness/queue-processor.ts +54 -143
  28. package/src/harness/runtime-factory.ts +11 -7
  29. package/src/harness/todo-advance.ts +128 -0
  30. package/src/plugins/ai-sdk/index.ts +0 -3
  31. package/src/plugins/ai-sdk/runtime.ts +284 -300
  32. package/src/plugins/ai-sdk/system-prompt.ts +18 -4
  33. package/src/plugins/delegation/index.ts +7 -50
  34. package/src/plugins/storage-tools/index.ts +8 -19
  35. package/src/plugins/todo/index.ts +64 -0
  36. package/src/registry/plugins.ts +2 -3
  37. 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
- system?: string | ((context: RuntimeContext) => string | Promise<string>),
147
- context?: RuntimeContext,
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
- const sections: string[] = [];
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
- (builder) => {
170
- const {
171
- model: modelString = 'openai/gpt-4o-mini',
172
- system,
173
- storage,
174
- contextEngine = createDefaultContextEngine(),
175
- toolDefinitions = {},
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
- const mapToCoreMessages = (messages: ShortTermMessage[]): ModelMessage[] => {
188
- return messages.map((m): ModelMessage => {
189
- if (m.role === 'assistant' && m.toolCalls) {
190
- return {
191
- role: 'assistant',
192
- content: [
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
- return m;
220
- });
221
- };
176
+ };
222
177
 
223
- const runLLM = async function* (
224
- context: RuntimeContext<OpenBotState, OpenBotEvent>,
225
- threadId?: string,
226
- ): AsyncGenerator<OpenBotEvent> {
227
- ensureShortTermMessages(context.state);
228
- const systemPrompt = await buildSystemPrompt(
229
- context.state,
230
- system,
231
- context,
232
- storage,
233
- contextEngine,
234
- );
235
-
236
- const coreMessages = mapToCoreMessages(
237
- buildMessageWindow(repairOpenToolCalls(context.state.shortTermMessages || [])),
238
- );
239
-
240
- try {
241
- const result = await generateText({
242
- model,
243
- system: systemPrompt,
244
- messages: coreMessages,
245
- tools: toolDefinitions as Record<string, { description: string; inputSchema: any }>,
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
- const toolCalls = result.toolCalls ?? [];
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
- if (toolCalls.length > 0) {
251
- context.state.shortTermMessages = [
252
- ...(context.state.shortTermMessages ?? []),
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
- for (const toolCall of toolCalls) {
269
- yield {
270
- type: `action:${toolCall.toolName}` as OpenBotEvent['type'],
271
- data: toolCall.input,
272
- meta: {
273
- toolCallId: toolCall.toolCallId,
274
- agentId: context.state.agentId,
275
- threadId,
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
- if (result.text) {
282
- if (toolCalls.length === 0) {
235
+ if (toolCalls.length > 0) {
283
236
  context.state.shortTermMessages = [
284
237
  ...(context.state.shortTermMessages ?? []),
285
- { role: 'assistant', content: result.text },
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
- yield {
291
- type: 'agent:output',
292
- data: { content: result.text },
293
- meta: { agentId: context.state.agentId, threadId },
294
- };
295
- }
296
- } catch (error: unknown) {
297
- const errorMessage = error instanceof Error ? error.message : String(error);
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
- id: 'apiKey',
338
- label: 'API Key',
339
- type: 'text',
340
- placeholder: `sk-...`,
341
- required: true,
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
- meta: { agentId: context.state.agentId, threadId },
350
- } as OpenBotEvent;
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
- throw error;
355
- }
356
- };
349
+ const threadId = event.meta?.threadId || context.state.threadId;
357
350
 
358
- builder.on('agent:invoke', async function* (event, context) {
359
- const routedTo = (event as { data?: { agentId?: string } }).data?.agentId;
360
- if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
361
- return;
362
- }
363
-
364
- const threadId = event.meta?.threadId || context.state.threadId;
365
-
366
- ensureShortTermMessages(context.state);
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
- builder.on('*', async function* (event, context) {
380
- if (!event.type.endsWith(':result')) return;
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
- if (lastAssistant && lastAssistant.toolCalls) {
404
- const allFulfilled = lastAssistant.toolCalls.every((tc) =>
405
- context.state.shortTermMessages?.some(
406
- (m) => m.role === 'tool' && m.toolCallId === tc.id,
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
- if (allFulfilled) {
411
- if (toolName === 'handoff') return;
412
- const threadId = event.meta?.threadId || context.state.threadId;
413
- yield* runLLM(context, threadId);
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
- builder.on('client:ui:widget:response', async function* (event, context) {
419
- const { metadata, values } = event.data;
420
- if (metadata?.type !== 'api_key_request') return;
421
- if (!values?.apiKey || !values?.provider || !values?.model) return;
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
- saveConfig({ model: currentModelString });
448
- } catch {
449
- // best-effort: config persistence failure shouldn't block the conversation
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
- yield {
453
- type: 'agent:output',
454
- data: {
455
- content: `Saved ${provider} API key and set model to \`${newModelString}\`.`,
456
- },
457
- meta: { agentId: context.state.agentId },
458
- };
459
-
460
- yield {
461
- type: 'client:ui:widget',
462
- data: {
463
- widgetId: event.data.widgetId,
464
- kind: 'message',
465
- title: 'API Key Saved',
466
- body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
467
- state: 'submitted',
468
- actions: [{ id: 'ok', label: 'Got it', variant: 'primary' }],
469
- },
470
- meta: { agentId: context.state.agentId },
471
- };
472
- } catch (error) {
473
- yield {
474
- type: 'agent:output',
475
- data: {
476
- content: `Failed to save API key: ${
477
- error instanceof Error ? error.message : 'Unknown error'
478
- }`,
479
- },
480
- meta: { agentId: context.state.agentId },
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');