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
package/README.md
CHANGED
|
@@ -18,7 +18,8 @@ OpenBot is a local-first harness for running AI agents. It is built around a sma
|
|
|
18
18
|
|
|
19
19
|
- Runs a local agent server.
|
|
20
20
|
- Stores channels, threads, agents, plugins, config, and variables under `~/.openbot`.
|
|
21
|
-
- Ships with a built-in `system` agent named OpenBot.
|
|
21
|
+
- Ships with a built-in `system` agent named OpenBot (orchestrator, includes the LLM runtime).
|
|
22
|
+
- Ships with a built-in `state` agent for deterministic, non-LLM handling (e.g. `/api/state` defaults).
|
|
22
23
|
- Loads custom agents from `~/.openbot/agents/<agent-id>/AGENT.md`.
|
|
23
24
|
- Loads shared plugins from `~/.openbot/plugins`.
|
|
24
25
|
- Streams events to clients with Server-Sent Events.
|
|
@@ -50,15 +51,17 @@ npm run dev
|
|
|
50
51
|
OpenBot intentionally keeps the public API small:
|
|
51
52
|
|
|
52
53
|
- `GET /api/events` opens an SSE stream for a channel or thread.
|
|
53
|
-
- `POST /api/publish` publishes an event into the harness.
|
|
54
|
-
- `GET /api/state` runs an event and returns the resulting events without opening a stream.
|
|
54
|
+
- `POST /api/publish` publishes an event into the harness (defaults to the built-in `system` agent with the OpenBot / LLM runtime).
|
|
55
|
+
- `GET /api/state` runs an event and returns the resulting events without opening a stream (defaults to the built-in `state` agent: storage-oriented plugins, no LLM).
|
|
56
|
+
|
|
57
|
+
You can override the agent with `agentId` (header, query, or body where applicable).
|
|
55
58
|
|
|
56
59
|
Example:
|
|
57
60
|
|
|
58
61
|
```bash
|
|
59
62
|
curl -X POST http://localhost:4132/api/publish \
|
|
60
63
|
-H "content-type: application/json" \
|
|
61
|
-
-d '{"type":"
|
|
64
|
+
-d '{"type":"agent:invoke","data":{"role":"user","content":"hello"}}'
|
|
62
65
|
```
|
|
63
66
|
|
|
64
67
|
Useful context can be passed as headers, query params, or body fields:
|
|
@@ -76,8 +79,7 @@ OpenBot reads config from `~/.openbot/config.json`.
|
|
|
76
79
|
{
|
|
77
80
|
"port": 4132,
|
|
78
81
|
"baseDir": "~/.openbot",
|
|
79
|
-
"model": "openai/gpt-4o-mini"
|
|
80
|
-
"mcpServers": []
|
|
82
|
+
"model": "openai/gpt-4o-mini"
|
|
81
83
|
}
|
|
82
84
|
```
|
|
83
85
|
|
|
@@ -91,12 +93,11 @@ The built-in `system` agent is always available. Add a custom agent by creating
|
|
|
91
93
|
---
|
|
92
94
|
name: Researcher
|
|
93
95
|
description: Helps collect and summarize information.
|
|
94
|
-
runtime:
|
|
95
|
-
name: ai-sdk
|
|
96
|
-
config:
|
|
97
|
-
model: openai/gpt-4o-mini
|
|
98
96
|
plugins:
|
|
99
|
-
-
|
|
97
|
+
- id: openbot
|
|
98
|
+
config:
|
|
99
|
+
model: openai/gpt-4o-mini
|
|
100
|
+
- id: storage
|
|
100
101
|
---
|
|
101
102
|
|
|
102
103
|
You are a careful research assistant.
|
|
@@ -109,18 +110,16 @@ Agents are discovered from disk when the server starts.
|
|
|
109
110
|
|
|
110
111
|
Built-in plugins include:
|
|
111
112
|
|
|
112
|
-
- `storage`
|
|
113
|
+
- `storage-tools`
|
|
113
114
|
- `delegation`
|
|
114
|
-
- `
|
|
115
|
-
- `ui`
|
|
116
|
-
- `ai-sdk`
|
|
115
|
+
- `openbot`
|
|
117
116
|
|
|
118
117
|
Shared plugins can be placed in `~/.openbot/plugins` and referenced by agents.
|
|
119
118
|
|
|
120
119
|
## Project Layout
|
|
121
120
|
|
|
122
121
|
- `src/app`: CLI, server, event types, and app config.
|
|
123
|
-
- `src/harness`: orchestration
|
|
122
|
+
- `src/harness`: orchestration and process helpers.
|
|
124
123
|
- `src/plugins`: built-in plugin implementations.
|
|
125
124
|
- `src/services`: local storage service.
|
|
126
125
|
- `src/registry`: plugin registry.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Built-in orchestrator agent id. Optional `agents/system/AGENT.md` overrides code defaults. */
|
|
2
|
+
export const ORCHESTRATOR_AGENT_ID = 'system';
|
|
3
|
+
/** Built-in infra agent for deterministic `/api/state` and marketplace/plugin lifecycle; optional AGENT.md overlay. */
|
|
4
|
+
export const STATE_AGENT_ID = 'state';
|
package/dist/app/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ function checkNodeVersion() {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
checkNodeVersion();
|
|
19
|
-
program.name('openbot').description('OpenBot CLI').version('0.
|
|
19
|
+
program.name('openbot').description('OpenBot CLI').version('0.4.2');
|
|
20
20
|
program
|
|
21
21
|
.command('start')
|
|
22
22
|
.description('Start the OpenBot harness')
|
package/dist/app/config.js
CHANGED
|
@@ -2,6 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
export const DEFAULT_BASE_DIR = '~/.openbot';
|
|
5
|
+
/** Default parent directory for per-channel working directories (user-facing workspace). */
|
|
6
|
+
export const DEFAULT_CHANNELS_WORKSPACE_DIR = '~/openbot';
|
|
5
7
|
export const DEFAULT_PLUGINS_DIR = 'plugins';
|
|
6
8
|
export const DEFAULT_AGENTS_DIR = 'agents';
|
|
7
9
|
export const DEFAULT_CHANNELS_DIR = 'channels';
|
|
@@ -12,6 +14,14 @@ export const DEFAULT_MARKETPLACE_REGISTRY_URL = 'https://raw.githubusercontent.c
|
|
|
12
14
|
export function resolvePath(p) {
|
|
13
15
|
return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : path.resolve(p);
|
|
14
16
|
}
|
|
17
|
+
/** Default absolute cwd for a channel when none is provided at creation time. */
|
|
18
|
+
export function getDefaultChannelCwd(channelId) {
|
|
19
|
+
const id = channelId.trim();
|
|
20
|
+
if (!id) {
|
|
21
|
+
throw new Error('channelId is required');
|
|
22
|
+
}
|
|
23
|
+
return resolvePath(`${DEFAULT_CHANNELS_WORKSPACE_DIR}/${id}`);
|
|
24
|
+
}
|
|
15
25
|
export function loadConfig() {
|
|
16
26
|
const configPath = path.join(os.homedir(), '.openbot', CONFIG_FILE);
|
|
17
27
|
if (fs.existsSync(configPath)) {
|
|
@@ -46,22 +56,3 @@ export function loadVariables() {
|
|
|
46
56
|
}
|
|
47
57
|
return { version: 1, variables: [] };
|
|
48
58
|
}
|
|
49
|
-
export const DEFAULT_AGENT_MD = `---
|
|
50
|
-
description: A specialized AI agent
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
# Agent Profile
|
|
54
|
-
|
|
55
|
-
You are a specialized AI agent within the OpenBot system.
|
|
56
|
-
Your role is defined by your configuration and the tools you have access to.
|
|
57
|
-
|
|
58
|
-
## Persona
|
|
59
|
-
- Helpful and precise
|
|
60
|
-
- Focused on my specific domain
|
|
61
|
-
- Professional in all interactions
|
|
62
|
-
`;
|
|
63
|
-
export const DEFAULT_USER_MD = `# About Me
|
|
64
|
-
|
|
65
|
-
<!-- OpenBot reads this file to understand who you are and how you like to work. -->
|
|
66
|
-
<!-- Edit it here or just chat — agents can update it with the "remember" tool. -->
|
|
67
|
-
`;
|
package/dist/app/server.js
CHANGED
|
@@ -9,11 +9,13 @@ const require = createRequire(import.meta.url);
|
|
|
9
9
|
const pkg = require('../../package.json');
|
|
10
10
|
import { generateId } from 'melony';
|
|
11
11
|
import { DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
|
|
12
|
-
import { processService } from '../
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
12
|
+
import { processService } from '../services/process.js';
|
|
13
|
+
import { runAgent, STATE_AGENT_ID, ORCHESTRATOR_AGENT_ID } from '../harness/index.js';
|
|
14
|
+
import { initPlugins } from '../services/plugins/registry.js';
|
|
15
|
+
import { storageService } from '../plugins/storage/service.js';
|
|
16
|
+
import { buildWorkspaceFileUrl, getPublicBaseUrl, openChannelFileStream, } from '../plugins/storage/files.js';
|
|
16
17
|
import { ensureEventId, openBotEventFromQuery } from './utils.js';
|
|
18
|
+
import { abortRegistry, abortKey } from '../services/abort.js';
|
|
17
19
|
export async function startServer(options = {}) {
|
|
18
20
|
const publishEventSchema = z
|
|
19
21
|
.object({
|
|
@@ -37,6 +39,9 @@ export async function startServer(options = {}) {
|
|
|
37
39
|
await fs.mkdir(agentsDir, { recursive: true });
|
|
38
40
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
39
41
|
initPlugins(pluginsDir);
|
|
42
|
+
// Pre-warm caches for agents and plugins to speed up first UI load
|
|
43
|
+
storageService.getAgents().catch((err) => console.warn('[server] Failed to pre-warm agents cache', err));
|
|
44
|
+
storageService.getPlugins().catch((err) => console.warn('[server] Failed to pre-warm plugins cache', err));
|
|
40
45
|
const getContext = (req) => {
|
|
41
46
|
const channelId = req.get('x-openbot-channel-id') || req.query.channelId || (req.body && req.body.channelId);
|
|
42
47
|
const threadId = req.get('x-openbot-thread-id') || req.query.threadId || (req.body && req.body.threadId);
|
|
@@ -49,7 +54,7 @@ export async function startServer(options = {}) {
|
|
|
49
54
|
req.query.responseType ||
|
|
50
55
|
(req.body && req.body.responseType);
|
|
51
56
|
return {
|
|
52
|
-
channelId: (channelId || (threadId ? '
|
|
57
|
+
channelId: (channelId || (threadId ? 'uncategorized' : 'uncategorized')), // Default to uncategorized if none
|
|
53
58
|
threadId: threadId,
|
|
54
59
|
agentId: agentId,
|
|
55
60
|
runId: runId,
|
|
@@ -60,8 +65,15 @@ export async function startServer(options = {}) {
|
|
|
60
65
|
const getRunKey = (runId, agentId, channelId, threadId) => `${runId}:${agentId}:${channelId}:${threadId || ''}`;
|
|
61
66
|
const sendToClientKey = (clientKey, chunk) => {
|
|
62
67
|
const threadClients = clients.get(clientKey);
|
|
63
|
-
if (!threadClients)
|
|
68
|
+
if (!threadClients || threadClients.length === 0)
|
|
64
69
|
return;
|
|
70
|
+
// Auto-detect "read" state: if someone is listening, they just "read" this event.
|
|
71
|
+
if (chunk.id && clientKey !== GLOBAL_CHANNEL_ID) {
|
|
72
|
+
const parts = clientKey.split(':');
|
|
73
|
+
const channelId = parts[0];
|
|
74
|
+
const threadId = parts[1]; // undefined if no ":"
|
|
75
|
+
storageService.setLastRead({ channelId, threadId, lastReadEventId: chunk.id }).catch(() => { });
|
|
76
|
+
}
|
|
65
77
|
threadClients.forEach((client) => {
|
|
66
78
|
if (!client.writableEnded) {
|
|
67
79
|
client.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
@@ -104,8 +116,31 @@ export async function startServer(options = {}) {
|
|
|
104
116
|
data: { channels },
|
|
105
117
|
};
|
|
106
118
|
};
|
|
119
|
+
// Drop every tracked run for a channel/thread. A stop aborts the whole
|
|
120
|
+
// chain (parent + delegated sub-agents), but the sub-agents' `agent:run:end`
|
|
121
|
+
// events can be swallowed when the parent run loop breaks on abort, leaving
|
|
122
|
+
// orphaned entries that keep a channel falsely "active". Purging by
|
|
123
|
+
// channel/thread guarantees the snapshot self-heals after a stop.
|
|
124
|
+
const purgeActiveRunsForThread = (channelId, threadId) => {
|
|
125
|
+
const target = threadId || undefined;
|
|
126
|
+
for (const [key, run] of activeRuns) {
|
|
127
|
+
if (run.channelId === channelId && (run.threadId || undefined) === target) {
|
|
128
|
+
activeRuns.delete(key);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
107
132
|
app.use(cors());
|
|
108
|
-
|
|
133
|
+
const resolvePublicBaseUrl = () => getPublicBaseUrl(PORT, config.publicUrl);
|
|
134
|
+
app.use((req, res, next) => {
|
|
135
|
+
const isWorkspaceUpload = req.method === 'POST' &&
|
|
136
|
+
req.path === '/api/publish' &&
|
|
137
|
+
req.get('x-openbot-event-type') === 'action:storage:upload-file';
|
|
138
|
+
if (isWorkspaceUpload) {
|
|
139
|
+
express.raw({ type: () => true, limit: '100mb' })(req, res, next);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
express.json({ limit: '20mb' })(req, res, next);
|
|
143
|
+
});
|
|
109
144
|
app.get('/api/health', (req, res) => {
|
|
110
145
|
res.json({ status: 'ok', version: pkg.version });
|
|
111
146
|
});
|
|
@@ -129,6 +164,18 @@ export async function startServer(options = {}) {
|
|
|
129
164
|
clients.set(clientKey, []);
|
|
130
165
|
}
|
|
131
166
|
clients.get(clientKey).push(res);
|
|
167
|
+
// Auto-detect "read" state on connection: mark the latest event as seen.
|
|
168
|
+
if (channelId !== GLOBAL_CHANNEL_ID) {
|
|
169
|
+
storageService
|
|
170
|
+
.getEvents({ channelId, threadId })
|
|
171
|
+
.then((events) => {
|
|
172
|
+
const latestId = events[events.length - 1]?.id;
|
|
173
|
+
if (latestId) {
|
|
174
|
+
return storageService.setLastRead({ channelId, threadId, lastReadEventId: latestId });
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
.catch(() => { });
|
|
178
|
+
}
|
|
132
179
|
if (channelId === GLOBAL_CHANNEL_ID) {
|
|
133
180
|
const snapshot = buildActiveRunsSnapshot();
|
|
134
181
|
ensureEventId(snapshot);
|
|
@@ -156,6 +203,53 @@ export async function startServer(options = {}) {
|
|
|
156
203
|
});
|
|
157
204
|
});
|
|
158
205
|
app.post('/api/publish', async (req, res) => {
|
|
206
|
+
if (req.get('x-openbot-event-type') === 'action:storage:upload-file') {
|
|
207
|
+
const channelId = req.get('x-openbot-channel-id') ||
|
|
208
|
+
(typeof req.query.channelId === 'string' ? req.query.channelId : undefined);
|
|
209
|
+
const filePath = req.get('x-openbot-file-path');
|
|
210
|
+
const overwrite = req.get('x-openbot-file-overwrite') === 'true';
|
|
211
|
+
if (!channelId?.trim()) {
|
|
212
|
+
res.status(400).json({ error: 'channelId is required' });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!filePath?.trim()) {
|
|
216
|
+
res.status(400).json({ error: 'x-openbot-file-path header is required' });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const body = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
|
|
220
|
+
if (body.length === 0) {
|
|
221
|
+
res.status(400).json({ error: 'Request body is empty' });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const result = await storageService.uploadChannelFile({
|
|
226
|
+
channelId: channelId.trim(),
|
|
227
|
+
path: filePath.trim(),
|
|
228
|
+
body,
|
|
229
|
+
overwrite,
|
|
230
|
+
});
|
|
231
|
+
const url = buildWorkspaceFileUrl({
|
|
232
|
+
baseUrl: resolvePublicBaseUrl(),
|
|
233
|
+
channelId: channelId.trim(),
|
|
234
|
+
filePath: result.path,
|
|
235
|
+
});
|
|
236
|
+
res.json({
|
|
237
|
+
type: 'action:storage:upload-file:result',
|
|
238
|
+
data: { success: true, ...result, url },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
res.status(400).json({
|
|
243
|
+
type: 'action:storage:upload-file:result',
|
|
244
|
+
data: {
|
|
245
|
+
success: false,
|
|
246
|
+
path: filePath,
|
|
247
|
+
error: error instanceof Error ? error.message : 'Upload failed',
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
159
253
|
const parseResult = publishEventSchema.safeParse(req.body);
|
|
160
254
|
if (!parseResult.success) {
|
|
161
255
|
res.status(400).json({
|
|
@@ -170,8 +264,77 @@ export async function startServer(options = {}) {
|
|
|
170
264
|
res.status(400).json({ error: 'channelId is required' });
|
|
171
265
|
return;
|
|
172
266
|
}
|
|
267
|
+
if (event.type === 'action:storage:write-file') {
|
|
268
|
+
const data = (event.data ?? {});
|
|
269
|
+
if (!data.path?.trim()) {
|
|
270
|
+
res.status(400).json({
|
|
271
|
+
type: 'action:storage:write-file:result',
|
|
272
|
+
data: { success: false, path: '', error: 'path is required' },
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (typeof data.content !== 'string') {
|
|
277
|
+
res.status(400).json({
|
|
278
|
+
type: 'action:storage:write-file:result',
|
|
279
|
+
data: { success: false, path: data.path, error: 'content is required' },
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const result = await storageService.writeChannelFile({
|
|
285
|
+
channelId,
|
|
286
|
+
path: data.path.trim(),
|
|
287
|
+
content: data.content,
|
|
288
|
+
encoding: data.encoding ?? 'utf8',
|
|
289
|
+
overwrite: data.overwrite ?? false,
|
|
290
|
+
});
|
|
291
|
+
const url = buildWorkspaceFileUrl({
|
|
292
|
+
baseUrl: resolvePublicBaseUrl(),
|
|
293
|
+
channelId,
|
|
294
|
+
filePath: result.path,
|
|
295
|
+
});
|
|
296
|
+
res.json({
|
|
297
|
+
type: 'action:storage:write-file:result',
|
|
298
|
+
data: { success: true, ...result, url },
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
res.status(400).json({
|
|
303
|
+
type: 'action:storage:write-file:result',
|
|
304
|
+
data: {
|
|
305
|
+
success: false,
|
|
306
|
+
path: data.path,
|
|
307
|
+
error: error instanceof Error ? error.message : 'Write failed',
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// Stop request: cancel the in-flight run (and any delegated sub-agents in the
|
|
314
|
+
// same thread) instead of spinning up a new agent turn.
|
|
315
|
+
if (event.type === 'action:agent_run_stop') {
|
|
316
|
+
const data = (event.data ?? {});
|
|
317
|
+
const targetChannelId = data.channelId || channelId;
|
|
318
|
+
const targetThreadId = data.threadId || threadId;
|
|
319
|
+
const stopped = abortRegistry.abort(abortKey(targetChannelId, targetThreadId));
|
|
320
|
+
purgeActiveRunsForThread(targetChannelId, targetThreadId);
|
|
321
|
+
const stoppedEvent = {
|
|
322
|
+
type: 'agent:run:stopped',
|
|
323
|
+
data: {
|
|
324
|
+
runId: data.runId || runId,
|
|
325
|
+
agentId: data.agentId || agentId || ORCHESTRATOR_AGENT_ID,
|
|
326
|
+
channelId: targetChannelId,
|
|
327
|
+
threadId: targetThreadId,
|
|
328
|
+
reason: data.reason,
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
ensureEventId(stoppedEvent);
|
|
332
|
+
sendToClientKey(getClientKey(targetChannelId, targetThreadId), stoppedEvent);
|
|
333
|
+
sendToClientKey(GLOBAL_CHANNEL_ID, stoppedEvent);
|
|
334
|
+
res.json({ success: stopped });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
173
337
|
const onEvent = async (chunk, state) => {
|
|
174
|
-
ensureEventId(chunk);
|
|
175
338
|
const targetChannelId = state?.channelId || channelId;
|
|
176
339
|
const targetThreadId = state?.threadId || threadId;
|
|
177
340
|
const targetClientKey = getClientKey(targetChannelId, targetThreadId);
|
|
@@ -186,11 +349,9 @@ export async function startServer(options = {}) {
|
|
|
186
349
|
else if (chunk.type === 'agent:run:end') {
|
|
187
350
|
activeRuns.delete(getRunKey(chunk.data.runId, chunk.data.agentId, chunk.data.channelId, chunk.data.threadId));
|
|
188
351
|
}
|
|
189
|
-
|
|
190
|
-
channelId
|
|
191
|
-
|
|
192
|
-
event: chunk,
|
|
193
|
-
});
|
|
352
|
+
else if (chunk.type === 'agent:run:stopped') {
|
|
353
|
+
purgeActiveRunsForThread(chunk.data.channelId, chunk.data.threadId);
|
|
354
|
+
}
|
|
194
355
|
sendToClientKey(targetClientKey, chunk);
|
|
195
356
|
if (chunk.type === 'agent:run:start' ||
|
|
196
357
|
chunk.type === 'agent:run:end' ||
|
|
@@ -200,12 +361,13 @@ export async function startServer(options = {}) {
|
|
|
200
361
|
};
|
|
201
362
|
try {
|
|
202
363
|
ensureEventId(event);
|
|
203
|
-
await
|
|
364
|
+
await runAgent({
|
|
204
365
|
runId,
|
|
205
|
-
agentId: agentId ||
|
|
366
|
+
agentId: agentId || ORCHESTRATOR_AGENT_ID,
|
|
206
367
|
event,
|
|
207
368
|
channelId,
|
|
208
369
|
threadId,
|
|
370
|
+
publicBaseUrl: resolvePublicBaseUrl(),
|
|
209
371
|
onEvent,
|
|
210
372
|
});
|
|
211
373
|
res.sendStatus(200);
|
|
@@ -232,18 +394,47 @@ export async function startServer(options = {}) {
|
|
|
232
394
|
return;
|
|
233
395
|
}
|
|
234
396
|
const { channelId, threadId, agentId, runId } = getContext(req);
|
|
397
|
+
if (event.type === 'action:storage:serve-file') {
|
|
398
|
+
const filePath = event.data?.path;
|
|
399
|
+
if (!channelId?.trim()) {
|
|
400
|
+
res.status(400).json({ error: 'channelId is required' });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (!filePath?.trim()) {
|
|
404
|
+
res.status(400).json({ error: 'path is required' });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
const { abs, size, mimeType } = await storageService.getChannelFileStat({
|
|
409
|
+
channelId,
|
|
410
|
+
path: filePath.trim(),
|
|
411
|
+
});
|
|
412
|
+
res.setHeader('Content-Type', mimeType);
|
|
413
|
+
res.setHeader('Content-Length', String(size));
|
|
414
|
+
res.setHeader('Cache-Control', 'private, max-age=3600');
|
|
415
|
+
openChannelFileStream(abs).pipe(res);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
res.status(404).json({
|
|
419
|
+
error: error instanceof Error ? error.message : 'File not found',
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
235
424
|
const events = [];
|
|
236
425
|
const onEvent = async (chunk) => {
|
|
237
426
|
events.push(chunk);
|
|
238
427
|
};
|
|
239
428
|
try {
|
|
240
429
|
ensureEventId(event);
|
|
241
|
-
await
|
|
430
|
+
await runAgent({
|
|
242
431
|
runId,
|
|
243
|
-
agentId: agentId ||
|
|
432
|
+
agentId: agentId || STATE_AGENT_ID,
|
|
244
433
|
event,
|
|
245
434
|
channelId,
|
|
246
435
|
threadId,
|
|
436
|
+
persistEvents: false,
|
|
437
|
+
publicBaseUrl: resolvePublicBaseUrl(),
|
|
247
438
|
onEvent,
|
|
248
439
|
});
|
|
249
440
|
res.json({ events });
|
package/dist/bus/services.js
CHANGED
|
@@ -1,25 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_MARKETPLACE_REGISTRY_URL, loadConfig } from '../app/config.js';
|
|
2
2
|
import { storageService } from '../services/storage.js';
|
|
3
3
|
import { pluginService } from '../services/plugins.js';
|
|
4
|
-
const readTodos = (state) => {
|
|
5
|
-
const raw = state.threadDetails?.state?.todos;
|
|
6
|
-
return Array.isArray(raw) ? raw : [];
|
|
7
|
-
};
|
|
8
|
-
let todoCounter = 0;
|
|
9
|
-
const newTodoId = (now, idx) => `todo_${now.toString(36)}_${(todoCounter++).toString(36)}_${idx}`;
|
|
10
|
-
async function persistTodos(storage, state, todos) {
|
|
11
|
-
if (!state.threadId)
|
|
12
|
-
throw new Error('No active thread');
|
|
13
|
-
await storage.patchThreadState({
|
|
14
|
-
channelId: state.channelId,
|
|
15
|
-
threadId: state.threadId,
|
|
16
|
-
state: { todos },
|
|
17
|
-
});
|
|
18
|
-
state.threadDetails = await storage.getThreadDetails({
|
|
19
|
-
channelId: state.channelId,
|
|
20
|
-
threadId: state.threadId,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
4
|
/**
|
|
24
5
|
* Resolve a scope alias to a concrete scope string. Aliases let tools accept
|
|
25
6
|
* `agent`/`channel`/`global` without knowing the active ids; the bus rewrites
|
|
@@ -44,26 +25,7 @@ function resolveMemoryScopeFilter(alias, state) {
|
|
|
44
25
|
}
|
|
45
26
|
return [resolveMemoryScope(alias, state)];
|
|
46
27
|
}
|
|
47
|
-
const DEFAULT_MARKETPLACE_AGENTS = [
|
|
48
|
-
{
|
|
49
|
-
id: 'researcher',
|
|
50
|
-
name: 'Researcher',
|
|
51
|
-
description: 'Specialized in web research and information synthesis.',
|
|
52
|
-
instructions: 'You are a research assistant. Use available tools to find information.',
|
|
53
|
-
plugins: [
|
|
54
|
-
{ id: 'ai-sdk', config: { model: 'openai/gpt-4o' } },
|
|
55
|
-
{ id: 'mcp' },
|
|
56
|
-
{ id: 'shell' },
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
id: 'coder',
|
|
61
|
-
name: 'Coder',
|
|
62
|
-
description: 'Expert in multiple programming languages and software architecture.',
|
|
63
|
-
instructions: 'You are an expert software engineer. Help the user with coding tasks.',
|
|
64
|
-
plugins: [{ id: 'claude-code' }],
|
|
65
|
-
},
|
|
66
|
-
];
|
|
28
|
+
const DEFAULT_MARKETPLACE_AGENTS = [];
|
|
67
29
|
function isRecord(value) {
|
|
68
30
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
69
31
|
}
|
|
@@ -311,6 +273,15 @@ export const busServicesPlugin = (options) => (builder) => {
|
|
|
311
273
|
context.state.channelDetails = await storage.getChannelDetails({
|
|
312
274
|
channelId: context.state.channelId,
|
|
313
275
|
});
|
|
276
|
+
yield {
|
|
277
|
+
type: "client:ui:widget",
|
|
278
|
+
data: {
|
|
279
|
+
kind: "message",
|
|
280
|
+
title: "Channel details updated.",
|
|
281
|
+
body: "The channel details have been updated.",
|
|
282
|
+
},
|
|
283
|
+
meta: resultMeta,
|
|
284
|
+
};
|
|
314
285
|
yield {
|
|
315
286
|
type: 'action:patch_channel_details:result',
|
|
316
287
|
data: { success: true, updatedFields },
|
|
@@ -358,91 +329,30 @@ export const busServicesPlugin = (options) => (builder) => {
|
|
|
358
329
|
};
|
|
359
330
|
}
|
|
360
331
|
});
|
|
361
|
-
builder.on('
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
await persistTodos(storage, context.state, next);
|
|
386
|
-
yield {
|
|
387
|
-
type: 'action:todo_write:result',
|
|
388
|
-
data: { success: true, todos: next },
|
|
389
|
-
meta: resultMeta,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
catch (error) {
|
|
393
|
-
yield {
|
|
394
|
-
type: 'action:todo_write:result',
|
|
395
|
-
data: {
|
|
396
|
-
success: false,
|
|
397
|
-
todos: readTodos(context.state),
|
|
398
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
399
|
-
},
|
|
400
|
-
meta: resultMeta,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
builder.on('action:todo_update', async function* (event, context) {
|
|
405
|
-
const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
|
|
406
|
-
const patch = event.data;
|
|
407
|
-
try {
|
|
408
|
-
if (!context.state.threadId) {
|
|
409
|
-
throw new Error('todo_update requires an active thread');
|
|
410
|
-
}
|
|
411
|
-
const existing = readTodos(context.state);
|
|
412
|
-
const idx = existing.findIndex((t) => t.id === patch.id);
|
|
413
|
-
if (idx === -1) {
|
|
414
|
-
throw new Error(`Todo "${patch.id}" not found`);
|
|
415
|
-
}
|
|
416
|
-
const now = Date.now();
|
|
417
|
-
const updated = {
|
|
418
|
-
...existing[idx],
|
|
419
|
-
...(patch.content !== undefined ? { content: patch.content } : {}),
|
|
420
|
-
...(patch.status !== undefined ? { status: patch.status } : {}),
|
|
421
|
-
...(patch.assignee !== undefined
|
|
422
|
-
? { assignee: patch.assignee === '' ? undefined : patch.assignee }
|
|
423
|
-
: {}),
|
|
424
|
-
updatedAt: now,
|
|
425
|
-
};
|
|
426
|
-
const next = [...existing];
|
|
427
|
-
next[idx] = updated;
|
|
428
|
-
await persistTodos(storage, context.state, next);
|
|
429
|
-
yield {
|
|
430
|
-
type: 'action:todo_update:result',
|
|
431
|
-
data: { success: true, todo: updated, todos: next },
|
|
432
|
-
meta: resultMeta,
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
catch (error) {
|
|
436
|
-
yield {
|
|
437
|
-
type: 'action:todo_update:result',
|
|
438
|
-
data: {
|
|
439
|
-
success: false,
|
|
440
|
-
todos: readTodos(context.state),
|
|
441
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
442
|
-
},
|
|
443
|
-
meta: resultMeta,
|
|
444
|
-
};
|
|
445
|
-
}
|
|
332
|
+
builder.on('agent:usage', async function* (event, context) {
|
|
333
|
+
const { usage } = event.data;
|
|
334
|
+
if (!context.state.threadId)
|
|
335
|
+
return;
|
|
336
|
+
const currentState = context.state.threadDetails?.state || {};
|
|
337
|
+
const currentUsage = currentState.usage || {
|
|
338
|
+
promptTokens: 0,
|
|
339
|
+
completionTokens: 0,
|
|
340
|
+
totalTokens: 0,
|
|
341
|
+
};
|
|
342
|
+
const nextUsage = {
|
|
343
|
+
promptTokens: (currentUsage.promptTokens || 0) + usage.promptTokens,
|
|
344
|
+
completionTokens: (currentUsage.completionTokens || 0) + usage.completionTokens,
|
|
345
|
+
totalTokens: (currentUsage.totalTokens || 0) + usage.totalTokens,
|
|
346
|
+
};
|
|
347
|
+
await storage.patchThreadState({
|
|
348
|
+
channelId: context.state.channelId,
|
|
349
|
+
threadId: context.state.threadId,
|
|
350
|
+
state: { usage: nextUsage },
|
|
351
|
+
});
|
|
352
|
+
context.state.threadDetails = await storage.getThreadDetails({
|
|
353
|
+
channelId: context.state.channelId,
|
|
354
|
+
threadId: context.state.threadId,
|
|
355
|
+
});
|
|
446
356
|
});
|
|
447
357
|
builder.on('action:storage:get-channels', async function* () {
|
|
448
358
|
const channels = await storage.getChannels();
|