openbot 0.3.6 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) 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 +10 -19
  5. package/dist/app/server.js +208 -17
  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 +109 -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 +120 -149
  27. package/dist/plugins/bash/index.js +195 -0
  28. package/dist/plugins/delegation/index.js +121 -32
  29. package/dist/plugins/memory/index.js +103 -14
  30. package/dist/plugins/memory/service.js +152 -0
  31. package/dist/plugins/openbot/context.js +125 -0
  32. package/dist/plugins/openbot/history.js +144 -0
  33. package/dist/plugins/openbot/index.js +71 -0
  34. package/dist/plugins/openbot/runtime.js +381 -0
  35. package/dist/plugins/openbot/system-prompt.js +25 -0
  36. package/dist/plugins/plugin-manager/index.js +189 -0
  37. package/dist/plugins/shell/index.js +2 -1
  38. package/dist/plugins/storage/files.js +67 -0
  39. package/dist/plugins/storage/index.js +750 -0
  40. package/dist/plugins/storage/service.js +1316 -0
  41. package/dist/plugins/storage-tools/index.js +2 -2
  42. package/dist/plugins/thread-namer/index.js +72 -0
  43. package/dist/plugins/thread-naming/generate-title.js +44 -0
  44. package/dist/plugins/thread-naming/index.js +103 -0
  45. package/dist/plugins/threads/index.js +114 -0
  46. package/dist/plugins/todo/index.js +24 -25
  47. package/dist/plugins/ui/index.js +109 -180
  48. package/dist/registry/plugins.js +3 -9
  49. package/dist/services/abort.js +43 -0
  50. package/dist/services/plugins/domain.js +1 -0
  51. package/dist/services/plugins/plugin-cache.js +9 -0
  52. package/dist/services/plugins/registry.js +112 -0
  53. package/dist/services/plugins/service.js +232 -0
  54. package/dist/services/plugins/types.js +1 -0
  55. package/dist/services/process.js +29 -0
  56. package/dist/services/storage.js +11 -10
  57. package/dist/services/thread-naming.js +81 -0
  58. package/docs/agents.md +15 -12
  59. package/docs/architecture.md +2 -2
  60. package/docs/plugins.md +29 -17
  61. package/docs/templates/AGENT.example.md +8 -14
  62. package/package.json +1 -2
  63. package/src/app/agent-ids.ts +5 -0
  64. package/src/app/cli.ts +1 -1
  65. package/src/app/config.ts +14 -31
  66. package/src/app/server.ts +243 -19
  67. package/src/app/types.ts +331 -187
  68. package/src/harness/index.ts +166 -0
  69. package/src/plugins/approval/index.ts +107 -188
  70. package/src/plugins/bash/index.ts +232 -0
  71. package/src/plugins/delegation/index.ts +139 -39
  72. package/src/plugins/memory/index.ts +112 -15
  73. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  74. package/src/plugins/openbot/context.ts +140 -0
  75. package/src/plugins/openbot/history.ts +158 -0
  76. package/src/plugins/openbot/index.ts +79 -0
  77. package/src/plugins/openbot/runtime.ts +478 -0
  78. package/src/plugins/openbot/system-prompt.ts +27 -0
  79. package/src/plugins/plugin-manager/index.ts +224 -0
  80. package/src/plugins/storage/files.ts +81 -0
  81. package/src/plugins/storage/index.ts +823 -0
  82. package/src/{services/storage.ts → plugins/storage/service.ts} +485 -105
  83. package/src/plugins/ui/index.ts +117 -221
  84. package/src/services/abort.ts +46 -0
  85. package/src/{bus/types.ts → services/plugins/domain.ts} +50 -8
  86. package/src/services/plugins/plugin-cache.ts +13 -0
  87. package/src/{registry/plugins.ts → services/plugins/registry.ts} +28 -28
  88. package/src/services/plugins/service.ts +318 -0
  89. package/src/{bus/plugin.ts → services/plugins/types.ts} +7 -3
  90. package/src/bus/services.ts +0 -954
  91. package/src/harness/context.ts +0 -365
  92. package/src/harness/dispatcher.ts +0 -379
  93. package/src/harness/mcp.ts +0 -78
  94. package/src/harness/runtime-factory.ts +0 -129
  95. package/src/harness/todo-advance.ts +0 -128
  96. package/src/plugins/ai-sdk/index.ts +0 -41
  97. package/src/plugins/ai-sdk/runtime.ts +0 -468
  98. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  99. package/src/plugins/mcp/index.ts +0 -128
  100. package/src/plugins/shell/index.ts +0 -123
  101. package/src/plugins/storage-tools/index.ts +0 -90
  102. package/src/plugins/todo/index.ts +0 -64
  103. package/src/services/plugins.ts +0 -133
  104. /package/src/{harness → services}/process.ts +0 -0
@@ -0,0 +1,232 @@
1
+ import fs from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { exec } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, DEFAULT_MARKETPLACE_REGISTRY_URL, loadConfig, resolvePath, } from '../../app/config.js';
7
+ import { invalidatePlugin } from './plugin-cache.js';
8
+ const execAsync = promisify(exec);
9
+ const DEFAULT_MARKETPLACE_AGENTS = [];
10
+ const DEFAULT_MARKETPLACE_CHANNELS = [];
11
+ function isRecord(value) {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+ /**
15
+ * Parses JSON from a remote registry file. Supports either
16
+ * `{ "agents": [ ... ], "channels": [ ... ] }` or a top-level array (legacy agents-only).
17
+ */
18
+ export function parseMarketplaceRegistryJson(data) {
19
+ const isLegacyArray = Array.isArray(data);
20
+ const rawAgents = isLegacyArray
21
+ ? data
22
+ : isRecord(data) && Array.isArray(data.agents)
23
+ ? data.agents
24
+ : [];
25
+ const rawChannels = !isLegacyArray && isRecord(data) && Array.isArray(data.channels)
26
+ ? data.channels
27
+ : isRecord(data) && Array.isArray(data.templates)
28
+ ? data.templates
29
+ : [];
30
+ const agents = (Array.isArray(rawAgents) ? rawAgents : []).map((item, i) => {
31
+ if (!isRecord(item)) {
32
+ throw new Error(`agents[${i}]: expected object`);
33
+ }
34
+ const id = item.id;
35
+ const name = item.name;
36
+ const description = item.description;
37
+ const instructions = item.instructions;
38
+ const pluginsRaw = item.plugins;
39
+ if (typeof id !== 'string' || !id)
40
+ throw new Error(`agents[${i}].id must be a non-empty string`);
41
+ if (typeof name !== 'string')
42
+ throw new Error(`agents[${i}].name must be a string`);
43
+ if (typeof description !== 'string')
44
+ throw new Error(`agents[${i}].description must be a string`);
45
+ if (typeof instructions !== 'string') {
46
+ throw new Error(`agents[${i}].instructions must be a string`);
47
+ }
48
+ if (!Array.isArray(pluginsRaw))
49
+ throw new Error(`agents[${i}].plugins must be an array`);
50
+ const plugins = pluginsRaw.map((p, j) => {
51
+ if (!isRecord(p) || typeof p.id !== 'string' || !p.id) {
52
+ throw new Error(`agents[${i}].plugins[${j}]: expected { "id": string, "config"?: object }`);
53
+ }
54
+ const ref = { id: p.id };
55
+ if (p.config !== undefined) {
56
+ if (!isRecord(p.config))
57
+ throw new Error(`agents[${i}].plugins[${j}].config must be an object`);
58
+ ref.config = p.config;
59
+ }
60
+ return ref;
61
+ });
62
+ const listing = { id, name, description, instructions, plugins };
63
+ if (item.image !== undefined) {
64
+ if (typeof item.image !== 'string')
65
+ throw new Error(`agents[${i}].image must be a string`);
66
+ listing.image = item.image;
67
+ }
68
+ return listing;
69
+ });
70
+ const channels = (Array.isArray(rawChannels) ? rawChannels : []).map((item, i) => {
71
+ if (!isRecord(item)) {
72
+ throw new Error(`channels[${i}]: expected object`);
73
+ }
74
+ const id = item.id;
75
+ const name = item.name;
76
+ const description = item.description;
77
+ const participants = item.participants;
78
+ if (typeof id !== 'string' || !id)
79
+ throw new Error(`channels[${i}].id must be a non-empty string`);
80
+ if (typeof name !== 'string')
81
+ throw new Error(`channels[${i}].name must be a string`);
82
+ if (typeof description !== 'string')
83
+ 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
+ const listing = {
87
+ id,
88
+ name,
89
+ description,
90
+ participants: participants.filter((p) => typeof p === 'string'),
91
+ };
92
+ if (typeof item.image === 'string')
93
+ listing.image = item.image;
94
+ if (typeof item.spec === 'string')
95
+ listing.spec = item.spec;
96
+ if (isRecord(item.initialState))
97
+ listing.initialState = item.initialState;
98
+ if (Array.isArray(item.starterPrompts)) {
99
+ listing.starterPrompts = item.starterPrompts.map((p, j) => {
100
+ if (!isRecord(p) || typeof p.label !== 'string' || typeof p.prompt !== 'string') {
101
+ throw new Error(`channels[${i}].starterPrompts[${j}] must have label and prompt`);
102
+ }
103
+ return { label: p.label, prompt: p.prompt };
104
+ });
105
+ }
106
+ return listing;
107
+ });
108
+ return { agents, channels };
109
+ }
110
+ async function fetchMarketplaceRegistryFromUrl(url) {
111
+ const res = await fetch(url, {
112
+ headers: { Accept: 'application/json' },
113
+ signal: AbortSignal.timeout(15000),
114
+ });
115
+ if (!res.ok) {
116
+ throw new Error(`Registry HTTP ${res.status} ${res.statusText}`);
117
+ }
118
+ const json = await res.json();
119
+ return parseMarketplaceRegistryJson(json);
120
+ }
121
+ /**
122
+ * Resolves marketplace registry (agents and channels) from configured registry URL.
123
+ */
124
+ export async function resolveMarketplaceRegistry() {
125
+ const { marketplaceRegistryUrl } = loadConfig();
126
+ const registryUrl = marketplaceRegistryUrl?.trim() || DEFAULT_MARKETPLACE_REGISTRY_URL;
127
+ try {
128
+ return await fetchMarketplaceRegistryFromUrl(registryUrl);
129
+ }
130
+ catch (err) {
131
+ console.warn(`[plugins] marketplace registry fetch failed (${registryUrl}), using built-in list:`, err instanceof Error ? err.message : err);
132
+ return { agents: DEFAULT_MARKETPLACE_AGENTS, channels: DEFAULT_MARKETPLACE_CHANNELS };
133
+ }
134
+ }
135
+ /**
136
+ * Resolves marketplace agent listings from configured registry URL.
137
+ * @deprecated Use resolveMarketplaceRegistry instead.
138
+ */
139
+ export async function resolveMarketplaceAgentList() {
140
+ const registry = await resolveMarketplaceRegistry();
141
+ return registry.agents;
142
+ }
143
+ const getPluginsDir = () => {
144
+ const config = loadConfig();
145
+ const baseDir = resolvePath(config.baseDir || DEFAULT_BASE_DIR);
146
+ return path.join(baseDir, DEFAULT_PLUGINS_DIR);
147
+ };
148
+ /**
149
+ * Lifecycle for community-built plugins distributed via npm.
150
+ * Each plugin is installed to `<plugins>/<npm-name>/` and is identified
151
+ * everywhere (AGENT.md `plugins[].id`, registry, runtime resolution) by its
152
+ * npm name. Scoped packages (`@scope/foo`) live under `<plugins>/@scope/foo/`.
153
+ */
154
+ export const pluginService = {
155
+ isInstalled: async (packageName) => {
156
+ const finalPath = path.join(getPluginsDir(), packageName);
157
+ return existsSync(path.join(finalPath, 'dist', 'index.js'));
158
+ },
159
+ install: async ({ packageName, version }) => {
160
+ const pluginsDir = getPluginsDir();
161
+ await fs.mkdir(pluginsDir, { recursive: true });
162
+ const finalPath = path.join(pluginsDir, packageName);
163
+ if (existsSync(path.join(finalPath, 'package.json'))) {
164
+ try {
165
+ const pkgJson = JSON.parse(await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'));
166
+ if (!version || pkgJson.version === version) {
167
+ console.log(`[plugins] ${packageName}${version ? `@${version}` : ''} is already installed.`);
168
+ return { name: pkgJson.name, version: pkgJson.version };
169
+ }
170
+ }
171
+ catch {
172
+ // corrupted; reinstall below
173
+ }
174
+ }
175
+ const target = version ? `${packageName}@${version}` : packageName;
176
+ console.log(`[plugins] Installing ${target} to ${pluginsDir}...`);
177
+ const tempDir = path.join(pluginsDir, '.tmp_' + Date.now());
178
+ try {
179
+ await fs.mkdir(tempDir, { recursive: true });
180
+ await execAsync(`npm install ${target} --no-save --prefix "${tempDir}"`);
181
+ const installedPath = path.join(tempDir, 'node_modules', packageName);
182
+ if (!existsSync(installedPath)) {
183
+ throw new Error(`npm did not produce ${installedPath}`);
184
+ }
185
+ await fs.mkdir(path.dirname(finalPath), { recursive: true });
186
+ await fs.rm(finalPath, { recursive: true, force: true });
187
+ await fs.rename(installedPath, finalPath);
188
+ console.log(`[plugins] Running npm install in ${finalPath}...`);
189
+ try {
190
+ await execAsync(`npm install`, { cwd: finalPath });
191
+ console.log(`[plugins] npm install completed in ${finalPath}`);
192
+ }
193
+ catch (e) {
194
+ console.warn(`[plugins] Failed to run npm install in ${finalPath}:`, e);
195
+ }
196
+ const pkgJson = JSON.parse(await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'));
197
+ invalidatePlugin(packageName);
198
+ return { name: pkgJson.name, version: pkgJson.version };
199
+ }
200
+ catch (error) {
201
+ console.error(`[plugins] Failed to install ${packageName}:`, error);
202
+ throw new Error(`Failed to install plugin ${packageName}: ${error.message}`);
203
+ }
204
+ finally {
205
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
206
+ }
207
+ },
208
+ uninstall: async (packageName) => {
209
+ const pluginsDir = getPluginsDir();
210
+ const pluginPath = path.join(pluginsDir, packageName);
211
+ try {
212
+ await fs.rm(pluginPath, { recursive: true, force: true });
213
+ invalidatePlugin(packageName);
214
+ console.log(`[plugins] Uninstalled plugin ${packageName}`);
215
+ if (packageName.startsWith('@')) {
216
+ const scopeDir = path.dirname(pluginPath);
217
+ try {
218
+ const remaining = await fs.readdir(scopeDir);
219
+ if (remaining.length === 0)
220
+ await fs.rmdir(scopeDir);
221
+ }
222
+ catch {
223
+ // ignore
224
+ }
225
+ }
226
+ }
227
+ catch (error) {
228
+ console.error(`[plugins] Failed to uninstall ${packageName}:`, error);
229
+ throw new Error(`Failed to uninstall plugin ${packageName}: ${error.message}`);
230
+ }
231
+ },
232
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { loadVariables } from '../app/config.js';
2
+ /** Keys last applied from workspace `variables.json` (used to unset removed entries). */
3
+ let lastWorkspaceVariableKeys = new Set();
4
+ function applyVariablesList(variables) {
5
+ const nextKeys = new Set(variables.map((v) => v.key));
6
+ for (const key of lastWorkspaceVariableKeys) {
7
+ if (!nextKeys.has(key)) {
8
+ delete process.env[key];
9
+ }
10
+ }
11
+ for (const variable of variables) {
12
+ process.env[variable.key] = variable.value;
13
+ }
14
+ lastWorkspaceVariableKeys = nextKeys;
15
+ }
16
+ export const processService = {
17
+ /**
18
+ * Reload workspace variables from disk into `process.env`.
19
+ * Call after server start and whenever `variables.json` changes.
20
+ */
21
+ syncWorkspaceVariablesToProcessEnv: () => {
22
+ const { variables } = loadVariables();
23
+ applyVariablesList(variables);
24
+ },
25
+ /** Apply a variable list directly (same unset semantics as sync). Prefer `syncWorkspaceVariablesToProcessEnv` when reading from disk. */
26
+ applyVariablesToProcessEnv: (variables) => {
27
+ applyVariablesList(variables);
28
+ },
29
+ };
@@ -5,10 +5,10 @@ import path from 'node:path';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import crypto from 'node:crypto';
7
7
  import matter from 'gray-matter';
8
- import { aiSdkPlugin } from '../plugins/ai-sdk/index.js';
9
- import { AI_SDK_SYSTEM_PROMPT } from '../plugins/ai-sdk/system-prompt.js';
8
+ import { openbotPlugin } from '../plugins/openbot/index.js';
9
+ import { OPENBOT_SYSTEM_PROMPT } from '../plugins/openbot/system-prompt.js';
10
10
  import { listBuiltInPlugins, parsePluginModule } from '../registry/plugins.js';
11
- import { processService } from '../harness/process.js';
11
+ import { processService } from '../harness/index.js';
12
12
  import { memoryService } from './memory.js';
13
13
  const resolveBaseDir = () => {
14
14
  const config = loadConfig();
@@ -79,11 +79,9 @@ const getConversationDir = (channelId, threadId) => {
79
79
  /** Built-in orchestrator agent id. Not creatable as a normal disk agent. */
80
80
  const SYSTEM_AGENT_ID = 'system';
81
81
  const SYSTEM_DEFAULT_PLUGINS = [
82
- { id: 'ai-sdk', config: { model: 'openai/gpt-5.4-nano' } },
82
+ { id: 'openbot', config: { model: 'openai/gpt-5.4-nano' } },
83
83
  { id: 'storage-tools' },
84
- // { id: 'mcp' },
85
84
  { id: 'shell' },
86
- { id: 'todo' },
87
85
  // { id: 'ui' },
88
86
  { id: 'approval' },
89
87
  { id: 'memory' },
@@ -93,8 +91,8 @@ function getSystemAgentDetails(overrides) {
93
91
  id: SYSTEM_AGENT_ID,
94
92
  name: 'OpenBot',
95
93
  image: getBundledSystemAgentImage(),
96
- description: 'First-party orchestration agent for OpenBot. Coordinates other agents via handoff.',
97
- instructions: AI_SDK_SYSTEM_PROMPT,
94
+ description: 'First-party orchestration agent for OpenBot.',
95
+ instructions: OPENBOT_SYSTEM_PROMPT,
98
96
  plugins: SYSTEM_DEFAULT_PLUGINS.map((ref) => ref.id),
99
97
  pluginRefs: SYSTEM_DEFAULT_PLUGINS,
100
98
  createdAt: new Date(),
@@ -105,18 +103,21 @@ function getSystemAgentDetails(overrides) {
105
103
  const refs = overrides.pluginRefs && overrides.pluginRefs.length > 0
106
104
  ? overrides.pluginRefs
107
105
  : defaults.pluginRefs;
106
+ const diskInstructions = overrides.instructions?.trim();
107
+ const instructions = diskInstructions && diskInstructions.length > 0 ? diskInstructions : defaults.instructions;
108
108
  return {
109
109
  ...defaults,
110
110
  ...overrides,
111
111
  id: SYSTEM_AGENT_ID,
112
+ instructions,
112
113
  image: overrides.image || defaults.image,
113
114
  plugins: refs.map((ref) => ref.id),
114
115
  pluginRefs: refs,
115
116
  updatedAt: new Date(),
116
117
  };
117
118
  }
118
- // Suppress unused warning until system agent customization re-uses aiSdkPlugin metadata.
119
- void aiSdkPlugin;
119
+ // Suppress unused warning until system agent customization re-uses openbotPlugin metadata.
120
+ void openbotPlugin;
120
121
  const RESERVED_DISK_AGENT_IDS = new Set([SYSTEM_AGENT_ID]);
121
122
  const assertValidDiskAgentId = (agentId) => {
122
123
  if (!agentId || typeof agentId !== 'string') {
@@ -0,0 +1,81 @@
1
+ import { generateText } from 'ai';
2
+ import { openai } from '@ai-sdk/openai';
3
+ import { anthropic } from '@ai-sdk/anthropic';
4
+ import { loadConfig } from '../app/config.js';
5
+ import { storageService } from '../plugins/storage/service.js';
6
+ const THREAD_TITLE_MAX_LENGTH = 80;
7
+ const namingInFlight = new Set();
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
+ function normalizeTitle(raw) {
24
+ let title = raw
25
+ .replace(/^["'`]+|["'`]+$/g, '')
26
+ .replace(/[.!?]+$/g, '')
27
+ .replace(/\s+/g, ' ')
28
+ .trim();
29
+ if (!title)
30
+ return '';
31
+ if (title.length > THREAD_TITLE_MAX_LENGTH) {
32
+ title = `${title.slice(0, THREAD_TITLE_MAX_LENGTH).trimEnd()}...`;
33
+ }
34
+ return title;
35
+ }
36
+ export async function generateThreadTitle(content, modelString) {
37
+ const normalized = content.replace(/\s+/g, ' ').trim();
38
+ if (!normalized)
39
+ return undefined;
40
+ const config = loadConfig();
41
+ const model = resolveModel(modelString || config.model || 'openai/gpt-4o-mini');
42
+ const result = await generateText({
43
+ model,
44
+ system: 'You name chat threads. Reply with ONLY a short title (3-6 words). No quotes, no trailing punctuation.',
45
+ prompt: normalized.slice(0, 500),
46
+ maxOutputTokens: 20,
47
+ });
48
+ return normalizeTitle(result.text) || undefined;
49
+ }
50
+ export async function maybeGenerateThreadName(args) {
51
+ const key = `${args.channelId}:${args.threadId}`;
52
+ if (namingInFlight.has(key))
53
+ return;
54
+ namingInFlight.add(key);
55
+ try {
56
+ const details = await storageService.getThreadDetails({
57
+ channelId: args.channelId,
58
+ threadId: args.threadId,
59
+ });
60
+ const state = details.state || {};
61
+ if (state.nameStatus === 'llm' || state.nameStatus === 'manual')
62
+ return;
63
+ if (state.nameStatus !== 'provisional')
64
+ return;
65
+ const title = await generateThreadTitle(args.content);
66
+ if (!title)
67
+ return;
68
+ await storageService.patchThreadState({
69
+ channelId: args.channelId,
70
+ threadId: args.threadId,
71
+ state: { generatedName: title, nameStatus: 'llm' },
72
+ });
73
+ await args.onUpdated?.(title);
74
+ }
75
+ catch (error) {
76
+ console.warn('[thread-naming] Failed to generate thread name:', error);
77
+ }
78
+ finally {
79
+ namingInFlight.delete(key);
80
+ }
81
+ }
package/docs/agents.md CHANGED
@@ -14,12 +14,9 @@ You define an agent with a YAML-fronted markdown file at
14
14
  name: Researcher
15
15
  description: Web research and synthesis specialist.
16
16
  plugins:
17
- - id: ai-sdk
17
+ - id: openbot
18
18
  config:
19
19
  model: anthropic/claude-3-5-sonnet-20240620
20
- - id: mcp
21
- - id: shell
22
- - id: delegation
23
20
  ---
24
21
 
25
22
  You are a web research specialist. Use the available tools to gather and
@@ -29,25 +26,31 @@ synthesize information. Be concise and cite sources where relevant.
29
26
  The body below the frontmatter is the system prompt passed to the runtime
30
27
  plugin as `agentDetails.instructions`.
31
28
 
29
+ Set `hidden: true` to omit the agent from `action:storage:get-agents` (it
30
+ remains available via `action:storage:get-agent-details` and can still run on
31
+ the bus). Built-in **`state`** is hidden by default.
32
+
32
33
  ### Required: at least one runtime plugin
33
34
 
34
35
  A runtime plugin is one that handles `agent:invoke` (the LLM loop). Without
35
36
  one, the agent will not respond to user input. Built-in runtime plugins:
36
37
 
37
- - `ai-sdk` — generic LLM runtime (Vercel AI SDK). Consumes tools from other
38
- plugins listed alongside it.
38
+ - `openbot` — the standard, opinionated OpenBot agent runtime. It is
39
+ **batteries-included** and provides inbuilt tools (bash, memory, storage,
40
+ delegation, and approval).
39
41
  - `claude-code` — runs Claude inside the Claude Agent SDK with its own tools.
40
42
  - `gemini-cli` — spawns Google's `gemini` CLI in headless mode.
41
43
 
42
44
  `claude-code` and `gemini-cli` own their own tool loops, so attaching tool
43
- plugins like `shell` or `mcp` to them has no effect. Pair tool plugins with
44
- `ai-sdk`.
45
+ plugins like `bash` to them has no effect.
46
+
47
+ ## Built-in agents
45
48
 
46
- ## Built-in agent
49
+ OpenBot ships a built-in **`system`** agent (the orchestrator) with the `openbot`
50
+ runtime. A built-in **`state`** agent backs deterministic
51
+ `/api/state` handling and infra events.
47
52
 
48
- OpenBot ships a built-in `system` agent (the orchestrator) with the
49
- `ai-sdk` runtime plus the standard tool plugins (storage, shell, mcp,
50
- delegation, ui, approval, memory). It cannot be deleted.
53
+ You can optionally persist overrides for either id at `~/.openbot/agents/system/AGENT.md` or `~/.openbot/agents/state/AGENT.md`. When present, settings are merged on top of the code defaults (`getAgentDetails`). The **`state`** agent is not listed by **`action:storage:get-agents`** (`hidden: true`); **`system`** is listed. Use **`action:storage:create-agent`** to create an overlay once, **`action:storage:update-agent`** for partial updates (creating the file if missing for `system` / `state`), and **`action:storage:delete-agent`** to remove only that `AGENT.md` and revert to defaults (other files under the folder are left untouched).
51
54
 
52
55
  ## Memory
53
56
 
@@ -5,7 +5,7 @@ OpenBot is an orchestration platform built on a modular, event-driven architectu
5
5
  ## Core Components
6
6
 
7
7
  ### 1. Orchestrator & routing
8
- The orchestrator is the execution entry point for agent work: it normalizes incoming events, runs the queue processor (handoffs and todo-driven assignees), builds per-agent Melony runtimes, and streams emitted events back to callers (for example storage and SSE). Routing across the agent network uses:
8
+ The orchestrator is the execution entry point for agent work: it normalizes incoming events, runs the queue processor (todo-driven assignees), builds per-agent Melony runtimes, and streams emitted events back to callers (for example storage and SSE). Routing across the agent network uses:
9
9
 
10
10
  1. **Command Prefix** — Explicit delegation to a specific agent (e.g., `/os list files`).
11
11
  2. **DM context** — Direct communication with a specific agent.
@@ -18,7 +18,7 @@ A dynamic registry that manages all available agents. Agents can be:
18
18
  - **TS Packages**: Advanced agents with custom logic in `~/.openbot/agents/*/index.ts`.
19
19
 
20
20
  ### 3. Plugin registry
21
- The "capability layer" that provides tools and logic shared across the platform. Plugins (like `shell`, `file-system`, or `mcp`) define the actions agents can perform.
21
+ The "capability layer" that provides tools and logic shared across the platform. Plugins (like `bash` or `file-system`) define the actions agents can perform.
22
22
 
23
23
  ### 4. Orchestration layer (Melony)
24
24
  The underlying event bus that handles all communication. It ensures that agents can collaborate asynchronously, share context, and emit real-time updates to the UI.
package/docs/plugins.md CHANGED
@@ -43,17 +43,36 @@ name collisions.
43
43
 
44
44
  | Id | Role | Notes |
45
45
  | --------------- | ---------- | --------------------------------------------------------- |
46
- | `ai-sdk` | Runtime | Generic LLM loop on Vercel AI SDK; consumes external tools |
46
+ | `openbot` | Runtime | Standard batteries-included OpenBot agent runtime. |
47
47
  | `claude-code` | Runtime | Claude Agent SDK; owns its own tool loop |
48
48
  | `gemini-cli` | Runtime | Google `gemini` CLI in headless mode |
49
- | `shell` | Tool | `shell_exec` |
50
- | `mcp` | Tool | `mcp_list_tools`, `mcp_call` |
51
- | `delegation` | Tool | `handoff`, `delegate` |
52
- | `storage-tools` | Tool | `create_channel`, `patch_*`, `create_variable`, ... |
53
- | `ui` | Tool | `render_ui_widget` |
54
- | `approval` | Middleware | Gates protected actions behind a UI confirmation widget |
49
+ | `bash` | Tool | `bash` (inbuilt in `openbot`) |
50
+ | `storage` | Tool | `create_channel`, `patch_*`, ... (inbuilt in `openbot`) |
51
+ | `memory` | Tool | `remember`, `recall`, `forget` (inbuilt in `openbot`) |
52
+ | `plugin-manager`| Infra | Marketplace list, npm plugin install/uninstall, agent install |
55
53
 
56
- ## Community plugins
54
+ ## Batteries-included: `openbot` runtime
55
+
56
+ The `openbot` plugin is the standard runtime for OpenBot agents. It is designed
57
+ to be isolated and self-contained, providing a core ecosystem of inbuilt tools:
58
+
59
+ - **Bash**: Stateful system tasks and file operations.
60
+ - **Memory**: Long-term durable fact storage.
61
+ - **Storage**: Channel and thread management.
62
+ - **Delegation**: Calling upon other specialized agents.
63
+ - **Approval**: Gating protected actions behind UI confirmation.
64
+
65
+ When you use the `openbot` runtime, these tools are automatically available.
66
+ You can configure the inbuilt `approval` plugin via the `openbot` plugin config:
67
+
68
+ ```yaml
69
+ plugins:
70
+ - id: openbot
71
+ config:
72
+ model: openai/gpt-4o-mini
73
+ approval:
74
+ actions: [action:bash, action:create_channel]
75
+ ```
57
76
 
58
77
  A community plugin is just an npm package whose default export matches the
59
78
  `Plugin` interface. Reference it by its npm package name in AGENT.md:
@@ -71,18 +90,11 @@ On first use OpenBot installs the package into
71
90
 
72
91
  ## Approval plugin
73
92
 
74
- The `approval` plugin reads its rules from per-agent config:
93
+ The `approval` plugin gates protected tool calls behind a UI confirmation widget. By default, it gates `action:bash`.
75
94
 
76
95
  ```yaml
77
96
  plugins:
78
97
  - id: approval
79
98
  config:
80
- rules:
81
- - action: action:shell_exec
82
- message: The agent wants to run a terminal command.
83
- detailKeys: [command, cwd, shell, timeoutMs]
84
- hiddenKeys: [env]
99
+ actions: [action:bash]
85
100
  ```
86
-
87
- If `rules` is omitted, sensible defaults are applied (currently: gate
88
- `action:shell_exec`).
@@ -9,32 +9,26 @@ description: One-line description shown in agent pickers and lists.
9
9
 
10
10
  # Plugins compose the agent. Order matters for tool collisions (first wins).
11
11
  # At least one plugin must handle `agent:invoke` (a "runtime" plugin like
12
- # `ai-sdk`, `claude-code`, or `gemini-cli`). Tool plugins like `shell`, `mcp`,
13
- # `delegation`, `storage-tools`, and `ui` contribute tools to whichever runtime
12
+ # `openbot`, `claude-code`, or `gemini-cli`). Tool plugins like `bash`,
13
+ # `delegation`, and `storage-tools` contribute tools to whichever runtime
14
14
  # plugin can consume them.
15
15
  #
16
- # Built-in plugin ids: ai-sdk, claude-code, gemini-cli, shell, mcp, delegation,
17
- # storage-tools, ui, approval.
16
+ # Built-in plugin ids: openbot, claude-code, gemini-cli, bash, delegation,
17
+ # storage-tools, approval.
18
18
  #
19
19
  # Community plugins are referenced by their npm package name (e.g.
20
20
  # `openbot-plugin-search` or `@scope/openbot-plugin-foo`) and are auto-installed
21
21
  # on first use into ~/.openbot/plugins/<id>/.
22
22
  plugins:
23
- - id: ai-sdk
23
+ - id: openbot
24
24
  config:
25
25
  model: openai/gpt-4o-mini
26
- - id: shell
27
- - id: mcp
26
+ - id: bash
28
27
  - id: delegation
29
- - id: storage-tools
30
- - id: ui
28
+ - id: storage
31
29
  - id: approval
32
30
  config:
33
- rules:
34
- - action: action:shell_exec
35
- message: The agent wants to run a terminal command.
36
- detailKeys: [command, cwd, shell, timeoutMs]
37
- hiddenKeys: [env]
31
+ actions: [action:bash]
38
32
  ---
39
33
 
40
34
  <!--
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.3.6",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -13,7 +13,6 @@
13
13
  "@ai-sdk/anthropic": "^3.0.33",
14
14
  "@ai-sdk/openai": "^3.0.13",
15
15
  "@anthropic-ai/claude-agent-sdk": "^0.2.138",
16
- "@modelcontextprotocol/sdk": "^1.29.0",
17
16
  "@types/cors": "^2.8.19",
18
17
  "ai": "^6.0.42",
19
18
  "commander": "^14.0.2",
@@ -0,0 +1,5 @@
1
+ /** Built-in orchestrator agent id. Optional `agents/system/AGENT.md` overrides code defaults. */
2
+ export const ORCHESTRATOR_AGENT_ID = 'system';
3
+
4
+ /** Built-in infra agent for deterministic `/api/state` and marketplace/plugin lifecycle; optional AGENT.md overlay. */
5
+ export const STATE_AGENT_ID = 'state';
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.3.6');
28
+ program.name('openbot').description('OpenBot CLI').version('0.4.2');
29
29
 
30
30
  program
31
31
  .command('start')