openbot 0.4.0 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +10 -0
- package/dist/app/server.js +200 -3
- package/dist/harness/index.js +18 -0
- package/dist/plugins/approval/index.js +35 -20
- package/dist/plugins/bash/index.js +195 -0
- package/dist/plugins/delegation/index.js +6 -2
- package/dist/plugins/openbot/context.js +54 -9
- package/dist/plugins/openbot/history.js +47 -1
- package/dist/plugins/openbot/index.js +43 -3
- package/dist/plugins/openbot/runtime.js +91 -27
- package/dist/plugins/openbot/system-prompt.js +21 -1
- package/dist/plugins/plugin-manager/index.js +87 -3
- package/dist/plugins/shell/index.js +2 -1
- package/dist/plugins/storage/files.js +67 -0
- package/dist/plugins/storage/index.js +184 -7
- package/dist/plugins/storage/service.js +215 -59
- package/dist/plugins/ui/index.js +109 -150
- package/dist/services/abort.js +43 -0
- package/dist/services/plugins/registry.js +5 -3
- package/dist/services/plugins/service.js +66 -11
- package/docs/agents.md +5 -8
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +28 -7
- package/docs/templates/AGENT.example.md +4 -4
- package/package.json +7 -7
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +13 -0
- package/src/app/server.ts +235 -3
- package/src/app/types.ts +284 -14
- package/src/harness/index.ts +21 -0
- package/src/plugins/approval/index.ts +37 -20
- package/src/plugins/bash/index.ts +232 -0
- package/src/plugins/delegation/index.ts +7 -2
- package/src/plugins/openbot/context.ts +58 -9
- package/src/plugins/openbot/history.ts +52 -1
- package/src/plugins/openbot/index.ts +45 -3
- package/src/plugins/openbot/runtime.ts +121 -27
- package/src/plugins/openbot/system-prompt.ts +21 -1
- package/src/plugins/plugin-manager/index.ts +105 -3
- package/src/plugins/storage/files.ts +81 -0
- package/src/plugins/storage/index.ts +198 -8
- package/src/plugins/storage/service.ts +282 -59
- package/src/plugins/ui/index.ts +123 -0
- package/src/services/abort.ts +46 -0
- package/src/services/plugins/domain.ts +34 -1
- package/src/services/plugins/registry.ts +5 -3
- package/src/services/plugins/service.ts +136 -45
- package/src/services/plugins/types.ts +5 -1
- package/src/plugins/shell/index.ts +0 -123
package/dist/plugins/ui/index.js
CHANGED
|
@@ -1,154 +1,113 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const optionSchema = z.object({
|
|
10
|
-
label: z.string(),
|
|
11
|
-
value: z.string(),
|
|
12
|
-
});
|
|
13
|
-
const fieldSchema = z.object({
|
|
14
|
-
id: z.string().describe('Stable field ID used as the submitted value key.'),
|
|
15
|
-
label: z.string(),
|
|
16
|
-
type: z.enum(['text', 'textarea', 'number', 'boolean', 'select', 'multiselect', 'date']),
|
|
17
|
-
description: z.string().optional(),
|
|
18
|
-
placeholder: z.string().optional(),
|
|
19
|
-
required: z.boolean().optional(),
|
|
20
|
-
options: z.array(optionSchema).optional(),
|
|
21
|
-
defaultValue: z.unknown().optional(),
|
|
22
|
-
});
|
|
23
|
-
const listItemSchema = z.object({
|
|
24
|
-
id: z.string(),
|
|
25
|
-
label: z.string(),
|
|
26
|
-
description: z.string().optional(),
|
|
27
|
-
status: z.enum(['pending', 'in_progress', 'done', 'error', 'cancelled']).optional(),
|
|
28
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
29
|
-
});
|
|
30
|
-
const widgetBaseSchema = {
|
|
31
|
-
widgetId: z.string().optional().describe('Stable widget ID. Defaults from toolCallId.'),
|
|
32
|
-
title: z.string().optional(),
|
|
33
|
-
description: z.string().optional(),
|
|
34
|
-
body: z.string().optional(),
|
|
35
|
-
state: z.enum(['open', 'submitted', 'cancelled', 'error']).optional(),
|
|
36
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
37
|
-
};
|
|
38
|
-
const renderWidgetSchema = z.union([
|
|
39
|
-
z.object({
|
|
40
|
-
...widgetBaseSchema,
|
|
41
|
-
kind: z.literal('message'),
|
|
42
|
-
actions: z.array(actionSchema).optional(),
|
|
43
|
-
}),
|
|
44
|
-
z.object({
|
|
45
|
-
...widgetBaseSchema,
|
|
46
|
-
kind: z.literal('choice'),
|
|
47
|
-
actions: z.array(actionSchema).min(1),
|
|
48
|
-
}),
|
|
49
|
-
z.object({
|
|
50
|
-
...widgetBaseSchema,
|
|
51
|
-
kind: z.literal('form'),
|
|
52
|
-
fields: z.array(fieldSchema).optional(),
|
|
53
|
-
submitLabel: z.string().optional(),
|
|
54
|
-
actions: z.array(actionSchema).optional(),
|
|
55
|
-
props: z.record(z.string(), z.unknown()).optional(),
|
|
56
|
-
}),
|
|
57
|
-
z.object({
|
|
58
|
-
...widgetBaseSchema,
|
|
59
|
-
kind: z.literal('list'),
|
|
60
|
-
items: z.array(listItemSchema).optional(),
|
|
61
|
-
actions: z.array(actionSchema).optional(),
|
|
62
|
-
}),
|
|
63
|
-
z.object({
|
|
64
|
-
kind: z.literal('approval').describe('Legacy preset. Prefer choice or list.'),
|
|
65
|
-
widgetId: z.string().optional(),
|
|
66
|
-
title: z.string().optional(),
|
|
67
|
-
props: z.record(z.string(), z.unknown()).optional(),
|
|
68
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
69
|
-
}),
|
|
70
|
-
]);
|
|
71
|
-
const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
72
|
-
const readString = (value) => typeof value === 'string' && value.trim() ? value : undefined;
|
|
73
|
-
const asFields = (value) => Array.isArray(value) ? value : undefined;
|
|
74
|
-
const asListItems = (value) => Array.isArray(value) ? value : undefined;
|
|
75
|
-
const createWidgetId = (data, toolCallId) => {
|
|
76
|
-
if ('widgetId' in data && data.widgetId)
|
|
77
|
-
return data.widgetId;
|
|
78
|
-
if (toolCallId)
|
|
79
|
-
return `widget_${toolCallId}`;
|
|
80
|
-
return `widget_${Date.now()}`;
|
|
81
|
-
};
|
|
82
|
-
const normalizeWidget = (data, state, toolCallId) => {
|
|
83
|
-
const widgetId = createWidgetId(data, toolCallId);
|
|
84
|
-
if (data.kind === 'approval') {
|
|
85
|
-
const props = data.props || {};
|
|
86
|
-
return {
|
|
87
|
-
widgetId,
|
|
88
|
-
kind: 'choice',
|
|
89
|
-
title: data.title || 'Approval Required',
|
|
90
|
-
body: readString(props.message) ||
|
|
91
|
-
readString(props.summary) ||
|
|
92
|
-
'Please approve or deny this action.',
|
|
93
|
-
metadata: {
|
|
94
|
-
...(data.metadata || {}),
|
|
95
|
-
legacyKind: 'approval',
|
|
96
|
-
actionId: props.actionId,
|
|
97
|
-
},
|
|
98
|
-
actions: [
|
|
99
|
-
{ id: 'approve', label: 'Approve', value: props.actionId || 'approve', variant: 'primary' },
|
|
100
|
-
{ id: 'deny', label: 'Deny', value: props.actionId || 'deny', variant: 'danger' },
|
|
101
|
-
],
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
if (data.kind === 'form') {
|
|
105
|
-
const propsSource = data.props;
|
|
106
|
-
const props = isRecord(propsSource) ? propsSource : {};
|
|
107
|
-
return {
|
|
108
|
-
widgetId,
|
|
109
|
-
kind: 'form',
|
|
110
|
-
title: data.title || 'Details Required',
|
|
111
|
-
description: data.description,
|
|
112
|
-
body: data.body,
|
|
113
|
-
state: data.state,
|
|
114
|
-
metadata: data.metadata,
|
|
115
|
-
fields: data.fields || asFields(props.schema) || [],
|
|
116
|
-
submitLabel: data.submitLabel || readString(props.submitLabel),
|
|
117
|
-
actions: data.actions,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
if (data.kind === 'list') {
|
|
121
|
-
return { ...data, widgetId, title: data.title || 'Task List', items: data.items || [] };
|
|
122
|
-
}
|
|
123
|
-
if (data.kind === 'choice') {
|
|
124
|
-
return { ...data, widgetId, title: data.title || 'Choose an Option' };
|
|
125
|
-
}
|
|
126
|
-
if (data.kind === 'message') {
|
|
127
|
-
return { ...data, widgetId, title: data.title || 'Message' };
|
|
128
|
-
}
|
|
129
|
-
throw new Error(`Unsupported UI widget kind: ${data.kind || 'unknown'}`);
|
|
130
|
-
};
|
|
131
|
-
const uiToolDefinitions = {
|
|
132
|
-
render_ui_widget: {
|
|
133
|
-
description: 'Render a small server-driven UI widget in the conversation. Prefer primitive kinds: message, choice, form, or list. Legacy preset approval is accepted.',
|
|
134
|
-
inputSchema: renderWidgetSchema,
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
const uiPluginRuntime = () => (builder) => {
|
|
138
|
-
builder.on('action:render_ui_widget', async function* (event, context) {
|
|
139
|
-
const widget = normalizeWidget(event.data, context.state, event.meta?.toolCallId);
|
|
140
|
-
yield {
|
|
141
|
-
type: 'client:ui:widget',
|
|
142
|
-
data: widget,
|
|
143
|
-
meta: event.meta,
|
|
144
|
-
};
|
|
145
|
-
});
|
|
146
|
-
};
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* `ui` — provides a tool for the agent to render interactive UI widgets.
|
|
5
|
+
*
|
|
6
|
+
* The model can choose which widget to render (form, choice, list, message)
|
|
7
|
+
* depending on the situation.
|
|
8
|
+
*/
|
|
147
9
|
export const uiPlugin = {
|
|
148
10
|
id: 'ui',
|
|
149
|
-
name: 'UI
|
|
150
|
-
description: 'Render
|
|
151
|
-
toolDefinitions:
|
|
152
|
-
|
|
11
|
+
name: 'UI',
|
|
12
|
+
description: 'Render interactive UI widgets to interact with the user.',
|
|
13
|
+
toolDefinitions: {
|
|
14
|
+
render_widget: {
|
|
15
|
+
description: 'Render a UI widget to the user. Use "form" for data collection, "choice" for simple selection, "list" for displaying items, and "message" for simple notifications with actions. When using form widge to unquire user, do not provide complex forms with many fields, always try to make it simple to keep the experience smooth and straightforward.',
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
kind: z.enum(['message', 'choice', 'form', 'list']).describe('The type of widget to render.'),
|
|
18
|
+
title: z.string().describe('The title of the widget.'),
|
|
19
|
+
description: z.string().optional().describe('A description or body text.'),
|
|
20
|
+
fields: z.array(z.object({
|
|
21
|
+
id: z.string().describe('Unique ID for the field.'),
|
|
22
|
+
label: z.string().describe('Label shown to the user.'),
|
|
23
|
+
type: z.enum(['text', 'textarea', 'number', 'boolean', 'select', 'multiselect', 'date']),
|
|
24
|
+
description: z.string().optional(),
|
|
25
|
+
placeholder: z.string().optional(),
|
|
26
|
+
required: z.boolean().optional(),
|
|
27
|
+
options: z.array(z.object({ label: z.string(), value: z.string() })).optional(),
|
|
28
|
+
defaultValue: z.any().optional()
|
|
29
|
+
})).optional().describe('Required for kind="form". List of form fields.'),
|
|
30
|
+
actions: z.array(z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
label: z.string(),
|
|
33
|
+
variant: z.enum(['primary', 'secondary', 'danger']).optional(),
|
|
34
|
+
})).optional().describe('Buttons or actions available on the widget.'),
|
|
35
|
+
items: z.array(z.object({
|
|
36
|
+
id: z.string(),
|
|
37
|
+
label: z.string(),
|
|
38
|
+
description: z.string().optional(),
|
|
39
|
+
status: z.enum(['pending', 'in_progress', 'done', 'error', 'cancelled']).optional(),
|
|
40
|
+
metadata: z.record(z.string(), z.any()).optional()
|
|
41
|
+
})).optional().describe('Required for kind="list". List of items to display.'),
|
|
42
|
+
submitLabel: z.string().optional().describe('Label for the primary action button (e.g. "Submit", "Save").')
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
factory: () => (builder) => {
|
|
47
|
+
// Handle the tool call from the agent
|
|
48
|
+
builder.on('action:render_widget', async function* (event, context) {
|
|
49
|
+
const widgetEvent = event;
|
|
50
|
+
const toolCallId = widgetEvent.meta?.toolCallId;
|
|
51
|
+
const threadId = widgetEvent.meta?.threadId || context.state.threadId;
|
|
52
|
+
if (!toolCallId)
|
|
53
|
+
return;
|
|
54
|
+
const widgetId = randomUUID();
|
|
55
|
+
// Emit the UI widget event to the client
|
|
56
|
+
yield {
|
|
57
|
+
type: 'client:ui:widget',
|
|
58
|
+
data: {
|
|
59
|
+
...widgetEvent.data,
|
|
60
|
+
widgetId,
|
|
61
|
+
metadata: {
|
|
62
|
+
type: 'ui:request',
|
|
63
|
+
originalEvent: widgetEvent
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
meta: { agentId: context.state.agentId, threadId }
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
// Handle the user's response from the UI widget
|
|
70
|
+
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
71
|
+
const responseEvent = event;
|
|
72
|
+
const { widgetId, actionId, values, metadata } = responseEvent.data;
|
|
73
|
+
if (metadata?.type !== 'ui:request')
|
|
74
|
+
return;
|
|
75
|
+
const originalEvent = metadata.originalEvent;
|
|
76
|
+
const toolCallId = originalEvent?.meta?.toolCallId;
|
|
77
|
+
const threadId = originalEvent?.meta?.threadId || context.state.threadId;
|
|
78
|
+
if (!toolCallId)
|
|
79
|
+
return;
|
|
80
|
+
// Yield a "submitted" widget update to the UI to collapse/disable it
|
|
81
|
+
yield {
|
|
82
|
+
type: 'client:ui:widget',
|
|
83
|
+
data: {
|
|
84
|
+
widgetId,
|
|
85
|
+
title: originalEvent.data.title,
|
|
86
|
+
kind: originalEvent.data.kind,
|
|
87
|
+
state: 'submitted',
|
|
88
|
+
body: "Thank you for your response. We will process it and get back to you soon.",
|
|
89
|
+
display: 'collapsed',
|
|
90
|
+
disabled: true,
|
|
91
|
+
actions: [], // Clear actions to disable buttons in UI
|
|
92
|
+
},
|
|
93
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
94
|
+
};
|
|
95
|
+
// Emit the tool result event so the agent runtime can resume
|
|
96
|
+
yield {
|
|
97
|
+
type: 'action:render_widget:result',
|
|
98
|
+
data: {
|
|
99
|
+
success: true,
|
|
100
|
+
actionId,
|
|
101
|
+
values,
|
|
102
|
+
output: JSON.stringify(values)
|
|
103
|
+
},
|
|
104
|
+
meta: {
|
|
105
|
+
agentId: context.state.agentId,
|
|
106
|
+
threadId,
|
|
107
|
+
toolCallId
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
},
|
|
153
112
|
};
|
|
154
113
|
export default uiPlugin;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks in-flight agent runs so they can be cancelled.
|
|
3
|
+
*
|
|
4
|
+
* Runs are grouped by `channelId:threadId`. Delegated sub-agents run in the
|
|
5
|
+
* same channel/thread as their parent, so aborting that key stops the whole
|
|
6
|
+
* chain (parent + any delegated runs) in one shot.
|
|
7
|
+
*/
|
|
8
|
+
export const abortKey = (channelId, threadId) => `${channelId}:${threadId || ''}`;
|
|
9
|
+
class AbortRegistry {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.entries = new Map();
|
|
12
|
+
}
|
|
13
|
+
/** Register interest in a run. Returns a shared signal for the key. */
|
|
14
|
+
acquire(key) {
|
|
15
|
+
let entry = this.entries.get(key);
|
|
16
|
+
if (!entry) {
|
|
17
|
+
entry = { controller: new AbortController(), refs: 0 };
|
|
18
|
+
this.entries.set(key, entry);
|
|
19
|
+
}
|
|
20
|
+
entry.refs += 1;
|
|
21
|
+
return entry.controller.signal;
|
|
22
|
+
}
|
|
23
|
+
/** Release interest. Removes the entry once no runs reference it. */
|
|
24
|
+
release(key) {
|
|
25
|
+
const entry = this.entries.get(key);
|
|
26
|
+
if (!entry)
|
|
27
|
+
return;
|
|
28
|
+
entry.refs -= 1;
|
|
29
|
+
if (entry.refs <= 0) {
|
|
30
|
+
this.entries.delete(key);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Abort all runs for the key. Returns true if something was active. */
|
|
34
|
+
abort(key) {
|
|
35
|
+
const entry = this.entries.get(key);
|
|
36
|
+
if (!entry)
|
|
37
|
+
return false;
|
|
38
|
+
entry.controller.abort();
|
|
39
|
+
this.entries.delete(key);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export const abortRegistry = new AbortRegistry();
|
|
@@ -2,22 +2,24 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { openbotPlugin } from '../../plugins/openbot/index.js';
|
|
5
|
-
import {
|
|
5
|
+
import { bashPlugin } from '../../plugins/bash/index.js';
|
|
6
6
|
import { storagePlugin } from '../../plugins/storage/index.js';
|
|
7
7
|
import { approvalPlugin } from '../../plugins/approval/index.js';
|
|
8
8
|
import { memoryPlugin } from '../../plugins/memory/index.js';
|
|
9
9
|
import { delegationPlugin } from '../../plugins/delegation/index.js';
|
|
10
|
+
import { uiPlugin } from '../../plugins/ui/index.js';
|
|
10
11
|
import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
|
|
11
12
|
import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../../app/config.js';
|
|
12
13
|
import { invalidatePlugin as clearResolvedPluginEntry, loadedCommunityPlugins, resolvedPluginCache, } from './plugin-cache.js';
|
|
13
14
|
let pluginsDir = null;
|
|
14
15
|
const BUILT_IN = {
|
|
15
16
|
[openbotPlugin.id]: openbotPlugin,
|
|
16
|
-
[
|
|
17
|
+
[bashPlugin.id]: bashPlugin,
|
|
17
18
|
[storagePlugin.id]: storagePlugin,
|
|
18
19
|
[approvalPlugin.id]: approvalPlugin,
|
|
19
20
|
[memoryPlugin.id]: memoryPlugin,
|
|
20
21
|
[delegationPlugin.id]: delegationPlugin,
|
|
22
|
+
[uiPlugin.id]: uiPlugin,
|
|
21
23
|
[pluginManagerPlugin.id]: pluginManagerPlugin,
|
|
22
24
|
};
|
|
23
25
|
/** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
|
|
@@ -56,7 +58,7 @@ export function initPlugins(dir) {
|
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
60
|
* Resolve a Plugin by id. The id is either:
|
|
59
|
-
* - a built-in id (e.g. "openbot", "
|
|
61
|
+
* - a built-in id (e.g. "openbot", "bash"), or
|
|
60
62
|
* - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
|
|
61
63
|
* in which case the folder layout is `plugins/<id>/dist/index.js`.
|
|
62
64
|
*/
|
|
@@ -7,19 +7,27 @@ import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, DEFAULT_MARKETPLACE_REGISTRY_URL
|
|
|
7
7
|
import { invalidatePlugin } from './plugin-cache.js';
|
|
8
8
|
const execAsync = promisify(exec);
|
|
9
9
|
const DEFAULT_MARKETPLACE_AGENTS = [];
|
|
10
|
+
const DEFAULT_MARKETPLACE_CHANNELS = [];
|
|
10
11
|
function isRecord(value) {
|
|
11
12
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
13
|
}
|
|
13
14
|
/**
|
|
14
15
|
* Parses JSON from a remote registry file. Supports either
|
|
15
|
-
* `{ "agents": [ ... ] }` or a top-level array.
|
|
16
|
+
* `{ "agents": [ ... ], "channels": [ ... ] }` or a top-level array (legacy agents-only).
|
|
16
17
|
*/
|
|
17
18
|
export function parseMarketplaceRegistryJson(data) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const isLegacyArray = Array.isArray(data);
|
|
20
|
+
const rawAgents = isLegacyArray
|
|
21
|
+
? data
|
|
22
|
+
: isRecord(data) && Array.isArray(data.agents)
|
|
23
|
+
? data.agents
|
|
24
|
+
: [];
|
|
25
|
+
const rawChannels = !isLegacyArray && isRecord(data) && Array.isArray(data.channels)
|
|
26
|
+
? data.channels
|
|
27
|
+
: isRecord(data) && Array.isArray(data.templates)
|
|
28
|
+
? data.templates
|
|
29
|
+
: [];
|
|
30
|
+
const agents = (Array.isArray(rawAgents) ? rawAgents : []).map((item, i) => {
|
|
23
31
|
if (!isRecord(item)) {
|
|
24
32
|
throw new Error(`agents[${i}]: expected object`);
|
|
25
33
|
}
|
|
@@ -59,8 +67,47 @@ export function parseMarketplaceRegistryJson(data) {
|
|
|
59
67
|
}
|
|
60
68
|
return listing;
|
|
61
69
|
});
|
|
70
|
+
const channels = (Array.isArray(rawChannels) ? rawChannels : []).map((item, i) => {
|
|
71
|
+
if (!isRecord(item)) {
|
|
72
|
+
throw new Error(`channels[${i}]: expected object`);
|
|
73
|
+
}
|
|
74
|
+
const id = item.id;
|
|
75
|
+
const name = item.name;
|
|
76
|
+
const description = item.description;
|
|
77
|
+
const participants = item.participants;
|
|
78
|
+
if (typeof id !== 'string' || !id)
|
|
79
|
+
throw new Error(`channels[${i}].id must be a non-empty string`);
|
|
80
|
+
if (typeof name !== 'string')
|
|
81
|
+
throw new Error(`channels[${i}].name must be a string`);
|
|
82
|
+
if (typeof description !== 'string')
|
|
83
|
+
throw new Error(`channels[${i}].description must be a string`);
|
|
84
|
+
if (!Array.isArray(participants))
|
|
85
|
+
throw new Error(`channels[${i}].participants must be an array`);
|
|
86
|
+
const listing = {
|
|
87
|
+
id,
|
|
88
|
+
name,
|
|
89
|
+
description,
|
|
90
|
+
participants: participants.filter((p) => typeof p === 'string'),
|
|
91
|
+
};
|
|
92
|
+
if (typeof item.image === 'string')
|
|
93
|
+
listing.image = item.image;
|
|
94
|
+
if (typeof item.spec === 'string')
|
|
95
|
+
listing.spec = item.spec;
|
|
96
|
+
if (isRecord(item.initialState))
|
|
97
|
+
listing.initialState = item.initialState;
|
|
98
|
+
if (Array.isArray(item.starterPrompts)) {
|
|
99
|
+
listing.starterPrompts = item.starterPrompts.map((p, j) => {
|
|
100
|
+
if (!isRecord(p) || typeof p.label !== 'string' || typeof p.prompt !== 'string') {
|
|
101
|
+
throw new Error(`channels[${i}].starterPrompts[${j}] must have label and prompt`);
|
|
102
|
+
}
|
|
103
|
+
return { label: p.label, prompt: p.prompt };
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return listing;
|
|
107
|
+
});
|
|
108
|
+
return { agents, channels };
|
|
62
109
|
}
|
|
63
|
-
async function
|
|
110
|
+
async function fetchMarketplaceRegistryFromUrl(url) {
|
|
64
111
|
const res = await fetch(url, {
|
|
65
112
|
headers: { Accept: 'application/json' },
|
|
66
113
|
signal: AbortSignal.timeout(15000),
|
|
@@ -72,19 +119,27 @@ async function fetchMarketplaceAgentsFromUrl(url) {
|
|
|
72
119
|
return parseMarketplaceRegistryJson(json);
|
|
73
120
|
}
|
|
74
121
|
/**
|
|
75
|
-
* Resolves marketplace
|
|
122
|
+
* Resolves marketplace registry (agents and channels) from configured registry URL.
|
|
76
123
|
*/
|
|
77
|
-
export async function
|
|
124
|
+
export async function resolveMarketplaceRegistry() {
|
|
78
125
|
const { marketplaceRegistryUrl } = loadConfig();
|
|
79
126
|
const registryUrl = marketplaceRegistryUrl?.trim() || DEFAULT_MARKETPLACE_REGISTRY_URL;
|
|
80
127
|
try {
|
|
81
|
-
return await
|
|
128
|
+
return await fetchMarketplaceRegistryFromUrl(registryUrl);
|
|
82
129
|
}
|
|
83
130
|
catch (err) {
|
|
84
131
|
console.warn(`[plugins] marketplace registry fetch failed (${registryUrl}), using built-in list:`, err instanceof Error ? err.message : err);
|
|
85
|
-
return DEFAULT_MARKETPLACE_AGENTS;
|
|
132
|
+
return { agents: DEFAULT_MARKETPLACE_AGENTS, channels: DEFAULT_MARKETPLACE_CHANNELS };
|
|
86
133
|
}
|
|
87
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolves marketplace agent listings from configured registry URL.
|
|
137
|
+
* @deprecated Use resolveMarketplaceRegistry instead.
|
|
138
|
+
*/
|
|
139
|
+
export async function resolveMarketplaceAgentList() {
|
|
140
|
+
const registry = await resolveMarketplaceRegistry();
|
|
141
|
+
return registry.agents;
|
|
142
|
+
}
|
|
88
143
|
const getPluginsDir = () => {
|
|
89
144
|
const config = loadConfig();
|
|
90
145
|
const baseDir = resolvePath(config.baseDir || DEFAULT_BASE_DIR);
|
package/docs/agents.md
CHANGED
|
@@ -17,8 +17,6 @@ plugins:
|
|
|
17
17
|
- id: openbot
|
|
18
18
|
config:
|
|
19
19
|
model: anthropic/claude-3-5-sonnet-20240620
|
|
20
|
-
- id: shell
|
|
21
|
-
- id: delegation
|
|
22
20
|
---
|
|
23
21
|
|
|
24
22
|
You are a web research specialist. Use the available tools to gather and
|
|
@@ -37,20 +35,19 @@ the bus). Built-in **`state`** is hidden by default.
|
|
|
37
35
|
A runtime plugin is one that handles `agent:invoke` (the LLM loop). Without
|
|
38
36
|
one, the agent will not respond to user input. Built-in runtime plugins:
|
|
39
37
|
|
|
40
|
-
- `openbot` — the standard, opinionated OpenBot agent runtime.
|
|
41
|
-
|
|
38
|
+
- `openbot` — the standard, opinionated OpenBot agent runtime. It is
|
|
39
|
+
**batteries-included** and provides inbuilt tools (bash, memory, storage,
|
|
40
|
+
delegation, and approval).
|
|
42
41
|
- `claude-code` — runs Claude inside the Claude Agent SDK with its own tools.
|
|
43
42
|
- `gemini-cli` — spawns Google's `gemini` CLI in headless mode.
|
|
44
43
|
|
|
45
44
|
`claude-code` and `gemini-cli` own their own tool loops, so attaching tool
|
|
46
|
-
plugins like `
|
|
47
|
-
`openbot`.
|
|
45
|
+
plugins like `bash` to them has no effect.
|
|
48
46
|
|
|
49
47
|
## Built-in agents
|
|
50
48
|
|
|
51
49
|
OpenBot ships a built-in **`system`** agent (the orchestrator) with the `openbot`
|
|
52
|
-
runtime
|
|
53
|
-
approval, memory, etc.). A built-in **`state`** agent backs deterministic
|
|
50
|
+
runtime. A built-in **`state`** agent backs deterministic
|
|
54
51
|
`/api/state` handling and infra events.
|
|
55
52
|
|
|
56
53
|
You can optionally persist overrides for either id at `~/.openbot/agents/system/AGENT.md` or `~/.openbot/agents/state/AGENT.md`. When present, settings are merged on top of the code defaults (`getAgentDetails`). The **`state`** agent is not listed by **`action:storage:get-agents`** (`hidden: true`); **`system`** is listed. Use **`action:storage:create-agent`** to create an overlay once, **`action:storage:update-agent`** for partial updates (creating the file if missing for `system` / `state`), and **`action:storage:delete-agent`** to remove only that `AGENT.md` and revert to defaults (other files under the folder are left untouched).
|
package/docs/architecture.md
CHANGED
|
@@ -18,7 +18,7 @@ A dynamic registry that manages all available agents. Agents can be:
|
|
|
18
18
|
- **TS Packages**: Advanced agents with custom logic in `~/.openbot/agents/*/index.ts`.
|
|
19
19
|
|
|
20
20
|
### 3. Plugin registry
|
|
21
|
-
The "capability layer" that provides tools and logic shared across the platform. Plugins (like `
|
|
21
|
+
The "capability layer" that provides tools and logic shared across the platform. Plugins (like `bash` or `file-system`) define the actions agents can perform.
|
|
22
22
|
|
|
23
23
|
### 4. Orchestration layer (Melony)
|
|
24
24
|
The underlying event bus that handles all communication. It ensures that agents can collaborate asynchronously, share context, and emit real-time updates to the UI.
|
package/docs/plugins.md
CHANGED
|
@@ -43,15 +43,36 @@ name collisions.
|
|
|
43
43
|
|
|
44
44
|
| Id | Role | Notes |
|
|
45
45
|
| --------------- | ---------- | --------------------------------------------------------- |
|
|
46
|
-
| `openbot` | Runtime |
|
|
46
|
+
| `openbot` | Runtime | Standard batteries-included OpenBot agent runtime. |
|
|
47
47
|
| `claude-code` | Runtime | Claude Agent SDK; owns its own tool loop |
|
|
48
48
|
| `gemini-cli` | Runtime | Google `gemini` CLI in headless mode |
|
|
49
|
-
| `
|
|
50
|
-
| `storage` | Tool | `create_channel`, `patch_*`, `
|
|
51
|
-
| `memory` | Tool | `remember`, `recall`, `forget`
|
|
49
|
+
| `bash` | Tool | `bash` (inbuilt in `openbot`) |
|
|
50
|
+
| `storage` | Tool | `create_channel`, `patch_*`, ... (inbuilt in `openbot`) |
|
|
51
|
+
| `memory` | Tool | `remember`, `recall`, `forget` (inbuilt in `openbot`) |
|
|
52
52
|
| `plugin-manager`| Infra | Marketplace list, npm plugin install/uninstall, agent install |
|
|
53
53
|
|
|
54
|
-
##
|
|
54
|
+
## Batteries-included: `openbot` runtime
|
|
55
|
+
|
|
56
|
+
The `openbot` plugin is the standard runtime for OpenBot agents. It is designed
|
|
57
|
+
to be isolated and self-contained, providing a core ecosystem of inbuilt tools:
|
|
58
|
+
|
|
59
|
+
- **Bash**: Stateful system tasks and file operations.
|
|
60
|
+
- **Memory**: Long-term durable fact storage.
|
|
61
|
+
- **Storage**: Channel and thread management.
|
|
62
|
+
- **Delegation**: Calling upon other specialized agents.
|
|
63
|
+
- **Approval**: Gating protected actions behind UI confirmation.
|
|
64
|
+
|
|
65
|
+
When you use the `openbot` runtime, these tools are automatically available.
|
|
66
|
+
You can configure the inbuilt `approval` plugin via the `openbot` plugin config:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
plugins:
|
|
70
|
+
- id: openbot
|
|
71
|
+
config:
|
|
72
|
+
model: openai/gpt-4o-mini
|
|
73
|
+
approval:
|
|
74
|
+
actions: [action:bash, action:create_channel]
|
|
75
|
+
```
|
|
55
76
|
|
|
56
77
|
A community plugin is just an npm package whose default export matches the
|
|
57
78
|
`Plugin` interface. Reference it by its npm package name in AGENT.md:
|
|
@@ -69,11 +90,11 @@ On first use OpenBot installs the package into
|
|
|
69
90
|
|
|
70
91
|
## Approval plugin
|
|
71
92
|
|
|
72
|
-
The `approval` plugin gates protected tool calls behind a UI confirmation widget. By default, it gates `action:
|
|
93
|
+
The `approval` plugin gates protected tool calls behind a UI confirmation widget. By default, it gates `action:bash`.
|
|
73
94
|
|
|
74
95
|
```yaml
|
|
75
96
|
plugins:
|
|
76
97
|
- id: approval
|
|
77
98
|
config:
|
|
78
|
-
actions: [action:
|
|
99
|
+
actions: [action:bash]
|
|
79
100
|
```
|
|
@@ -9,11 +9,11 @@ description: One-line description shown in agent pickers and lists.
|
|
|
9
9
|
|
|
10
10
|
# Plugins compose the agent. Order matters for tool collisions (first wins).
|
|
11
11
|
# At least one plugin must handle `agent:invoke` (a "runtime" plugin like
|
|
12
|
-
# `openbot`, `claude-code`, or `gemini-cli`). Tool plugins like `
|
|
12
|
+
# `openbot`, `claude-code`, or `gemini-cli`). Tool plugins like `bash`,
|
|
13
13
|
# `delegation`, and `storage-tools` contribute tools to whichever runtime
|
|
14
14
|
# plugin can consume them.
|
|
15
15
|
#
|
|
16
|
-
# Built-in plugin ids: openbot, claude-code, gemini-cli,
|
|
16
|
+
# Built-in plugin ids: openbot, claude-code, gemini-cli, bash, delegation,
|
|
17
17
|
# storage-tools, approval.
|
|
18
18
|
#
|
|
19
19
|
# Community plugins are referenced by their npm package name (e.g.
|
|
@@ -23,12 +23,12 @@ plugins:
|
|
|
23
23
|
- id: openbot
|
|
24
24
|
config:
|
|
25
25
|
model: openai/gpt-4o-mini
|
|
26
|
-
- id:
|
|
26
|
+
- id: bash
|
|
27
27
|
- id: delegation
|
|
28
28
|
- id: storage
|
|
29
29
|
- id: approval
|
|
30
30
|
config:
|
|
31
|
-
actions: [action:
|
|
31
|
+
actions: [action:bash]
|
|
32
32
|
---
|
|
33
33
|
|
|
34
34
|
<!--
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openbot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.12.0"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsx watch src/app/cli.ts start",
|
|
11
|
+
"build": "tsc && mkdir -p dist/assets && cp src/assets/icon.svg dist/assets/icon.svg",
|
|
12
|
+
"start": "node dist/app/cli.js start"
|
|
13
|
+
},
|
|
9
14
|
"bin": {
|
|
10
15
|
"openbot": "./dist/app/cli.js"
|
|
11
16
|
},
|
|
@@ -30,10 +35,5 @@
|
|
|
30
35
|
"@types/node": "^20.10.1",
|
|
31
36
|
"tsx": "^4.21.0",
|
|
32
37
|
"typescript": "^5.9.3"
|
|
33
|
-
},
|
|
34
|
-
"scripts": {
|
|
35
|
-
"dev": "tsx watch src/app/cli.ts start",
|
|
36
|
-
"build": "tsc && mkdir -p dist/assets && cp src/assets/icon.svg dist/assets/icon.svg",
|
|
37
|
-
"start": "node dist/app/cli.js start"
|
|
38
38
|
}
|
|
39
|
-
}
|
|
39
|
+
}
|
package/src/app/cli.ts
CHANGED
package/src/app/config.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface OpenBotconfig {
|
|
|
14
14
|
* {@link DEFAULT_MARKETPLACE_REGISTRY_URL} is used.
|
|
15
15
|
*/
|
|
16
16
|
marketplaceRegistryUrl?: string;
|
|
17
|
+
/** Public base URL for workspace file links (e.g. https://my-host.example). Falls back to OPENBOT_PUBLIC_URL env or http://localhost:{port}. */
|
|
18
|
+
publicUrl?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface StoredVariable {
|
|
@@ -23,6 +25,8 @@ export interface StoredVariable {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export const DEFAULT_BASE_DIR = '~/.openbot';
|
|
28
|
+
/** Default parent directory for per-channel working directories (user-facing workspace). */
|
|
29
|
+
export const DEFAULT_CHANNELS_WORKSPACE_DIR = '~/openbot';
|
|
26
30
|
export const DEFAULT_PLUGINS_DIR = 'plugins';
|
|
27
31
|
export const DEFAULT_AGENTS_DIR = 'agents';
|
|
28
32
|
export const DEFAULT_CHANNELS_DIR = 'channels';
|
|
@@ -37,6 +41,15 @@ export function resolvePath(p: string) {
|
|
|
37
41
|
return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : path.resolve(p);
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
/** Default absolute cwd for a channel when none is provided at creation time. */
|
|
45
|
+
export function getDefaultChannelCwd(channelId: string): string {
|
|
46
|
+
const id = channelId.trim();
|
|
47
|
+
if (!id) {
|
|
48
|
+
throw new Error('channelId is required');
|
|
49
|
+
}
|
|
50
|
+
return resolvePath(`${DEFAULT_CHANNELS_WORKSPACE_DIR}/${id}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
export function loadConfig(): OpenBotconfig {
|
|
41
54
|
const configPath = path.join(os.homedir(), '.openbot', CONFIG_FILE);
|
|
42
55
|
if (fs.existsSync(configPath)) {
|