openbot 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -16
- package/dist/app/agent-ids.js +4 -0
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +0 -19
- package/dist/app/server.js +8 -14
- package/dist/bus/services.js +34 -124
- package/dist/harness/agent-invoke-run.js +44 -0
- package/dist/harness/agent-turn.js +99 -0
- package/dist/harness/channel-participants.js +40 -0
- package/dist/harness/constants.js +2 -0
- package/dist/harness/context-meter.js +97 -0
- package/dist/harness/context.js +95 -47
- package/dist/harness/dispatch.js +144 -0
- package/dist/harness/dispatcher.js +45 -156
- package/dist/harness/history.js +177 -0
- package/dist/harness/index.js +91 -0
- package/dist/harness/orchestration.js +88 -0
- package/dist/harness/participants.js +22 -0
- package/dist/harness/run-harness.js +154 -0
- package/dist/harness/run.js +98 -0
- package/dist/harness/runtime-factory.js +0 -34
- package/dist/harness/runtime.js +57 -0
- package/dist/harness/todo-dispatch.js +51 -0
- package/dist/harness/todos.js +5 -0
- package/dist/harness/turn.js +79 -0
- package/dist/plugins/approval/index.js +105 -149
- package/dist/plugins/delegation/index.js +119 -32
- package/dist/plugins/memory/index.js +103 -14
- package/dist/plugins/memory/service.js +152 -0
- package/dist/plugins/openbot/context.js +80 -0
- package/dist/plugins/openbot/history.js +98 -0
- package/dist/plugins/openbot/index.js +31 -0
- package/dist/plugins/openbot/runtime.js +317 -0
- package/dist/plugins/openbot/system-prompt.js +5 -0
- package/dist/plugins/plugin-manager/index.js +105 -0
- package/dist/plugins/storage/index.js +573 -0
- package/dist/plugins/storage/service.js +1159 -0
- package/dist/plugins/storage-tools/index.js +2 -2
- package/dist/plugins/thread-namer/index.js +72 -0
- package/dist/plugins/thread-naming/generate-title.js +44 -0
- package/dist/plugins/thread-naming/index.js +103 -0
- package/dist/plugins/threads/index.js +114 -0
- package/dist/plugins/todo/index.js +24 -25
- package/dist/plugins/ui/index.js +2 -32
- package/dist/registry/plugins.js +3 -9
- package/dist/services/plugins/domain.js +1 -0
- package/dist/services/plugins/plugin-cache.js +9 -0
- package/dist/services/plugins/registry.js +110 -0
- package/dist/services/plugins/service.js +177 -0
- package/dist/services/plugins/types.js +1 -0
- package/dist/services/process.js +29 -0
- package/dist/services/storage.js +11 -10
- package/dist/services/thread-naming.js +81 -0
- package/docs/agents.md +16 -10
- package/docs/architecture.md +2 -2
- package/docs/plugins.md +6 -15
- package/docs/templates/AGENT.example.md +7 -13
- package/package.json +1 -2
- package/src/app/agent-ids.ts +5 -0
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +1 -31
- package/src/app/server.ts +8 -16
- package/src/app/types.ts +63 -189
- package/src/harness/index.ts +145 -0
- package/src/plugins/approval/index.ts +91 -189
- package/src/plugins/delegation/index.ts +136 -39
- package/src/plugins/memory/index.ts +112 -15
- package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
- package/src/plugins/openbot/context.ts +91 -0
- package/src/plugins/openbot/history.ts +107 -0
- package/src/plugins/openbot/index.ts +37 -0
- package/src/plugins/openbot/runtime.ts +384 -0
- package/src/plugins/openbot/system-prompt.ts +7 -0
- package/src/plugins/plugin-manager/index.ts +122 -0
- package/src/plugins/shell/index.ts +1 -1
- package/src/plugins/storage/index.ts +633 -0
- package/src/{services/storage.ts → plugins/storage/service.ts} +224 -67
- package/src/{bus/types.ts → services/plugins/domain.ts} +16 -7
- package/src/services/plugins/plugin-cache.ts +13 -0
- package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
- package/src/services/{plugins.ts → plugins/service.ts} +96 -2
- package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
- package/src/bus/services.ts +0 -954
- package/src/harness/context.ts +0 -365
- package/src/harness/dispatcher.ts +0 -379
- package/src/harness/mcp.ts +0 -78
- package/src/harness/runtime-factory.ts +0 -129
- package/src/harness/todo-advance.ts +0 -128
- package/src/plugins/ai-sdk/index.ts +0 -41
- package/src/plugins/ai-sdk/runtime.ts +0 -468
- package/src/plugins/ai-sdk/system-prompt.ts +0 -18
- package/src/plugins/mcp/index.ts +0 -128
- package/src/plugins/storage-tools/index.ts +0 -90
- package/src/plugins/todo/index.ts +0 -64
- package/src/plugins/ui/index.ts +0 -227
- /package/src/{harness → services}/process.ts +0 -0
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.0');
|
|
20
20
|
program
|
|
21
21
|
.command('start')
|
|
22
22
|
.description('Start the OpenBot harness')
|
package/dist/app/config.js
CHANGED
|
@@ -46,22 +46,3 @@ export function loadVariables() {
|
|
|
46
46
|
}
|
|
47
47
|
return { version: 1, variables: [] };
|
|
48
48
|
}
|
|
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,10 +9,9 @@ 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 { initPlugins } from '../registry/plugins.js';
|
|
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';
|
|
16
15
|
import { ensureEventId, openBotEventFromQuery } from './utils.js';
|
|
17
16
|
export async function startServer(options = {}) {
|
|
18
17
|
const publishEventSchema = z
|
|
@@ -171,7 +170,6 @@ export async function startServer(options = {}) {
|
|
|
171
170
|
return;
|
|
172
171
|
}
|
|
173
172
|
const onEvent = async (chunk, state) => {
|
|
174
|
-
ensureEventId(chunk);
|
|
175
173
|
const targetChannelId = state?.channelId || channelId;
|
|
176
174
|
const targetThreadId = state?.threadId || threadId;
|
|
177
175
|
const targetClientKey = getClientKey(targetChannelId, targetThreadId);
|
|
@@ -186,11 +184,6 @@ export async function startServer(options = {}) {
|
|
|
186
184
|
else if (chunk.type === 'agent:run:end') {
|
|
187
185
|
activeRuns.delete(getRunKey(chunk.data.runId, chunk.data.agentId, chunk.data.channelId, chunk.data.threadId));
|
|
188
186
|
}
|
|
189
|
-
await storageService.storeEvent({
|
|
190
|
-
channelId: targetChannelId,
|
|
191
|
-
threadId: targetThreadId,
|
|
192
|
-
event: chunk,
|
|
193
|
-
});
|
|
194
187
|
sendToClientKey(targetClientKey, chunk);
|
|
195
188
|
if (chunk.type === 'agent:run:start' ||
|
|
196
189
|
chunk.type === 'agent:run:end' ||
|
|
@@ -200,9 +193,9 @@ export async function startServer(options = {}) {
|
|
|
200
193
|
};
|
|
201
194
|
try {
|
|
202
195
|
ensureEventId(event);
|
|
203
|
-
await
|
|
196
|
+
await runAgent({
|
|
204
197
|
runId,
|
|
205
|
-
agentId: agentId ||
|
|
198
|
+
agentId: agentId || ORCHESTRATOR_AGENT_ID,
|
|
206
199
|
event,
|
|
207
200
|
channelId,
|
|
208
201
|
threadId,
|
|
@@ -238,12 +231,13 @@ export async function startServer(options = {}) {
|
|
|
238
231
|
};
|
|
239
232
|
try {
|
|
240
233
|
ensureEventId(event);
|
|
241
|
-
await
|
|
234
|
+
await runAgent({
|
|
242
235
|
runId,
|
|
243
|
-
agentId: agentId ||
|
|
236
|
+
agentId: agentId || STATE_AGENT_ID,
|
|
244
237
|
event,
|
|
245
238
|
channelId,
|
|
246
239
|
threadId,
|
|
240
|
+
persistEvents: false,
|
|
247
241
|
onEvent,
|
|
248
242
|
});
|
|
249
243
|
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();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createAgentRuntime } from './runtime-factory.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shared `runtime.run(agent:invoke)` loop: tags chunks with `agentId`, skips echo of the
|
|
4
|
+
* trigger event, optionally polls for stop, tracks last `agent:output` text.
|
|
5
|
+
*
|
|
6
|
+
* Does not emit `agent:run:start` / `agent:run:end` — callers bracket those.
|
|
7
|
+
*/
|
|
8
|
+
export async function* streamTaggedAgentInvokeRuntime(options) {
|
|
9
|
+
const { target, event, state, pollInterrupt } = options;
|
|
10
|
+
const { runId, agentId } = target;
|
|
11
|
+
let lastAgentOutput;
|
|
12
|
+
const runtime = await createAgentRuntime(state);
|
|
13
|
+
for await (const chunk of runtime.run(event, { state, runId })) {
|
|
14
|
+
const interrupt = pollInterrupt?.();
|
|
15
|
+
if (interrupt) {
|
|
16
|
+
yield interrupt;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
if (chunk.id === event.id && chunk.type === event.type)
|
|
20
|
+
continue;
|
|
21
|
+
if (chunk.type === 'agent:output' &&
|
|
22
|
+
chunk.meta?.agentId === agentId) {
|
|
23
|
+
const content = chunk.data?.content;
|
|
24
|
+
if (typeof content === 'string' && content.trim()) {
|
|
25
|
+
lastAgentOutput = content.trim();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
chunk.meta = { ...chunk.meta, agentId };
|
|
29
|
+
yield chunk;
|
|
30
|
+
}
|
|
31
|
+
return { lastAgentOutput };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Drive {@link streamTaggedAgentInvokeRuntime} with a callback per chunk (dispatcher path).
|
|
35
|
+
*/
|
|
36
|
+
export async function consumeAgentInvokeStream(options, onChunk) {
|
|
37
|
+
const gen = streamTaggedAgentInvokeRuntime(options);
|
|
38
|
+
let step = await gen.next();
|
|
39
|
+
while (!step.done) {
|
|
40
|
+
await onChunk(step.value);
|
|
41
|
+
step = await gen.next();
|
|
42
|
+
}
|
|
43
|
+
return step.value;
|
|
44
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ensureEventId } from '../app/utils.js';
|
|
2
|
+
import { storageService } from '../services/storage.js';
|
|
3
|
+
import { createAgentRuntime } from './runtime-factory.js';
|
|
4
|
+
const TODO_RESULT_MAX_CHARS = 12000;
|
|
5
|
+
const readThreadState = (state) => state.threadDetails?.state ?? {};
|
|
6
|
+
const readTodos = (state) => {
|
|
7
|
+
const raw = readThreadState(state).todos;
|
|
8
|
+
return Array.isArray(raw) ? raw : [];
|
|
9
|
+
};
|
|
10
|
+
function truncateTodoResult(text, maxChars = TODO_RESULT_MAX_CHARS) {
|
|
11
|
+
const trimmed = text.trim();
|
|
12
|
+
if (!trimmed)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (trimmed.length <= maxChars)
|
|
15
|
+
return trimmed;
|
|
16
|
+
return `${trimmed.slice(0, maxChars)}\n…[truncated]`;
|
|
17
|
+
}
|
|
18
|
+
function resolveTodoIdForWorker(todos, workerId, delegationTodoId) {
|
|
19
|
+
if (delegationTodoId && todos.some((t) => t.id === delegationTodoId)) {
|
|
20
|
+
return delegationTodoId;
|
|
21
|
+
}
|
|
22
|
+
const inProgress = todos.find((t) => t.status === 'in_progress' && t.assignee === workerId);
|
|
23
|
+
if (inProgress)
|
|
24
|
+
return inProgress.id;
|
|
25
|
+
const assigned = todos.find((t) => (t.status === 'pending' || t.status === 'in_progress') && t.assignee === workerId);
|
|
26
|
+
return assigned?.id;
|
|
27
|
+
}
|
|
28
|
+
export async function recordWorkerTodoResult(state, workerId, output, delegationTodoId) {
|
|
29
|
+
if (!state.threadId)
|
|
30
|
+
return;
|
|
31
|
+
const result = truncateTodoResult(output ?? '');
|
|
32
|
+
if (!result)
|
|
33
|
+
return;
|
|
34
|
+
const todos = readTodos(state);
|
|
35
|
+
if (todos.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
const todoId = resolveTodoIdForWorker(todos, workerId, delegationTodoId);
|
|
38
|
+
if (!todoId)
|
|
39
|
+
return;
|
|
40
|
+
const prior = todos.find((t) => t.id === todoId);
|
|
41
|
+
if (prior?.result === result)
|
|
42
|
+
return;
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const next = todos.map((t) => (t.id === todoId ? { ...t, result, updatedAt: now } : t));
|
|
45
|
+
await storageService.patchThreadState({
|
|
46
|
+
channelId: state.channelId,
|
|
47
|
+
threadId: state.threadId,
|
|
48
|
+
state: { todos: next },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function makeInternalInvoke(content, threadId) {
|
|
52
|
+
return ensureEventId({
|
|
53
|
+
type: 'agent:invoke',
|
|
54
|
+
data: { role: 'user', content },
|
|
55
|
+
meta: { threadId, internal: true },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Run one agent turn (no dispatcher chaining). Yields all runtime events for
|
|
60
|
+
* persistence/streaming; returns the last non-empty `agent:output` text.
|
|
61
|
+
*/
|
|
62
|
+
export async function* runAgentTurn(options) {
|
|
63
|
+
const { runId, channelId, threadId, agentId, event, delegationTodoId } = options;
|
|
64
|
+
const target = { runId, agentId, channelId, threadId };
|
|
65
|
+
let state;
|
|
66
|
+
try {
|
|
67
|
+
state = await storageService.getOpenBotState({ ...target, event });
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error.code === 'AGENT_NOT_FOUND') {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
yield { type: 'agent:run:start', data: { ...target } };
|
|
76
|
+
let lastAgentOutput;
|
|
77
|
+
try {
|
|
78
|
+
const runtime = await createAgentRuntime(state);
|
|
79
|
+
for await (const chunk of runtime.run(event, { state, runId })) {
|
|
80
|
+
if (chunk.id === event.id && chunk.type === event.type)
|
|
81
|
+
continue;
|
|
82
|
+
if (chunk.type === 'agent:output' &&
|
|
83
|
+
chunk.meta?.agentId === agentId) {
|
|
84
|
+
const content = chunk.data?.content;
|
|
85
|
+
if (typeof content === 'string' && content.trim()) {
|
|
86
|
+
lastAgentOutput = content.trim();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
chunk.meta = { ...chunk.meta, agentId };
|
|
90
|
+
yield chunk;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
const stateAfterRun = await storageService.getOpenBotState({ ...target, event });
|
|
95
|
+
yield { type: 'agent:run:end', data: { ...target } };
|
|
96
|
+
await recordWorkerTodoResult(stateAfterRun, agentId, lastAgentOutput, delegationTodoId);
|
|
97
|
+
}
|
|
98
|
+
return lastAgentOutput;
|
|
99
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel `participants` (from `state.json`) scope which agents may collaborate
|
|
3
|
+
* in that channel. Used for system-prompt hints and dispatch guards.
|
|
4
|
+
*/
|
|
5
|
+
/** Multi-participant channel: user messages always route to the orchestrator. */
|
|
6
|
+
export function isMultiAgentChannel(participants) {
|
|
7
|
+
return participants.length > 1;
|
|
8
|
+
}
|
|
9
|
+
/** Solo DM: exactly one participant and it is the acting agent (no peer bots). */
|
|
10
|
+
export function isDmSoloChannel(participants, actingAgentId) {
|
|
11
|
+
return participants.length === 1 && participants[0] === actingAgentId;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve which agent handles an incoming user message.
|
|
15
|
+
* Multi-participant channels always route to the orchestrator (hub-and-spoke).
|
|
16
|
+
*/
|
|
17
|
+
export function resolveMessageTargetAgent(participants, orchestratorAgentId, requestedAgentId) {
|
|
18
|
+
if (isMultiAgentChannel(participants)) {
|
|
19
|
+
return orchestratorAgentId;
|
|
20
|
+
}
|
|
21
|
+
if (participants.length === 1) {
|
|
22
|
+
return requestedAgentId || participants[0];
|
|
23
|
+
}
|
|
24
|
+
return requestedAgentId || orchestratorAgentId;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* When `participants` is non-empty, todo dispatch targets must appear
|
|
28
|
+
* in that list. Solo DM forbids targeting any agent other than yourself (for
|
|
29
|
+
* chained steps); there are no peer bots.
|
|
30
|
+
*/
|
|
31
|
+
export function isParticipantDispatchAllowed(participants, actingAgentId, targetAgentId) {
|
|
32
|
+
if (participants.length === 0)
|
|
33
|
+
return true;
|
|
34
|
+
if (!participants.includes(targetAgentId))
|
|
35
|
+
return false;
|
|
36
|
+
if (isDmSoloChannel(participants, actingAgentId) && targetAgentId !== actingAgentId) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { OPENBOT_SYSTEM_PROMPT } from '../plugins/openbot/system-prompt.js';
|
|
2
|
+
import { ORCHESTRATOR_AGENT_ID, estimateTokens, } from './context.js';
|
|
3
|
+
import { reconstructHistory } from './history.js';
|
|
4
|
+
/** Reserved headroom for model output when computing fill percentage. */
|
|
5
|
+
export const CONTEXT_METER_OUTPUT_RESERVE = 4096;
|
|
6
|
+
const DEFAULT_CONTEXT_LIMIT = 128000;
|
|
7
|
+
const MODEL_CONTEXT_LIMITS = {
|
|
8
|
+
'gpt-4o': 128000,
|
|
9
|
+
'gpt-4o-mini': 128000,
|
|
10
|
+
'gpt-4-turbo': 128000,
|
|
11
|
+
'gpt-4': 128000,
|
|
12
|
+
'gpt-3.5-turbo': 16385,
|
|
13
|
+
'claude-3-5-sonnet-20240620': 200000,
|
|
14
|
+
'claude-3-5-sonnet-20241022': 200000,
|
|
15
|
+
'claude-3-opus-20240229': 200000,
|
|
16
|
+
'claude-3-haiku-20240307': 200000,
|
|
17
|
+
};
|
|
18
|
+
export function getModelContextLimit(modelString) {
|
|
19
|
+
const modelId = modelString.split('/').slice(1).join('/');
|
|
20
|
+
if (MODEL_CONTEXT_LIMITS[modelId])
|
|
21
|
+
return MODEL_CONTEXT_LIMITS[modelId];
|
|
22
|
+
if (modelId.includes('claude'))
|
|
23
|
+
return 200000;
|
|
24
|
+
return DEFAULT_CONTEXT_LIMIT;
|
|
25
|
+
}
|
|
26
|
+
function buildInstructions(state) {
|
|
27
|
+
if (state.agentId === ORCHESTRATOR_AGENT_ID) {
|
|
28
|
+
return state.agentDetails?.instructions?.trim() || OPENBOT_SYSTEM_PROMPT;
|
|
29
|
+
}
|
|
30
|
+
return OPENBOT_SYSTEM_PROMPT;
|
|
31
|
+
}
|
|
32
|
+
function estimateMessagesTokens(messages) {
|
|
33
|
+
if (messages.length === 0)
|
|
34
|
+
return 0;
|
|
35
|
+
return estimateTokens(JSON.stringify(messages));
|
|
36
|
+
}
|
|
37
|
+
function estimateToolsTokens(tools) {
|
|
38
|
+
const names = Object.keys(tools);
|
|
39
|
+
if (names.length === 0)
|
|
40
|
+
return 0;
|
|
41
|
+
return estimateTokens(JSON.stringify(tools));
|
|
42
|
+
}
|
|
43
|
+
function applyTrigger(messages, trigger) {
|
|
44
|
+
const triggerContent = trigger?.data?.content?.trim();
|
|
45
|
+
if (!triggerContent || !trigger)
|
|
46
|
+
return messages;
|
|
47
|
+
const role = (trigger.data?.role || 'user');
|
|
48
|
+
const last = messages[messages.length - 1];
|
|
49
|
+
const alreadyLast = last &&
|
|
50
|
+
last.role === role &&
|
|
51
|
+
typeof last.content === 'string' &&
|
|
52
|
+
last.content.trim() === triggerContent;
|
|
53
|
+
if (alreadyLast)
|
|
54
|
+
return messages;
|
|
55
|
+
return [...messages, { role, content: triggerContent }];
|
|
56
|
+
}
|
|
57
|
+
function computePercent(used, limit) {
|
|
58
|
+
const budget = Math.max(limit - CONTEXT_METER_OUTPUT_RESERVE, 1);
|
|
59
|
+
return Math.min(100, Math.round((used / budget) * 100));
|
|
60
|
+
}
|
|
61
|
+
export async function computeContextMeter(options) {
|
|
62
|
+
const { state, storage, modelString, contextEngine, toolDefinitions = {}, trigger, lastUsage, } = options;
|
|
63
|
+
const limit = getModelContextLimit(modelString);
|
|
64
|
+
const instructions = buildInstructions(state);
|
|
65
|
+
const contextBlock = await contextEngine.buildContext(state, storage);
|
|
66
|
+
const systemTokens = estimateTokens([instructions, contextBlock].filter(Boolean).join('\n\n'));
|
|
67
|
+
const events = await storage.getEvents({
|
|
68
|
+
channelId: state.channelId,
|
|
69
|
+
threadId: state.threadId,
|
|
70
|
+
});
|
|
71
|
+
const messages = applyTrigger(reconstructHistory(events), trigger);
|
|
72
|
+
const historyTokens = estimateMessagesTokens(messages);
|
|
73
|
+
const toolsTokens = estimateToolsTokens(toolDefinitions);
|
|
74
|
+
let used = systemTokens + historyTokens + toolsTokens;
|
|
75
|
+
let estimated = true;
|
|
76
|
+
if (lastUsage?.input && lastUsage.input > 0) {
|
|
77
|
+
used = lastUsage.input;
|
|
78
|
+
estimated = false;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
model: modelString,
|
|
82
|
+
limit,
|
|
83
|
+
used,
|
|
84
|
+
percent: computePercent(used, limit),
|
|
85
|
+
estimated,
|
|
86
|
+
breakdown: {
|
|
87
|
+
system: systemTokens,
|
|
88
|
+
history: historyTokens,
|
|
89
|
+
tools: toolsTokens,
|
|
90
|
+
},
|
|
91
|
+
messageCount: messages.length,
|
|
92
|
+
...(lastUsage ? { lastUsage } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function contextMeterEvent(snapshot, meta) {
|
|
96
|
+
return { type: 'client:ui:context-meter', data: snapshot, meta };
|
|
97
|
+
}
|