openbot 0.3.6 → 0.4.0

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 (96) hide show
  1. package/README.md +15 -16
  2. package/dist/app/agent-ids.js +4 -0
  3. package/dist/app/cli.js +1 -1
  4. package/dist/app/config.js +0 -19
  5. package/dist/app/server.js +8 -14
  6. package/dist/bus/services.js +34 -124
  7. package/dist/harness/agent-invoke-run.js +44 -0
  8. package/dist/harness/agent-turn.js +99 -0
  9. package/dist/harness/channel-participants.js +40 -0
  10. package/dist/harness/constants.js +2 -0
  11. package/dist/harness/context-meter.js +97 -0
  12. package/dist/harness/context.js +95 -47
  13. package/dist/harness/dispatch.js +144 -0
  14. package/dist/harness/dispatcher.js +45 -156
  15. package/dist/harness/history.js +177 -0
  16. package/dist/harness/index.js +91 -0
  17. package/dist/harness/orchestration.js +88 -0
  18. package/dist/harness/participants.js +22 -0
  19. package/dist/harness/run-harness.js +154 -0
  20. package/dist/harness/run.js +98 -0
  21. package/dist/harness/runtime-factory.js +0 -34
  22. package/dist/harness/runtime.js +57 -0
  23. package/dist/harness/todo-dispatch.js +51 -0
  24. package/dist/harness/todos.js +5 -0
  25. package/dist/harness/turn.js +79 -0
  26. package/dist/plugins/approval/index.js +105 -149
  27. package/dist/plugins/delegation/index.js +119 -32
  28. package/dist/plugins/memory/index.js +103 -14
  29. package/dist/plugins/memory/service.js +152 -0
  30. package/dist/plugins/openbot/context.js +80 -0
  31. package/dist/plugins/openbot/history.js +98 -0
  32. package/dist/plugins/openbot/index.js +31 -0
  33. package/dist/plugins/openbot/runtime.js +317 -0
  34. package/dist/plugins/openbot/system-prompt.js +5 -0
  35. package/dist/plugins/plugin-manager/index.js +105 -0
  36. package/dist/plugins/storage/index.js +573 -0
  37. package/dist/plugins/storage/service.js +1159 -0
  38. package/dist/plugins/storage-tools/index.js +2 -2
  39. package/dist/plugins/thread-namer/index.js +72 -0
  40. package/dist/plugins/thread-naming/generate-title.js +44 -0
  41. package/dist/plugins/thread-naming/index.js +103 -0
  42. package/dist/plugins/threads/index.js +114 -0
  43. package/dist/plugins/todo/index.js +24 -25
  44. package/dist/plugins/ui/index.js +2 -32
  45. package/dist/registry/plugins.js +3 -9
  46. package/dist/services/plugins/domain.js +1 -0
  47. package/dist/services/plugins/plugin-cache.js +9 -0
  48. package/dist/services/plugins/registry.js +110 -0
  49. package/dist/services/plugins/service.js +177 -0
  50. package/dist/services/plugins/types.js +1 -0
  51. package/dist/services/process.js +29 -0
  52. package/dist/services/storage.js +11 -10
  53. package/dist/services/thread-naming.js +81 -0
  54. package/docs/agents.md +16 -10
  55. package/docs/architecture.md +2 -2
  56. package/docs/plugins.md +6 -15
  57. package/docs/templates/AGENT.example.md +7 -13
  58. package/package.json +1 -2
  59. package/src/app/agent-ids.ts +5 -0
  60. package/src/app/cli.ts +1 -1
  61. package/src/app/config.ts +1 -31
  62. package/src/app/server.ts +8 -16
  63. package/src/app/types.ts +63 -189
  64. package/src/harness/index.ts +145 -0
  65. package/src/plugins/approval/index.ts +91 -189
  66. package/src/plugins/delegation/index.ts +136 -39
  67. package/src/plugins/memory/index.ts +112 -15
  68. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  69. package/src/plugins/openbot/context.ts +91 -0
  70. package/src/plugins/openbot/history.ts +107 -0
  71. package/src/plugins/openbot/index.ts +37 -0
  72. package/src/plugins/openbot/runtime.ts +384 -0
  73. package/src/plugins/openbot/system-prompt.ts +7 -0
  74. package/src/plugins/plugin-manager/index.ts +122 -0
  75. package/src/plugins/shell/index.ts +1 -1
  76. package/src/plugins/storage/index.ts +633 -0
  77. package/src/{services/storage.ts → plugins/storage/service.ts} +224 -67
  78. package/src/{bus/types.ts → services/plugins/domain.ts} +16 -7
  79. package/src/services/plugins/plugin-cache.ts +13 -0
  80. package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
  81. package/src/services/{plugins.ts → plugins/service.ts} +96 -2
  82. package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
  83. package/src/bus/services.ts +0 -954
  84. package/src/harness/context.ts +0 -365
  85. package/src/harness/dispatcher.ts +0 -379
  86. package/src/harness/mcp.ts +0 -78
  87. package/src/harness/runtime-factory.ts +0 -129
  88. package/src/harness/todo-advance.ts +0 -128
  89. package/src/plugins/ai-sdk/index.ts +0 -41
  90. package/src/plugins/ai-sdk/runtime.ts +0 -468
  91. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  92. package/src/plugins/mcp/index.ts +0 -128
  93. package/src/plugins/storage-tools/index.ts +0 -90
  94. package/src/plugins/todo/index.ts +0 -64
  95. package/src/plugins/ui/index.ts +0 -227
  96. /package/src/{harness → services}/process.ts +0 -0
@@ -0,0 +1,317 @@
1
+ import { generateText } from 'ai';
2
+ import { openai } from '@ai-sdk/openai';
3
+ import { anthropic } from '@ai-sdk/anthropic';
4
+ import { eventsToModelMessages } from './history.js';
5
+ import { ORCHESTRATOR_AGENT_ID, buildContext, } from './context.js';
6
+ import { saveConfig } from '../../app/config.js';
7
+ import { API_KEY_SETUP_MESSAGE, OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
8
+ function resolveModel(modelString) {
9
+ const [provider, ...rest] = modelString.split('/');
10
+ const modelId = rest.join('/');
11
+ if (!modelId) {
12
+ throw new Error(`Invalid model string: "${modelString}". Expected "provider/model-id".`);
13
+ }
14
+ switch (provider) {
15
+ case 'openai':
16
+ return openai(modelId);
17
+ case 'anthropic':
18
+ return anthropic(modelId);
19
+ default:
20
+ throw new Error(`Unsupported AI provider: "${provider}"`);
21
+ }
22
+ }
23
+ async function buildSystemPrompt(state, storage) {
24
+ const context = await buildContext(state, storage);
25
+ const instructions = state.agentId === ORCHESTRATOR_AGENT_ID
26
+ ? (state.agentDetails?.instructions?.trim() || OPENBOT_SYSTEM_PROMPT)
27
+ : OPENBOT_SYSTEM_PROMPT;
28
+ const sections = [instructions, '', context];
29
+ // Hardcoded naming hint logic
30
+ const threadState = state.threadDetails?.state;
31
+ if (!threadState?.isSmartNamed) {
32
+ sections.push('', '## SYSTEM HINT', '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.');
33
+ }
34
+ return sections.join('\n');
35
+ }
36
+ /**
37
+ * Tracks tool-call IDs from one LLM turn until matching `:result` events arrive.
38
+ *
39
+ * Melony runs yielded `action:*` events depth-first, so parallel tool calls from
40
+ * a single `generateText` response execute one-by-one. We must wait for every ID
41
+ * in the batch before calling the LLM again — not after the first result.
42
+ */
43
+ function createToolBatchTracker() {
44
+ let pending = null;
45
+ return {
46
+ startBatch(toolCallIds) {
47
+ pending = new Set(toolCallIds);
48
+ },
49
+ clear() {
50
+ pending = null;
51
+ },
52
+ /** Returns true when this result completes the batch (time to call the LLM again). */
53
+ recordResult(toolCallId) {
54
+ if (!pending?.has(toolCallId))
55
+ return false;
56
+ pending.delete(toolCallId);
57
+ if (pending.size > 0)
58
+ return false;
59
+ pending = null;
60
+ return true;
61
+ },
62
+ };
63
+ }
64
+ /**
65
+ * OpenBot agent runtime.
66
+ *
67
+ * - One `generateText` call per `runLLM` (tools have no `execute`; SDK stops at 1 step).
68
+ * - Tool calls become `action:*` events; plugins emit `:result` when done.
69
+ * - When a full batch of results is in, `runLLM` runs again with updated history.
70
+ */
71
+ export const openbotRuntime = (options) => (builder) => {
72
+ const { model: modelString = 'openai/gpt-4o-mini', storage, toolDefinitions = {}, } = options;
73
+ let currentModelString = modelString;
74
+ let model = resolveModel(currentModelString);
75
+ const toolBatch = createToolBatchTracker();
76
+ const runLLM = async function* (context, threadId, trigger) {
77
+ if (!storage)
78
+ return;
79
+ // Capture parent metadata for event enrichment
80
+ const triggerEvent = trigger || context.state.triggerEvent;
81
+ const parentAgentId = triggerEvent?.meta?.parentAgentId;
82
+ const parentToolCallId = triggerEvent?.meta?.parentToolCallId;
83
+ context.state.model = currentModelString;
84
+ const systemPrompt = await buildSystemPrompt(context.state, storage);
85
+ const events = await storage.getEvents({
86
+ channelId: context.state.channelId,
87
+ threadId: context.state.threadId,
88
+ });
89
+ const messages = eventsToModelMessages(events);
90
+ // console.log('systemPrompt:::::::\n', systemPrompt);
91
+ // console.log('messages:::::::\n', JSON.stringify(messages, null, 2));
92
+ // console.log('toolDefinitions:::::::\n', JSON.stringify(toolDefinitions, null, 2));
93
+ try {
94
+ // Single LLM request — tool execution happens externally via action:* handlers.
95
+ const result = await generateText({
96
+ model,
97
+ system: systemPrompt,
98
+ messages,
99
+ tools: toolDefinitions,
100
+ stopWhen: ({ steps }) => steps.length === 1,
101
+ allowSystemInMessages: true,
102
+ });
103
+ const toolCalls = result.toolCalls ?? [];
104
+ // if (result.usage) {
105
+ // const usage = result.usage;
106
+ // yield {
107
+ // type: 'agent:usage',
108
+ // data: {
109
+ // usage: {
110
+ // promptTokens: usage.inputTokens,
111
+ // completionTokens: usage.outputTokens,
112
+ // totalTokens: usage.totalTokens,
113
+ // currentContextTokens: usage.inputTokens,
114
+ // contextBudget: getContextBudgetForModel(currentModelString),
115
+ // },
116
+ // model: currentModelString,
117
+ // },
118
+ // meta: {
119
+ // agentId: context.state.agentId,
120
+ // threadId,
121
+ // runId: context.state.runId,
122
+ // },
123
+ // } as OpenBotEvent;
124
+ // }
125
+ const outputMeta = {
126
+ agentId: context.state.agentId,
127
+ threadId,
128
+ parentAgentId,
129
+ parentToolCallId,
130
+ };
131
+ // Text before actions so history/UI show the model's intent first.
132
+ if (result.text) {
133
+ yield {
134
+ type: 'agent:output',
135
+ data: { content: result.text },
136
+ meta: outputMeta,
137
+ };
138
+ }
139
+ if (toolCalls.length > 0) {
140
+ // when multiple tool calls are made, Melony runtime handles them one by one, thats why we need to start a new batch
141
+ toolBatch.startBatch(toolCalls.map((tc) => tc.toolCallId));
142
+ for (const toolCall of toolCalls) {
143
+ yield {
144
+ type: `action:${toolCall.toolName}`,
145
+ data: toolCall.input,
146
+ meta: {
147
+ toolCallId: toolCall.toolCallId,
148
+ ...outputMeta,
149
+ },
150
+ };
151
+ }
152
+ }
153
+ else {
154
+ // clear the tool batch if there are no tool calls
155
+ toolBatch.clear();
156
+ }
157
+ }
158
+ catch (error) {
159
+ const errorMessage = error instanceof Error ? error.message : String(error);
160
+ const isApiKeyError = errorMessage.includes('API key') ||
161
+ errorMessage.includes('401') ||
162
+ errorMessage.includes('Unauthorized') ||
163
+ errorMessage.includes('authentication');
164
+ if (isApiKeyError) {
165
+ const [currentProvider, ...rest] = currentModelString.split('/');
166
+ const currentModelId = rest.join('/');
167
+ yield {
168
+ type: 'client:ui:widget',
169
+ data: {
170
+ kind: 'form',
171
+ widgetId: `api_key_request_${Date.now()}`,
172
+ title: `AI Provider API Key Required`,
173
+ description: API_KEY_SETUP_MESSAGE,
174
+ fields: [
175
+ {
176
+ id: 'provider',
177
+ label: 'Provider',
178
+ type: 'select',
179
+ required: true,
180
+ options: [
181
+ { label: 'OpenAI', value: 'openai' },
182
+ { label: 'Anthropic', value: 'anthropic' },
183
+ ],
184
+ defaultValue: currentProvider === 'anthropic' ? 'anthropic' : 'openai',
185
+ },
186
+ {
187
+ id: 'model',
188
+ label: 'Model',
189
+ type: 'text',
190
+ description: 'Model name without the provider prefix (e.g. `gpt-4o-mini` or `claude-3-5-sonnet-20240620`).',
191
+ placeholder: 'gpt-4o-mini',
192
+ required: true,
193
+ defaultValue: currentModelId,
194
+ },
195
+ {
196
+ id: 'apiKey',
197
+ label: 'API Key',
198
+ type: 'text',
199
+ placeholder: `sk-...`,
200
+ required: true,
201
+ },
202
+ ],
203
+ submitLabel: 'Save & Continue',
204
+ metadata: {
205
+ type: 'api_key_request',
206
+ },
207
+ },
208
+ meta: { agentId: context.state.agentId, threadId },
209
+ };
210
+ return;
211
+ }
212
+ throw error;
213
+ }
214
+ };
215
+ builder.on('agent:invoke', async function* (event, context) {
216
+ const routedTo = event.data?.agentId;
217
+ if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
218
+ return;
219
+ }
220
+ // clear the tool batch if the agent is invoked
221
+ // this is to prevent the tool batch from being used for a new agent invocation
222
+ toolBatch.clear();
223
+ const threadId = event.meta?.threadId || context.state.threadId;
224
+ yield* runLLM(context, threadId, event);
225
+ });
226
+ // this is to handle the tool results from the tool calls
227
+ // because Melony runtime handles them one by one, thats why we need to record the result
228
+ builder.on('*', async function* (event, context) {
229
+ if (!event.type.endsWith(':result'))
230
+ return;
231
+ if (event.meta?.agentId !== context.state.agentId)
232
+ return;
233
+ const toolCallId = event.meta?.toolCallId;
234
+ // record the result of the tool call
235
+ if (!toolCallId || !toolBatch.recordResult(toolCallId))
236
+ return;
237
+ const threadId = event.meta?.threadId || context.state.threadId;
238
+ yield* runLLM(context, threadId);
239
+ });
240
+ builder.on('client:ui:widget:response', async function* (event, context) {
241
+ const { metadata, values } = event.data;
242
+ if (metadata?.type !== 'api_key_request')
243
+ return;
244
+ if (!values?.apiKey || !values?.provider || !values?.model)
245
+ return;
246
+ const provider = String(values.provider);
247
+ const modelId = String(values.model).trim();
248
+ const apiKey = String(values.apiKey);
249
+ if (provider !== 'openai' && provider !== 'anthropic') {
250
+ yield {
251
+ type: 'agent:output',
252
+ data: { content: `Unsupported provider: ${provider}` },
253
+ meta: { agentId: context.state.agentId },
254
+ };
255
+ return;
256
+ }
257
+ const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
258
+ const newModelString = `${provider}/${modelId}`;
259
+ if (!storage)
260
+ return;
261
+ try {
262
+ await storage.createVariable({ key: envVar, value: apiKey, secret: true });
263
+ process.env[envVar] = apiKey;
264
+ currentModelString = newModelString;
265
+ model = resolveModel(currentModelString);
266
+ try {
267
+ saveConfig({ model: currentModelString });
268
+ // Also update the agent's AGENT.md if it has an openbot plugin config
269
+ const details = await storage.getAgentDetails({ agentId: context.state.agentId });
270
+ const updatedPlugins = details.pluginRefs.map((ref) => {
271
+ if (ref.id === 'openbot') {
272
+ return {
273
+ ...ref,
274
+ config: { ...ref.config, model: currentModelString },
275
+ };
276
+ }
277
+ return ref;
278
+ });
279
+ await storage.updateAgent({
280
+ agentId: context.state.agentId,
281
+ plugins: updatedPlugins,
282
+ });
283
+ }
284
+ catch {
285
+ // best-effort: config persistence failure shouldn't block the conversation
286
+ }
287
+ yield {
288
+ type: 'agent:output',
289
+ data: {
290
+ content: `Saved ${provider} API key and set model to \`${newModelString}\`.`,
291
+ },
292
+ meta: { agentId: context.state.agentId },
293
+ };
294
+ yield {
295
+ type: 'client:ui:widget',
296
+ data: {
297
+ widgetId: event.data.widgetId,
298
+ kind: 'message',
299
+ title: 'API Key Saved',
300
+ body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
301
+ state: 'submitted',
302
+ actions: [{ id: 'ok', label: 'Got it', variant: 'primary' }],
303
+ },
304
+ meta: { agentId: context.state.agentId },
305
+ };
306
+ }
307
+ catch (error) {
308
+ yield {
309
+ type: 'agent:output',
310
+ data: {
311
+ content: `Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'}`,
312
+ },
313
+ meta: { agentId: context.state.agentId },
314
+ };
315
+ }
316
+ });
317
+ };
@@ -0,0 +1,5 @@
1
+ export const OPENBOT_SYSTEM_PROMPT = [
2
+ 'You are a helpful AI assistant for your human. Your job is to help the user with their questions and tasks.',
3
+ ].join('\n');
4
+ /** Shown in the API key setup form when no provider credentials are configured. */
5
+ 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.';
@@ -0,0 +1,105 @@
1
+ import { STATE_AGENT_ID } from '../../app/agent-ids.js';
2
+ import { pluginService, resolveMarketplaceAgentList, } from '../../services/plugins/service.js';
3
+ /**
4
+ * `plugin-manager` — marketplace listing, npm plugin install/uninstall, and
5
+ * installing agents from the registry. Wired on the **`state`** built-in agent
6
+ * via its default `pluginRefs`.
7
+ *
8
+ * Handlers register only when `agentId === state` so attaching this plugin to
9
+ * other agents via AGENT.md does not widen infra privileges.
10
+ */
11
+ export const pluginManagerPlugin = {
12
+ id: 'plugin-manager',
13
+ name: 'Plugin manager',
14
+ description: 'Marketplace listings, npm-based plugin lifecycle, and agent installs from marketplace metadata.',
15
+ factory: ({ agentId, storage }) => {
16
+ if (agentId !== STATE_AGENT_ID) {
17
+ return () => { };
18
+ }
19
+ return (builder) => {
20
+ builder.on('action:plugin:install', async function* (event) {
21
+ try {
22
+ const { name, version } = event.data;
23
+ const result = await pluginService.install({ packageName: name, version });
24
+ yield {
25
+ type: 'action:plugin:install:result',
26
+ data: { success: true, plugin: result },
27
+ };
28
+ }
29
+ catch (error) {
30
+ yield {
31
+ type: 'action:plugin:install:result',
32
+ data: { success: false, error: error.message },
33
+ };
34
+ }
35
+ });
36
+ builder.on('action:plugin:uninstall', async function* (event) {
37
+ try {
38
+ await pluginService.uninstall(event.data.id);
39
+ yield { type: 'action:plugin:uninstall:result', data: { success: true } };
40
+ }
41
+ catch (error) {
42
+ yield {
43
+ type: 'action:plugin:uninstall:result',
44
+ data: { success: false, error: error.message },
45
+ };
46
+ }
47
+ });
48
+ builder.on('action:marketplace:list', async function* () {
49
+ const agents = await resolveMarketplaceAgentList();
50
+ yield {
51
+ type: 'action:marketplace:list:result',
52
+ data: { success: true, agents },
53
+ };
54
+ });
55
+ builder.on('action:agent:install', async function* (event) {
56
+ try {
57
+ const { agentId: newAgentId, name, description, image, instructions, plugins, } = event.data;
58
+ for (const ref of plugins) {
59
+ const installed = await pluginService.isInstalled(ref.id);
60
+ if (!installed && ref.id.includes('/') === false && ref.id.includes('-plugin-') === false) {
61
+ continue;
62
+ }
63
+ if (!installed) {
64
+ try {
65
+ await pluginService.install({ packageName: ref.id });
66
+ }
67
+ catch (err) {
68
+ console.warn(`[plugins] Failed to pre-install plugin ${ref.id}`, err);
69
+ }
70
+ }
71
+ }
72
+ await storage.createAgent({
73
+ agentId: newAgentId,
74
+ name,
75
+ description,
76
+ image,
77
+ instructions,
78
+ plugins,
79
+ });
80
+ yield {
81
+ type: 'action:agent:install:result',
82
+ data: { success: true, agentId: newAgentId },
83
+ };
84
+ yield {
85
+ type: 'agent:output',
86
+ data: {
87
+ content: `Successfully installed agent **${name}** (${newAgentId}) from marketplace.`,
88
+ },
89
+ meta: { agentId: 'system' },
90
+ };
91
+ }
92
+ catch (error) {
93
+ yield {
94
+ type: 'action:agent:install:result',
95
+ data: {
96
+ success: false,
97
+ agentId: event.data.agentId,
98
+ error: error instanceof Error ? error.message : 'Unknown error',
99
+ },
100
+ };
101
+ }
102
+ });
103
+ };
104
+ },
105
+ };