openbot 0.4.7 → 0.5.5
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/.claude/agents/runtime-security-reviewer.md +32 -0
- package/.claude/hooks/format.sh +18 -0
- package/.claude/hooks/protect-secrets.sh +23 -0
- package/.claude/settings.json +26 -0
- package/.claude/skills/deploy/SKILL.md +26 -0
- package/.claude/skills/new-plugin/SKILL.md +46 -0
- package/.dockerignore +9 -0
- package/.mcp.json +8 -0
- package/AGENTS.md +2 -2
- package/CLAUDE.md +51 -0
- package/Dockerfile +41 -0
- package/deploy/README.md +59 -0
- package/deploy/entrypoint.sh +6 -0
- package/deploy/fly.toml +36 -0
- package/dist/app/bootstrap.js +29 -0
- package/dist/app/cli.js +5 -1
- package/dist/app/cloud-mode.js +15 -0
- package/dist/app/config.js +61 -16
- package/dist/app/ensure-default-stack.js +54 -0
- package/dist/app/openbot-plugin.js +4 -0
- package/dist/app/server.js +37 -6
- package/dist/harness/index.js +3 -0
- package/dist/plugins/approval/index.js +1 -1
- package/dist/plugins/bash/index.js +386 -164
- package/dist/plugins/memory/service.js +2 -5
- package/dist/plugins/openbot/context.js +17 -1
- package/dist/plugins/openbot/index.js +23 -7
- package/dist/plugins/openbot/model.js +20 -0
- package/dist/plugins/openbot/runtime.js +45 -57
- package/dist/plugins/openbot/system-prompt.js +43 -8
- package/dist/plugins/preview/index.js +267 -0
- package/dist/plugins/storage/index.js +3 -325
- package/dist/plugins/storage/service.js +61 -18
- package/dist/plugins/todo/index.js +131 -38
- package/dist/plugins/todo/service.js +93 -0
- package/dist/plugins/ui/index.js +8 -1
- package/dist/services/plugins/host.js +21 -0
- package/dist/services/plugins/model-registry.js +126 -0
- package/dist/services/plugins/registry.js +28 -42
- package/dist/services/plugins/service.js +2 -6
- package/docs/agents.md +20 -2
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +13 -3
- package/docs/templates/AGENT.example.md +8 -14
- package/package.json +5 -2
- package/pnpm-workspace.yaml +2 -0
- package/src/app/bootstrap.ts +43 -0
- package/src/app/cli.ts +5 -1
- package/src/app/cloud-mode.ts +26 -0
- package/src/app/config.ts +71 -19
- package/src/app/ensure-default-stack.ts +63 -0
- package/src/app/openbot-plugin.ts +5 -0
- package/src/app/server.ts +42 -6
- package/src/app/types.ts +158 -32
- package/src/harness/index.ts +4 -0
- package/src/plugins/memory/service.ts +2 -5
- package/src/plugins/storage/index.ts +3 -371
- package/src/plugins/storage/service.ts +81 -22
- package/src/services/plugins/domain.ts +2 -0
- package/src/services/plugins/host.ts +36 -0
- package/src/services/plugins/model-registry.ts +178 -0
- package/src/services/plugins/registry.ts +30 -43
- package/src/services/plugins/service.ts +8 -17
- package/src/services/plugins/types.ts +36 -2
- package/src/services/todo/types.ts +12 -0
- package/dist/agents/openbot/index.js +0 -76
- package/dist/agents/openbot/middleware/approval.js +0 -132
- package/dist/agents/openbot/runtime.js +0 -289
- package/dist/agents/openbot/system-prompt.js +0 -32
- package/dist/agents/openbot/tools/delegation.js +0 -78
- package/dist/agents/openbot/tools/mcp.js +0 -99
- package/dist/agents/openbot/tools/shell.js +0 -91
- package/dist/agents/openbot/tools/storage.js +0 -75
- package/dist/agents/openbot/tools/ui.js +0 -176
- package/dist/agents/system.js +0 -33
- package/dist/bus/agent-package.js +0 -1
- package/dist/bus/plugin.js +0 -1
- package/dist/bus/services.js +0 -754
- package/dist/harness/agent-harness.js +0 -45
- package/dist/harness/agent-invoke-run.js +0 -44
- package/dist/harness/agent-turn.js +0 -99
- package/dist/harness/channel-participants.js +0 -40
- package/dist/harness/constants.js +0 -2
- package/dist/harness/context-meter.js +0 -97
- package/dist/harness/context.js +0 -363
- package/dist/harness/dispatch.js +0 -144
- package/dist/harness/dispatcher.js +0 -156
- package/dist/harness/event-normalizer.js +0 -59
- package/dist/harness/history.js +0 -177
- package/dist/harness/mcp.js +0 -61
- package/dist/harness/orchestration.js +0 -88
- package/dist/harness/orchestrator.js +0 -149
- package/dist/harness/participants.js +0 -22
- package/dist/harness/process.js +0 -29
- package/dist/harness/queue-processor.js +0 -187
- package/dist/harness/run-harness.js +0 -154
- package/dist/harness/run.js +0 -98
- package/dist/harness/runtime-factory.js +0 -73
- package/dist/harness/runtime.js +0 -57
- package/dist/harness/todo-advance.js +0 -93
- package/dist/harness/todo-dispatch.js +0 -51
- package/dist/harness/todos.js +0 -5
- package/dist/harness/turn.js +0 -79
- package/dist/harness/types.js +0 -1
- package/dist/plugins/ai-sdk/index.js +0 -34
- package/dist/plugins/ai-sdk/runtime.js +0 -395
- package/dist/plugins/ai-sdk/system-prompt.js +0 -18
- package/dist/plugins/ai-sdk.js +0 -331
- package/dist/plugins/approval.js +0 -163
- package/dist/plugins/delegation.js +0 -108
- package/dist/plugins/mcp/index.js +0 -108
- package/dist/plugins/mcp.js +0 -140
- package/dist/plugins/shell/index.js +0 -100
- package/dist/plugins/shell.js +0 -123
- package/dist/plugins/storage-tools/index.js +0 -76
- package/dist/plugins/storage.js +0 -737
- package/dist/plugins/thread-namer/index.js +0 -72
- package/dist/plugins/threads/index.js +0 -114
- package/dist/plugins/ui.js +0 -211
- package/dist/plugins/workflow/index.js +0 -65
- package/dist/registry/agents.js +0 -138
- package/dist/registry/plugins.js +0 -110
- package/dist/services/agent-packages.js +0 -103
- package/dist/services/memory.js +0 -152
- package/dist/services/plugins.js +0 -98
- package/dist/services/storage.js +0 -1028
- package/dist/workflow/service.js +0 -106
- package/dist/workflow/types.js +0 -3
- package/src/plugins/approval/index.ts +0 -147
- package/src/plugins/bash/index.ts +0 -232
- package/src/plugins/delegation/index.ts +0 -153
- package/src/plugins/memory/index.ts +0 -182
- package/src/plugins/openbot/context.ts +0 -121
- package/src/plugins/openbot/history.ts +0 -158
- package/src/plugins/openbot/index.ts +0 -80
- package/src/plugins/openbot/runtime.ts +0 -526
- package/src/plugins/openbot/system-prompt.ts +0 -22
- package/src/plugins/ui/index.ts +0 -123
- /package/dist/{bus → services/todo}/types.js +0 -0
|
@@ -1,53 +1,146 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
|
-
|
|
2
|
+
import { todoService } from './service.js';
|
|
3
|
+
/**
|
|
4
|
+
* `todo` — thread-scoped task checklist for multi-step agent work.
|
|
5
|
+
*
|
|
6
|
+
* Persisted at `~/.openbot/channels/<channelId>/threads/<threadId>/todos.json`.
|
|
7
|
+
* The agent replaces the full list via `todo_write`; the runtime injects the
|
|
8
|
+
* current list into context each turn.
|
|
9
|
+
*/
|
|
10
|
+
/** Map todo statuses onto UI list-item status label + variant. */
|
|
11
|
+
function toWidgetItemStatus(status) {
|
|
12
|
+
switch (status) {
|
|
13
|
+
case 'pending':
|
|
14
|
+
return { status: 'Pending', statusVariant: 'default' };
|
|
15
|
+
case 'in_progress':
|
|
16
|
+
return { status: 'In progress', statusVariant: 'info' };
|
|
17
|
+
case 'completed':
|
|
18
|
+
return { status: 'Done', statusVariant: 'success' };
|
|
19
|
+
case 'cancelled':
|
|
20
|
+
return { status: 'Cancelled', statusVariant: 'danger' };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function formatTodoOutput(list) {
|
|
24
|
+
if (list.items.length === 0)
|
|
25
|
+
return 'Todo list is empty.';
|
|
26
|
+
const lines = list.items.map((t) => `- [${t.status}] (${t.id}) ${t.content}`);
|
|
27
|
+
return `Todos (${list.items.length}):\n${lines.join('\n')}`;
|
|
28
|
+
}
|
|
29
|
+
function todoListWidget(args) {
|
|
30
|
+
const items = args.list.items.map((t) => ({
|
|
31
|
+
id: t.id,
|
|
32
|
+
label: t.content,
|
|
33
|
+
...toWidgetItemStatus(t.status),
|
|
34
|
+
}));
|
|
35
|
+
return {
|
|
36
|
+
type: 'client:ui:widget',
|
|
37
|
+
data: {
|
|
38
|
+
// One widget per agent run — repeated todo_write calls update in place.
|
|
39
|
+
widgetId: `todos:${args.runId}`,
|
|
40
|
+
kind: 'list',
|
|
41
|
+
title: 'Todos',
|
|
42
|
+
description: args.list.items.length === 0
|
|
43
|
+
? 'No todos'
|
|
44
|
+
: `${args.list.items.filter((t) => t.status === 'completed').length}/${args.list.items.length} completed`,
|
|
45
|
+
items,
|
|
46
|
+
display: 'expanded',
|
|
47
|
+
state: 'open',
|
|
48
|
+
},
|
|
49
|
+
meta: {},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const todoItemSchema = z.object({
|
|
53
|
+
id: z.string().min(1).describe('Stable todo id (e.g. "1", "setup-repo").'),
|
|
54
|
+
content: z.string().min(1).describe('Short imperative description of the step.'),
|
|
55
|
+
status: z
|
|
56
|
+
.enum(['pending', 'in_progress', 'completed', 'cancelled'])
|
|
57
|
+
.describe('Todo status. At most one item may be in_progress.'),
|
|
58
|
+
});
|
|
3
59
|
const todoToolDefinitions = {
|
|
4
60
|
todo_write: {
|
|
5
|
-
description: '
|
|
61
|
+
description: 'Replace the current thread todo list with the provided items. Use for multi-step tasks: write the plan first, keep exactly one item in_progress while working, mark items completed immediately after finishing, and when done leave all items as completed (do not clear the list). Pass the full intended list each call (not a partial patch).',
|
|
6
62
|
inputSchema: z.object({
|
|
7
|
-
|
|
8
|
-
.array(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
.optional()
|
|
12
|
-
.describe('Stable id. Reuse existing ids to update; omit to create.'),
|
|
13
|
-
content: z.string().min(1).optional().describe('What needs to be done.'),
|
|
14
|
-
status: todoStatus.optional().describe('Defaults to `pending`.'),
|
|
15
|
-
assignee: z
|
|
16
|
-
.string()
|
|
17
|
-
.optional()
|
|
18
|
-
.describe('Suggested agent id for this step (plain id, no @ prefix).'),
|
|
19
|
-
deleted: z.boolean().optional().describe('If true, remove this item.'),
|
|
20
|
-
}))
|
|
21
|
-
.describe('List of todo items to write or patch.'),
|
|
22
|
-
merge: z
|
|
23
|
-
.boolean()
|
|
24
|
-
.optional()
|
|
25
|
-
.describe('If true (default), patches existing items by id and appends new ones. If false, replaces the entire list.'),
|
|
63
|
+
items: z
|
|
64
|
+
.array(todoItemSchema)
|
|
65
|
+
.max(20)
|
|
66
|
+
.describe('Full todo list for this thread (max 20). Replaces any previous list.'),
|
|
26
67
|
}),
|
|
27
68
|
},
|
|
28
|
-
|
|
29
|
-
description: '
|
|
30
|
-
|
|
31
|
-
inputSchema: z.object({
|
|
32
|
-
agentId: z
|
|
33
|
-
.string()
|
|
34
|
-
.min(1)
|
|
35
|
-
.describe('Worker agent id from channel participants (plain id, no @ prefix).'),
|
|
36
|
-
task: z
|
|
37
|
-
.string()
|
|
38
|
-
.min(1)
|
|
39
|
-
.describe('Complete instruction for the worker — they do not see the full todo plan.'),
|
|
40
|
-
todoId: z.string().optional().describe('Optional todo id this step relates to.'),
|
|
41
|
-
}),
|
|
69
|
+
todo_read: {
|
|
70
|
+
description: 'Read the current thread todo list. Usually unnecessary — the list is already injected into context each turn. Use only if you need an explicit refresh after an external change.',
|
|
71
|
+
inputSchema: z.object({}),
|
|
42
72
|
},
|
|
43
73
|
};
|
|
44
74
|
export const todoPlugin = {
|
|
45
75
|
id: 'todo',
|
|
46
76
|
name: 'Todo',
|
|
47
|
-
description: '
|
|
77
|
+
description: 'Thread-scoped todo checklist for multi-step task tracking (todo_write / todo_read).',
|
|
48
78
|
toolDefinitions: todoToolDefinitions,
|
|
49
|
-
factory: () => () => {
|
|
50
|
-
|
|
79
|
+
factory: () => (builder) => {
|
|
80
|
+
builder.on('action:todo_write', async function* (event, context) {
|
|
81
|
+
const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
|
|
82
|
+
try {
|
|
83
|
+
const channelId = context.state.channelId;
|
|
84
|
+
const threadId = context.state.threadId;
|
|
85
|
+
if (!channelId || !threadId) {
|
|
86
|
+
throw new Error('Missing channelId or threadId for todo_write');
|
|
87
|
+
}
|
|
88
|
+
const items = event.data.items;
|
|
89
|
+
const list = await todoService.writeTodos({ channelId, threadId, items });
|
|
90
|
+
const runId = context.state.runId;
|
|
91
|
+
if (!runId) {
|
|
92
|
+
throw new Error('Missing runId for todo_write widget');
|
|
93
|
+
}
|
|
94
|
+
const widget = todoListWidget({ runId, list });
|
|
95
|
+
widget.meta = { ...resultMeta, threadId, runId };
|
|
96
|
+
yield widget;
|
|
97
|
+
yield {
|
|
98
|
+
type: 'action:todo_write:result',
|
|
99
|
+
data: { success: true, list, output: formatTodoOutput(list) },
|
|
100
|
+
meta: resultMeta,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
105
|
+
yield {
|
|
106
|
+
type: 'action:todo_write:result',
|
|
107
|
+
data: {
|
|
108
|
+
success: false,
|
|
109
|
+
error: message,
|
|
110
|
+
output: message,
|
|
111
|
+
},
|
|
112
|
+
meta: resultMeta,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
builder.on('action:todo_read', async function* (event, context) {
|
|
117
|
+
const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
|
|
118
|
+
try {
|
|
119
|
+
const channelId = context.state.channelId;
|
|
120
|
+
const threadId = context.state.threadId;
|
|
121
|
+
if (!channelId || !threadId) {
|
|
122
|
+
throw new Error('Missing channelId or threadId for todo_read');
|
|
123
|
+
}
|
|
124
|
+
const list = await todoService.getTodos({ channelId, threadId });
|
|
125
|
+
yield {
|
|
126
|
+
type: 'action:todo_read:result',
|
|
127
|
+
data: { success: true, list, output: formatTodoOutput(list) },
|
|
128
|
+
meta: resultMeta,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
133
|
+
yield {
|
|
134
|
+
type: 'action:todo_read:result',
|
|
135
|
+
data: {
|
|
136
|
+
success: false,
|
|
137
|
+
error: message,
|
|
138
|
+
output: message,
|
|
139
|
+
},
|
|
140
|
+
meta: resultMeta,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
});
|
|
51
144
|
},
|
|
52
145
|
};
|
|
53
146
|
export default todoPlugin;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DEFAULT_CHANNELS_DIR, getBaseDir, resolvePath } from '../../app/config.js';
|
|
4
|
+
export const MAX_TODO_ITEMS = 20;
|
|
5
|
+
const TODO_STATUSES = new Set([
|
|
6
|
+
'pending',
|
|
7
|
+
'in_progress',
|
|
8
|
+
'completed',
|
|
9
|
+
'cancelled',
|
|
10
|
+
]);
|
|
11
|
+
const getThreadDir = (channelId, threadId) => {
|
|
12
|
+
const base = resolvePath(path.join(getBaseDir(), DEFAULT_CHANNELS_DIR, channelId));
|
|
13
|
+
return path.join(base, 'threads', threadId);
|
|
14
|
+
};
|
|
15
|
+
const getTodosPath = (channelId, threadId) => path.join(getThreadDir(channelId, threadId), 'todos.json');
|
|
16
|
+
const emptyList = () => ({
|
|
17
|
+
items: [],
|
|
18
|
+
updatedAt: new Date().toISOString(),
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Validate and normalize a todo list write. Throws on invalid input.
|
|
22
|
+
* Enforces: non-empty content, known statuses, unique ids, max size,
|
|
23
|
+
* and at most one `in_progress` item.
|
|
24
|
+
*/
|
|
25
|
+
export function validateTodoItems(items) {
|
|
26
|
+
if (!Array.isArray(items)) {
|
|
27
|
+
throw new Error('todos must be an array');
|
|
28
|
+
}
|
|
29
|
+
if (items.length > MAX_TODO_ITEMS) {
|
|
30
|
+
throw new Error(`At most ${MAX_TODO_ITEMS} todos allowed`);
|
|
31
|
+
}
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
let inProgressCount = 0;
|
|
34
|
+
const normalized = [];
|
|
35
|
+
for (const raw of items) {
|
|
36
|
+
if (!raw || typeof raw !== 'object') {
|
|
37
|
+
throw new Error('Each todo must be an object');
|
|
38
|
+
}
|
|
39
|
+
const id = typeof raw.id === 'string' ? raw.id.trim() : '';
|
|
40
|
+
const content = typeof raw.content === 'string' ? raw.content.trim() : '';
|
|
41
|
+
const status = raw.status;
|
|
42
|
+
if (!id)
|
|
43
|
+
throw new Error('Each todo requires a non-empty id');
|
|
44
|
+
if (!content)
|
|
45
|
+
throw new Error(`Todo "${id}" requires non-empty content`);
|
|
46
|
+
if (!TODO_STATUSES.has(status)) {
|
|
47
|
+
throw new Error(`Todo "${id}" has invalid status "${String(status)}"; expected pending|in_progress|completed|cancelled`);
|
|
48
|
+
}
|
|
49
|
+
if (seen.has(id))
|
|
50
|
+
throw new Error(`Duplicate todo id "${id}"`);
|
|
51
|
+
seen.add(id);
|
|
52
|
+
if (status === 'in_progress')
|
|
53
|
+
inProgressCount += 1;
|
|
54
|
+
normalized.push({ id, content, status: status });
|
|
55
|
+
}
|
|
56
|
+
if (inProgressCount > 1) {
|
|
57
|
+
throw new Error('At most one todo may be in_progress');
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
export const todoService = {
|
|
62
|
+
getTodos: async (args) => {
|
|
63
|
+
const filePath = getTodosPath(args.channelId, args.threadId);
|
|
64
|
+
try {
|
|
65
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
66
|
+
const parsed = JSON.parse(raw);
|
|
67
|
+
const items = Array.isArray(parsed.items) ? parsed.items : [];
|
|
68
|
+
return {
|
|
69
|
+
items,
|
|
70
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
if (e?.code === 'ENOENT')
|
|
75
|
+
return emptyList();
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
writeTodos: async (args) => {
|
|
80
|
+
const items = validateTodoItems(args.items);
|
|
81
|
+
const list = {
|
|
82
|
+
items,
|
|
83
|
+
updatedAt: new Date().toISOString(),
|
|
84
|
+
};
|
|
85
|
+
const threadDir = getThreadDir(args.channelId, args.threadId);
|
|
86
|
+
await fs.mkdir(threadDir, { recursive: true });
|
|
87
|
+
const filePath = getTodosPath(args.channelId, args.threadId);
|
|
88
|
+
const tmp = `${filePath}.tmp`;
|
|
89
|
+
await fs.writeFile(tmp, `${JSON.stringify(list, null, 2)}\n`, 'utf-8');
|
|
90
|
+
await fs.rename(tmp, filePath);
|
|
91
|
+
return list;
|
|
92
|
+
},
|
|
93
|
+
};
|
package/dist/plugins/ui/index.js
CHANGED
|
@@ -36,7 +36,14 @@ export const uiPlugin = {
|
|
|
36
36
|
id: z.string(),
|
|
37
37
|
label: z.string(),
|
|
38
38
|
description: z.string().optional(),
|
|
39
|
-
status: z
|
|
39
|
+
status: z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('Status label shown on the item (e.g. "Pending", "Shipped").'),
|
|
43
|
+
statusVariant: z
|
|
44
|
+
.enum(['default', 'success', 'warning', 'danger', 'info'])
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Semantic hint for status badge coloring in the client.'),
|
|
40
47
|
metadata: z.record(z.string(), z.any()).optional()
|
|
41
48
|
})).optional().describe('Required for kind="list". List of items to display.'),
|
|
42
49
|
submitLabel: z.string().optional().describe('Label for the primary action button (e.g. "Submit", "Save").')
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DEFAULT_CLOUD_OPENBOT_AUTH_MODE, isCloudMode, isCloudSystemAgent, parseOpenbotAuthMode, } from '../../app/cloud-mode.js';
|
|
2
|
+
import { ORCHESTRATOR_AGENT_ID } from '../../app/agent-ids.js';
|
|
3
|
+
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
4
|
+
import { getBaseDir, resolvePath, saveConfig } from '../../app/config.js';
|
|
5
|
+
import { listApiKeyProvidersFromRegistry, resolveModelRegistry, } from './model-registry.js';
|
|
6
|
+
export function createPluginHost(runAgent) {
|
|
7
|
+
return {
|
|
8
|
+
runAgent,
|
|
9
|
+
isCloudSystemAgent,
|
|
10
|
+
isCloudMode,
|
|
11
|
+
parseOpenbotAuthMode,
|
|
12
|
+
resolveModelRegistry,
|
|
13
|
+
listApiKeyProvidersFromRegistry,
|
|
14
|
+
saveConfig,
|
|
15
|
+
getBaseDir,
|
|
16
|
+
resolvePath,
|
|
17
|
+
orchestratorAgentId: ORCHESTRATOR_AGENT_ID,
|
|
18
|
+
openbotPluginId: OPENBOT_PLUGIN_ID,
|
|
19
|
+
defaultCloudAuthMode: DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { DEFAULT_MARKETPLACE_REGISTRY_URL, loadConfig, } from '../../app/config.js';
|
|
2
|
+
import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
|
|
3
|
+
import { DEFAULT_CLOUD_OPENBOT_AUTH_MODE, isCloudMode, } from '../../app/cloud-mode.js';
|
|
4
|
+
let cachedRegistry = null;
|
|
5
|
+
let cacheUrl = null;
|
|
6
|
+
function getRegistryUrl() {
|
|
7
|
+
const { marketplaceRegistryUrl } = loadConfig();
|
|
8
|
+
return marketplaceRegistryUrl?.trim() || DEFAULT_MARKETPLACE_REGISTRY_URL;
|
|
9
|
+
}
|
|
10
|
+
export async function resolveModelRegistry() {
|
|
11
|
+
const url = getRegistryUrl();
|
|
12
|
+
if (cachedRegistry && cacheUrl === url)
|
|
13
|
+
return cachedRegistry;
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(url, {
|
|
16
|
+
headers: { Accept: 'application/json' },
|
|
17
|
+
signal: AbortSignal.timeout(15000),
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
throw new Error(`Registry HTTP ${res.status} ${res.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
cachedRegistry = (await res.json());
|
|
23
|
+
cacheUrl = url;
|
|
24
|
+
return cachedRegistry;
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.warn('[model-registry] fetch failed:', err instanceof Error ? err.message : err);
|
|
28
|
+
return { providers: {} };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function listModelOptionsFromRegistry(registry) {
|
|
32
|
+
const providers = registry.providers ?? {};
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const [providerId, provider] of Object.entries(providers)) {
|
|
35
|
+
for (const model of provider.models ?? []) {
|
|
36
|
+
out.push({
|
|
37
|
+
value: `${providerId}/${model.id}`,
|
|
38
|
+
label: `${provider.label} — ${model.label}`,
|
|
39
|
+
description: model.description,
|
|
40
|
+
provider: providerId,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
export async function listRegistryModelOptions() {
|
|
47
|
+
const registry = await resolveModelRegistry();
|
|
48
|
+
return listModelOptionsFromRegistry(registry);
|
|
49
|
+
}
|
|
50
|
+
export function listApiKeyProvidersFromRegistry(registry) {
|
|
51
|
+
const providers = registry.providers ?? {};
|
|
52
|
+
return Object.entries(providers).map(([id, provider]) => ({
|
|
53
|
+
id,
|
|
54
|
+
label: provider.label,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
function pickDefaultModelValue(options) {
|
|
58
|
+
if (options.length === 0)
|
|
59
|
+
return undefined;
|
|
60
|
+
const values = options.map((option) => option.value);
|
|
61
|
+
const preferred = values.find((value) => value.startsWith('openai/'));
|
|
62
|
+
return preferred ?? values[0];
|
|
63
|
+
}
|
|
64
|
+
export function enrichOpenbotPluginDescriptor(descriptor, modelOptions) {
|
|
65
|
+
if (descriptor.id !== OPENBOT_PLUGIN_ID || !descriptor.configSchema)
|
|
66
|
+
return descriptor;
|
|
67
|
+
const modelProperty = descriptor.configSchema.properties.model;
|
|
68
|
+
if (!modelProperty)
|
|
69
|
+
return descriptor;
|
|
70
|
+
const values = modelOptions.map((option) => option.value);
|
|
71
|
+
const staticDefault = typeof modelProperty.default === 'string' ? modelProperty.default : undefined;
|
|
72
|
+
const defaultModel = staticDefault && values.includes(staticDefault)
|
|
73
|
+
? staticDefault
|
|
74
|
+
: pickDefaultModelValue(modelOptions) ?? staticDefault;
|
|
75
|
+
const nextModelProperty = {
|
|
76
|
+
...modelProperty,
|
|
77
|
+
description: 'Model from the hosted marketplace registry.',
|
|
78
|
+
};
|
|
79
|
+
if (values.length > 0) {
|
|
80
|
+
nextModelProperty.enum = values;
|
|
81
|
+
nextModelProperty.options = modelOptions.map((option) => ({
|
|
82
|
+
label: option.label,
|
|
83
|
+
value: option.value,
|
|
84
|
+
description: option.description,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
if (defaultModel) {
|
|
88
|
+
nextModelProperty.default = defaultModel;
|
|
89
|
+
}
|
|
90
|
+
const { model: _model, authMode: _authMode, ...otherProperties } = descriptor.configSchema.properties;
|
|
91
|
+
const cloudAuthModeProperty = {
|
|
92
|
+
type: 'string',
|
|
93
|
+
description: 'Credits uses OpenBot platform billing. BYOK uses your own API keys.',
|
|
94
|
+
enum: ['credits', 'byok'],
|
|
95
|
+
default: DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
|
|
96
|
+
options: [
|
|
97
|
+
{
|
|
98
|
+
label: 'Credits',
|
|
99
|
+
value: 'credits',
|
|
100
|
+
description: 'Use OpenBot platform credits.',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
label: 'Bring your own key',
|
|
104
|
+
value: 'byok',
|
|
105
|
+
description: 'Use your own provider API keys.',
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
const nextProperties = isCloudMode()
|
|
110
|
+
? {
|
|
111
|
+
authMode: cloudAuthModeProperty,
|
|
112
|
+
model: nextModelProperty,
|
|
113
|
+
...otherProperties,
|
|
114
|
+
}
|
|
115
|
+
: {
|
|
116
|
+
model: nextModelProperty,
|
|
117
|
+
...otherProperties,
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
...descriptor,
|
|
121
|
+
configSchema: {
|
|
122
|
+
...descriptor.configSchema,
|
|
123
|
+
properties: nextProperties,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -1,25 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import { openbotPlugin } from '../../plugins/openbot/index.js';
|
|
5
|
-
import { bashPlugin } from '../../plugins/bash/index.js';
|
|
6
4
|
import { storagePlugin } from '../../plugins/storage/index.js';
|
|
7
|
-
import { approvalPlugin } from '../../plugins/approval/index.js';
|
|
8
|
-
import { memoryPlugin } from '../../plugins/memory/index.js';
|
|
9
|
-
import { delegationPlugin } from '../../plugins/delegation/index.js';
|
|
10
|
-
import { uiPlugin } from '../../plugins/ui/index.js';
|
|
11
5
|
import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
|
|
12
|
-
import { DEFAULT_PLUGINS_DIR,
|
|
6
|
+
import { DEFAULT_PLUGINS_DIR, getBaseDir } from '../../app/config.js';
|
|
13
7
|
import { invalidatePlugin as clearResolvedPluginEntry, loadedCommunityPlugins, resolvedPluginCache, } from './plugin-cache.js';
|
|
14
8
|
let pluginsDir = null;
|
|
15
9
|
const BUILT_IN = {
|
|
16
|
-
[openbotPlugin.id]: openbotPlugin,
|
|
17
|
-
[bashPlugin.id]: bashPlugin,
|
|
18
10
|
[storagePlugin.id]: storagePlugin,
|
|
19
|
-
[approvalPlugin.id]: approvalPlugin,
|
|
20
|
-
[memoryPlugin.id]: memoryPlugin,
|
|
21
|
-
[delegationPlugin.id]: delegationPlugin,
|
|
22
|
-
[uiPlugin.id]: uiPlugin,
|
|
23
11
|
[pluginManagerPlugin.id]: pluginManagerPlugin,
|
|
24
12
|
};
|
|
25
13
|
/** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
|
|
@@ -51,53 +39,51 @@ export function initPlugins(dir) {
|
|
|
51
39
|
pluginsDir = dir;
|
|
52
40
|
}
|
|
53
41
|
else {
|
|
54
|
-
|
|
55
|
-
const baseDir = config.baseDir || DEFAULT_BASE_DIR;
|
|
56
|
-
pluginsDir = path.join(resolvePath(baseDir), DEFAULT_PLUGINS_DIR);
|
|
42
|
+
pluginsDir = path.join(getBaseDir(), DEFAULT_PLUGINS_DIR);
|
|
57
43
|
}
|
|
58
44
|
}
|
|
59
45
|
/**
|
|
60
46
|
* Resolve a Plugin by id. The id is either:
|
|
61
|
-
* - a built-in id (e.g. "
|
|
62
|
-
* - an npm package name (e.g. "
|
|
47
|
+
* - a built-in id (e.g. "storage", "plugin-manager"), or
|
|
48
|
+
* - an npm package name (e.g. "@meetopenbot/openbot"),
|
|
63
49
|
* in which case the folder layout is `plugins/<id>/dist/index.js`.
|
|
64
50
|
*/
|
|
65
51
|
export async function resolvePlugin(id) {
|
|
66
52
|
if (resolvedPluginCache.has(id))
|
|
67
53
|
return resolvedPluginCache.get(id);
|
|
68
|
-
if (BUILT_IN[id]) {
|
|
69
|
-
resolvedPluginCache.set(id, BUILT_IN[id]);
|
|
70
|
-
return BUILT_IN[id];
|
|
71
|
-
}
|
|
72
54
|
if (!pluginsDir) {
|
|
73
55
|
initPlugins();
|
|
74
56
|
}
|
|
75
57
|
if (!pluginsDir)
|
|
76
58
|
return null;
|
|
77
59
|
const distPath = path.join(pluginsDir, id, 'dist', 'index.js');
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
60
|
+
if (existsSync(distPath)) {
|
|
61
|
+
try {
|
|
62
|
+
const module = await import(pathToFileURL(distPath).href);
|
|
63
|
+
const parsed = parsePluginModule(module);
|
|
64
|
+
if (!parsed) {
|
|
65
|
+
console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const plugin = { id, ...parsed, name: parsed.name || id };
|
|
69
|
+
resolvedPluginCache.set(id, plugin);
|
|
70
|
+
if (!loadedCommunityPlugins.has(id)) {
|
|
71
|
+
console.log(`[plugins] Loaded plugin "${id}" from ${distPath}`);
|
|
72
|
+
loadedCommunityPlugins.add(id);
|
|
73
|
+
}
|
|
74
|
+
return plugin;
|
|
75
|
+
}
|
|
88
76
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (!loadedCommunityPlugins.has(id)) {
|
|
92
|
-
console.log(`[plugins] Loaded community plugin "${id}" from ${distPath}`);
|
|
93
|
-
loadedCommunityPlugins.add(id);
|
|
77
|
+
catch (e) {
|
|
78
|
+
console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
|
|
94
79
|
}
|
|
95
|
-
return plugin;
|
|
96
80
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return
|
|
81
|
+
if (BUILT_IN[id]) {
|
|
82
|
+
resolvedPluginCache.set(id, BUILT_IN[id]);
|
|
83
|
+
return BUILT_IN[id];
|
|
100
84
|
}
|
|
85
|
+
console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
|
|
86
|
+
return null;
|
|
101
87
|
}
|
|
102
88
|
/** Drop a single id from the in-memory cache (e.g. after fresh install). */
|
|
103
89
|
export function invalidatePlugin(id) {
|
|
@@ -3,7 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { exec } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import { DEFAULT_PLUGINS_DIR,
|
|
6
|
+
import { DEFAULT_PLUGINS_DIR, DEFAULT_MARKETPLACE_REGISTRY_URL, getBaseDir, loadConfig, } from '../../app/config.js';
|
|
7
7
|
import { invalidatePlugin } from './plugin-cache.js';
|
|
8
8
|
const execAsync = promisify(exec);
|
|
9
9
|
const DEFAULT_MARKETPLACE_AGENTS = [];
|
|
@@ -136,11 +136,7 @@ export async function resolveMarketplaceAgentList() {
|
|
|
136
136
|
const registry = await resolveMarketplaceRegistry();
|
|
137
137
|
return registry.agents;
|
|
138
138
|
}
|
|
139
|
-
const getPluginsDir = () =>
|
|
140
|
-
const config = loadConfig();
|
|
141
|
-
const baseDir = resolvePath(config.baseDir || DEFAULT_BASE_DIR);
|
|
142
|
-
return path.join(baseDir, DEFAULT_PLUGINS_DIR);
|
|
143
|
-
};
|
|
139
|
+
const getPluginsDir = () => path.join(getBaseDir(), DEFAULT_PLUGINS_DIR);
|
|
144
140
|
/**
|
|
145
141
|
* Lifecycle for community-built plugins distributed via npm.
|
|
146
142
|
* Each plugin is installed to `<plugins>/<npm-name>/` and is identified
|
package/docs/agents.md
CHANGED
|
@@ -36,8 +36,8 @@ A runtime plugin is one that handles `agent:invoke` (the LLM loop). Without
|
|
|
36
36
|
one, the agent will not respond to user input. Built-in runtime plugins:
|
|
37
37
|
|
|
38
38
|
- `openbot` — the standard, opinionated OpenBot agent runtime. It is
|
|
39
|
-
**batteries-included** and provides inbuilt tools (bash, memory,
|
|
40
|
-
delegation, and approval).
|
|
39
|
+
**batteries-included** and provides inbuilt tools (bash, memory, todo,
|
|
40
|
+
storage, delegation, and approval).
|
|
41
41
|
- `claude-code` — runs Claude inside the Claude Agent SDK with its own tools.
|
|
42
42
|
- `gemini-cli` — spawns Google's `gemini` CLI in headless mode.
|
|
43
43
|
|
|
@@ -66,6 +66,24 @@ On every LLM turn the runtime injects matching memories into the system prompt
|
|
|
66
66
|
via the `MemoryProvider` in the context engine, so the model treats remembered
|
|
67
67
|
facts as ground truth without needing to call `recall` first.
|
|
68
68
|
|
|
69
|
+
## Todos
|
|
70
|
+
|
|
71
|
+
The `todo` plugin gives agents `todo_write` and `todo_read` for thread-scoped
|
|
72
|
+
multi-step task tracking. The list is stored at
|
|
73
|
+
`~/.openbot/channels/<channelId>/threads/<threadId>/todos.json`.
|
|
74
|
+
|
|
75
|
+
Rules enforced by the runtime:
|
|
76
|
+
|
|
77
|
+
- At most 20 items
|
|
78
|
+
- At most one item may be `in_progress`
|
|
79
|
+
- Each `todo_write` replaces the full list
|
|
80
|
+
|
|
81
|
+
On every LLM turn the current list is injected into context as `## TODOS`, so
|
|
82
|
+
the model usually does not need `todo_read`. Each successful `todo_write` also
|
|
83
|
+
emits a `client:ui:widget` list (`widgetId` `todos:<runId>`, stable for the
|
|
84
|
+
agent run so multiple `todo_write` calls update one widget), and both tools return
|
|
85
|
+
`data.output` for model feedback.
|
|
86
|
+
|
|
69
87
|
## Installing community agents
|
|
70
88
|
|
|
71
89
|
Marketplace entries reference plugin ids (built-in or npm package names).
|
package/docs/architecture.md
CHANGED
|
@@ -5,7 +5,7 @@ OpenBot is an orchestration platform built on a modular, event-driven architectu
|
|
|
5
5
|
## Core Components
|
|
6
6
|
|
|
7
7
|
### 1. Orchestrator & routing
|
|
8
|
-
The orchestrator is the execution entry point for agent work: it normalizes incoming events,
|
|
8
|
+
The orchestrator is the execution entry point for agent work: it normalizes incoming events, builds per-agent Melony runtimes, and streams emitted events back to callers (for example storage and SSE). Routing across the agent network uses:
|
|
9
9
|
|
|
10
10
|
1. **Command Prefix** — Explicit delegation to a specific agent (e.g., `/os list files`).
|
|
11
11
|
2. **DM context** — Direct communication with a specific agent.
|