openbot 0.3.5 → 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 (98) 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/assets/icon.svg +9 -3
  7. package/dist/bus/services.js +78 -132
  8. package/dist/harness/agent-invoke-run.js +44 -0
  9. package/dist/harness/agent-turn.js +99 -0
  10. package/dist/harness/channel-participants.js +40 -0
  11. package/dist/harness/constants.js +2 -0
  12. package/dist/harness/context-meter.js +97 -0
  13. package/dist/harness/context.js +98 -45
  14. package/dist/harness/dispatch.js +144 -0
  15. package/dist/harness/dispatcher.js +45 -156
  16. package/dist/harness/history.js +177 -0
  17. package/dist/harness/index.js +91 -0
  18. package/dist/harness/orchestration.js +88 -0
  19. package/dist/harness/participants.js +22 -0
  20. package/dist/harness/run-harness.js +154 -0
  21. package/dist/harness/run.js +98 -0
  22. package/dist/harness/runtime-factory.js +0 -34
  23. package/dist/harness/runtime.js +57 -0
  24. package/dist/harness/todo-dispatch.js +51 -0
  25. package/dist/harness/todos.js +5 -0
  26. package/dist/harness/turn.js +79 -0
  27. package/dist/plugins/approval/index.js +105 -149
  28. package/dist/plugins/delegation/index.js +119 -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 +80 -0
  32. package/dist/plugins/openbot/history.js +98 -0
  33. package/dist/plugins/openbot/index.js +31 -0
  34. package/dist/plugins/openbot/runtime.js +317 -0
  35. package/dist/plugins/openbot/system-prompt.js +5 -0
  36. package/dist/plugins/plugin-manager/index.js +105 -0
  37. package/dist/plugins/storage/index.js +573 -0
  38. package/dist/plugins/storage/service.js +1159 -0
  39. package/dist/plugins/storage-tools/index.js +2 -2
  40. package/dist/plugins/thread-namer/index.js +72 -0
  41. package/dist/plugins/thread-naming/generate-title.js +44 -0
  42. package/dist/plugins/thread-naming/index.js +103 -0
  43. package/dist/plugins/threads/index.js +114 -0
  44. package/dist/plugins/todo/index.js +24 -25
  45. package/dist/plugins/ui/index.js +2 -32
  46. package/dist/registry/plugins.js +3 -9
  47. package/dist/services/plugins/domain.js +1 -0
  48. package/dist/services/plugins/plugin-cache.js +9 -0
  49. package/dist/services/plugins/registry.js +110 -0
  50. package/dist/services/plugins/service.js +177 -0
  51. package/dist/services/plugins/types.js +1 -0
  52. package/dist/services/process.js +29 -0
  53. package/dist/services/storage.js +41 -15
  54. package/dist/services/thread-naming.js +81 -0
  55. package/docs/agents.md +16 -10
  56. package/docs/architecture.md +2 -2
  57. package/docs/plugins.md +6 -15
  58. package/docs/templates/AGENT.example.md +7 -13
  59. package/package.json +1 -2
  60. package/src/app/agent-ids.ts +5 -0
  61. package/src/app/cli.ts +1 -1
  62. package/src/app/config.ts +1 -31
  63. package/src/app/server.ts +8 -16
  64. package/src/app/types.ts +70 -190
  65. package/src/assets/icon.svg +9 -3
  66. package/src/harness/index.ts +145 -0
  67. package/src/plugins/approval/index.ts +91 -189
  68. package/src/plugins/delegation/index.ts +136 -39
  69. package/src/plugins/memory/index.ts +112 -15
  70. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  71. package/src/plugins/openbot/context.ts +91 -0
  72. package/src/plugins/openbot/history.ts +107 -0
  73. package/src/plugins/openbot/index.ts +37 -0
  74. package/src/plugins/openbot/runtime.ts +384 -0
  75. package/src/plugins/openbot/system-prompt.ts +7 -0
  76. package/src/plugins/plugin-manager/index.ts +122 -0
  77. package/src/plugins/shell/index.ts +1 -1
  78. package/src/plugins/storage/index.ts +633 -0
  79. package/src/{services/storage.ts → plugins/storage/service.ts} +257 -72
  80. package/src/{bus/types.ts → services/plugins/domain.ts} +20 -7
  81. package/src/services/plugins/plugin-cache.ts +13 -0
  82. package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
  83. package/src/services/{plugins.ts → plugins/service.ts} +96 -2
  84. package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
  85. package/src/bus/services.ts +0 -908
  86. package/src/harness/context.ts +0 -356
  87. package/src/harness/dispatcher.ts +0 -379
  88. package/src/harness/mcp.ts +0 -78
  89. package/src/harness/runtime-factory.ts +0 -129
  90. package/src/harness/todo-advance.ts +0 -128
  91. package/src/plugins/ai-sdk/index.ts +0 -41
  92. package/src/plugins/ai-sdk/runtime.ts +0 -468
  93. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  94. package/src/plugins/mcp/index.ts +0 -128
  95. package/src/plugins/storage-tools/index.ts +0 -90
  96. package/src/plugins/todo/index.ts +0 -64
  97. package/src/plugins/ui/index.ts +0 -227
  98. /package/src/{harness → services}/process.ts +0 -0
@@ -0,0 +1,177 @@
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
+ function isRecord(value) {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+ /**
14
+ * Parses JSON from a remote registry file. Supports either
15
+ * `{ "agents": [ ... ] }` or a top-level array.
16
+ */
17
+ export function parseMarketplaceRegistryJson(data) {
18
+ const rawAgents = Array.isArray(data) ? data : isRecord(data) && Array.isArray(data.agents) ? data.agents : null;
19
+ if (!Array.isArray(rawAgents)) {
20
+ throw new Error('Registry JSON must be an array or an object with an "agents" array');
21
+ }
22
+ return rawAgents.map((item, i) => {
23
+ if (!isRecord(item)) {
24
+ throw new Error(`agents[${i}]: expected object`);
25
+ }
26
+ const id = item.id;
27
+ const name = item.name;
28
+ const description = item.description;
29
+ const instructions = item.instructions;
30
+ const pluginsRaw = item.plugins;
31
+ if (typeof id !== 'string' || !id)
32
+ throw new Error(`agents[${i}].id must be a non-empty string`);
33
+ if (typeof name !== 'string')
34
+ throw new Error(`agents[${i}].name must be a string`);
35
+ if (typeof description !== 'string')
36
+ throw new Error(`agents[${i}].description must be a string`);
37
+ if (typeof instructions !== 'string') {
38
+ throw new Error(`agents[${i}].instructions must be a string`);
39
+ }
40
+ if (!Array.isArray(pluginsRaw))
41
+ throw new Error(`agents[${i}].plugins must be an array`);
42
+ const plugins = pluginsRaw.map((p, j) => {
43
+ if (!isRecord(p) || typeof p.id !== 'string' || !p.id) {
44
+ throw new Error(`agents[${i}].plugins[${j}]: expected { "id": string, "config"?: object }`);
45
+ }
46
+ const ref = { id: p.id };
47
+ if (p.config !== undefined) {
48
+ if (!isRecord(p.config))
49
+ throw new Error(`agents[${i}].plugins[${j}].config must be an object`);
50
+ ref.config = p.config;
51
+ }
52
+ return ref;
53
+ });
54
+ const listing = { id, name, description, instructions, plugins };
55
+ if (item.image !== undefined) {
56
+ if (typeof item.image !== 'string')
57
+ throw new Error(`agents[${i}].image must be a string`);
58
+ listing.image = item.image;
59
+ }
60
+ return listing;
61
+ });
62
+ }
63
+ async function fetchMarketplaceAgentsFromUrl(url) {
64
+ const res = await fetch(url, {
65
+ headers: { Accept: 'application/json' },
66
+ signal: AbortSignal.timeout(15000),
67
+ });
68
+ if (!res.ok) {
69
+ throw new Error(`Registry HTTP ${res.status} ${res.statusText}`);
70
+ }
71
+ const json = await res.json();
72
+ return parseMarketplaceRegistryJson(json);
73
+ }
74
+ /**
75
+ * Resolves marketplace agent listings from configured registry URL, or falls back to an empty list.
76
+ */
77
+ export async function resolveMarketplaceAgentList() {
78
+ const { marketplaceRegistryUrl } = loadConfig();
79
+ const registryUrl = marketplaceRegistryUrl?.trim() || DEFAULT_MARKETPLACE_REGISTRY_URL;
80
+ try {
81
+ return await fetchMarketplaceAgentsFromUrl(registryUrl);
82
+ }
83
+ catch (err) {
84
+ console.warn(`[plugins] marketplace registry fetch failed (${registryUrl}), using built-in list:`, err instanceof Error ? err.message : err);
85
+ return DEFAULT_MARKETPLACE_AGENTS;
86
+ }
87
+ }
88
+ const getPluginsDir = () => {
89
+ const config = loadConfig();
90
+ const baseDir = resolvePath(config.baseDir || DEFAULT_BASE_DIR);
91
+ return path.join(baseDir, DEFAULT_PLUGINS_DIR);
92
+ };
93
+ /**
94
+ * Lifecycle for community-built plugins distributed via npm.
95
+ * Each plugin is installed to `<plugins>/<npm-name>/` and is identified
96
+ * everywhere (AGENT.md `plugins[].id`, registry, runtime resolution) by its
97
+ * npm name. Scoped packages (`@scope/foo`) live under `<plugins>/@scope/foo/`.
98
+ */
99
+ export const pluginService = {
100
+ isInstalled: async (packageName) => {
101
+ const finalPath = path.join(getPluginsDir(), packageName);
102
+ return existsSync(path.join(finalPath, 'dist', 'index.js'));
103
+ },
104
+ install: async ({ packageName, version }) => {
105
+ const pluginsDir = getPluginsDir();
106
+ await fs.mkdir(pluginsDir, { recursive: true });
107
+ const finalPath = path.join(pluginsDir, packageName);
108
+ if (existsSync(path.join(finalPath, 'package.json'))) {
109
+ try {
110
+ const pkgJson = JSON.parse(await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'));
111
+ if (!version || pkgJson.version === version) {
112
+ console.log(`[plugins] ${packageName}${version ? `@${version}` : ''} is already installed.`);
113
+ return { name: pkgJson.name, version: pkgJson.version };
114
+ }
115
+ }
116
+ catch {
117
+ // corrupted; reinstall below
118
+ }
119
+ }
120
+ const target = version ? `${packageName}@${version}` : packageName;
121
+ console.log(`[plugins] Installing ${target} to ${pluginsDir}...`);
122
+ const tempDir = path.join(pluginsDir, '.tmp_' + Date.now());
123
+ try {
124
+ await fs.mkdir(tempDir, { recursive: true });
125
+ await execAsync(`npm install ${target} --no-save --prefix "${tempDir}"`);
126
+ const installedPath = path.join(tempDir, 'node_modules', packageName);
127
+ if (!existsSync(installedPath)) {
128
+ throw new Error(`npm did not produce ${installedPath}`);
129
+ }
130
+ await fs.mkdir(path.dirname(finalPath), { recursive: true });
131
+ await fs.rm(finalPath, { recursive: true, force: true });
132
+ await fs.rename(installedPath, finalPath);
133
+ console.log(`[plugins] Running npm install in ${finalPath}...`);
134
+ try {
135
+ await execAsync(`npm install`, { cwd: finalPath });
136
+ console.log(`[plugins] npm install completed in ${finalPath}`);
137
+ }
138
+ catch (e) {
139
+ console.warn(`[plugins] Failed to run npm install in ${finalPath}:`, e);
140
+ }
141
+ const pkgJson = JSON.parse(await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'));
142
+ invalidatePlugin(packageName);
143
+ return { name: pkgJson.name, version: pkgJson.version };
144
+ }
145
+ catch (error) {
146
+ console.error(`[plugins] Failed to install ${packageName}:`, error);
147
+ throw new Error(`Failed to install plugin ${packageName}: ${error.message}`);
148
+ }
149
+ finally {
150
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
151
+ }
152
+ },
153
+ uninstall: async (packageName) => {
154
+ const pluginsDir = getPluginsDir();
155
+ const pluginPath = path.join(pluginsDir, packageName);
156
+ try {
157
+ await fs.rm(pluginPath, { recursive: true, force: true });
158
+ invalidatePlugin(packageName);
159
+ console.log(`[plugins] Uninstalled plugin ${packageName}`);
160
+ if (packageName.startsWith('@')) {
161
+ const scopeDir = path.dirname(pluginPath);
162
+ try {
163
+ const remaining = await fs.readdir(scopeDir);
164
+ if (remaining.length === 0)
165
+ await fs.rmdir(scopeDir);
166
+ }
167
+ catch {
168
+ // ignore
169
+ }
170
+ }
171
+ }
172
+ catch (error) {
173
+ console.error(`[plugins] Failed to uninstall ${packageName}:`, error);
174
+ throw new Error(`Failed to uninstall plugin ${packageName}: ${error.message}`);
175
+ }
176
+ },
177
+ };
@@ -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') {
@@ -262,6 +263,22 @@ const listPluginsFromDisk = async () => {
262
263
  return descriptors.filter((d) => d !== null);
263
264
  };
264
265
  const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
266
+ /** Display-oriented fields persisted in a channel's `state.json`. */
267
+ const readChannelStateFileFields = (parsed) => {
268
+ if (!isRecord(parsed)) {
269
+ return { participants: [] };
270
+ }
271
+ const name = typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name.trim() : undefined;
272
+ const cwd = typeof parsed.cwd === 'string' ? parsed.cwd : undefined;
273
+ const participants = [];
274
+ if (Array.isArray(parsed.participants)) {
275
+ for (const x of parsed.participants) {
276
+ if (typeof x === 'string' && x.trim())
277
+ participants.push(x.trim());
278
+ }
279
+ }
280
+ return { name, cwd, participants };
281
+ };
265
282
  /**
266
283
  * Parse the `plugins:` array from AGENT.md frontmatter. Each entry must have an
267
284
  * `id`; `config` is optional. Strings are accepted as a shorthand for `{ id }`.
@@ -308,19 +325,25 @@ export const storageService = {
308
325
  const channelDir = getConversationDir(name);
309
326
  const statePath = path.join(channelDir, 'state.json');
310
327
  let cwd;
328
+ let displayName = name;
329
+ let participants = [];
311
330
  try {
312
331
  const stateContent = await fs.readFile(statePath, 'utf-8');
313
- const state = JSON.parse(stateContent);
314
- cwd = typeof state.cwd === 'string' ? state.cwd : undefined;
332
+ const parsed = JSON.parse(stateContent);
333
+ const fields = readChannelStateFileFields(parsed);
334
+ cwd = fields.cwd;
335
+ displayName = fields.name ?? name;
336
+ participants = fields.participants;
315
337
  }
316
338
  catch {
317
339
  // ignore
318
340
  }
319
341
  const channel = {
320
342
  id: name,
321
- name: name,
343
+ name: displayName,
322
344
  description: '',
323
345
  cwd,
346
+ participants,
324
347
  createdAt: new Date(),
325
348
  updatedAt: new Date(),
326
349
  };
@@ -484,13 +507,16 @@ export const storageService = {
484
507
  console.error(`Failed to read state file for channel ${channelId}`, error);
485
508
  }
486
509
  }
487
- const cwd = isRecord(state) && typeof state.cwd === 'string' ? state.cwd : undefined;
510
+ const diskFields = readChannelStateFileFields(state);
511
+ const cwd = diskFields.cwd;
512
+ const displayName = diskFields.name ?? channelId;
488
513
  const details = {
489
514
  id: channelId,
490
- name: channelId,
515
+ name: displayName,
491
516
  spec,
492
517
  state,
493
518
  cwd,
519
+ participants: diskFields.participants,
494
520
  };
495
521
  details.threads = await storageService.getThreads({ channelId });
496
522
  return details;
@@ -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,10 +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
20
  - id: shell
22
21
  - id: delegation
23
22
  ---
@@ -29,25 +28,32 @@ synthesize information. Be concise and cite sources where relevant.
29
28
  The body below the frontmatter is the system prompt passed to the runtime
30
29
  plugin as `agentDetails.instructions`.
31
30
 
31
+ Set `hidden: true` to omit the agent from `action:storage:get-agents` (it
32
+ remains available via `action:storage:get-agent-details` and can still run on
33
+ the bus). Built-in **`state`** is hidden by default.
34
+
32
35
  ### Required: at least one runtime plugin
33
36
 
34
37
  A runtime plugin is one that handles `agent:invoke` (the LLM loop). Without
35
38
  one, the agent will not respond to user input. Built-in runtime plugins:
36
39
 
37
- - `ai-sdk` — generic LLM runtime (Vercel AI SDK). Consumes tools from other
38
- plugins listed alongside it.
40
+ - `openbot` — the standard, opinionated OpenBot agent runtime. Consumes tools
41
+ from other plugins listed alongside it.
39
42
  - `claude-code` — runs Claude inside the Claude Agent SDK with its own tools.
40
43
  - `gemini-cli` — spawns Google's `gemini` CLI in headless mode.
41
44
 
42
45
  `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`.
46
+ plugins like `shell` to them has no effect. Pair tool plugins with
47
+ `openbot`.
48
+
49
+ ## Built-in agents
45
50
 
46
- ## Built-in agent
51
+ OpenBot ships a built-in **`system`** agent (the orchestrator) with the `openbot`
52
+ runtime plus the standard tool plugins (storage, shell, delegation,
53
+ approval, memory, etc.). A built-in **`state`** agent backs deterministic
54
+ `/api/state` handling and infra events.
47
55
 
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.
56
+ 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
57
 
52
58
  ## Memory
53
59
 
@@ -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 `shell` 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,15 +43,13 @@ 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 | The standard, opinionated 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
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 |
50
+ | `storage` | Tool | `create_channel`, `patch_*`, `create_variable`, ... |
51
+ | `memory` | Tool | `remember`, `recall`, `forget` |
52
+ | `plugin-manager`| Infra | Marketplace list, npm plugin install/uninstall, agent install |
55
53
 
56
54
  ## Community plugins
57
55
 
@@ -71,18 +69,11 @@ On first use OpenBot installs the package into
71
69
 
72
70
  ## Approval plugin
73
71
 
74
- The `approval` plugin reads its rules from per-agent config:
72
+ The `approval` plugin gates protected tool calls behind a UI confirmation widget. By default, it gates `action:shell_exec`.
75
73
 
76
74
  ```yaml
77
75
  plugins:
78
76
  - id: approval
79
77
  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]
78
+ actions: [action:shell_exec]
85
79
  ```
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 `shell`,
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, shell, 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
26
  - id: shell
27
- - id: mcp
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:shell_exec]
38
32
  ---
39
33
 
40
34
  <!--
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
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.4');
28
+ program.name('openbot').description('OpenBot CLI').version('0.4.0');
29
29
 
30
30
  program
31
31
  .command('start')
package/src/app/config.ts CHANGED
@@ -9,7 +9,6 @@ export interface OpenBotconfig {
9
9
  image?: string;
10
10
  baseDir?: string;
11
11
  port?: number;
12
- mcpServers?: MCPServerConfig[];
13
12
  /**
14
13
  * Overrides the default public marketplace registry URL. If omitted or blank,
15
14
  * {@link DEFAULT_MARKETPLACE_REGISTRY_URL} is used.
@@ -17,14 +16,6 @@ export interface OpenBotconfig {
17
16
  marketplaceRegistryUrl?: string;
18
17
  }
19
18
 
20
- export interface MCPServerConfig {
21
- id: string;
22
- command: string;
23
- args?: string[];
24
- env?: Record<string, string>;
25
- cwd?: string;
26
- }
27
-
28
19
  export interface StoredVariable {
29
20
  key: string;
30
21
  value: string;
@@ -90,25 +81,4 @@ export function loadVariables(): { version: number; variables: StoredVariable[]
90
81
  };
91
82
  }
92
83
  return { version: 1, variables: [] };
93
- }
94
-
95
- export const DEFAULT_AGENT_MD = `---
96
- description: A specialized AI agent
97
- ---
98
-
99
- # Agent Profile
100
-
101
- You are a specialized AI agent within the OpenBot system.
102
- Your role is defined by your configuration and the tools you have access to.
103
-
104
- ## Persona
105
- - Helpful and precise
106
- - Focused on my specific domain
107
- - Professional in all interactions
108
- `;
109
-
110
- export const DEFAULT_USER_MD = `# About Me
111
-
112
- <!-- OpenBot reads this file to understand who you are and how you like to work. -->
113
- <!-- Edit it here or just chat — agents can update it with the "remember" tool. -->
114
- `;
84
+ }