openbot 0.4.7 → 0.5.5

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 (139) hide show
  1. package/.claude/agents/runtime-security-reviewer.md +32 -0
  2. package/.claude/hooks/format.sh +18 -0
  3. package/.claude/hooks/protect-secrets.sh +23 -0
  4. package/.claude/settings.json +26 -0
  5. package/.claude/skills/deploy/SKILL.md +26 -0
  6. package/.claude/skills/new-plugin/SKILL.md +46 -0
  7. package/.dockerignore +9 -0
  8. package/.mcp.json +8 -0
  9. package/AGENTS.md +2 -2
  10. package/CLAUDE.md +51 -0
  11. package/Dockerfile +41 -0
  12. package/deploy/README.md +59 -0
  13. package/deploy/entrypoint.sh +6 -0
  14. package/deploy/fly.toml +36 -0
  15. package/dist/app/bootstrap.js +29 -0
  16. package/dist/app/cli.js +5 -1
  17. package/dist/app/cloud-mode.js +15 -0
  18. package/dist/app/config.js +61 -16
  19. package/dist/app/ensure-default-stack.js +54 -0
  20. package/dist/app/openbot-plugin.js +4 -0
  21. package/dist/app/server.js +37 -6
  22. package/dist/harness/index.js +3 -0
  23. package/dist/plugins/approval/index.js +1 -1
  24. package/dist/plugins/bash/index.js +386 -164
  25. package/dist/plugins/memory/service.js +2 -5
  26. package/dist/plugins/openbot/context.js +17 -1
  27. package/dist/plugins/openbot/index.js +23 -7
  28. package/dist/plugins/openbot/model.js +20 -0
  29. package/dist/plugins/openbot/runtime.js +45 -57
  30. package/dist/plugins/openbot/system-prompt.js +43 -8
  31. package/dist/plugins/preview/index.js +267 -0
  32. package/dist/plugins/storage/index.js +3 -325
  33. package/dist/plugins/storage/service.js +61 -18
  34. package/dist/plugins/todo/index.js +131 -38
  35. package/dist/plugins/todo/service.js +93 -0
  36. package/dist/plugins/ui/index.js +8 -1
  37. package/dist/services/plugins/host.js +21 -0
  38. package/dist/services/plugins/model-registry.js +126 -0
  39. package/dist/services/plugins/registry.js +28 -42
  40. package/dist/services/plugins/service.js +2 -6
  41. package/docs/agents.md +20 -2
  42. package/docs/architecture.md +1 -1
  43. package/docs/plugins.md +13 -3
  44. package/docs/templates/AGENT.example.md +8 -14
  45. package/package.json +5 -2
  46. package/pnpm-workspace.yaml +2 -0
  47. package/src/app/bootstrap.ts +43 -0
  48. package/src/app/cli.ts +5 -1
  49. package/src/app/cloud-mode.ts +26 -0
  50. package/src/app/config.ts +71 -19
  51. package/src/app/ensure-default-stack.ts +63 -0
  52. package/src/app/openbot-plugin.ts +5 -0
  53. package/src/app/server.ts +42 -6
  54. package/src/app/types.ts +158 -32
  55. package/src/harness/index.ts +4 -0
  56. package/src/plugins/memory/service.ts +2 -5
  57. package/src/plugins/storage/index.ts +3 -371
  58. package/src/plugins/storage/service.ts +81 -22
  59. package/src/services/plugins/domain.ts +2 -0
  60. package/src/services/plugins/host.ts +36 -0
  61. package/src/services/plugins/model-registry.ts +178 -0
  62. package/src/services/plugins/registry.ts +30 -43
  63. package/src/services/plugins/service.ts +8 -17
  64. package/src/services/plugins/types.ts +36 -2
  65. package/src/services/todo/types.ts +12 -0
  66. package/dist/agents/openbot/index.js +0 -76
  67. package/dist/agents/openbot/middleware/approval.js +0 -132
  68. package/dist/agents/openbot/runtime.js +0 -289
  69. package/dist/agents/openbot/system-prompt.js +0 -32
  70. package/dist/agents/openbot/tools/delegation.js +0 -78
  71. package/dist/agents/openbot/tools/mcp.js +0 -99
  72. package/dist/agents/openbot/tools/shell.js +0 -91
  73. package/dist/agents/openbot/tools/storage.js +0 -75
  74. package/dist/agents/openbot/tools/ui.js +0 -176
  75. package/dist/agents/system.js +0 -33
  76. package/dist/bus/agent-package.js +0 -1
  77. package/dist/bus/plugin.js +0 -1
  78. package/dist/bus/services.js +0 -754
  79. package/dist/harness/agent-harness.js +0 -45
  80. package/dist/harness/agent-invoke-run.js +0 -44
  81. package/dist/harness/agent-turn.js +0 -99
  82. package/dist/harness/channel-participants.js +0 -40
  83. package/dist/harness/constants.js +0 -2
  84. package/dist/harness/context-meter.js +0 -97
  85. package/dist/harness/context.js +0 -363
  86. package/dist/harness/dispatch.js +0 -144
  87. package/dist/harness/dispatcher.js +0 -156
  88. package/dist/harness/event-normalizer.js +0 -59
  89. package/dist/harness/history.js +0 -177
  90. package/dist/harness/mcp.js +0 -61
  91. package/dist/harness/orchestration.js +0 -88
  92. package/dist/harness/orchestrator.js +0 -149
  93. package/dist/harness/participants.js +0 -22
  94. package/dist/harness/process.js +0 -29
  95. package/dist/harness/queue-processor.js +0 -187
  96. package/dist/harness/run-harness.js +0 -154
  97. package/dist/harness/run.js +0 -98
  98. package/dist/harness/runtime-factory.js +0 -73
  99. package/dist/harness/runtime.js +0 -57
  100. package/dist/harness/todo-advance.js +0 -93
  101. package/dist/harness/todo-dispatch.js +0 -51
  102. package/dist/harness/todos.js +0 -5
  103. package/dist/harness/turn.js +0 -79
  104. package/dist/harness/types.js +0 -1
  105. package/dist/plugins/ai-sdk/index.js +0 -34
  106. package/dist/plugins/ai-sdk/runtime.js +0 -395
  107. package/dist/plugins/ai-sdk/system-prompt.js +0 -18
  108. package/dist/plugins/ai-sdk.js +0 -331
  109. package/dist/plugins/approval.js +0 -163
  110. package/dist/plugins/delegation.js +0 -108
  111. package/dist/plugins/mcp/index.js +0 -108
  112. package/dist/plugins/mcp.js +0 -140
  113. package/dist/plugins/shell/index.js +0 -100
  114. package/dist/plugins/shell.js +0 -123
  115. package/dist/plugins/storage-tools/index.js +0 -76
  116. package/dist/plugins/storage.js +0 -737
  117. package/dist/plugins/thread-namer/index.js +0 -72
  118. package/dist/plugins/threads/index.js +0 -114
  119. package/dist/plugins/ui.js +0 -211
  120. package/dist/plugins/workflow/index.js +0 -65
  121. package/dist/registry/agents.js +0 -138
  122. package/dist/registry/plugins.js +0 -110
  123. package/dist/services/agent-packages.js +0 -103
  124. package/dist/services/memory.js +0 -152
  125. package/dist/services/plugins.js +0 -98
  126. package/dist/services/storage.js +0 -1028
  127. package/dist/workflow/service.js +0 -106
  128. package/dist/workflow/types.js +0 -3
  129. package/src/plugins/approval/index.ts +0 -147
  130. package/src/plugins/bash/index.ts +0 -232
  131. package/src/plugins/delegation/index.ts +0 -153
  132. package/src/plugins/memory/index.ts +0 -182
  133. package/src/plugins/openbot/context.ts +0 -121
  134. package/src/plugins/openbot/history.ts +0 -158
  135. package/src/plugins/openbot/index.ts +0 -80
  136. package/src/plugins/openbot/runtime.ts +0 -526
  137. package/src/plugins/openbot/system-prompt.ts +0 -22
  138. package/src/plugins/ui/index.ts +0 -123
  139. /package/dist/{bus → services/todo}/types.js +0 -0
@@ -1,526 +0,0 @@
1
- import { MelonyPlugin, RuntimeContext } from 'melony';
2
- import { generateText, type LanguageModel } from 'ai';
3
- import { openai } from '@ai-sdk/openai';
4
- import { anthropic } from '@ai-sdk/anthropic';
5
- import { google } from '@ai-sdk/google';
6
- import { OpenBotEvent, OpenBotState, AgentInvokeEvent } from '../../app/types.js';
7
- import { eventsToModelMessages } from './history.js';
8
- import { Storage } from '../../services/plugins/domain.js';
9
- import type { ToolDefinition } from '../../services/plugins/types.js';
10
- import {
11
- ORCHESTRATOR_AGENT_ID,
12
- buildContext,
13
- } from './context.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
- }
41
-
42
- export interface OpenBotRuntimeOptions {
43
- /** Provider model string (e.g. `openai/gpt-4o-mini`, `anthropic/claude-3-5-sonnet-20240620`). */
44
- model?: string;
45
- storage?: Storage;
46
- /** Tool definitions merged from all tool plugins attached to this agent. */
47
- toolDefinitions?: Record<string, ToolDefinition>;
48
- /** Fires when the run is stopped; cancels the in-flight LLM call. */
49
- abortSignal?: AbortSignal;
50
- }
51
-
52
- function resolveModel(modelString: string): LanguageModel {
53
- const [provider, ...rest] = modelString.split('/');
54
- const modelId = rest.join('/');
55
- if (!modelId) {
56
- throw new Error(`Invalid model string: "${modelString}". Expected "provider/model-id".`);
57
- }
58
- switch (provider) {
59
- case 'openai':
60
- return openai(modelId);
61
- case 'anthropic':
62
- return anthropic(modelId);
63
- case 'google':
64
- return google(modelId);
65
- default:
66
- throw new Error(`Unsupported AI provider: "${provider}"`);
67
- }
68
- }
69
-
70
- async function buildSystemPrompt(
71
- state: OpenBotState,
72
- storage: Storage | undefined,
73
- ): Promise<string> {
74
- const context = await buildContext(state, storage);
75
-
76
- const sections = [OPENBOT_SYSTEM_PROMPT, '', context];
77
-
78
- // Hardcoded naming hint logic
79
- const threadState = state.threadDetails?.state as any;
80
- if (!threadState?.isSmartNamed) {
81
- sections.push(
82
- '',
83
- '## SYSTEM HINT',
84
- 'This thread is unnamed. Please use the `patch_thread_details` tool to set a concise, descriptive, and regular `name` (e.g., "Project Brainstorming" instead of "project-brainstorm") in the thread state and set `isSmartNamed: true` in the same patch. Only do this once.',
85
- );
86
- }
87
-
88
- return sections.join('\n');
89
- }
90
-
91
- /**
92
- * Tracks tool-call IDs from one LLM turn until matching `:result` events arrive.
93
- *
94
- * Melony runs yielded `action:*` events depth-first, so parallel tool calls from
95
- * a single `generateText` response execute one-by-one. We must wait for every ID
96
- * in the batch before calling the LLM again — not after the first result.
97
- */
98
- function createToolBatchTracker(
99
- state: OpenBotState,
100
- storage?: Storage,
101
- channelId?: string,
102
- threadId?: string,
103
- ) {
104
- const save = async (ids?: string[]) => {
105
- if (!storage || !channelId || !threadId) return;
106
- try {
107
- await storage.patchThreadState({
108
- channelId,
109
- threadId,
110
- state: { pendingToolCallIds: ids },
111
- });
112
- } catch (error) {
113
- console.error('[openbot] Failed to persist pendingToolCallIds:', error);
114
- }
115
- };
116
-
117
- return {
118
- async startBatch(toolCallIds: string[]) {
119
- state.pendingToolCallIds = [...toolCallIds];
120
- await save(state.pendingToolCallIds);
121
- },
122
- async clear() {
123
- state.pendingToolCallIds = undefined;
124
- await save(undefined);
125
- },
126
- /** Returns true when this result completes the batch (time to call the LLM again). */
127
- async recordResult(toolCallId: string): Promise<boolean> {
128
- if (!state.pendingToolCallIds?.includes(toolCallId)) return false;
129
- state.pendingToolCallIds = state.pendingToolCallIds.filter((id) => id !== toolCallId);
130
- const done = state.pendingToolCallIds.length === 0;
131
- if (done) {
132
- state.pendingToolCallIds = undefined;
133
- }
134
- await save(state.pendingToolCallIds);
135
- return done;
136
- },
137
- };
138
- }
139
-
140
- /**
141
- * OpenBot agent runtime.
142
- *
143
- * - One `generateText` call per `runLLM` (tools have no `execute`; SDK stops at 1 step).
144
- * - Tool calls become `action:*` events; plugins emit `:result` when done.
145
- * - When a full batch of results is in, `runLLM` runs again with updated history.
146
- */
147
- export const openbotRuntime =
148
- (options: OpenBotRuntimeOptions): MelonyPlugin<OpenBotState, OpenBotEvent> =>
149
- (builder) => {
150
- const {
151
- model: modelString = 'openai/gpt-4o-mini',
152
- storage,
153
- toolDefinitions = {},
154
- abortSignal,
155
- } = options;
156
-
157
- let currentModelString = modelString;
158
- let model = resolveModel(currentModelString);
159
-
160
- const runLLM = async function* (
161
- context: RuntimeContext<OpenBotState, OpenBotEvent>,
162
- threadId?: string,
163
- trigger?: AgentInvokeEvent,
164
- ): AsyncGenerator<OpenBotEvent> {
165
- if (!storage) return;
166
- if (abortSignal?.aborted) return;
167
-
168
- const toolBatch = createToolBatchTracker(
169
- context.state,
170
- storage,
171
- context.state.channelId,
172
- threadId || context.state.threadId,
173
- );
174
-
175
- // Capture parent metadata for event enrichment
176
- const triggerEvent = trigger || context.state.triggerEvent;
177
- const parentAgentId = triggerEvent?.meta?.parentAgentId;
178
- const parentToolCallId = triggerEvent?.meta?.parentToolCallId;
179
-
180
- context.state.model = currentModelString;
181
-
182
- const systemPrompt = await buildSystemPrompt(context.state, storage);
183
-
184
- const events = await storage.getEvents({
185
- channelId: context.state.channelId,
186
- threadId: context.state.threadId,
187
- });
188
-
189
- const messages = eventsToModelMessages(events);
190
-
191
- // console.log('systemPrompt:::::::\n', systemPrompt);
192
- // console.log('messages:::::::\n', JSON.stringify(messages));
193
- // console.log('toolDefinitions:::::::\n', JSON.stringify(toolDefinitions));
194
-
195
- try {
196
- // Single LLM request — tool execution happens externally via action:* handlers.
197
- const result = await generateText({
198
- model,
199
- system: systemPrompt,
200
- messages,
201
- tools: toolDefinitions as Record<string, { description: string; inputSchema: any }>,
202
- stopWhen: ({ steps }) => steps.length === 1,
203
- allowSystemInMessages: true,
204
- abortSignal,
205
- });
206
-
207
- const toolCalls = result.toolCalls ?? [];
208
-
209
- // if (result.usage) {
210
- // const usage = result.usage;
211
- // yield {
212
- // type: 'agent:usage',
213
- // data: {
214
- // usage: {
215
- // promptTokens: usage.inputTokens,
216
- // completionTokens: usage.outputTokens,
217
- // totalTokens: usage.totalTokens,
218
- // currentContextTokens: usage.inputTokens,
219
- // contextBudget: getContextBudgetForModel(currentModelString),
220
- // },
221
- // model: currentModelString,
222
- // },
223
- // meta: {
224
- // agentId: context.state.agentId,
225
- // threadId,
226
- // runId: context.state.runId,
227
- // },
228
- // } as OpenBotEvent;
229
- // }
230
-
231
- const outputMeta = {
232
- agentId: context.state.agentId,
233
- threadId,
234
- parentAgentId,
235
- parentToolCallId,
236
- };
237
-
238
- // Text before actions so history/UI show the model's intent first.
239
- if (result.text) {
240
- yield {
241
- type: 'agent:output',
242
- data: { content: result.text },
243
- meta: outputMeta,
244
- };
245
- }
246
-
247
- if (toolCalls.length > 0) {
248
- // when multiple tool calls are made, Melony runtime handles them one by one, thats why we need to start a new batch
249
- await toolBatch.startBatch(toolCalls.map((tc) => tc.toolCallId));
250
-
251
- for (const toolCall of toolCalls) {
252
- yield {
253
- type: `action:${toolCall.toolName}` as OpenBotEvent['type'],
254
- data: toolCall.input,
255
- meta: {
256
- toolCallId: toolCall.toolCallId,
257
- ...outputMeta,
258
- },
259
- } as unknown as OpenBotEvent;
260
- }
261
- } else {
262
- // clear the tool batch if there are no tool calls
263
- await toolBatch.clear();
264
- }
265
- } catch (error: unknown) {
266
- // Run was stopped — unwind quietly without surfacing an error.
267
- if (abortSignal?.aborted) return;
268
-
269
- const errorMessage = error instanceof Error ? error.message : String(error);
270
- const isApiKeyError =
271
- errorMessage.includes('API key') ||
272
- errorMessage.includes('401') ||
273
- errorMessage.includes('Unauthorized') ||
274
- errorMessage.includes('authentication');
275
-
276
- if (isApiKeyError) {
277
- yield {
278
- type: 'client:ui:widget',
279
- data: {
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
- ],
289
- metadata: {
290
- type: 'api_provider_selection',
291
- },
292
- },
293
- meta: { agentId: context.state.agentId, threadId },
294
- } as OpenBotEvent;
295
- return;
296
- }
297
-
298
- throw error;
299
- }
300
- };
301
-
302
- builder.on('agent:invoke', async function* (event, context) {
303
- const routedTo = (event as { data?: { agentId?: string } }).data?.agentId;
304
- if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
305
- return;
306
- }
307
-
308
- // Capture user info from meta if available
309
- if (event.meta?.userName) {
310
- context.state.currentUser = {
311
- userName: event.meta.userName,
312
- };
313
- }
314
-
315
- const threadId = event.meta?.threadId || context.state.threadId;
316
-
317
- // clear the tool batch if the agent is invoked
318
- // this is to prevent the tool batch from being used for a new agent invocation
319
- await createToolBatchTracker(
320
- context.state,
321
- storage,
322
- context.state.channelId,
323
- threadId,
324
- ).clear();
325
-
326
- yield* runLLM(context, threadId, event as AgentInvokeEvent);
327
- });
328
-
329
- // this is to handle the tool results from the tool calls
330
- // because Melony runtime handles them one by one, thats why we need to record the result
331
- builder.on('*', async function* (event, context) {
332
- if (!event.type.endsWith(':result')) return;
333
- if (event.meta?.agentId !== context.state.agentId) return;
334
-
335
- const toolCallId = event.meta?.toolCallId;
336
- // record the result of the tool call
337
- if (
338
- !toolCallId ||
339
- !(await createToolBatchTracker(
340
- context.state,
341
- storage,
342
- context.state.channelId,
343
- event.meta?.threadId || context.state.threadId,
344
- ).recordResult(toolCallId))
345
- )
346
- return;
347
-
348
- const threadId = event.meta?.threadId || context.state.threadId;
349
- yield* runLLM(context, threadId);
350
- });
351
-
352
- builder.on('client:ui:widget:response', async function* (event, context) {
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
-
437
- if (metadata?.type !== 'api_key_request') return;
438
- if (!values?.apiKey || !values?.model) return;
439
-
440
- const provider = String(values.provider || metadata.provider);
441
- const modelId = String(values.model).trim();
442
- const apiKey = String(values.apiKey);
443
-
444
- if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'google') {
445
- yield {
446
- type: 'agent:output',
447
- data: { content: `Unsupported provider: ${provider}` },
448
- meta: { agentId: context.state.agentId },
449
- };
450
- return;
451
- }
452
-
453
- const envVar =
454
- provider === 'openai'
455
- ? 'OPENAI_API_KEY'
456
- : provider === 'anthropic'
457
- ? 'ANTHROPIC_API_KEY'
458
- : 'GOOGLE_GENERATIVE_AI_API_KEY';
459
- const newModelString = `${provider}/${modelId}`;
460
-
461
- if (!storage) return;
462
- try {
463
- await storage.createVariable({ key: envVar, value: apiKey, secret: true });
464
- process.env[envVar] = apiKey;
465
-
466
- currentModelString = newModelString;
467
- model = resolveModel(currentModelString);
468
- try {
469
- saveConfig({ model: currentModelString });
470
-
471
- // Also update the agent's AGENT.md if it has an openbot plugin config
472
- const details = await storage.getAgentDetails({ agentId: context.state.agentId });
473
- const updatedPlugins = details.pluginRefs.map((ref) => {
474
- if (ref.id === 'openbot') {
475
- return {
476
- ...ref,
477
- config: { ...ref.config, model: currentModelString },
478
- };
479
- }
480
- return ref;
481
- });
482
-
483
- await storage.updateAgent({
484
- agentId: context.state.agentId,
485
- plugins: updatedPlugins,
486
- });
487
- } catch {
488
- // best-effort: config persistence failure shouldn't block the conversation
489
- }
490
-
491
- yield {
492
- type: 'agent:output',
493
- data: {
494
- content: `Saved ${provider} API key and set model to \`${newModelString}\`.`,
495
- },
496
- meta: { agentId: context.state.agentId },
497
- };
498
-
499
- yield {
500
- type: 'client:ui:widget',
501
- data: {
502
- widgetId: event.data.widgetId,
503
- kind: 'message',
504
- title: 'API Key Saved',
505
- body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
506
- state: 'submitted',
507
- display: 'collapsed',
508
- disabled: true,
509
- actions: [],
510
- },
511
- meta: { agentId: context.state.agentId },
512
- };
513
-
514
- yield* runLLM(context, threadId);
515
- } catch (error) {
516
- yield {
517
- type: 'agent:output',
518
- data: {
519
- content: `Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'
520
- }`,
521
- },
522
- meta: { agentId: context.state.agentId },
523
- };
524
- }
525
- });
526
- };
@@ -1,22 +0,0 @@
1
- export const OPENBOT_SYSTEM_PROMPT = [
2
- '# ROLE',
3
- 'You are an OpenBot, the main coordinator and router agent. Your primary role is to orchestrate specialized agents to help the human achieve their goals.',
4
- '',
5
- '# SECURITY POLICY',
6
- '- **CRITICAL**: Never request API keys, passwords, or sensitive credentials via text or UI widgets; these are managed deterministically via secure forms and must never enter your context.',
7
- '- **Credential Guidance**: If an agent or tool requires credentials, inform the user they can be managed under "Settings > Variables".',
8
- '',
9
- '# CORE MISSION',
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
- '',
12
- '# OPERATIONAL GUIDELINES',
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
- '- **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.',
16
- '- **Context Awareness**: Use the provided ENVIRONMENT, CHANNEL SPECIFICATION, and MEMORIES to maintain continuity. Do not ask for information already present in these sections.',
17
- '- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.',
18
- '- **Hub-and-Spoke**: Specialized agents cannot communicate directly; as coordinator, you must pass relevant data from one agent to another.',
19
- '',
20
- '# COMMUNICATION STYLE',
21
- '- Be always concise, professional, and proactive.',
22
- ].join('\n');
@@ -1,123 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { z } from 'zod';
3
- import type { Plugin } from '../../services/plugins/types.js';
4
- import { OpenBotEvent, RenderWidgetEvent, UIWidgetResponseEvent } from '../../app/types.js';
5
-
6
- /**
7
- * `ui` — provides a tool for the agent to render interactive UI widgets.
8
- *
9
- * The model can choose which widget to render (form, choice, list, message)
10
- * depending on the situation.
11
- */
12
-
13
- export const uiPlugin: Plugin = {
14
- id: 'ui',
15
- name: 'UI',
16
- description: 'Render interactive UI widgets to interact with the user.',
17
- toolDefinitions: {
18
- render_widget: {
19
- description: 'Render a UI widget to the user. Use "form" for data collection, "choice" for simple selection, "list" for displaying items, and "message" for simple notifications with actions. When using form widge to unquire user, do not provide complex forms with many fields, always try to make it simple to keep the experience smooth and straightforward.',
20
- inputSchema: z.object({
21
- kind: z.enum(['message', 'choice', 'form', 'list']).describe('The type of widget to render.'),
22
- title: z.string().describe('The title of the widget.'),
23
- description: z.string().optional().describe('A description or body text.'),
24
- fields: z.array(z.object({
25
- id: z.string().describe('Unique ID for the field.'),
26
- label: z.string().describe('Label shown to the user.'),
27
- type: z.enum(['text', 'textarea', 'number', 'boolean', 'select', 'multiselect', 'date']),
28
- description: z.string().optional(),
29
- placeholder: z.string().optional(),
30
- required: z.boolean().optional(),
31
- options: z.array(z.object({ label: z.string(), value: z.string() })).optional(),
32
- defaultValue: z.any().optional()
33
- })).optional().describe('Required for kind="form". List of form fields.'),
34
- actions: z.array(z.object({
35
- id: z.string(),
36
- label: z.string(),
37
- variant: z.enum(['primary', 'secondary', 'danger']).optional(),
38
- })).optional().describe('Buttons or actions available on the widget.'),
39
- items: z.array(z.object({
40
- id: z.string(),
41
- label: z.string(),
42
- description: z.string().optional(),
43
- status: z.enum(['pending', 'in_progress', 'done', 'error', 'cancelled']).optional(),
44
- metadata: z.record(z.string(), z.any()).optional()
45
- })).optional().describe('Required for kind="list". List of items to display.'),
46
- submitLabel: z.string().optional().describe('Label for the primary action button (e.g. "Submit", "Save").')
47
- })
48
- }
49
- },
50
- factory: () => (builder) => {
51
- // Handle the tool call from the agent
52
- builder.on('action:render_widget', async function* (event, context) {
53
- const widgetEvent = event as RenderWidgetEvent;
54
- const toolCallId = widgetEvent.meta?.toolCallId;
55
- const threadId = widgetEvent.meta?.threadId || context.state.threadId;
56
-
57
- if (!toolCallId) return;
58
-
59
- const widgetId = randomUUID();
60
-
61
- // Emit the UI widget event to the client
62
- yield {
63
- type: 'client:ui:widget',
64
- data: {
65
- ...widgetEvent.data,
66
- widgetId,
67
- metadata: {
68
- type: 'ui:request',
69
- originalEvent: widgetEvent
70
- }
71
- },
72
- meta: { agentId: context.state.agentId, threadId }
73
- } as OpenBotEvent;
74
- });
75
-
76
- // Handle the user's response from the UI widget
77
- builder.on('client:ui:widget:response', async function* (event, context) {
78
- const responseEvent = event as UIWidgetResponseEvent;
79
- const { widgetId, actionId, values, metadata } = responseEvent.data;
80
- if (metadata?.type !== 'ui:request') return;
81
-
82
- const originalEvent = metadata.originalEvent as RenderWidgetEvent;
83
- const toolCallId = originalEvent?.meta?.toolCallId;
84
- const threadId = originalEvent?.meta?.threadId || context.state.threadId;
85
-
86
- if (!toolCallId) return;
87
-
88
- // Yield a "submitted" widget update to the UI to collapse/disable it
89
- yield {
90
- type: 'client:ui:widget',
91
- data: {
92
- widgetId,
93
- title: originalEvent.data.title,
94
- kind: originalEvent.data.kind as any,
95
- state: 'submitted',
96
- body: "Thank you for your response. We will process it and get back to you soon.",
97
- display: 'collapsed',
98
- disabled: true,
99
- actions: [], // Clear actions to disable buttons in UI
100
- },
101
- meta: { agentId: context.state.agentId, threadId },
102
- } as OpenBotEvent;
103
-
104
- // Emit the tool result event so the agent runtime can resume
105
- yield {
106
- type: 'action:render_widget:result',
107
- data: {
108
- success: true,
109
- actionId,
110
- values,
111
- output: JSON.stringify(values)
112
- },
113
- meta: {
114
- agentId: context.state.agentId,
115
- threadId,
116
- toolCallId
117
- }
118
- } as any as OpenBotEvent;
119
- });
120
- },
121
- };
122
-
123
- export default uiPlugin;
File without changes