openbot 0.4.4 → 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.
@@ -0,0 +1,3 @@
1
+ /** Default channel when requests omit channelId (general-purpose conversation). */
2
+ export const UNCATEGORIZED_CHANNEL_ID = 'uncategorized';
3
+ export const DEFAULT_UNCATEGORIZED_SPEC = '# Uncategorized\n\nGeneral-purpose channel for conversations without a dedicated channel.';
package/dist/app/cli.js CHANGED
@@ -16,7 +16,7 @@ function checkNodeVersion() {
16
16
  }
17
17
  }
18
18
  checkNodeVersion();
19
- program.name('openbot').description('OpenBot CLI').version('0.4.4');
19
+ program.name('openbot').description('OpenBot CLI').version('0.4.6');
20
20
  program
21
21
  .command('start')
22
22
  .description('Start the OpenBot harness')
@@ -0,0 +1,48 @@
1
+ import { ORCHESTRATOR_AGENT_ID } from './agent-ids.js';
2
+ import { storageService } from '../plugins/storage/service.js';
3
+ /** Thread `state.json` key for the sticky responding agent id. */
4
+ export const THREAD_RESPONDING_AGENT_ID_KEY = 'respondingAgentId';
5
+ const readBoundAgentId = (state) => {
6
+ if (!state || typeof state !== 'object')
7
+ return undefined;
8
+ const value = state[THREAD_RESPONDING_AGENT_ID_KEY];
9
+ if (typeof value !== 'string')
10
+ return undefined;
11
+ const trimmed = value.trim();
12
+ return trimmed || undefined;
13
+ };
14
+ /**
15
+ * Resolves which agent should handle a thread-scoped publish.
16
+ * Once `respondingAgentId` is stored on the thread, that value wins over request overrides.
17
+ */
18
+ export async function resolveRespondingAgentId(options) {
19
+ const { channelId, threadId, requestedAgentId, bindIfUnbound = false } = options;
20
+ const requested = requestedAgentId?.trim() || undefined;
21
+ const fallback = requested || ORCHESTRATOR_AGENT_ID;
22
+ if (!threadId) {
23
+ return { agentId: fallback, bound: false, overridden: false };
24
+ }
25
+ const details = await storageService.getThreadDetails({ channelId, threadId });
26
+ const bound = readBoundAgentId(details.state);
27
+ if (bound) {
28
+ const overridden = !!requested && requested !== bound;
29
+ if (overridden) {
30
+ console.warn('[publish] Ignoring agentId override; thread is bound to responding agent', {
31
+ threadId,
32
+ bound,
33
+ requested,
34
+ });
35
+ }
36
+ return { agentId: bound, bound: true, overridden };
37
+ }
38
+ if (!bindIfUnbound) {
39
+ return { agentId: fallback, bound: false, overridden: false };
40
+ }
41
+ await storageService.getAgentDetails({ agentId: fallback });
42
+ await storageService.patchThreadState({
43
+ channelId,
44
+ threadId,
45
+ state: { [THREAD_RESPONDING_AGENT_ID_KEY]: fallback },
46
+ });
47
+ return { agentId: fallback, bound: true, overridden: false };
48
+ }
@@ -16,6 +16,8 @@ import { storageService } from '../plugins/storage/service.js';
16
16
  import { buildWorkspaceFileUrl, getPublicBaseUrl, openChannelFileStream, } from '../plugins/storage/files.js';
17
17
  import { ensureEventId, openBotEventFromQuery } from './utils.js';
18
18
  import { abortRegistry, abortKey } from '../services/abort.js';
19
+ import { resolveRespondingAgentId } from './responding-agent.js';
20
+ import { DEFAULT_UNCATEGORIZED_SPEC, UNCATEGORIZED_CHANNEL_ID, } from './channel-ids.js';
19
21
  export async function startServer(options = {}) {
20
22
  const publishEventSchema = z
21
23
  .object({
@@ -42,8 +44,17 @@ export async function startServer(options = {}) {
42
44
  // Pre-warm caches for agents and plugins to speed up first UI load
43
45
  storageService.getAgents().catch((err) => console.warn('[server] Failed to pre-warm agents cache', err));
44
46
  storageService.getPlugins().catch((err) => console.warn('[server] Failed to pre-warm plugins cache', err));
47
+ const getRawChannelId = (req) => {
48
+ const raw = req.get('x-openbot-channel-id') ||
49
+ req.query.channelId ||
50
+ (req.body && req.body.channelId);
51
+ if (typeof raw !== 'string')
52
+ return undefined;
53
+ const trimmed = raw.trim();
54
+ return trimmed || undefined;
55
+ };
45
56
  const getContext = (req) => {
46
- const channelId = req.get('x-openbot-channel-id') || req.query.channelId || (req.body && req.body.channelId);
57
+ const rawChannelId = getRawChannelId(req);
47
58
  const threadId = req.get('x-openbot-thread-id') || req.query.threadId || (req.body && req.body.threadId);
48
59
  const agentId = req.get('x-openbot-agent-id') || req.query.agentId || (req.body && req.body.agentId);
49
60
  const runId = req.get('x-openbot-run-id') ||
@@ -54,7 +65,8 @@ export async function startServer(options = {}) {
54
65
  req.query.responseType ||
55
66
  (req.body && req.body.responseType);
56
67
  return {
57
- channelId: (channelId || (threadId ? 'uncategorized' : 'uncategorized')), // Default to uncategorized if none
68
+ channelId: (rawChannelId || UNCATEGORIZED_CHANNEL_ID),
69
+ rawChannelId,
58
70
  threadId: threadId,
59
71
  agentId: agentId,
60
72
  runId: runId,
@@ -116,18 +128,58 @@ export async function startServer(options = {}) {
116
128
  data: { channels },
117
129
  };
118
130
  };
131
+ const broadcastActiveRunsSnapshot = () => {
132
+ const snapshot = buildActiveRunsSnapshot();
133
+ ensureEventId(snapshot);
134
+ sendToClientKey(GLOBAL_CHANNEL_ID, snapshot);
135
+ };
136
+ const persistLifecycleEvent = (event, targetChannelId, targetThreadId) => {
137
+ ensureEventId(event);
138
+ storageService
139
+ .storeEvent({
140
+ channelId: targetChannelId,
141
+ threadId: targetThreadId,
142
+ event,
143
+ })
144
+ .catch((error) => {
145
+ console.error('[server] Failed to persist lifecycle event', {
146
+ type: event.type,
147
+ channelId: targetChannelId,
148
+ threadId: targetThreadId,
149
+ error,
150
+ });
151
+ });
152
+ };
119
153
  // Drop every tracked run for a channel/thread. A stop aborts the whole
120
154
  // chain (parent + delegated sub-agents), but the sub-agents' `agent:run:end`
121
155
  // events can be swallowed when the parent run loop breaks on abort, leaving
122
- // orphaned entries that keep a channel falsely "active". Purging by
123
- // channel/thread guarantees the snapshot self-heals after a stop.
156
+ // orphaned entries that keep a channel falsely "active". Emit explicit
157
+ // `agent:run:end` for each purged run and refresh the global snapshot so
158
+ // clients stay in sync even when the parent harness stops yielding.
124
159
  const purgeActiveRunsForThread = (channelId, threadId) => {
125
160
  const target = threadId || undefined;
161
+ const removed = [];
126
162
  for (const [key, run] of activeRuns) {
127
163
  if (run.channelId === channelId && (run.threadId || undefined) === target) {
164
+ removed.push(run);
128
165
  activeRuns.delete(key);
129
166
  }
130
167
  }
168
+ for (const run of removed) {
169
+ const endEvent = {
170
+ type: 'agent:run:end',
171
+ data: {
172
+ runId: run.runId,
173
+ agentId: run.agentId,
174
+ channelId: run.channelId,
175
+ threadId: run.threadId,
176
+ },
177
+ };
178
+ ensureEventId(endEvent);
179
+ persistLifecycleEvent(endEvent, run.channelId, run.threadId);
180
+ sendToClientKey(getClientKey(run.channelId, run.threadId), endEvent);
181
+ sendToClientKey(GLOBAL_CHANNEL_ID, endEvent);
182
+ }
131
183
  };
132
184
  // Support for Chrome's Private Network Access (PNA)
133
185
  // https://developer.chrome.com/blog/private-network-access-preflight/
@@ -328,19 +380,38 @@ export async function startServer(options = {}) {
328
380
  const data = (event.data ?? {});
329
381
  const targetChannelId = data.channelId || channelId;
330
382
  const targetThreadId = data.threadId || threadId;
383
+ let resolvedStopAgentId = data.agentId || agentId || ORCHESTRATOR_AGENT_ID;
384
+ try {
385
+ const resolved = await resolveRespondingAgentId({
386
+ channelId: targetChannelId,
387
+ threadId: targetThreadId,
388
+ requestedAgentId: data.agentId || agentId,
389
+ });
390
+ resolvedStopAgentId = resolved.agentId;
391
+ }
392
+ catch (error) {
393
+ console.warn('[publish] Failed to resolve responding agent for stop request', {
394
+ channelId: targetChannelId,
395
+ threadId: targetThreadId,
396
+ error,
397
+ });
398
+ }
331
399
  const stopped = abortRegistry.abort(abortKey(targetChannelId, targetThreadId));
332
400
  purgeActiveRunsForThread(targetChannelId, targetThreadId);
401
+ // Resync global clients even when nothing was tracked server-side.
402
+ broadcastActiveRunsSnapshot();
333
403
  const stoppedEvent = {
334
404
  type: 'agent:run:stopped',
335
405
  data: {
336
406
  runId: data.runId || runId,
337
- agentId: data.agentId || agentId || ORCHESTRATOR_AGENT_ID,
407
+ agentId: resolvedStopAgentId,
338
408
  channelId: targetChannelId,
339
409
  threadId: targetThreadId,
340
410
  reason: data.reason,
341
411
  },
342
412
  };
343
413
  ensureEventId(stoppedEvent);
414
+ persistLifecycleEvent(stoppedEvent, targetChannelId, targetThreadId);
344
415
  sendToClientKey(getClientKey(targetChannelId, targetThreadId), stoppedEvent);
345
416
  sendToClientKey(GLOBAL_CHANNEL_ID, stoppedEvent);
346
417
  res.json({ success: stopped });
@@ -363,6 +434,7 @@ export async function startServer(options = {}) {
363
434
  }
364
435
  else if (chunk.type === 'agent:run:stopped') {
365
436
  purgeActiveRunsForThread(chunk.data.channelId, chunk.data.threadId);
437
+ broadcastActiveRunsSnapshot();
366
438
  }
367
439
  sendToClientKey(targetClientKey, chunk);
368
440
  if (chunk.type === 'agent:run:start' ||
@@ -373,9 +445,27 @@ export async function startServer(options = {}) {
373
445
  };
374
446
  try {
375
447
  ensureEventId(event);
448
+ const isUserConversationStart = event.type === 'agent:invoke' &&
449
+ event.data?.role === 'user' &&
450
+ typeof event.data.content === 'string' &&
451
+ event.data.content.trim().length > 0;
452
+ if (isUserConversationStart && channelId === UNCATEGORIZED_CHANNEL_ID) {
453
+ await storageService.ensureChannel({
454
+ channelId: UNCATEGORIZED_CHANNEL_ID,
455
+ spec: DEFAULT_UNCATEGORIZED_SPEC,
456
+ initialState: { name: 'Uncategorized' },
457
+ });
458
+ }
459
+ const bindIfUnbound = event.type === 'agent:invoke';
460
+ const resolved = await resolveRespondingAgentId({
461
+ channelId,
462
+ threadId,
463
+ requestedAgentId: agentId,
464
+ bindIfUnbound,
465
+ });
376
466
  await runAgent({
377
467
  runId,
378
- agentId: agentId || ORCHESTRATOR_AGENT_ID,
468
+ agentId: resolved.agentId,
379
469
  event,
380
470
  channelId,
381
471
  threadId,
@@ -385,6 +475,11 @@ export async function startServer(options = {}) {
385
475
  res.sendStatus(200);
386
476
  }
387
477
  catch (error) {
478
+ const message = error instanceof Error ? error.message : String(error);
479
+ const isUnknownAgent = (error instanceof Error &&
480
+ (error.code === 'AGENT_NOT_FOUND' ||
481
+ error.message.includes('does not exist'))) ||
482
+ message.includes('does not exist');
388
483
  console.error('[publish] Failed to dispatch event', {
389
484
  runId,
390
485
  channelId,
@@ -392,6 +487,10 @@ export async function startServer(options = {}) {
392
487
  eventType: event.type,
393
488
  error,
394
489
  });
490
+ if (isUnknownAgent) {
491
+ res.status(400).json({ error: message });
492
+ return;
493
+ }
395
494
  res.status(500).json({ error: 'Failed to process publish event' });
396
495
  }
397
496
  });
@@ -406,6 +505,13 @@ export async function startServer(options = {}) {
406
505
  return;
407
506
  }
408
507
  const { channelId, threadId, agentId, runId } = getContext(req);
508
+ // In-memory active runs (not persisted). Mirrors the initial SSE frame on __global__.
509
+ if (channelId === GLOBAL_CHANNEL_ID && event.type === 'action:storage:get-active-runs') {
510
+ const snapshot = buildActiveRunsSnapshot();
511
+ ensureEventId(snapshot);
512
+ res.json({ events: [snapshot] });
513
+ return;
514
+ }
409
515
  if (event.type === 'action:storage:serve-file') {
410
516
  const filePath = event.data?.path;
411
517
  if (!channelId?.trim()) {
@@ -20,19 +20,11 @@ export const getContextBudgetForModel = (modelString) => {
20
20
  };
21
21
  /** Built-in orchestrator agent id. */
22
22
  export const ORCHESTRATOR_AGENT_ID = 'system';
23
- /**
24
- * Check if a channel is a solo DM (only the agent is present).
25
- */
26
- export function isDmSoloChannel(participants, agentId) {
27
- return participants.length === 0 || (participants.length === 1 && participants[0] === agentId);
28
- }
29
23
  /**
30
24
  * Simplified context builder for MVP.
31
25
  */
32
26
  export async function buildContext(state, storage) {
33
27
  const { channelId, threadId, channelDetails, agentId, threadDetails, agentDetails } = state;
34
- const participants = channelDetails?.participants || [];
35
- const isDm = isDmSoloChannel(participants, agentId);
36
28
  const sections = [];
37
29
  // Fetch agents once if storage is available
38
30
  const allAgents = storage?.getAgents ? await storage.getAgents().catch(() => []) : [];
@@ -42,24 +34,13 @@ export async function buildContext(state, storage) {
42
34
  }
43
35
  // 2. Environment
44
36
  let env = '## ENVIRONMENT\n';
45
- if (isDm) {
46
- env += '- Mode: Direct Message (Solo)\n';
37
+ const channelName = channelDetails?.name || channelId;
38
+ env += `- Mode: Channel (#${channelName})\n`;
39
+ if (channelDetails?.cwd) {
40
+ env += `- Workspace: ${channelDetails.cwd}\n`;
47
41
  }
48
- else {
49
- const channelName = channelDetails?.name || channelId;
50
- env += `- Mode: Channel (#${channelName})\n`;
51
- if (channelDetails?.cwd) {
52
- env += `- Workspace: ${channelDetails.cwd}\n`;
53
- }
54
- if (threadId) {
55
- env += `- Thread: ${threadDetails?.name || threadId}\n`;
56
- }
57
- const peerIds = participants.filter((id) => id !== agentId);
58
- const participantLabels = peerIds.map((id) => {
59
- const agent = allAgents.find((a) => a.id === id);
60
- return agent ? `${agent.name} (${id})` : id;
61
- });
62
- env += `- Participants: ${participantLabels.length > 0 ? participantLabels.join(', ') : 'None'}\n`;
42
+ if (threadId) {
43
+ env += `- Thread: ${threadDetails?.name || threadId}\n`;
63
44
  }
64
45
  sections.push(env);
65
46
  // 2.5 Installed Agents
@@ -1,10 +1,27 @@
1
1
  import { generateText } from 'ai';
2
2
  import { openai } from '@ai-sdk/openai';
3
3
  import { anthropic } from '@ai-sdk/anthropic';
4
+ import { google } from '@ai-sdk/google';
4
5
  import { eventsToModelMessages } from './history.js';
5
6
  import { buildContext, } from './context.js';
6
- import { saveConfig } from '../../app/config.js';
7
- import { API_KEY_SETUP_MESSAGE, OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
7
+ import { saveConfig, DEFAULT_MARKETPLACE_REGISTRY_URL } from '../../app/config.js';
8
+ import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
9
+ let cachedRegistry = null;
10
+ async function fetchRegistry() {
11
+ if (cachedRegistry)
12
+ return cachedRegistry;
13
+ try {
14
+ const response = await fetch(DEFAULT_MARKETPLACE_REGISTRY_URL);
15
+ if (!response.ok)
16
+ throw new Error(`Failed to fetch registry: ${response.statusText}`);
17
+ cachedRegistry = (await response.json());
18
+ return cachedRegistry;
19
+ }
20
+ catch (error) {
21
+ console.error('[openbot] Failed to fetch model registry:', error);
22
+ return null;
23
+ }
24
+ }
8
25
  function resolveModel(modelString) {
9
26
  const [provider, ...rest] = modelString.split('/');
10
27
  const modelId = rest.join('/');
@@ -16,6 +33,8 @@ function resolveModel(modelString) {
16
33
  return openai(modelId);
17
34
  case 'anthropic':
18
35
  return anthropic(modelId);
36
+ case 'google':
37
+ return google(modelId);
19
38
  default:
20
39
  throw new Error(`Unsupported AI provider: "${provider}"`);
21
40
  }
@@ -182,47 +201,20 @@ export const openbotRuntime = (options) => (builder) => {
182
201
  errorMessage.includes('Unauthorized') ||
183
202
  errorMessage.includes('authentication');
184
203
  if (isApiKeyError) {
185
- const [currentProvider, ...rest] = currentModelString.split('/');
186
- const currentModelId = rest.join('/');
187
204
  yield {
188
205
  type: 'client:ui:widget',
189
206
  data: {
190
- kind: 'form',
191
- widgetId: `api_key_request_${Date.now()}`,
192
- title: `AI Provider API Key Required`,
193
- description: API_KEY_SETUP_MESSAGE,
194
- fields: [
195
- {
196
- id: 'provider',
197
- label: 'Provider',
198
- type: 'select',
199
- required: true,
200
- options: [
201
- { label: 'OpenAI', value: 'openai' },
202
- { label: 'Anthropic', value: 'anthropic' },
203
- ],
204
- defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
205
- },
206
- {
207
- id: 'model',
208
- label: 'Model',
209
- type: 'text',
210
- description: 'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
211
- placeholder: 'gpt-4o-mini',
212
- required: true,
213
- defaultValue: currentModelId,
214
- },
215
- {
216
- id: 'apiKey',
217
- label: 'API Key',
218
- type: 'text',
219
- placeholder: `sk-...`,
220
- required: true,
221
- },
207
+ kind: 'choice',
208
+ widgetId: `api_provider_selection_${Date.now()}`,
209
+ title: `Setup AI Provider`,
210
+ description: `Select a provider to continue.`,
211
+ actions: [
212
+ { id: 'openai', label: 'OpenAI', variant: 'primary' },
213
+ { id: 'anthropic', label: 'Anthropic', variant: 'primary' },
214
+ { id: 'google', label: 'Google', variant: 'primary' },
222
215
  ],
223
- submitLabel: 'Save & Continue',
224
216
  metadata: {
225
- type: 'api_key_request',
217
+ type: 'api_provider_selection',
226
218
  },
227
219
  },
228
220
  meta: { agentId: context.state.agentId, threadId },
@@ -244,43 +236,6 @@ export const openbotRuntime = (options) => (builder) => {
244
236
  };
245
237
  }
246
238
  const threadId = event.meta?.threadId || context.state.threadId;
247
- // Auto-add participants if tagged in the prompt
248
- const content = event.data?.content;
249
- if (content && storage) {
250
- try {
251
- const allAgents = await storage.getAgents();
252
- const tags = content.match(/@([\w-]+)/g);
253
- if (tags) {
254
- const taggedAgentIds = tags.map((t) => t.slice(1));
255
- const validAgentIds = taggedAgentIds.filter((id) => allAgents.some((a) => a.id === id));
256
- const currentParticipants = context.state.channelDetails?.participants || [];
257
- const newParticipants = [...new Set([...currentParticipants, ...validAgentIds])];
258
- if (newParticipants.length > currentParticipants.length) {
259
- // Update storage
260
- await storage.patchChannelState({
261
- channelId: context.state.channelId,
262
- state: { participants: newParticipants },
263
- });
264
- // Refresh local state
265
- context.state.channelDetails = await storage.getChannelDetails({
266
- channelId: context.state.channelId,
267
- });
268
- // Notify UI/others about the change
269
- yield {
270
- type: 'action:patch_channel_details:result',
271
- data: { success: true, updatedFields: ['participants'] },
272
- meta: {
273
- agentId: context.state.agentId,
274
- threadId,
275
- },
276
- };
277
- }
278
- }
279
- }
280
- catch (error) {
281
- console.warn('[openbot] Failed to auto-add participants from tags:', error);
282
- }
283
- }
284
239
  // clear the tool batch if the agent is invoked
285
240
  // this is to prevent the tool batch from being used for a new agent invocation
286
241
  await createToolBatchTracker(context.state, storage, context.state.channelId, threadId).clear();
@@ -302,15 +257,88 @@ export const openbotRuntime = (options) => (builder) => {
302
257
  yield* runLLM(context, threadId);
303
258
  });
304
259
  builder.on('client:ui:widget:response', async function* (event, context) {
305
- const { metadata, values } = event.data;
260
+ const { metadata, values, actionId } = event.data;
261
+ const threadId = event.meta?.threadId || context.state.threadId;
262
+ if (metadata?.type === 'api_provider_selection') {
263
+ const provider = actionId;
264
+ const [_, ...rest] = currentModelString.split('/');
265
+ const currentModelId = rest.join('/');
266
+ const registry = await fetchRegistry();
267
+ const providerData = registry?.providers[provider];
268
+ const providerLinks = {
269
+ openai: 'https://platform.openai.com/api-keys',
270
+ anthropic: 'https://console.anthropic.com/settings/keys',
271
+ google: 'https://aistudio.google.com/app/apikey',
272
+ };
273
+ const label = providerData?.label || provider;
274
+ const link = providerLinks[provider] || '';
275
+ const modelOptions = providerData?.models.map((m) => ({
276
+ label: m.label,
277
+ value: m.id,
278
+ }));
279
+ const defaultModel = modelOptions?.[0]?.value || 'gpt-4o-mini';
280
+ const defaultValue = modelOptions?.find((m) => m.value === currentModelId)?.value ||
281
+ currentModelId ||
282
+ defaultModel;
283
+ yield {
284
+ type: 'client:ui:widget',
285
+ data: {
286
+ widgetId: event.data.widgetId,
287
+ kind: 'message',
288
+ title: 'Provider Selected',
289
+ body: `${label} provider was selected.`,
290
+ state: 'submitted',
291
+ display: 'collapsed',
292
+ disabled: true,
293
+ actions: [],
294
+ },
295
+ meta: { agentId: context.state.agentId, threadId },
296
+ };
297
+ yield {
298
+ type: 'client:ui:widget',
299
+ data: {
300
+ kind: 'form',
301
+ widgetId: `api_key_request_${Date.now()}`,
302
+ title: `${label} Setup`,
303
+ description: `Enter your API key and select a model.`,
304
+ fields: [
305
+ {
306
+ id: 'model',
307
+ label: 'Model',
308
+ type: modelOptions ? 'select' : 'text',
309
+ description: modelOptions ? undefined : `Model name (e.g. \`${defaultModel}\`).`,
310
+ options: modelOptions,
311
+ placeholder: defaultModel,
312
+ required: true,
313
+ defaultValue,
314
+ },
315
+ {
316
+ id: 'apiKey',
317
+ label: 'API Key',
318
+ type: 'password',
319
+ description: `Get your key here: [${link}](${link})`,
320
+ placeholder: `sk-...`,
321
+ required: true,
322
+ },
323
+ ],
324
+ submitLabel: 'Save & Continue',
325
+ metadata: {
326
+ type: 'api_key_request',
327
+ provider,
328
+ },
329
+ },
330
+ meta: { agentId: context.state.agentId, threadId },
331
+ };
332
+ return;
333
+ }
306
334
  if (metadata?.type !== 'api_key_request')
307
335
  return;
308
- if (!values?.apiKey || !values?.provider || !values?.model)
336
+ if (!values?.apiKey || !values?.model)
309
337
  return;
310
- const provider = String(values.provider);
338
+ const provider = String(values.provider || metadata.provider);
311
339
  const modelId = String(values.model).trim();
312
340
  const apiKey = String(values.apiKey);
313
- if (provider !== 'openai' && provider !== 'anthropic') {
341
+ if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'google') {
314
342
  yield {
315
343
  type: 'agent:output',
316
344
  data: { content: `Unsupported provider: ${provider}` },
@@ -318,7 +346,11 @@ export const openbotRuntime = (options) => (builder) => {
318
346
  };
319
347
  return;
320
348
  }
321
- const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
349
+ const envVar = provider === 'openai'
350
+ ? 'OPENAI_API_KEY'
351
+ : provider === 'anthropic'
352
+ ? 'ANTHROPIC_API_KEY'
353
+ : 'GOOGLE_GENERATIVE_AI_API_KEY';
322
354
  const newModelString = `${provider}/${modelId}`;
323
355
  if (!storage)
324
356
  return;
@@ -363,10 +395,13 @@ export const openbotRuntime = (options) => (builder) => {
363
395
  title: 'API Key Saved',
364
396
  body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
365
397
  state: 'submitted',
366
- actions: [{ id: 'ok', label: 'Got it', variant: 'primary' }],
398
+ display: 'collapsed',
399
+ disabled: true,
400
+ actions: [],
367
401
  },
368
402
  meta: { agentId: context.state.agentId },
369
403
  };
404
+ yield* runLLM(context, threadId);
370
405
  }
371
406
  catch (error) {
372
407
  yield {
@@ -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,5 +20,3 @@ export const OPENBOT_SYSTEM_PROMPT = [
21
20
  '# COMMUNICATION STYLE',
22
21
  '- Be always concise, professional, and proactive.',
23
22
  ].join('\n');
24
- /** Shown in the API key setup form when no provider credentials are configured. */
25
- export const API_KEY_SETUP_MESSAGE = '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.';