openbot 0.2.14 → 0.3.1
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/dist/agents/openbot/index.js +76 -0
- package/dist/agents/openbot/middleware/approval.js +132 -0
- package/dist/agents/openbot/runtime.js +289 -0
- package/dist/agents/openbot/system-prompt.js +32 -0
- package/dist/agents/openbot/tools/delegation.js +78 -0
- package/dist/agents/openbot/tools/mcp.js +99 -0
- package/dist/agents/openbot/tools/shell.js +91 -0
- package/dist/agents/openbot/tools/storage.js +75 -0
- package/dist/agents/openbot/tools/ui.js +176 -0
- package/dist/agents/system.js +20 -93
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +4 -1
- package/dist/app/server.js +15 -8
- package/dist/bus/agent-package.js +1 -0
- package/dist/bus/plugin.js +1 -0
- package/dist/bus/services.js +711 -0
- package/dist/bus/types.js +1 -0
- package/dist/harness/context.js +250 -0
- package/dist/harness/event-normalizer.js +59 -0
- package/dist/harness/orchestrator.js +27 -227
- package/dist/harness/process.js +25 -3
- package/dist/harness/queue-processor.js +227 -0
- package/dist/harness/runtime-factory.js +103 -0
- package/dist/plugins/ai-sdk/index.js +37 -0
- package/dist/plugins/ai-sdk/runtime.js +402 -0
- package/dist/plugins/ai-sdk/system-prompt.js +3 -0
- package/dist/plugins/ai-sdk.js +277 -87
- package/dist/plugins/approval/index.js +159 -0
- package/dist/plugins/approval.js +163 -0
- package/dist/plugins/delegation/index.js +79 -0
- package/dist/plugins/delegation.js +67 -11
- package/dist/plugins/mcp/index.js +108 -0
- package/dist/plugins/memory/index.js +71 -0
- package/dist/plugins/shell/index.js +99 -0
- package/dist/plugins/shell.js +123 -0
- package/dist/plugins/storage-tools/index.js +85 -0
- package/dist/plugins/storage.js +240 -5
- package/dist/plugins/ui/index.js +184 -0
- package/dist/plugins/ui.js +185 -21
- package/dist/registry/agents.js +138 -0
- package/dist/registry/plugins.js +93 -50
- package/dist/services/agent-packages.js +103 -0
- package/dist/services/memory.js +152 -0
- package/dist/services/plugins.js +98 -0
- package/dist/services/storage.js +366 -94
- package/docs/agents.md +52 -65
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +70 -58
- package/docs/templates/AGENT.example.md +57 -0
- package/package.json +8 -7
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +14 -4
- package/src/app/server.ts +23 -10
- package/src/app/types.ts +445 -16
- package/src/assets/icon.svg +4 -1
- package/src/bus/plugin.ts +67 -0
- package/src/bus/services.ts +786 -0
- package/src/bus/types.ts +160 -0
- package/src/harness/context.ts +293 -0
- package/src/harness/event-normalizer.ts +82 -0
- package/src/harness/orchestrator.ts +35 -273
- package/src/harness/process.ts +28 -4
- package/src/harness/queue-processor.ts +309 -0
- package/src/harness/runtime-factory.ts +125 -0
- package/src/plugins/ai-sdk/index.ts +44 -0
- package/src/plugins/ai-sdk/runtime.ts +484 -0
- package/src/plugins/ai-sdk/system-prompt.ts +4 -0
- package/src/plugins/approval/index.ts +228 -0
- package/src/plugins/delegation/index.ts +94 -0
- package/src/plugins/mcp/index.ts +128 -0
- package/src/plugins/memory/index.ts +85 -0
- package/src/plugins/shell/index.ts +123 -0
- package/src/plugins/storage-tools/index.ts +101 -0
- package/src/plugins/ui/index.ts +227 -0
- package/src/registry/plugins.ts +108 -55
- package/src/services/memory.ts +213 -0
- package/src/services/plugins.ts +133 -0
- package/src/services/storage.ts +472 -137
- package/src/agents/system.ts +0 -112
- package/src/plugins/ai-sdk.ts +0 -197
- package/src/plugins/delegation.ts +0 -60
- package/src/plugins/mcp.ts +0 -154
- package/src/plugins/storage.ts +0 -725
- package/src/plugins/ui.ts +0 -57
package/dist/services/storage.js
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import { DEFAULT_AGENTS_DIR, DEFAULT_BASE_DIR, DEFAULT_CHANNELS_DIR,
|
|
1
|
+
import { DEFAULT_PLUGINS_DIR, DEFAULT_AGENTS_DIR, DEFAULT_BASE_DIR, DEFAULT_CHANNELS_DIR, loadConfig, resolvePath, VARIABLES_FILE, } from '../app/config.js';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import matter from 'gray-matter';
|
|
6
|
-
import {
|
|
6
|
+
import { aiSdkPlugin } from '../plugins/ai-sdk/index.js';
|
|
7
|
+
import { AI_SDK_SYSTEM_PROMPT } from '../plugins/ai-sdk/system-prompt.js';
|
|
8
|
+
import { listBuiltInPlugins, parsePluginModule } from '../registry/plugins.js';
|
|
9
|
+
import { processService } from '../harness/process.js';
|
|
10
|
+
import { memoryService } from './memory.js';
|
|
7
11
|
import { pathToFileURL } from 'node:url';
|
|
8
|
-
const mapNameToPlugin = (name, description, kind = 'tool', image) => ({
|
|
9
|
-
id: name,
|
|
10
|
-
name,
|
|
11
|
-
description,
|
|
12
|
-
kind,
|
|
13
|
-
image,
|
|
14
|
-
createdAt: new Date(),
|
|
15
|
-
updatedAt: new Date(),
|
|
16
|
-
});
|
|
17
12
|
const resolveBaseDir = () => {
|
|
18
13
|
const config = loadConfig();
|
|
19
14
|
return resolvePath(config.baseDir || DEFAULT_BASE_DIR);
|
|
@@ -32,15 +27,6 @@ const tryReadSvgDataUrl = async (filePath) => {
|
|
|
32
27
|
return null;
|
|
33
28
|
}
|
|
34
29
|
};
|
|
35
|
-
/**
|
|
36
|
-
* Auto-discovers an entity SVG avatar and returns it as a data URL.
|
|
37
|
-
*
|
|
38
|
-
* Search order:
|
|
39
|
-
* 1) <entity>/assets/avatar.svg|icon.svg|image.svg|logo.svg
|
|
40
|
-
* 2) <entity>/avatar.svg|icon.svg|image.svg|logo.svg
|
|
41
|
-
* 3) first *.svg in <entity>/assets
|
|
42
|
-
* 4) first *.svg in <entity>
|
|
43
|
-
*/
|
|
44
30
|
const resolveEntityImageDataUrl = async (entityDir) => {
|
|
45
31
|
const preferredDirs = [path.join(entityDir, 'assets'), entityDir];
|
|
46
32
|
for (const dir of preferredDirs) {
|
|
@@ -61,7 +47,7 @@ const resolveEntityImageDataUrl = async (entityDir) => {
|
|
|
61
47
|
return dataUrl;
|
|
62
48
|
}
|
|
63
49
|
catch {
|
|
64
|
-
// ignore
|
|
50
|
+
// ignore
|
|
65
51
|
}
|
|
66
52
|
}
|
|
67
53
|
return undefined;
|
|
@@ -70,6 +56,60 @@ const getConversationDir = (channelId, threadId) => {
|
|
|
70
56
|
const base = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR + '/' + channelId);
|
|
71
57
|
return threadId ? `${base}/threads/${threadId}` : base;
|
|
72
58
|
};
|
|
59
|
+
/** Built-in orchestrator agent id. Not creatable as a normal disk agent. */
|
|
60
|
+
const SYSTEM_AGENT_ID = 'system';
|
|
61
|
+
const SYSTEM_DEFAULT_PLUGINS = [
|
|
62
|
+
{ id: 'ai-sdk', config: { model: 'openai/gpt-5.4-nano' } },
|
|
63
|
+
{ id: 'storage-tools' },
|
|
64
|
+
// { id: 'mcp' },
|
|
65
|
+
{ id: 'shell' },
|
|
66
|
+
{ id: 'delegation' },
|
|
67
|
+
// { id: 'ui' },
|
|
68
|
+
{ id: 'approval' },
|
|
69
|
+
{ id: 'memory' },
|
|
70
|
+
];
|
|
71
|
+
function getSystemAgentDetails(overrides) {
|
|
72
|
+
const defaults = {
|
|
73
|
+
id: SYSTEM_AGENT_ID,
|
|
74
|
+
name: 'OpenBot',
|
|
75
|
+
image: undefined,
|
|
76
|
+
description: 'First-party orchestration agent for OpenBot. Coordinates other agents via handoff and delegation.',
|
|
77
|
+
instructions: AI_SDK_SYSTEM_PROMPT,
|
|
78
|
+
plugins: SYSTEM_DEFAULT_PLUGINS.map((ref) => ref.id),
|
|
79
|
+
pluginRefs: SYSTEM_DEFAULT_PLUGINS,
|
|
80
|
+
createdAt: new Date(),
|
|
81
|
+
updatedAt: new Date(),
|
|
82
|
+
};
|
|
83
|
+
if (!overrides)
|
|
84
|
+
return defaults;
|
|
85
|
+
const refs = overrides.pluginRefs && overrides.pluginRefs.length > 0
|
|
86
|
+
? overrides.pluginRefs
|
|
87
|
+
: defaults.pluginRefs;
|
|
88
|
+
return {
|
|
89
|
+
...defaults,
|
|
90
|
+
...overrides,
|
|
91
|
+
id: SYSTEM_AGENT_ID,
|
|
92
|
+
image: overrides.image || defaults.image,
|
|
93
|
+
plugins: refs.map((ref) => ref.id),
|
|
94
|
+
pluginRefs: refs,
|
|
95
|
+
updatedAt: new Date(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// Suppress unused warning until system agent customization re-uses aiSdkPlugin metadata.
|
|
99
|
+
void aiSdkPlugin;
|
|
100
|
+
const RESERVED_DISK_AGENT_IDS = new Set([SYSTEM_AGENT_ID]);
|
|
101
|
+
const assertValidDiskAgentId = (agentId) => {
|
|
102
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
103
|
+
throw new Error('agentId is required');
|
|
104
|
+
}
|
|
105
|
+
if (RESERVED_DISK_AGENT_IDS.has(agentId)) {
|
|
106
|
+
throw new Error(`Agent id "${agentId}" is reserved`);
|
|
107
|
+
}
|
|
108
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
|
|
109
|
+
throw new Error('agentId must contain only letters, digits, underscores, and hyphens');
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const getAgentsRootDir = () => path.join(resolveBaseDir(), DEFAULT_AGENTS_DIR);
|
|
73
113
|
const getLastReadFilePath = () => path.join(resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR), '_meta', 'last-read.json');
|
|
74
114
|
const THREAD_TITLE_MAX_LENGTH = 80;
|
|
75
115
|
const buildThreadTitleFromEvent = (event) => {
|
|
@@ -104,25 +144,67 @@ const toVariablesRecord = (raw) => {
|
|
|
104
144
|
if (!raw || typeof raw !== 'object') {
|
|
105
145
|
return {};
|
|
106
146
|
}
|
|
107
|
-
// Current format: { version: number, variables: StoredVariable[] }
|
|
108
147
|
if ('variables' in raw && Array.isArray(raw.variables)) {
|
|
109
148
|
const entries = raw.variables
|
|
110
149
|
.filter((variable) => typeof variable?.key === 'string')
|
|
111
150
|
.map((variable) => [variable.key, String(variable.value ?? '')]);
|
|
112
151
|
return Object.fromEntries(entries);
|
|
113
152
|
}
|
|
114
|
-
// Legacy format: { [key: string]: string }
|
|
115
153
|
return Object.fromEntries(Object.entries(raw).map(([key, value]) => [
|
|
116
154
|
key,
|
|
117
155
|
String(value ?? ''),
|
|
118
156
|
]));
|
|
119
157
|
};
|
|
120
|
-
const
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
158
|
+
const listBuiltInPluginDescriptors = async () => {
|
|
159
|
+
return listBuiltInPlugins().map((plugin) => ({
|
|
160
|
+
id: plugin.id,
|
|
161
|
+
name: plugin.name,
|
|
162
|
+
description: plugin.description,
|
|
163
|
+
builtIn: true,
|
|
164
|
+
image: plugin.image,
|
|
165
|
+
defaultInstructions: plugin.defaultInstructions,
|
|
166
|
+
configSchema: plugin.configSchema,
|
|
167
|
+
createdAt: new Date(),
|
|
168
|
+
updatedAt: new Date(),
|
|
169
|
+
}));
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Walk `plugins/` and yield candidate plugin ids (npm names). Includes scoped
|
|
173
|
+
* packages by recursing one level into directories starting with `@`.
|
|
174
|
+
*/
|
|
175
|
+
const listInstalledPluginIds = async (pluginsDir) => {
|
|
176
|
+
const out = [];
|
|
177
|
+
let topEntries;
|
|
178
|
+
try {
|
|
179
|
+
topEntries = await fs.readdir(pluginsDir, { withFileTypes: true });
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
for (const entry of topEntries) {
|
|
185
|
+
if (entry.name.startsWith('.'))
|
|
186
|
+
continue;
|
|
187
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
188
|
+
continue;
|
|
189
|
+
if (entry.name.startsWith('@')) {
|
|
190
|
+
try {
|
|
191
|
+
const inner = await fs.readdir(path.join(pluginsDir, entry.name), { withFileTypes: true });
|
|
192
|
+
for (const sub of inner) {
|
|
193
|
+
if (sub.name.startsWith('.'))
|
|
194
|
+
continue;
|
|
195
|
+
if (sub.isDirectory() || sub.isSymbolicLink()) {
|
|
196
|
+
out.push(`${entry.name}/${sub.name}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// ignore
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
out.push(entry.name);
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
126
208
|
};
|
|
127
209
|
const listPluginsFromDisk = async () => {
|
|
128
210
|
const pluginsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_PLUGINS_DIR);
|
|
@@ -132,17 +214,57 @@ const listPluginsFromDisk = async () => {
|
|
|
132
214
|
catch {
|
|
133
215
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
134
216
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
217
|
+
const ids = await listInstalledPluginIds(pluginsDir);
|
|
218
|
+
const descriptors = await Promise.all(ids.map(async (id) => {
|
|
219
|
+
try {
|
|
220
|
+
const pluginDir = path.join(pluginsDir, id);
|
|
221
|
+
const distPath = path.join(pluginDir, 'dist', 'index.js');
|
|
222
|
+
const module = await import(pathToFileURL(distPath).href);
|
|
223
|
+
const parsed = parsePluginModule(module);
|
|
224
|
+
if (!parsed)
|
|
225
|
+
return null;
|
|
226
|
+
const image = await resolveEntityImageDataUrl(pluginDir);
|
|
227
|
+
return {
|
|
228
|
+
id,
|
|
229
|
+
name: parsed.name || id,
|
|
230
|
+
description: parsed.description || '',
|
|
231
|
+
builtIn: false,
|
|
232
|
+
image: parsed.image || image,
|
|
233
|
+
defaultInstructions: parsed.defaultInstructions,
|
|
234
|
+
configSchema: parsed.configSchema,
|
|
235
|
+
createdAt: new Date(),
|
|
236
|
+
updatedAt: new Date(),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
console.warn(`[storage] Failed to load plugin ${id}:`, error);
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}));
|
|
244
|
+
return descriptors.filter((d) => d !== null);
|
|
245
|
+
};
|
|
246
|
+
const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
247
|
+
/**
|
|
248
|
+
* Parse the `plugins:` array from AGENT.md frontmatter. Each entry must have an
|
|
249
|
+
* `id`; `config` is optional. Strings are accepted as a shorthand for `{ id }`.
|
|
250
|
+
*/
|
|
251
|
+
const parsePluginRefs = (raw) => {
|
|
252
|
+
if (!Array.isArray(raw))
|
|
253
|
+
return [];
|
|
254
|
+
const refs = [];
|
|
255
|
+
for (const entry of raw) {
|
|
256
|
+
if (typeof entry === 'string' && entry.trim()) {
|
|
257
|
+
refs.push({ id: entry.trim() });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (isRecord(entry) && typeof entry.id === 'string' && entry.id.trim()) {
|
|
261
|
+
const config = isRecord(entry.config) ? entry.config : undefined;
|
|
262
|
+
refs.push({ id: entry.id.trim(), ...(config ? { config } : {}) });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return refs;
|
|
145
266
|
};
|
|
267
|
+
const serializePluginRefs = (refs) => refs.map((ref) => (ref.config ? { id: ref.id, config: ref.config } : { id: ref.id }));
|
|
146
268
|
export const storageService = {
|
|
147
269
|
getLastReadByChannel: async () => {
|
|
148
270
|
return readJsonFile(getLastReadFilePath(), {});
|
|
@@ -174,7 +296,7 @@ export const storageService = {
|
|
|
174
296
|
cwd = typeof state.cwd === 'string' ? state.cwd : undefined;
|
|
175
297
|
}
|
|
176
298
|
catch {
|
|
177
|
-
//
|
|
299
|
+
// ignore
|
|
178
300
|
}
|
|
179
301
|
const channel = {
|
|
180
302
|
id: name,
|
|
@@ -194,7 +316,6 @@ export const storageService = {
|
|
|
194
316
|
channel.hasUnseenMessages = false;
|
|
195
317
|
}
|
|
196
318
|
try {
|
|
197
|
-
// Fetch up to 5 most recent threads for the sidebar
|
|
198
319
|
const allThreads = await storageService.getThreads({ channelId: name });
|
|
199
320
|
channel.recentThreads = allThreads
|
|
200
321
|
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
|
|
@@ -220,7 +341,8 @@ export const storageService = {
|
|
|
220
341
|
throw new Error(`Channel "${normalizedChannelId}" already exists`);
|
|
221
342
|
}
|
|
222
343
|
catch (error) {
|
|
223
|
-
|
|
344
|
+
const code = error?.code;
|
|
345
|
+
if (code !== 'ENOENT') {
|
|
224
346
|
throw error;
|
|
225
347
|
}
|
|
226
348
|
}
|
|
@@ -238,12 +360,10 @@ export const storageService = {
|
|
|
238
360
|
createThread: async ({ channelId, threadId, threadTitle, spec, initialState, }) => {
|
|
239
361
|
const normalizedChannelId = channelId.trim();
|
|
240
362
|
const normalizedThreadId = threadId.trim();
|
|
241
|
-
if (!normalizedChannelId)
|
|
363
|
+
if (!normalizedChannelId)
|
|
242
364
|
throw new Error('channelId is required');
|
|
243
|
-
|
|
244
|
-
if (!normalizedThreadId) {
|
|
365
|
+
if (!normalizedThreadId)
|
|
245
366
|
throw new Error('threadId is required');
|
|
246
|
-
}
|
|
247
367
|
const threadDir = getConversationDir(normalizedChannelId, normalizedThreadId);
|
|
248
368
|
const specPath = `${threadDir}/SPEC.md`;
|
|
249
369
|
const statePath = `${threadDir}/state.json`;
|
|
@@ -252,7 +372,8 @@ export const storageService = {
|
|
|
252
372
|
throw new Error(`Thread "${normalizedThreadId}" already exists in channel "${normalizedChannelId}"`);
|
|
253
373
|
}
|
|
254
374
|
catch (error) {
|
|
255
|
-
|
|
375
|
+
const code = error?.code;
|
|
376
|
+
if (code !== 'ENOENT') {
|
|
256
377
|
throw error;
|
|
257
378
|
}
|
|
258
379
|
}
|
|
@@ -288,7 +409,7 @@ export const storageService = {
|
|
|
288
409
|
}
|
|
289
410
|
}
|
|
290
411
|
catch (error) {
|
|
291
|
-
if (error
|
|
412
|
+
if (error?.code !== 'ENOENT') {
|
|
292
413
|
console.error(`Failed to read thread state for channel ${channelId} thread ${name}`, error);
|
|
293
414
|
}
|
|
294
415
|
}
|
|
@@ -311,7 +432,7 @@ export const storageService = {
|
|
|
311
432
|
spec = await fs.readFile(specPath, 'utf-8');
|
|
312
433
|
}
|
|
313
434
|
catch (error) {
|
|
314
|
-
if (error
|
|
435
|
+
if (error?.code !== 'ENOENT') {
|
|
315
436
|
console.error(`Failed to read thread spec for channel ${channelId} thread ${threadId}`, error);
|
|
316
437
|
}
|
|
317
438
|
}
|
|
@@ -321,11 +442,11 @@ export const storageService = {
|
|
|
321
442
|
state = JSON.parse(stateContent);
|
|
322
443
|
}
|
|
323
444
|
catch (error) {
|
|
324
|
-
if (error
|
|
445
|
+
if (error?.code !== 'ENOENT') {
|
|
325
446
|
console.error(`Failed to read thread state for channel ${channelId} thread ${threadId}`, error);
|
|
326
447
|
}
|
|
327
448
|
}
|
|
328
|
-
const generatedName = typeof state.generatedName === 'string'
|
|
449
|
+
const generatedName = isRecord(state) && typeof state.generatedName === 'string'
|
|
329
450
|
? state.generatedName.trim()
|
|
330
451
|
: '';
|
|
331
452
|
return {
|
|
@@ -345,7 +466,7 @@ export const storageService = {
|
|
|
345
466
|
spec = await fs.readFile(specPath, 'utf-8');
|
|
346
467
|
}
|
|
347
468
|
catch (error) {
|
|
348
|
-
if (error
|
|
469
|
+
if (error?.code !== 'ENOENT') {
|
|
349
470
|
console.error(`Failed to read spec file for channel ${channelId}`, error);
|
|
350
471
|
}
|
|
351
472
|
}
|
|
@@ -355,16 +476,17 @@ export const storageService = {
|
|
|
355
476
|
state = JSON.parse(stateContent);
|
|
356
477
|
}
|
|
357
478
|
catch (error) {
|
|
358
|
-
if (error
|
|
479
|
+
if (error?.code !== 'ENOENT') {
|
|
359
480
|
console.error(`Failed to read state file for channel ${channelId}`, error);
|
|
360
481
|
}
|
|
361
482
|
}
|
|
483
|
+
const cwd = isRecord(state) && typeof state.cwd === 'string' ? state.cwd : undefined;
|
|
362
484
|
const details = {
|
|
363
485
|
id: channelId,
|
|
364
486
|
name: channelId,
|
|
365
487
|
spec,
|
|
366
488
|
state,
|
|
367
|
-
cwd
|
|
489
|
+
cwd,
|
|
368
490
|
};
|
|
369
491
|
details.threads = await storageService.getThreads({ channelId });
|
|
370
492
|
return details;
|
|
@@ -373,15 +495,12 @@ export const storageService = {
|
|
|
373
495
|
const channelDir = getConversationDir(channelId);
|
|
374
496
|
const statePath = `${channelDir}/state.json`;
|
|
375
497
|
try {
|
|
376
|
-
// 1. Fetch current details to get the existing state
|
|
377
498
|
const currentDetails = await storageService.getChannelDetails({ channelId });
|
|
378
499
|
const currentState = currentDetails.state || {};
|
|
379
|
-
// 2. Perform a shallow merge (patch)
|
|
380
500
|
const newState = {
|
|
381
501
|
...currentState,
|
|
382
502
|
...patch,
|
|
383
503
|
};
|
|
384
|
-
// 3. Write back the merged state
|
|
385
504
|
await fs.mkdir(channelDir, { recursive: true });
|
|
386
505
|
await fs.writeFile(statePath, JSON.stringify(newState, null, 2));
|
|
387
506
|
}
|
|
@@ -394,15 +513,12 @@ export const storageService = {
|
|
|
394
513
|
const threadDir = getConversationDir(channelId, threadId);
|
|
395
514
|
const statePath = `${threadDir}/state.json`;
|
|
396
515
|
try {
|
|
397
|
-
// 1. Fetch current details to get the existing state
|
|
398
516
|
const currentDetails = await storageService.getThreadDetails({ channelId, threadId });
|
|
399
517
|
const currentState = currentDetails.state || {};
|
|
400
|
-
// 2. Perform a shallow merge (patch)
|
|
401
518
|
const newState = {
|
|
402
519
|
...currentState,
|
|
403
520
|
...patch,
|
|
404
521
|
};
|
|
405
|
-
// 3. Write back the merged state
|
|
406
522
|
await fs.mkdir(threadDir, { recursive: true });
|
|
407
523
|
await fs.writeFile(statePath, JSON.stringify(newState, null, 2));
|
|
408
524
|
}
|
|
@@ -452,7 +568,7 @@ export const storageService = {
|
|
|
452
568
|
name: details.name || id,
|
|
453
569
|
description: details.description || '',
|
|
454
570
|
image: details.image,
|
|
455
|
-
|
|
571
|
+
plugins: details.plugins,
|
|
456
572
|
createdAt: details.createdAt,
|
|
457
573
|
updatedAt: details.updatedAt,
|
|
458
574
|
};
|
|
@@ -462,18 +578,19 @@ export const storageService = {
|
|
|
462
578
|
id,
|
|
463
579
|
name: id,
|
|
464
580
|
description: '',
|
|
581
|
+
plugins: [],
|
|
465
582
|
createdAt: new Date(),
|
|
466
583
|
updatedAt: new Date(),
|
|
467
584
|
};
|
|
468
585
|
}
|
|
469
586
|
}));
|
|
470
|
-
const system = await storageService.getAgentDetails({ agentId:
|
|
587
|
+
const system = await storageService.getAgentDetails({ agentId: SYSTEM_AGENT_ID });
|
|
471
588
|
const builtInSystemAgent = {
|
|
472
589
|
id: system.id,
|
|
473
590
|
name: system.name,
|
|
474
591
|
description: system.description || '',
|
|
475
592
|
image: system.image,
|
|
476
|
-
|
|
593
|
+
plugins: system.plugins,
|
|
477
594
|
createdAt: system.createdAt,
|
|
478
595
|
updatedAt: system.updatedAt,
|
|
479
596
|
};
|
|
@@ -486,11 +603,11 @@ export const storageService = {
|
|
|
486
603
|
return Array.from(deduped.values());
|
|
487
604
|
},
|
|
488
605
|
getPlugins: async () => {
|
|
489
|
-
const [
|
|
490
|
-
|
|
606
|
+
const [builtIn, fromDisk] = await Promise.all([
|
|
607
|
+
listBuiltInPluginDescriptors(),
|
|
491
608
|
listPluginsFromDisk(),
|
|
492
609
|
]);
|
|
493
|
-
const merged = [...
|
|
610
|
+
const merged = [...builtIn, ...fromDisk];
|
|
494
611
|
const deduped = new Map();
|
|
495
612
|
for (const plugin of merged) {
|
|
496
613
|
if (!deduped.has(plugin.id)) {
|
|
@@ -508,26 +625,30 @@ export const storageService = {
|
|
|
508
625
|
const agentMd = await fs.readFile(agentMdPath, 'utf-8');
|
|
509
626
|
const { data, content: instructions } = matter(agentMd);
|
|
510
627
|
const discoveredImage = await resolveEntityImageDataUrl(agentDir);
|
|
628
|
+
const stats = await fs.stat(agentMdPath);
|
|
629
|
+
const pluginRefs = parsePluginRefs(data.plugins);
|
|
511
630
|
diskDetails = {
|
|
512
631
|
id: agentId,
|
|
513
|
-
name: data.name
|
|
632
|
+
name: typeof data.name === 'string' ? data.name : agentId,
|
|
514
633
|
instructions: instructions.trim(),
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
description: data.description
|
|
634
|
+
plugins: pluginRefs.map((ref) => ref.id),
|
|
635
|
+
pluginRefs,
|
|
636
|
+
description: typeof data.description === 'string' ? data.description : '',
|
|
518
637
|
image: discoveredImage || undefined,
|
|
519
|
-
createdAt:
|
|
520
|
-
updatedAt:
|
|
638
|
+
createdAt: stats.birthtime,
|
|
639
|
+
updatedAt: stats.mtime,
|
|
521
640
|
};
|
|
522
641
|
}
|
|
523
642
|
catch (error) {
|
|
524
|
-
if (agentId !==
|
|
643
|
+
if (agentId !== SYSTEM_AGENT_ID) {
|
|
525
644
|
const err = new Error(`Agent "${agentId}" does not exist.`);
|
|
526
645
|
err.code = 'AGENT_NOT_FOUND';
|
|
527
646
|
throw err;
|
|
528
647
|
}
|
|
648
|
+
// swallow: system agent has on-disk overrides optional
|
|
649
|
+
void error;
|
|
529
650
|
}
|
|
530
|
-
if (agentId ===
|
|
651
|
+
if (agentId === SYSTEM_AGENT_ID) {
|
|
531
652
|
return getSystemAgentDetails(diskDetails);
|
|
532
653
|
}
|
|
533
654
|
if (!diskDetails) {
|
|
@@ -537,6 +658,103 @@ export const storageService = {
|
|
|
537
658
|
}
|
|
538
659
|
return diskDetails;
|
|
539
660
|
},
|
|
661
|
+
createAgent: async ({ agentId, name, description = '', instructions, plugins, }) => {
|
|
662
|
+
assertValidDiskAgentId(agentId);
|
|
663
|
+
const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
|
|
664
|
+
const agentMdPath = path.join(agentDir, 'AGENT.md');
|
|
665
|
+
try {
|
|
666
|
+
await fs.access(agentMdPath);
|
|
667
|
+
throw new Error(`Agent "${agentId}" already exists`);
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
const code = error?.code;
|
|
671
|
+
if (code === 'ENOENT') {
|
|
672
|
+
// proceed
|
|
673
|
+
}
|
|
674
|
+
else if (error instanceof Error && error.message.includes('already exists')) {
|
|
675
|
+
throw error;
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
await fs.mkdir(agentDir, { recursive: true });
|
|
682
|
+
const data = {
|
|
683
|
+
name,
|
|
684
|
+
description,
|
|
685
|
+
plugins: serializePluginRefs(plugins),
|
|
686
|
+
};
|
|
687
|
+
const body = matter.stringify(`${instructions.trim()}\n`, data);
|
|
688
|
+
await fs.writeFile(agentMdPath, body, 'utf-8');
|
|
689
|
+
},
|
|
690
|
+
updateAgent: async ({ agentId, name, description, instructions, plugins, }) => {
|
|
691
|
+
assertValidDiskAgentId(agentId);
|
|
692
|
+
const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
|
|
693
|
+
const agentMdPath = path.join(agentDir, 'AGENT.md');
|
|
694
|
+
let raw;
|
|
695
|
+
try {
|
|
696
|
+
raw = await fs.readFile(agentMdPath, 'utf-8');
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
if (error?.code === 'ENOENT') {
|
|
700
|
+
const err = new Error(`Agent "${agentId}" does not exist.`);
|
|
701
|
+
err.code = 'AGENT_NOT_FOUND';
|
|
702
|
+
throw err;
|
|
703
|
+
}
|
|
704
|
+
throw error;
|
|
705
|
+
}
|
|
706
|
+
const parsed = matter(raw);
|
|
707
|
+
const nextData = { ...parsed.data };
|
|
708
|
+
if (name !== undefined)
|
|
709
|
+
nextData.name = name;
|
|
710
|
+
if (description !== undefined)
|
|
711
|
+
nextData.description = description;
|
|
712
|
+
if (plugins !== undefined)
|
|
713
|
+
nextData.plugins = serializePluginRefs(plugins);
|
|
714
|
+
const nextContent = instructions !== undefined ? instructions : parsed.content;
|
|
715
|
+
const body = matter.stringify(`${String(nextContent).trim()}\n`, nextData);
|
|
716
|
+
await fs.writeFile(agentMdPath, body, 'utf-8');
|
|
717
|
+
},
|
|
718
|
+
deleteAgent: async ({ agentId }) => {
|
|
719
|
+
assertValidDiskAgentId(agentId);
|
|
720
|
+
const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
|
|
721
|
+
const agentMdPath = path.join(agentDir, 'AGENT.md');
|
|
722
|
+
const packageJsonPath = path.join(agentDir, 'package.json');
|
|
723
|
+
try {
|
|
724
|
+
await fs.access(agentDir);
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
if (error?.code === 'ENOENT') {
|
|
728
|
+
const err = new Error(`Agent "${agentId}" does not exist.`);
|
|
729
|
+
err.code = 'AGENT_NOT_FOUND';
|
|
730
|
+
throw err;
|
|
731
|
+
}
|
|
732
|
+
throw error;
|
|
733
|
+
}
|
|
734
|
+
let hasPackage = false;
|
|
735
|
+
let hasAgentMd = false;
|
|
736
|
+
try {
|
|
737
|
+
await fs.access(packageJsonPath);
|
|
738
|
+
hasPackage = true;
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
// ignore
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
await fs.access(agentMdPath);
|
|
745
|
+
hasAgentMd = true;
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
// ignore
|
|
749
|
+
}
|
|
750
|
+
if (hasPackage && !hasAgentMd) {
|
|
751
|
+
throw new Error(`Cannot delete TypeScript agent package "${agentId}" through this action; remove the folder manually.`);
|
|
752
|
+
}
|
|
753
|
+
if (!hasAgentMd) {
|
|
754
|
+
throw new Error(`Agent "${agentId}" has no AGENT.md and cannot be deleted through this action.`);
|
|
755
|
+
}
|
|
756
|
+
await fs.rm(agentDir, { recursive: true, force: true });
|
|
757
|
+
},
|
|
540
758
|
getEvents: async ({ channelId, threadId, }) => {
|
|
541
759
|
try {
|
|
542
760
|
const threadDir = getConversationDir(channelId, threadId);
|
|
@@ -554,22 +772,18 @@ export const storageService = {
|
|
|
554
772
|
}
|
|
555
773
|
return event;
|
|
556
774
|
});
|
|
557
|
-
// If we are at the channel level (no threadId), check which events have threads
|
|
558
775
|
if (!threadId) {
|
|
559
776
|
const threadsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR + '/' + channelId + '/threads');
|
|
560
777
|
try {
|
|
561
778
|
const threadDirs = await fs.readdir(threadsDir);
|
|
562
779
|
const threadSet = new Set(threadDirs);
|
|
563
780
|
return events.map((event) => {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const threadId = event.id;
|
|
567
|
-
// If an explicit threadId exists and has a directory, use it
|
|
568
|
-
if (threadId && threadSet.has(threadId)) {
|
|
781
|
+
const eventThreadId = event.id;
|
|
782
|
+
if (eventThreadId && threadSet.has(eventThreadId)) {
|
|
569
783
|
return {
|
|
570
784
|
...event,
|
|
571
785
|
meta: {
|
|
572
|
-
...event
|
|
786
|
+
...(event.meta || {}),
|
|
573
787
|
hasThread: true,
|
|
574
788
|
},
|
|
575
789
|
};
|
|
@@ -578,14 +792,13 @@ export const storageService = {
|
|
|
578
792
|
});
|
|
579
793
|
}
|
|
580
794
|
catch {
|
|
581
|
-
// No threads folder or other error, just return events as is
|
|
582
795
|
return events;
|
|
583
796
|
}
|
|
584
797
|
}
|
|
585
798
|
return events;
|
|
586
799
|
}
|
|
587
800
|
catch (error) {
|
|
588
|
-
if (error
|
|
801
|
+
if (error?.code !== 'ENOENT') {
|
|
589
802
|
console.error(`Failed to get events for channel ${channelId} thread ${threadId}`, error);
|
|
590
803
|
}
|
|
591
804
|
return [];
|
|
@@ -599,7 +812,7 @@ export const storageService = {
|
|
|
599
812
|
await fs.access(threadDir);
|
|
600
813
|
}
|
|
601
814
|
catch (error) {
|
|
602
|
-
if (error
|
|
815
|
+
if (error?.code === 'ENOENT') {
|
|
603
816
|
const threadTitle = buildThreadTitleFromEvent(event);
|
|
604
817
|
await storageService.createThread({
|
|
605
818
|
channelId,
|
|
@@ -615,7 +828,6 @@ export const storageService = {
|
|
|
615
828
|
else {
|
|
616
829
|
await fs.mkdir(threadDir, { recursive: true });
|
|
617
830
|
}
|
|
618
|
-
// Ensure the event has a unique ID
|
|
619
831
|
if (!event.id) {
|
|
620
832
|
event.id = crypto.randomUUID();
|
|
621
833
|
}
|
|
@@ -629,15 +841,70 @@ export const storageService = {
|
|
|
629
841
|
getVariables: async () => {
|
|
630
842
|
const variablesFilePath = resolvePath(resolveBaseDir() + '/' + VARIABLES_FILE);
|
|
631
843
|
const raw = await readJsonFile(variablesFilePath, {});
|
|
632
|
-
if (raw &&
|
|
633
|
-
|
|
844
|
+
if (raw &&
|
|
845
|
+
typeof raw === 'object' &&
|
|
846
|
+
'variables' in raw &&
|
|
847
|
+
Array.isArray(raw.variables)) {
|
|
848
|
+
const entries = (raw.variables)
|
|
634
849
|
.filter((v) => typeof v?.key === 'string')
|
|
635
850
|
.map((v) => [v.key, { value: String(v.value ?? ''), secret: !!v.secret }]);
|
|
636
851
|
return Object.fromEntries(entries);
|
|
637
852
|
}
|
|
638
|
-
// Legacy or simple format
|
|
639
853
|
return toVariablesRecord(raw);
|
|
640
854
|
},
|
|
855
|
+
createVariable: async ({ key, value, secret = false, }) => {
|
|
856
|
+
const variablesFilePath = resolvePath(resolveBaseDir() + '/' + VARIABLES_FILE);
|
|
857
|
+
const raw = await readJsonFile(variablesFilePath, { version: 1, variables: [] });
|
|
858
|
+
let variables = [];
|
|
859
|
+
if (raw &&
|
|
860
|
+
typeof raw === 'object' &&
|
|
861
|
+
'variables' in raw &&
|
|
862
|
+
Array.isArray(raw.variables)) {
|
|
863
|
+
variables = raw.variables;
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
variables = Object.entries(toVariablesRecord(raw)).map(([k, v]) => ({
|
|
867
|
+
key: k,
|
|
868
|
+
value: v,
|
|
869
|
+
secret: false,
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
const existingIndex = variables.findIndex((v) => v.key === key);
|
|
873
|
+
if (existingIndex !== -1) {
|
|
874
|
+
variables[existingIndex] = { key, value, secret };
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
variables.push({ key, value, secret });
|
|
878
|
+
}
|
|
879
|
+
await fs.mkdir(path.dirname(variablesFilePath), { recursive: true });
|
|
880
|
+
await fs.writeFile(variablesFilePath, JSON.stringify({ version: 1, variables }, null, 2), 'utf-8');
|
|
881
|
+
processService.syncWorkspaceVariablesToProcessEnv();
|
|
882
|
+
},
|
|
883
|
+
deleteVariable: async ({ key }) => {
|
|
884
|
+
const variablesFilePath = resolvePath(resolveBaseDir() + '/' + VARIABLES_FILE);
|
|
885
|
+
const raw = await readJsonFile(variablesFilePath, { version: 1, variables: [] });
|
|
886
|
+
let variables = [];
|
|
887
|
+
if (raw &&
|
|
888
|
+
typeof raw === 'object' &&
|
|
889
|
+
'variables' in raw &&
|
|
890
|
+
Array.isArray(raw.variables)) {
|
|
891
|
+
variables = raw.variables;
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
variables = Object.entries(toVariablesRecord(raw)).map(([k, v]) => ({
|
|
895
|
+
key: k,
|
|
896
|
+
value: v,
|
|
897
|
+
secret: false,
|
|
898
|
+
}));
|
|
899
|
+
}
|
|
900
|
+
const newVariables = variables.filter((v) => v.key !== key);
|
|
901
|
+
if (newVariables.length === variables.length) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
await fs.mkdir(path.dirname(variablesFilePath), { recursive: true });
|
|
905
|
+
await fs.writeFile(variablesFilePath, JSON.stringify({ version: 1, variables: newVariables }, null, 2), 'utf-8');
|
|
906
|
+
processService.syncWorkspaceVariablesToProcessEnv();
|
|
907
|
+
},
|
|
641
908
|
listFiles: async ({ channelId, path: subPath = '', }) => {
|
|
642
909
|
const details = await storageService.getChannelDetails({ channelId });
|
|
643
910
|
const baseCwd = details.cwd;
|
|
@@ -646,19 +913,18 @@ export const storageService = {
|
|
|
646
913
|
}
|
|
647
914
|
const resolvedBase = path.resolve(baseCwd);
|
|
648
915
|
const targetDir = path.resolve(resolvedBase, subPath);
|
|
649
|
-
// Security check: ensure target is within baseCwd
|
|
650
916
|
if (!targetDir.startsWith(resolvedBase)) {
|
|
651
917
|
throw new Error('Access denied: directory escape');
|
|
652
918
|
}
|
|
653
919
|
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
|
654
920
|
return entries
|
|
655
|
-
.filter((e) => !e.name.startsWith('.'))
|
|
921
|
+
.filter((e) => !e.name.startsWith('.'))
|
|
656
922
|
.map((e) => ({
|
|
657
923
|
name: e.name,
|
|
658
924
|
isDirectory: e.isDirectory(),
|
|
659
925
|
}));
|
|
660
926
|
},
|
|
661
|
-
readFile: async ({ channelId, path: filePath }) => {
|
|
927
|
+
readFile: async ({ channelId, path: filePath, }) => {
|
|
662
928
|
const details = await storageService.getChannelDetails({ channelId });
|
|
663
929
|
const baseCwd = details.cwd;
|
|
664
930
|
if (!baseCwd) {
|
|
@@ -666,12 +932,15 @@ export const storageService = {
|
|
|
666
932
|
}
|
|
667
933
|
const resolvedBase = path.resolve(baseCwd);
|
|
668
934
|
const targetFile = path.resolve(resolvedBase, filePath);
|
|
669
|
-
// Security check: ensure target is within baseCwd
|
|
670
935
|
if (!targetFile.startsWith(resolvedBase)) {
|
|
671
936
|
throw new Error('Access denied: directory escape');
|
|
672
937
|
}
|
|
673
938
|
return fs.readFile(targetFile, 'utf-8');
|
|
674
939
|
},
|
|
940
|
+
appendMemory: memoryService.appendMemory,
|
|
941
|
+
listMemories: memoryService.listMemories,
|
|
942
|
+
deleteMemory: memoryService.deleteMemory,
|
|
943
|
+
updateMemory: memoryService.updateMemory,
|
|
675
944
|
/**
|
|
676
945
|
* Hydrates the full OpenBot state from disk/storage before a run.
|
|
677
946
|
*/
|
|
@@ -686,7 +955,7 @@ export const storageService = {
|
|
|
686
955
|
throw error;
|
|
687
956
|
}
|
|
688
957
|
let channelDetails;
|
|
689
|
-
if (channelId
|
|
958
|
+
if (channelId) {
|
|
690
959
|
try {
|
|
691
960
|
channelDetails = await storageService.getChannelDetails({ channelId });
|
|
692
961
|
}
|
|
@@ -713,9 +982,12 @@ export const storageService = {
|
|
|
713
982
|
id: agentDetails.id,
|
|
714
983
|
name: agentDetails.name,
|
|
715
984
|
description: agentDetails.description || '',
|
|
985
|
+
image: agentDetails.image,
|
|
716
986
|
instructions: agentDetails.instructions || '',
|
|
717
|
-
runtime: agentDetails.runtime,
|
|
718
987
|
plugins: agentDetails.plugins,
|
|
988
|
+
pluginRefs: agentDetails.pluginRefs,
|
|
989
|
+
createdAt: agentDetails.createdAt,
|
|
990
|
+
updatedAt: agentDetails.updatedAt,
|
|
719
991
|
},
|
|
720
992
|
channelDetails: channelDetails,
|
|
721
993
|
threadDetails: threadDetails,
|