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.
- 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 +10 -19
- package/dist/app/server.js +208 -17
- 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 +109 -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 +120 -149
- package/dist/plugins/bash/index.js +195 -0
- package/dist/plugins/delegation/index.js +121 -32
- package/dist/plugins/memory/index.js +103 -14
- package/dist/plugins/memory/service.js +152 -0
- package/dist/plugins/openbot/context.js +125 -0
- package/dist/plugins/openbot/history.js +144 -0
- package/dist/plugins/openbot/index.js +71 -0
- package/dist/plugins/openbot/runtime.js +381 -0
- package/dist/plugins/openbot/system-prompt.js +25 -0
- package/dist/plugins/plugin-manager/index.js +189 -0
- package/dist/plugins/shell/index.js +2 -1
- package/dist/plugins/storage/files.js +67 -0
- package/dist/plugins/storage/index.js +750 -0
- package/dist/plugins/storage/service.js +1316 -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 +109 -180
- package/dist/registry/plugins.js +3 -9
- package/dist/services/abort.js +43 -0
- package/dist/services/plugins/domain.js +1 -0
- package/dist/services/plugins/plugin-cache.js +9 -0
- package/dist/services/plugins/registry.js +112 -0
- package/dist/services/plugins/service.js +232 -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 +15 -12
- package/docs/architecture.md +2 -2
- package/docs/plugins.md +29 -17
- package/docs/templates/AGENT.example.md +8 -14
- 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 +14 -31
- package/src/app/server.ts +243 -19
- package/src/app/types.ts +331 -187
- package/src/harness/index.ts +166 -0
- package/src/plugins/approval/index.ts +107 -188
- package/src/plugins/bash/index.ts +232 -0
- package/src/plugins/delegation/index.ts +139 -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 +140 -0
- package/src/plugins/openbot/history.ts +158 -0
- package/src/plugins/openbot/index.ts +79 -0
- package/src/plugins/openbot/runtime.ts +478 -0
- package/src/plugins/openbot/system-prompt.ts +27 -0
- package/src/plugins/plugin-manager/index.ts +224 -0
- package/src/plugins/storage/files.ts +81 -0
- package/src/plugins/storage/index.ts +823 -0
- package/src/{services/storage.ts → plugins/storage/service.ts} +485 -105
- package/src/plugins/ui/index.ts +117 -221
- package/src/services/abort.ts +46 -0
- package/src/{bus/types.ts → services/plugins/domain.ts} +50 -8
- package/src/services/plugins/plugin-cache.ts +13 -0
- package/src/{registry/plugins.ts → services/plugins/registry.ts} +28 -28
- package/src/services/plugins/service.ts +318 -0
- package/src/{bus/plugin.ts → services/plugins/types.ts} +7 -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/shell/index.ts +0 -123
- package/src/plugins/storage-tools/index.ts +0 -90
- package/src/plugins/todo/index.ts +0 -64
- package/src/services/plugins.ts +0 -133
- /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
|
+
};
|
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,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:
|
|
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
|
-
- `
|
|
38
|
-
|
|
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 `
|
|
44
|
-
|
|
45
|
+
plugins like `bash` to them has no effect.
|
|
46
|
+
|
|
47
|
+
## Built-in agents
|
|
45
48
|
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
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
|
-
# `
|
|
13
|
-
# `delegation`, `storage-tools
|
|
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:
|
|
17
|
-
# storage-tools,
|
|
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:
|
|
23
|
+
- id: openbot
|
|
24
24
|
config:
|
|
25
25
|
model: openai/gpt-4o-mini
|
|
26
|
-
- id:
|
|
27
|
-
- id: mcp
|
|
26
|
+
- id: bash
|
|
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: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
|
+
"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