openbot 0.4.5 → 0.4.6

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.
@@ -340,18 +340,16 @@ const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArr
340
340
  /** Display-oriented fields persisted in a channel's `state.json`. */
341
341
  const readChannelStateFileFields = (parsed) => {
342
342
  if (!isRecord(parsed)) {
343
- return { participants: [] };
343
+ return {};
344
344
  }
345
345
  const name = typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name.trim() : undefined;
346
346
  const cwd = typeof parsed.cwd === 'string' ? parsed.cwd : undefined;
347
- const participants = [];
348
- if (Array.isArray(parsed.participants)) {
349
- for (const x of parsed.participants) {
350
- if (typeof x === 'string' && x.trim())
351
- participants.push(x.trim());
352
- }
353
- }
354
- return { name, cwd, participants };
347
+ return { name, cwd };
348
+ };
349
+ const isChannelProvisioned = async (channelId) => {
350
+ const statePath = `${getConversationDir(channelId)}/state.json`;
351
+ const state = await readJsonFile(statePath, {});
352
+ return !!readChannelStateFileFields(state).cwd;
355
353
  };
356
354
  /**
357
355
  * Parse the `plugins:` array from AGENT.md frontmatter. Each entry must have an
@@ -412,23 +410,23 @@ export const storageService = {
412
410
  const statePath = path.join(channelDir, 'state.json');
413
411
  let cwd;
414
412
  let displayName = name;
415
- let participants = [];
416
413
  try {
417
414
  const parsed = await readJsonFile(statePath, {});
418
415
  const fields = readChannelStateFileFields(parsed);
419
416
  cwd = fields.cwd;
420
417
  displayName = fields.name ?? name;
421
- participants = fields.participants;
422
418
  }
423
419
  catch {
424
420
  // ignore
425
421
  }
422
+ if (!cwd) {
423
+ return null;
424
+ }
426
425
  const channel = {
427
426
  id: name,
428
427
  name: displayName,
429
428
  description: '',
430
429
  cwd,
431
- participants,
432
430
  createdAt: stats.birthtime,
433
431
  updatedAt: stats.mtime,
434
432
  };
@@ -452,7 +450,9 @@ export const storageService = {
452
450
  }
453
451
  return channel;
454
452
  }));
455
- return channels.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
453
+ return channels
454
+ .filter((channel) => channel !== null)
455
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
456
456
  },
457
457
  createChannel: async ({ channelId, spec, initialState, cwd, }) => {
458
458
  const normalizedChannelId = channelId.trim();
@@ -486,6 +486,44 @@ export const storageService = {
486
486
  `# ${normalizedChannelId}\n\n`);
487
487
  await writeJsonFileAtomically(statePath, finalState);
488
488
  },
489
+ ensureChannel: async ({ channelId, spec, initialState, cwd, }) => {
490
+ const normalizedChannelId = channelId.trim();
491
+ if (!normalizedChannelId) {
492
+ throw new Error('channelId is required');
493
+ }
494
+ const channelDir = getConversationDir(normalizedChannelId);
495
+ const specPath = `${channelDir}/SPEC.md`;
496
+ const statePath = `${channelDir}/state.json`;
497
+ const existingState = await readJsonFile(statePath, {});
498
+ const existingFields = readChannelStateFileFields(existingState);
499
+ if (existingFields.cwd) {
500
+ await fs.mkdir(resolvePath(existingFields.cwd), { recursive: true });
501
+ return;
502
+ }
503
+ const finalState = {
504
+ ...existingState,
505
+ ...(initialState || {}),
506
+ };
507
+ const rawCwd = (typeof cwd === 'string' && cwd.trim()) ||
508
+ (typeof finalState.cwd === 'string' && finalState.cwd.trim()) ||
509
+ getDefaultChannelCwd(normalizedChannelId);
510
+ const resolvedCwd = resolvePath(rawCwd);
511
+ finalState.cwd = resolvedCwd;
512
+ await fs.mkdir(resolvedCwd, { recursive: true });
513
+ await fs.mkdir(channelDir, { recursive: true });
514
+ try {
515
+ await fs.access(specPath);
516
+ }
517
+ catch (error) {
518
+ if (error?.code === 'ENOENT') {
519
+ await fs.writeFile(specPath, spec?.trim() || `# ${normalizedChannelId}\n\n`);
520
+ }
521
+ else {
522
+ throw error;
523
+ }
524
+ }
525
+ await writeJsonFileAtomically(statePath, finalState);
526
+ },
489
527
  deleteChannel: async ({ channelId }) => {
490
528
  const normalizedChannelId = channelId.trim();
491
529
  if (!normalizedChannelId) {
@@ -648,7 +686,6 @@ export const storageService = {
648
686
  spec,
649
687
  state,
650
688
  cwd,
651
- participants: diskFields.participants,
652
689
  };
653
690
  details.threads = await storageService.getThreads({ channelId });
654
691
  return details;
@@ -1030,6 +1067,9 @@ export const storageService = {
1030
1067
  },
1031
1068
  storeEvent: async ({ channelId, threadId, event, }) => {
1032
1069
  try {
1070
+ if (!(await isChannelProvisioned(channelId))) {
1071
+ return;
1072
+ }
1033
1073
  const threadDir = getConversationDir(channelId, threadId);
1034
1074
  if (threadId) {
1035
1075
  let exists = false;
@@ -74,20 +74,16 @@ export function parseMarketplaceRegistryJson(data) {
74
74
  const id = item.id;
75
75
  const name = item.name;
76
76
  const description = item.description;
77
- const participants = item.participants;
78
77
  if (typeof id !== 'string' || !id)
79
78
  throw new Error(`channels[${i}].id must be a non-empty string`);
80
79
  if (typeof name !== 'string')
81
80
  throw new Error(`channels[${i}].name must be a string`);
82
81
  if (typeof description !== 'string')
83
82
  throw new Error(`channels[${i}].description must be a string`);
84
- if (!Array.isArray(participants))
85
- throw new Error(`channels[${i}].participants must be an array`);
86
83
  const listing = {
87
84
  id,
88
85
  name,
89
86
  description,
90
- participants: participants.filter((p) => typeof p === 'string'),
91
87
  };
92
88
  if (typeof item.image === 'string')
93
89
  listing.image = item.image;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@ai-sdk/anthropic": "^3.0.33",
19
+ "@ai-sdk/google": "^3.0.82",
19
20
  "@ai-sdk/openai": "^3.0.13",
20
21
  "@anthropic-ai/claude-agent-sdk": "^0.2.138",
21
22
  "@types/cors": "^2.8.19",
@@ -0,0 +1,5 @@
1
+ /** Default channel when requests omit channelId (general-purpose conversation). */
2
+ export const UNCATEGORIZED_CHANNEL_ID = 'uncategorized';
3
+
4
+ export const DEFAULT_UNCATEGORIZED_SPEC =
5
+ '# Uncategorized\n\nGeneral-purpose channel for conversations without a dedicated channel.';
package/src/app/cli.ts CHANGED
@@ -25,7 +25,7 @@ function checkNodeVersion() {
25
25
 
26
26
  checkNodeVersion();
27
27
 
28
- program.name('openbot').description('OpenBot CLI').version('0.4.5');
28
+ program.name('openbot').description('OpenBot CLI').version('0.4.6');
29
29
 
30
30
  program
31
31
  .command('start')
package/src/app/server.ts CHANGED
@@ -22,6 +22,10 @@ import {
22
22
  import { ensureEventId, openBotEventFromQuery } from './utils.js';
23
23
  import { abortRegistry, abortKey } from '../services/abort.js';
24
24
  import { resolveRespondingAgentId } from './responding-agent.js';
25
+ import {
26
+ DEFAULT_UNCATEGORIZED_SPEC,
27
+ UNCATEGORIZED_CHANNEL_ID,
28
+ } from './channel-ids.js';
25
29
 
26
30
  type Bucket = { channelId: string; threadId?: string; activeCount: number; agentIds: Set<string> };
27
31
 
@@ -65,9 +69,18 @@ export async function startServer(options: ServerOptions = {}) {
65
69
  storageService.getAgents().catch((err) => console.warn('[server] Failed to pre-warm agents cache', err));
66
70
  storageService.getPlugins().catch((err) => console.warn('[server] Failed to pre-warm plugins cache', err));
67
71
 
72
+ const getRawChannelId = (req: express.Request): string | undefined => {
73
+ const raw =
74
+ req.get('x-openbot-channel-id') ||
75
+ req.query.channelId ||
76
+ (req.body && req.body.channelId);
77
+ if (typeof raw !== 'string') return undefined;
78
+ const trimmed = raw.trim();
79
+ return trimmed || undefined;
80
+ };
81
+
68
82
  const getContext = (req: express.Request) => {
69
- const channelId =
70
- req.get('x-openbot-channel-id') || req.query.channelId || (req.body && req.body.channelId);
83
+ const rawChannelId = getRawChannelId(req);
71
84
  const threadId =
72
85
  req.get('x-openbot-thread-id') || req.query.threadId || (req.body && req.body.threadId);
73
86
  const agentId =
@@ -83,7 +96,8 @@ export async function startServer(options: ServerOptions = {}) {
83
96
  (req.body && req.body.responseType);
84
97
 
85
98
  return {
86
- channelId: (channelId || (threadId ? 'uncategorized' : 'uncategorized')) as string, // Default to uncategorized if none
99
+ channelId: (rawChannelId || UNCATEGORIZED_CHANNEL_ID) as string,
100
+ rawChannelId,
87
101
  threadId: threadId as string | undefined,
88
102
  agentId: agentId as string | undefined,
89
103
  runId: runId as string,
@@ -532,6 +546,20 @@ export async function startServer(options: ServerOptions = {}) {
532
546
  try {
533
547
  ensureEventId(event);
534
548
 
549
+ const isUserConversationStart =
550
+ event.type === 'agent:invoke' &&
551
+ event.data?.role === 'user' &&
552
+ typeof event.data.content === 'string' &&
553
+ event.data.content.trim().length > 0;
554
+
555
+ if (isUserConversationStart && channelId === UNCATEGORIZED_CHANNEL_ID) {
556
+ await storageService.ensureChannel({
557
+ channelId: UNCATEGORIZED_CHANNEL_ID,
558
+ spec: DEFAULT_UNCATEGORIZED_SPEC,
559
+ initialState: { name: 'Uncategorized' },
560
+ });
561
+ }
562
+
535
563
  const bindIfUnbound = event.type === 'agent:invoke';
536
564
  const resolved = await resolveRespondingAgentId({
537
565
  channelId,
package/src/app/types.ts CHANGED
@@ -301,8 +301,6 @@ export type PatchChannelDetailsEvent = BaseEvent & {
301
301
  state?: Record<string, unknown>;
302
302
  spec?: string;
303
303
  cwd?: string;
304
- /** When set, replaces `state.json` `participants` (merged after `state` if both are sent). */
305
- participants?: string[];
306
304
  };
307
305
  };
308
306
 
@@ -310,7 +308,7 @@ export type PatchChannelDetailsResultEvent = BaseEvent & {
310
308
  type: 'action:patch_channel_details:result';
311
309
  data: {
312
310
  success: boolean;
313
- updatedFields: ('state' | 'spec' | 'cwd' | 'participants')[];
311
+ updatedFields: ('state' | 'spec' | 'cwd')[];
314
312
  };
315
313
  };
316
314
 
@@ -569,8 +567,6 @@ export type CreateChannelEvent = BaseEvent & {
569
567
  spec?: string;
570
568
  initialState?: Record<string, unknown>;
571
569
  cwd?: string;
572
- /** Initial channel agent ids; written into `state.json` (overrides `initialState.participants` if both are set). */
573
- participants?: string[];
574
570
  };
575
571
  meta?: {
576
572
  toolCallId?: string;
@@ -594,8 +590,6 @@ export type UpdateChannelEvent = BaseEvent & {
594
590
  channelId?: string;
595
591
  name?: string;
596
592
  cwd?: string;
597
- /** Replaces the channel participant list when provided. */
598
- participants?: string[];
599
593
  };
600
594
  };
601
595
 
@@ -655,7 +649,7 @@ export type UIWidgetAction = {
655
649
  export type UIWidgetField = {
656
650
  id: string;
657
651
  label: string;
658
- type: 'text' | 'textarea' | 'number' | 'boolean' | 'select' | 'multiselect' | 'date';
652
+ type: 'text' | 'textarea' | 'number' | 'boolean' | 'select' | 'multiselect' | 'date' | 'password';
659
653
  description?: string;
660
654
  placeholder?: string;
661
655
  required?: boolean;
@@ -851,7 +845,6 @@ export type ListMarketplaceRegistryResultEvent = BaseEvent & {
851
845
  image?: string;
852
846
  spec?: string;
853
847
  initialState?: Record<string, unknown>;
854
- participants: string[];
855
848
  starterPrompts?: Array<{ label: string; prompt: string }>;
856
849
  }>;
857
850
  error?: string;
@@ -863,7 +856,6 @@ export type InstallChannelEvent = BaseEvent & {
863
856
  data: {
864
857
  channelId: string;
865
858
  name?: string;
866
- participants?: string[];
867
859
  initialState?: Record<string, unknown>;
868
860
  };
869
861
  };
@@ -27,20 +27,11 @@ export const getContextBudgetForModel = (modelString: string): number => {
27
27
  /** Built-in orchestrator agent id. */
28
28
  export const ORCHESTRATOR_AGENT_ID = 'system';
29
29
 
30
- /**
31
- * Check if a channel is a solo DM (only the agent is present).
32
- */
33
- export function isDmSoloChannel(participants: string[], agentId: string): boolean {
34
- return participants.length === 0 || (participants.length === 1 && participants[0] === agentId);
35
- }
36
-
37
30
  /**
38
31
  * Simplified context builder for MVP.
39
32
  */
40
33
  export async function buildContext(state: OpenBotState, storage?: Storage): Promise<string> {
41
34
  const { channelId, threadId, channelDetails, agentId, threadDetails, agentDetails } = state;
42
- const participants = channelDetails?.participants || [];
43
- const isDm = isDmSoloChannel(participants, agentId);
44
35
 
45
36
  const sections: string[] = [];
46
37
 
@@ -54,23 +45,13 @@ export async function buildContext(state: OpenBotState, storage?: Storage): Prom
54
45
 
55
46
  // 2. Environment
56
47
  let env = '## ENVIRONMENT\n';
57
- if (isDm) {
58
- env += '- Mode: Direct Message (Solo)\n';
59
- } else {
60
- const channelName = channelDetails?.name || channelId;
61
- env += `- Mode: Channel (#${channelName})\n`;
62
- if (channelDetails?.cwd) {
63
- env += `- Workspace: ${channelDetails.cwd}\n`;
64
- }
65
- if (threadId) {
66
- env += `- Thread: ${threadDetails?.name || threadId}\n`;
67
- }
68
- const peerIds = participants.filter((id: string) => id !== agentId);
69
- const participantLabels = peerIds.map((id) => {
70
- const agent = allAgents.find((a) => a.id === id);
71
- return agent ? `${agent.name} (${id})` : id;
72
- });
73
- env += `- Participants: ${participantLabels.length > 0 ? participantLabels.join(', ') : 'None'}\n`;
48
+ const channelName = channelDetails?.name || channelId;
49
+ env += `- Mode: Channel (#${channelName})\n`;
50
+ if (channelDetails?.cwd) {
51
+ env += `- Workspace: ${channelDetails.cwd}\n`;
52
+ }
53
+ if (threadId) {
54
+ env += `- Thread: ${threadDetails?.name || threadId}\n`;
74
55
  }
75
56
  sections.push(env);
76
57
 
@@ -2,6 +2,7 @@ import { MelonyPlugin, RuntimeContext } from 'melony';
2
2
  import { generateText, type LanguageModel } from 'ai';
3
3
  import { openai } from '@ai-sdk/openai';
4
4
  import { anthropic } from '@ai-sdk/anthropic';
5
+ import { google } from '@ai-sdk/google';
5
6
  import { OpenBotEvent, OpenBotState, AgentInvokeEvent } from '../../app/types.js';
6
7
  import { eventsToModelMessages } from './history.js';
7
8
  import { Storage } from '../../services/plugins/domain.js';
@@ -10,8 +11,33 @@ import {
10
11
  ORCHESTRATOR_AGENT_ID,
11
12
  buildContext,
12
13
  } from './context.js';
13
- import { saveConfig } from '../../app/config.js';
14
- import { API_KEY_SETUP_MESSAGE, OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
14
+ import { saveConfig, DEFAULT_MARKETPLACE_REGISTRY_URL } from '../../app/config.js';
15
+ import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
16
+
17
+ interface ModelRegistry {
18
+ providers: Record<
19
+ string,
20
+ {
21
+ label: string;
22
+ models: Array<{ id: string; label: string; description: string }>;
23
+ }
24
+ >;
25
+ }
26
+
27
+ let cachedRegistry: ModelRegistry | null = null;
28
+
29
+ async function fetchRegistry(): Promise<ModelRegistry | null> {
30
+ if (cachedRegistry) return cachedRegistry;
31
+ try {
32
+ const response = await fetch(DEFAULT_MARKETPLACE_REGISTRY_URL);
33
+ if (!response.ok) throw new Error(`Failed to fetch registry: ${response.statusText}`);
34
+ cachedRegistry = (await response.json()) as ModelRegistry;
35
+ return cachedRegistry;
36
+ } catch (error) {
37
+ console.error('[openbot] Failed to fetch model registry:', error);
38
+ return null;
39
+ }
40
+ }
15
41
 
16
42
  export interface OpenBotRuntimeOptions {
17
43
  /** Provider model string (e.g. `openai/gpt-4o-mini`, `anthropic/claude-3-5-sonnet-20240620`). */
@@ -34,6 +60,8 @@ function resolveModel(modelString: string): LanguageModel {
34
60
  return openai(modelId);
35
61
  case 'anthropic':
36
62
  return anthropic(modelId);
63
+ case 'google':
64
+ return google(modelId);
37
65
  default:
38
66
  throw new Error(`Unsupported AI provider: "${provider}"`);
39
67
  }
@@ -246,49 +274,20 @@ export const openbotRuntime =
246
274
  errorMessage.includes('authentication');
247
275
 
248
276
  if (isApiKeyError) {
249
- const [currentProvider, ...rest] = currentModelString.split('/');
250
- const currentModelId = rest.join('/');
251
-
252
277
  yield {
253
278
  type: 'client:ui:widget',
254
279
  data: {
255
- kind: 'form',
256
- widgetId: `api_key_request_${Date.now()}`,
257
- title: `AI Provider API Key Required`,
258
- description: API_KEY_SETUP_MESSAGE,
259
- fields: [
260
- {
261
- id: 'provider',
262
- label: 'Provider',
263
- type: 'select',
264
- required: true,
265
- options: [
266
- { label: 'OpenAI', value: 'openai' },
267
- { label: 'Anthropic', value: 'anthropic' },
268
- ],
269
- defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
270
- },
271
- {
272
- id: 'model',
273
- label: 'Model',
274
- type: 'text',
275
- description:
276
- 'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
277
- placeholder: 'gpt-4o-mini',
278
- required: true,
279
- defaultValue: currentModelId,
280
- },
281
- {
282
- id: 'apiKey',
283
- label: 'API Key',
284
- type: 'text',
285
- placeholder: `sk-...`,
286
- required: true,
287
- },
280
+ kind: 'choice',
281
+ widgetId: `api_provider_selection_${Date.now()}`,
282
+ title: `Setup AI Provider`,
283
+ description: `Select a provider to continue.`,
284
+ actions: [
285
+ { id: 'openai', label: 'OpenAI', variant: 'primary' },
286
+ { id: 'anthropic', label: 'Anthropic', variant: 'primary' },
287
+ { id: 'google', label: 'Google', variant: 'primary' },
288
288
  ],
289
- submitLabel: 'Save & Continue',
290
289
  metadata: {
291
- type: 'api_key_request',
290
+ type: 'api_provider_selection',
292
291
  },
293
292
  },
294
293
  meta: { agentId: context.state.agentId, threadId },
@@ -315,49 +314,6 @@ export const openbotRuntime =
315
314
 
316
315
  const threadId = event.meta?.threadId || context.state.threadId;
317
316
 
318
- // Auto-add participants if tagged in the prompt
319
- const content = (event as AgentInvokeEvent).data?.content;
320
- if (content && storage) {
321
- try {
322
- const allAgents = await storage.getAgents();
323
- const tags = content.match(/@([\w-]+)/g);
324
- if (tags) {
325
- const taggedAgentIds = tags.map((t) => t.slice(1));
326
- const validAgentIds = taggedAgentIds.filter((id) =>
327
- allAgents.some((a) => a.id === id),
328
- );
329
-
330
- const currentParticipants = context.state.channelDetails?.participants || [];
331
- const newParticipants = [...new Set([...currentParticipants, ...validAgentIds])];
332
-
333
- if (newParticipants.length > currentParticipants.length) {
334
- // Update storage
335
- await storage.patchChannelState({
336
- channelId: context.state.channelId,
337
- state: { participants: newParticipants },
338
- });
339
-
340
- // Refresh local state
341
- context.state.channelDetails = await storage.getChannelDetails({
342
- channelId: context.state.channelId,
343
- });
344
-
345
- // Notify UI/others about the change
346
- yield {
347
- type: 'action:patch_channel_details:result',
348
- data: { success: true, updatedFields: ['participants'] },
349
- meta: {
350
- agentId: context.state.agentId,
351
- threadId,
352
- },
353
- } as OpenBotEvent;
354
- }
355
- }
356
- } catch (error) {
357
- console.warn('[openbot] Failed to auto-add participants from tags:', error);
358
- }
359
- }
360
-
361
317
  // clear the tool batch if the agent is invoked
362
318
  // this is to prevent the tool batch from being used for a new agent invocation
363
319
  await createToolBatchTracker(
@@ -394,15 +350,98 @@ export const openbotRuntime =
394
350
  });
395
351
 
396
352
  builder.on('client:ui:widget:response', async function* (event, context) {
397
- const { metadata, values } = event.data;
353
+ const { metadata, values, actionId } = event.data;
354
+ const threadId = event.meta?.threadId || context.state.threadId;
355
+
356
+ if (metadata?.type === 'api_provider_selection') {
357
+ const provider = actionId;
358
+ const [_, ...rest] = currentModelString.split('/');
359
+ const currentModelId = rest.join('/');
360
+
361
+ const registry = await fetchRegistry();
362
+ const providerData = registry?.providers[provider as string];
363
+
364
+ const providerLinks: Record<string, string> = {
365
+ openai: 'https://platform.openai.com/api-keys',
366
+ anthropic: 'https://console.anthropic.com/settings/keys',
367
+ google: 'https://aistudio.google.com/app/apikey',
368
+ };
369
+
370
+ const label = providerData?.label || (provider as string);
371
+ const link = providerLinks[provider as string] || '';
372
+
373
+ const modelOptions = providerData?.models.map((m) => ({
374
+ label: m.label,
375
+ value: m.id,
376
+ }));
377
+
378
+ const defaultModel = modelOptions?.[0]?.value || 'gpt-4o-mini';
379
+ const defaultValue =
380
+ modelOptions?.find((m) => m.value === currentModelId)?.value ||
381
+ currentModelId ||
382
+ defaultModel;
383
+
384
+ yield {
385
+ type: 'client:ui:widget',
386
+ data: {
387
+ widgetId: event.data.widgetId,
388
+ kind: 'message',
389
+ title: 'Provider Selected',
390
+ body: `${label} provider was selected.`,
391
+ state: 'submitted',
392
+ display: 'collapsed',
393
+ disabled: true,
394
+ actions: [],
395
+ },
396
+ meta: { agentId: context.state.agentId, threadId },
397
+ } as OpenBotEvent;
398
+
399
+ yield {
400
+ type: 'client:ui:widget',
401
+ data: {
402
+ kind: 'form',
403
+ widgetId: `api_key_request_${Date.now()}`,
404
+ title: `${label} Setup`,
405
+ description: `Enter your API key and select a model.`,
406
+ fields: [
407
+ {
408
+ id: 'model',
409
+ label: 'Model',
410
+ type: modelOptions ? 'select' : 'text',
411
+ description: modelOptions ? undefined : `Model name (e.g. \`${defaultModel}\`).`,
412
+ options: modelOptions,
413
+ placeholder: defaultModel,
414
+ required: true,
415
+ defaultValue,
416
+ },
417
+ {
418
+ id: 'apiKey',
419
+ label: 'API Key',
420
+ type: 'password',
421
+ description: `Get your key here: [${link}](${link})`,
422
+ placeholder: `sk-...`,
423
+ required: true,
424
+ },
425
+ ],
426
+ submitLabel: 'Save & Continue',
427
+ metadata: {
428
+ type: 'api_key_request',
429
+ provider,
430
+ },
431
+ },
432
+ meta: { agentId: context.state.agentId, threadId },
433
+ } as OpenBotEvent;
434
+ return;
435
+ }
436
+
398
437
  if (metadata?.type !== 'api_key_request') return;
399
- if (!values?.apiKey || !values?.provider || !values?.model) return;
438
+ if (!values?.apiKey || !values?.model) return;
400
439
 
401
- const provider = String(values.provider);
440
+ const provider = String(values.provider || metadata.provider);
402
441
  const modelId = String(values.model).trim();
403
442
  const apiKey = String(values.apiKey);
404
443
 
405
- if (provider !== 'openai' && provider !== 'anthropic') {
444
+ if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'google') {
406
445
  yield {
407
446
  type: 'agent:output',
408
447
  data: { content: `Unsupported provider: ${provider}` },
@@ -411,7 +450,12 @@ export const openbotRuntime =
411
450
  return;
412
451
  }
413
452
 
414
- const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
453
+ const envVar =
454
+ provider === 'openai'
455
+ ? 'OPENAI_API_KEY'
456
+ : provider === 'anthropic'
457
+ ? 'ANTHROPIC_API_KEY'
458
+ : 'GOOGLE_GENERATIVE_AI_API_KEY';
415
459
  const newModelString = `${provider}/${modelId}`;
416
460
 
417
461
  if (!storage) return;
@@ -460,10 +504,14 @@ export const openbotRuntime =
460
504
  title: 'API Key Saved',
461
505
  body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
462
506
  state: 'submitted',
463
- actions: [{ id: 'ok', label: 'Got it', variant: 'primary' }],
507
+ display: 'collapsed',
508
+ disabled: true,
509
+ actions: [],
464
510
  },
465
511
  meta: { agentId: context.state.agentId },
466
512
  };
513
+
514
+ yield* runLLM(context, threadId);
467
515
  } catch (error) {
468
516
  yield {
469
517
  type: 'agent:output',
@@ -7,13 +7,12 @@ export const OPENBOT_SYSTEM_PROMPT = [
7
7
  '- **Credential Guidance**: If an agent or tool requires credentials, inform the user they can be managed under "Settings > Variables".',
8
8
  '',
9
9
  '# CORE MISSION',
10
- 'You almost never execute tasks yourself. Instead, you delegate tasks to specialized agents (channel participants). You act as a high-level manager, ensuring the right agent is working on the right task.',
10
+ 'You almost never execute tasks yourself. Instead, you delegate tasks to specialized agents. You act as a high-level manager, ensuring the right agent is working on the right task.',
11
11
  '',
12
12
  '# OPERATIONAL GUIDELINES',
13
13
  '- **Channel and Threads**: The main and only way to communicate and act is through channels and threads. There might be a channel called "uncategorized" for general purpose communication.',
14
- '- **Agent Participation**: ONLY add an agent via `patch_channel_details` if the user manually tags them (e.g., `@name`) AND they are missing from the `Participants` list in `ENVIRONMENT`.',
15
- '- **Delegation**: NEVER delegate to an agent who is not a participant. Only if existing participants clearly cannot handle a task should you suggest relevant agents from the `INSTALLED AGENTS` list.',
16
- '- **Bash Tool Usage**: You should use the `bash` tool very rarely. Only use it when the user explicitly requests a command to be run or when it is absolutely necessary for a task that no other participant can handle.',
14
+ '- **Delegation**: You can delegate tasks to any specialized agent in the `INSTALLED AGENTS` list.',
15
+ '- **Bash Tool Usage**: You should use the `bash` tool very rarely. Only use it when the user explicitly requests a command to be run or when it is absolutely necessary for a task that no other agent can handle.',
17
16
  '- **Context Awareness**: Use the provided ENVIRONMENT, CHANNEL SPECIFICATION, and MEMORIES to maintain continuity. Do not ask for information already present in these sections.',
18
17
  '- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.',
19
18
  '- **Structured Interaction**: Use the `render_widget` tool to collect information via forms, offer choices, or display lists. This is preferred over asking multiple separate questions in plain text.',
@@ -21,7 +20,3 @@ export const OPENBOT_SYSTEM_PROMPT = [
21
20
  '# COMMUNICATION STYLE',
22
21
  '- Be always concise, professional, and proactive.',
23
22
  ].join('\n');
24
-
25
- /** Shown in the API key setup form when no provider credentials are configured. */
26
- export const API_KEY_SETUP_MESSAGE =
27
- 'OpenBot runs AI agents locally with tools, memory, and delegation. Bring your own OpenAI or Anthropic key — it stays on your machine. Use the form below to get started.';