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.
- 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/assets/icon.svg +9 -3
- package/dist/bus/services.js +78 -132
- 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 +98 -45
- 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 +41 -15
- 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 +70 -190
- package/src/assets/icon.svg +9 -3
- 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} +257 -72
- package/src/{bus/types.ts → services/plugins/domain.ts} +20 -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 -908
- package/src/harness/context.ts +0 -356
- 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') {
|
|
@@ -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
|
|
314
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
+
}
|