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.
- package/README.md +15 -16
- package/dist/app/agent-ids.js +4 -0
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +0 -19
- package/dist/app/server.js +8 -14
- package/dist/bus/services.js +34 -124
- package/dist/harness/agent-invoke-run.js +44 -0
- package/dist/harness/agent-turn.js +99 -0
- package/dist/harness/channel-participants.js +40 -0
- package/dist/harness/constants.js +2 -0
- package/dist/harness/context-meter.js +97 -0
- package/dist/harness/context.js +95 -47
- package/dist/harness/dispatch.js +144 -0
- package/dist/harness/dispatcher.js +45 -156
- package/dist/harness/history.js +177 -0
- package/dist/harness/index.js +91 -0
- package/dist/harness/orchestration.js +88 -0
- package/dist/harness/participants.js +22 -0
- package/dist/harness/run-harness.js +154 -0
- package/dist/harness/run.js +98 -0
- package/dist/harness/runtime-factory.js +0 -34
- package/dist/harness/runtime.js +57 -0
- package/dist/harness/todo-dispatch.js +51 -0
- package/dist/harness/todos.js +5 -0
- package/dist/harness/turn.js +79 -0
- package/dist/plugins/approval/index.js +105 -149
- package/dist/plugins/delegation/index.js +119 -32
- package/dist/plugins/memory/index.js +103 -14
- package/dist/plugins/memory/service.js +152 -0
- package/dist/plugins/openbot/context.js +80 -0
- package/dist/plugins/openbot/history.js +98 -0
- package/dist/plugins/openbot/index.js +31 -0
- package/dist/plugins/openbot/runtime.js +317 -0
- package/dist/plugins/openbot/system-prompt.js +5 -0
- package/dist/plugins/plugin-manager/index.js +105 -0
- package/dist/plugins/storage/index.js +573 -0
- package/dist/plugins/storage/service.js +1159 -0
- package/dist/plugins/storage-tools/index.js +2 -2
- package/dist/plugins/thread-namer/index.js +72 -0
- package/dist/plugins/thread-naming/generate-title.js +44 -0
- package/dist/plugins/thread-naming/index.js +103 -0
- package/dist/plugins/threads/index.js +114 -0
- package/dist/plugins/todo/index.js +24 -25
- package/dist/plugins/ui/index.js +2 -32
- package/dist/registry/plugins.js +3 -9
- package/dist/services/plugins/domain.js +1 -0
- package/dist/services/plugins/plugin-cache.js +9 -0
- package/dist/services/plugins/registry.js +110 -0
- package/dist/services/plugins/service.js +177 -0
- package/dist/services/plugins/types.js +1 -0
- package/dist/services/process.js +29 -0
- package/dist/services/storage.js +11 -10
- package/dist/services/thread-naming.js +81 -0
- package/docs/agents.md +16 -10
- package/docs/architecture.md +2 -2
- package/docs/plugins.md +6 -15
- package/docs/templates/AGENT.example.md +7 -13
- package/package.json +1 -2
- package/src/app/agent-ids.ts +5 -0
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +1 -31
- package/src/app/server.ts +8 -16
- package/src/app/types.ts +63 -189
- package/src/harness/index.ts +145 -0
- package/src/plugins/approval/index.ts +91 -189
- package/src/plugins/delegation/index.ts +136 -39
- package/src/plugins/memory/index.ts +112 -15
- package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
- package/src/plugins/openbot/context.ts +91 -0
- package/src/plugins/openbot/history.ts +107 -0
- package/src/plugins/openbot/index.ts +37 -0
- package/src/plugins/openbot/runtime.ts +384 -0
- package/src/plugins/openbot/system-prompt.ts +7 -0
- package/src/plugins/plugin-manager/index.ts +122 -0
- package/src/plugins/shell/index.ts +1 -1
- package/src/plugins/storage/index.ts +633 -0
- package/src/{services/storage.ts → plugins/storage/service.ts} +224 -67
- package/src/{bus/types.ts → services/plugins/domain.ts} +16 -7
- package/src/services/plugins/plugin-cache.ts +13 -0
- package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
- package/src/services/{plugins.ts → plugins/service.ts} +96 -2
- package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
- package/src/bus/services.ts +0 -954
- package/src/harness/context.ts +0 -365
- package/src/harness/dispatcher.ts +0 -379
- package/src/harness/mcp.ts +0 -78
- package/src/harness/runtime-factory.ts +0 -129
- package/src/harness/todo-advance.ts +0 -128
- package/src/plugins/ai-sdk/index.ts +0 -41
- package/src/plugins/ai-sdk/runtime.ts +0 -468
- package/src/plugins/ai-sdk/system-prompt.ts +0 -18
- package/src/plugins/mcp/index.ts +0 -128
- package/src/plugins/storage-tools/index.ts +0 -90
- package/src/plugins/todo/index.ts +0 -64
- package/src/plugins/ui/index.ts +0 -227
- /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
|
+
};
|
package/dist/services/storage.js
CHANGED
|
@@ -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 {
|
|
9
|
-
import {
|
|
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/
|
|
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: '
|
|
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.
|
|
97
|
-
instructions:
|
|
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
|
|
119
|
-
void
|
|
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,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:
|
|
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
|
-
- `
|
|
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`
|
|
44
|
-
`
|
|
46
|
+
plugins like `shell` to them has no effect. Pair tool plugins with
|
|
47
|
+
`openbot`.
|
|
48
|
+
|
|
49
|
+
## Built-in agents
|
|
45
50
|
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
package/docs/architecture.md
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
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
|
|
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
|
-
|
|
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
|
-
# `
|
|
13
|
-
# `delegation`, `storage-tools
|
|
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:
|
|
17
|
-
# storage-tools,
|
|
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:
|
|
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
|
|
30
|
-
- id: ui
|
|
28
|
+
- id: storage
|
|
31
29
|
- id: approval
|
|
32
30
|
config:
|
|
33
|
-
|
|
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
|
+
"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
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
|
+
}
|
package/src/app/server.ts
CHANGED
|
@@ -10,10 +10,9 @@ const pkg = require('../../package.json');
|
|
|
10
10
|
import { generateId } from 'melony';
|
|
11
11
|
import { DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
|
|
12
12
|
import { ActiveRunsSnapshotEvent, OpenBotEvent, OpenBotState } from './types.js';
|
|
13
|
-
import { processService } from '../
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { initPlugins } from '../registry/plugins.js';
|
|
13
|
+
import { processService } from '../services/process.js';
|
|
14
|
+
import { runAgent, STATE_AGENT_ID, ORCHESTRATOR_AGENT_ID } from '../harness/index.js';
|
|
15
|
+
import { initPlugins } from '../services/plugins/registry.js';
|
|
17
16
|
import { ensureEventId, openBotEventFromQuery } from './utils.js';
|
|
18
17
|
|
|
19
18
|
type Bucket = { channelId: string; threadId?: string; activeCount: number; agentIds: Set<string> };
|
|
@@ -212,8 +211,6 @@ export async function startServer(options: ServerOptions = {}) {
|
|
|
212
211
|
}
|
|
213
212
|
|
|
214
213
|
const onEvent = async (chunk: OpenBotEvent, state?: OpenBotState) => {
|
|
215
|
-
ensureEventId(chunk);
|
|
216
|
-
|
|
217
214
|
const targetChannelId = state?.channelId || channelId;
|
|
218
215
|
const targetThreadId = state?.threadId || threadId;
|
|
219
216
|
const targetClientKey = getClientKey(targetChannelId, targetThreadId);
|
|
@@ -234,12 +231,6 @@ export async function startServer(options: ServerOptions = {}) {
|
|
|
234
231
|
);
|
|
235
232
|
}
|
|
236
233
|
|
|
237
|
-
await storageService.storeEvent({
|
|
238
|
-
channelId: targetChannelId,
|
|
239
|
-
threadId: targetThreadId,
|
|
240
|
-
event: chunk,
|
|
241
|
-
});
|
|
242
|
-
|
|
243
234
|
sendToClientKey(targetClientKey, chunk);
|
|
244
235
|
|
|
245
236
|
if (
|
|
@@ -254,9 +245,9 @@ export async function startServer(options: ServerOptions = {}) {
|
|
|
254
245
|
try {
|
|
255
246
|
ensureEventId(event);
|
|
256
247
|
|
|
257
|
-
await
|
|
248
|
+
await runAgent({
|
|
258
249
|
runId,
|
|
259
|
-
agentId: agentId ||
|
|
250
|
+
agentId: agentId || ORCHESTRATOR_AGENT_ID,
|
|
260
251
|
event,
|
|
261
252
|
channelId,
|
|
262
253
|
threadId,
|
|
@@ -295,12 +286,13 @@ export async function startServer(options: ServerOptions = {}) {
|
|
|
295
286
|
try {
|
|
296
287
|
ensureEventId(event);
|
|
297
288
|
|
|
298
|
-
await
|
|
289
|
+
await runAgent({
|
|
299
290
|
runId,
|
|
300
|
-
agentId: agentId ||
|
|
291
|
+
agentId: agentId || STATE_AGENT_ID,
|
|
301
292
|
event,
|
|
302
293
|
channelId,
|
|
303
294
|
threadId,
|
|
295
|
+
persistEvents: false,
|
|
304
296
|
onEvent,
|
|
305
297
|
});
|
|
306
298
|
res.json({ events });
|