openbot 0.3.6 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -16
- package/dist/app/agent-ids.js +4 -0
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +10 -19
- package/dist/app/server.js +208 -17
- package/dist/bus/services.js +34 -124
- package/dist/harness/agent-invoke-run.js +44 -0
- package/dist/harness/agent-turn.js +99 -0
- package/dist/harness/channel-participants.js +40 -0
- package/dist/harness/constants.js +2 -0
- package/dist/harness/context-meter.js +97 -0
- package/dist/harness/context.js +95 -47
- package/dist/harness/dispatch.js +144 -0
- package/dist/harness/dispatcher.js +45 -156
- package/dist/harness/history.js +177 -0
- package/dist/harness/index.js +109 -0
- package/dist/harness/orchestration.js +88 -0
- package/dist/harness/participants.js +22 -0
- package/dist/harness/run-harness.js +154 -0
- package/dist/harness/run.js +98 -0
- package/dist/harness/runtime-factory.js +0 -34
- package/dist/harness/runtime.js +57 -0
- package/dist/harness/todo-dispatch.js +51 -0
- package/dist/harness/todos.js +5 -0
- package/dist/harness/turn.js +79 -0
- package/dist/plugins/approval/index.js +120 -149
- package/dist/plugins/bash/index.js +195 -0
- package/dist/plugins/delegation/index.js +121 -32
- package/dist/plugins/memory/index.js +103 -14
- package/dist/plugins/memory/service.js +152 -0
- package/dist/plugins/openbot/context.js +125 -0
- package/dist/plugins/openbot/history.js +144 -0
- package/dist/plugins/openbot/index.js +71 -0
- package/dist/plugins/openbot/runtime.js +381 -0
- package/dist/plugins/openbot/system-prompt.js +25 -0
- package/dist/plugins/plugin-manager/index.js +189 -0
- package/dist/plugins/shell/index.js +2 -1
- package/dist/plugins/storage/files.js +67 -0
- package/dist/plugins/storage/index.js +750 -0
- package/dist/plugins/storage/service.js +1316 -0
- package/dist/plugins/storage-tools/index.js +2 -2
- package/dist/plugins/thread-namer/index.js +72 -0
- package/dist/plugins/thread-naming/generate-title.js +44 -0
- package/dist/plugins/thread-naming/index.js +103 -0
- package/dist/plugins/threads/index.js +114 -0
- package/dist/plugins/todo/index.js +24 -25
- package/dist/plugins/ui/index.js +109 -180
- package/dist/registry/plugins.js +3 -9
- package/dist/services/abort.js +43 -0
- package/dist/services/plugins/domain.js +1 -0
- package/dist/services/plugins/plugin-cache.js +9 -0
- package/dist/services/plugins/registry.js +112 -0
- package/dist/services/plugins/service.js +232 -0
- package/dist/services/plugins/types.js +1 -0
- package/dist/services/process.js +29 -0
- package/dist/services/storage.js +11 -10
- package/dist/services/thread-naming.js +81 -0
- package/docs/agents.md +15 -12
- package/docs/architecture.md +2 -2
- package/docs/plugins.md +29 -17
- package/docs/templates/AGENT.example.md +8 -14
- package/package.json +1 -2
- package/src/app/agent-ids.ts +5 -0
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +14 -31
- package/src/app/server.ts +243 -19
- package/src/app/types.ts +331 -187
- package/src/harness/index.ts +166 -0
- package/src/plugins/approval/index.ts +107 -188
- package/src/plugins/bash/index.ts +232 -0
- package/src/plugins/delegation/index.ts +139 -39
- package/src/plugins/memory/index.ts +112 -15
- package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
- package/src/plugins/openbot/context.ts +140 -0
- package/src/plugins/openbot/history.ts +158 -0
- package/src/plugins/openbot/index.ts +79 -0
- package/src/plugins/openbot/runtime.ts +478 -0
- package/src/plugins/openbot/system-prompt.ts +27 -0
- package/src/plugins/plugin-manager/index.ts +224 -0
- package/src/plugins/storage/files.ts +81 -0
- package/src/plugins/storage/index.ts +823 -0
- package/src/{services/storage.ts → plugins/storage/service.ts} +485 -105
- package/src/plugins/ui/index.ts +117 -221
- package/src/services/abort.ts +46 -0
- package/src/{bus/types.ts → services/plugins/domain.ts} +50 -8
- package/src/services/plugins/plugin-cache.ts +13 -0
- package/src/{registry/plugins.ts → services/plugins/registry.ts} +28 -28
- package/src/services/plugins/service.ts +318 -0
- package/src/{bus/plugin.ts → services/plugins/types.ts} +7 -3
- package/src/bus/services.ts +0 -954
- package/src/harness/context.ts +0 -365
- package/src/harness/dispatcher.ts +0 -379
- package/src/harness/mcp.ts +0 -78
- package/src/harness/runtime-factory.ts +0 -129
- package/src/harness/todo-advance.ts +0 -128
- package/src/plugins/ai-sdk/index.ts +0 -41
- package/src/plugins/ai-sdk/runtime.ts +0 -468
- package/src/plugins/ai-sdk/system-prompt.ts +0 -18
- package/src/plugins/mcp/index.ts +0 -128
- package/src/plugins/shell/index.ts +0 -123
- package/src/plugins/storage-tools/index.ts +0 -90
- package/src/plugins/todo/index.ts +0 -64
- package/src/services/plugins.ts +0 -133
- /package/src/{harness → services}/process.ts +0 -0
package/dist/plugins/ui/index.js
CHANGED
|
@@ -1,184 +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.enum(['approval', 'todo_list']).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 todoToListItem = (todo, index) => {
|
|
76
|
-
if (!isRecord(todo)) {
|
|
77
|
-
return { id: `todo_${index + 1}`, label: String(todo) };
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
id: readString(todo.id) || `todo_${index + 1}`,
|
|
81
|
-
label: readString(todo.label) ||
|
|
82
|
-
readString(todo.task) ||
|
|
83
|
-
readString(todo.title) ||
|
|
84
|
-
`Todo ${index + 1}`,
|
|
85
|
-
description: readString(todo.description),
|
|
86
|
-
status: readString(todo.status),
|
|
87
|
-
metadata: todo,
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
|
-
const createWidgetId = (data, toolCallId) => {
|
|
91
|
-
if ('widgetId' in data && data.widgetId)
|
|
92
|
-
return data.widgetId;
|
|
93
|
-
if (toolCallId)
|
|
94
|
-
return `widget_${toolCallId}`;
|
|
95
|
-
return `widget_${Date.now()}`;
|
|
96
|
-
};
|
|
97
|
-
const normalizeWidget = (data, state, toolCallId) => {
|
|
98
|
-
const widgetId = createWidgetId(data, toolCallId);
|
|
99
|
-
if (data.kind === 'approval') {
|
|
100
|
-
const props = data.props || {};
|
|
101
|
-
return {
|
|
102
|
-
widgetId,
|
|
103
|
-
kind: 'choice',
|
|
104
|
-
title: data.title || 'Approval Required',
|
|
105
|
-
body: readString(props.message) ||
|
|
106
|
-
readString(props.summary) ||
|
|
107
|
-
'Please approve or deny this action.',
|
|
108
|
-
metadata: {
|
|
109
|
-
...(data.metadata || {}),
|
|
110
|
-
legacyKind: 'approval',
|
|
111
|
-
actionId: props.actionId,
|
|
112
|
-
},
|
|
113
|
-
actions: [
|
|
114
|
-
{ id: 'approve', label: 'Approve', value: props.actionId || 'approve', variant: 'primary' },
|
|
115
|
-
{ id: 'deny', label: 'Deny', value: props.actionId || 'deny', variant: 'danger' },
|
|
116
|
-
],
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
if (data.kind === 'todo_list') {
|
|
120
|
-
const props = data.props || {};
|
|
121
|
-
const stateTodos = isRecord(state.threadDetails?.state)
|
|
122
|
-
? state.threadDetails.state.todos
|
|
123
|
-
: undefined;
|
|
124
|
-
const todos = asListItems(props.todos) || asListItems(stateTodos) || [];
|
|
125
|
-
return {
|
|
126
|
-
widgetId,
|
|
127
|
-
kind: 'list',
|
|
128
|
-
title: data.title || readString(props.title) || 'Task List',
|
|
129
|
-
description: readString(props.description),
|
|
130
|
-
metadata: { ...(data.metadata || {}), legacyKind: 'todo_list' },
|
|
131
|
-
items: todos.map(todoToListItem),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
if (data.kind === 'form') {
|
|
135
|
-
const propsSource = data.props;
|
|
136
|
-
const props = isRecord(propsSource) ? propsSource : {};
|
|
137
|
-
return {
|
|
138
|
-
widgetId,
|
|
139
|
-
kind: 'form',
|
|
140
|
-
title: data.title || 'Details Required',
|
|
141
|
-
description: data.description,
|
|
142
|
-
body: data.body,
|
|
143
|
-
state: data.state,
|
|
144
|
-
metadata: data.metadata,
|
|
145
|
-
fields: data.fields || asFields(props.schema) || [],
|
|
146
|
-
submitLabel: data.submitLabel || readString(props.submitLabel),
|
|
147
|
-
actions: data.actions,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
if (data.kind === 'list') {
|
|
151
|
-
return { ...data, widgetId, title: data.title || 'Task List', items: data.items || [] };
|
|
152
|
-
}
|
|
153
|
-
if (data.kind === 'choice') {
|
|
154
|
-
return { ...data, widgetId, title: data.title || 'Choose an Option' };
|
|
155
|
-
}
|
|
156
|
-
if (data.kind === 'message') {
|
|
157
|
-
return { ...data, widgetId, title: data.title || 'Message' };
|
|
158
|
-
}
|
|
159
|
-
throw new Error(`Unsupported UI widget kind: ${data.kind || 'unknown'}`);
|
|
160
|
-
};
|
|
161
|
-
const uiToolDefinitions = {
|
|
162
|
-
render_ui_widget: {
|
|
163
|
-
description: 'Render a small server-driven UI widget in the conversation. Prefer primitive kinds: message, choice, form, or list. Legacy presets approval and todo_list are accepted.',
|
|
164
|
-
inputSchema: renderWidgetSchema,
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
const uiPluginRuntime = () => (builder) => {
|
|
168
|
-
builder.on('action:render_ui_widget', async function* (event, context) {
|
|
169
|
-
const widget = normalizeWidget(event.data, context.state, event.meta?.toolCallId);
|
|
170
|
-
yield {
|
|
171
|
-
type: 'client:ui:widget',
|
|
172
|
-
data: widget,
|
|
173
|
-
meta: event.meta,
|
|
174
|
-
};
|
|
175
|
-
});
|
|
176
|
-
};
|
|
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
|
+
*/
|
|
177
9
|
export const uiPlugin = {
|
|
178
10
|
id: 'ui',
|
|
179
|
-
name: 'UI
|
|
180
|
-
description: 'Render
|
|
181
|
-
toolDefinitions:
|
|
182
|
-
|
|
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
|
+
},
|
|
183
112
|
};
|
|
184
113
|
export default uiPlugin;
|
package/dist/registry/plugins.js
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import {
|
|
4
|
+
import { openbotPlugin } from '../plugins/openbot/index.js';
|
|
5
5
|
import { shellPlugin } from '../plugins/shell/index.js';
|
|
6
|
-
import { mcpPlugin } from '../plugins/mcp/index.js';
|
|
7
|
-
import { delegationPlugin } from '../plugins/delegation/index.js';
|
|
8
6
|
import { storageToolsPlugin } from '../plugins/storage-tools/index.js';
|
|
9
7
|
import { uiPlugin } from '../plugins/ui/index.js';
|
|
10
8
|
import { approvalPlugin } from '../plugins/approval/index.js';
|
|
11
9
|
import { memoryPlugin } from '../plugins/memory/index.js';
|
|
12
|
-
import { todoPlugin } from '../plugins/todo/index.js';
|
|
13
10
|
import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
|
|
14
11
|
let pluginsDir = null;
|
|
15
12
|
const loadedPlugins = new Set();
|
|
16
13
|
const cache = new Map();
|
|
17
14
|
const BUILT_IN = {
|
|
18
|
-
[
|
|
15
|
+
[openbotPlugin.id]: openbotPlugin,
|
|
19
16
|
[shellPlugin.id]: shellPlugin,
|
|
20
|
-
[mcpPlugin.id]: mcpPlugin,
|
|
21
|
-
[delegationPlugin.id]: delegationPlugin,
|
|
22
17
|
[storageToolsPlugin.id]: storageToolsPlugin,
|
|
23
18
|
[uiPlugin.id]: uiPlugin,
|
|
24
19
|
[approvalPlugin.id]: approvalPlugin,
|
|
25
20
|
[memoryPlugin.id]: memoryPlugin,
|
|
26
|
-
[todoPlugin.id]: todoPlugin,
|
|
27
21
|
};
|
|
28
22
|
/** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
|
|
29
23
|
export function parsePluginModule(module) {
|
|
@@ -61,7 +55,7 @@ export function initPlugins(dir) {
|
|
|
61
55
|
}
|
|
62
56
|
/**
|
|
63
57
|
* Resolve a Plugin by id. The id is either:
|
|
64
|
-
* - a built-in id (e.g. "
|
|
58
|
+
* - a built-in id (e.g. "openbot", "shell"), or
|
|
65
59
|
* - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
|
|
66
60
|
* in which case the folder layout is `plugins/<id>/dist/index.js`.
|
|
67
61
|
*/
|
|
@@ -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();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Resolved plugins (built-in + community); shared with registry resolution. */
|
|
2
|
+
export const resolvedPluginCache = new Map();
|
|
3
|
+
/** Community plugin ids that have already been logged as loaded once. */
|
|
4
|
+
export const loadedCommunityPlugins = new Set();
|
|
5
|
+
/** Drop a single id from the in-memory resolver cache (e.g. after install/uninstall). */
|
|
6
|
+
export function invalidatePlugin(id) {
|
|
7
|
+
resolvedPluginCache.delete(id);
|
|
8
|
+
loadedCommunityPlugins.delete(id);
|
|
9
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { openbotPlugin } from '../../plugins/openbot/index.js';
|
|
5
|
+
import { bashPlugin } from '../../plugins/bash/index.js';
|
|
6
|
+
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
|
+
import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
|
|
12
|
+
import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../../app/config.js';
|
|
13
|
+
import { invalidatePlugin as clearResolvedPluginEntry, loadedCommunityPlugins, resolvedPluginCache, } from './plugin-cache.js';
|
|
14
|
+
let pluginsDir = null;
|
|
15
|
+
const BUILT_IN = {
|
|
16
|
+
[openbotPlugin.id]: openbotPlugin,
|
|
17
|
+
[bashPlugin.id]: bashPlugin,
|
|
18
|
+
[storagePlugin.id]: storagePlugin,
|
|
19
|
+
[approvalPlugin.id]: approvalPlugin,
|
|
20
|
+
[memoryPlugin.id]: memoryPlugin,
|
|
21
|
+
[delegationPlugin.id]: delegationPlugin,
|
|
22
|
+
[uiPlugin.id]: uiPlugin,
|
|
23
|
+
[pluginManagerPlugin.id]: pluginManagerPlugin,
|
|
24
|
+
};
|
|
25
|
+
/** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
|
|
26
|
+
export function parsePluginModule(module) {
|
|
27
|
+
const raw = module.plugin ??
|
|
28
|
+
module.default;
|
|
29
|
+
if (!raw || typeof raw !== 'object')
|
|
30
|
+
return null;
|
|
31
|
+
const factory = raw.factory;
|
|
32
|
+
if (typeof factory !== 'function')
|
|
33
|
+
return null;
|
|
34
|
+
const name = typeof raw.name === 'string' ? raw.name : '';
|
|
35
|
+
const description = typeof raw.description === 'string' ? raw.description : '';
|
|
36
|
+
const image = typeof raw.image === 'string' ? raw.image : undefined;
|
|
37
|
+
const configSchema = raw.configSchema;
|
|
38
|
+
const toolDefinitions = raw.toolDefinitions;
|
|
39
|
+
return {
|
|
40
|
+
name,
|
|
41
|
+
description,
|
|
42
|
+
image,
|
|
43
|
+
configSchema,
|
|
44
|
+
toolDefinitions,
|
|
45
|
+
factory: factory,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Initialize the on-disk plugins directory (defaults to ~/.openbot/plugins). */
|
|
49
|
+
export function initPlugins(dir) {
|
|
50
|
+
if (dir) {
|
|
51
|
+
pluginsDir = dir;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const config = loadConfig();
|
|
55
|
+
const baseDir = config.baseDir || DEFAULT_BASE_DIR;
|
|
56
|
+
pluginsDir = path.join(resolvePath(baseDir), DEFAULT_PLUGINS_DIR);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve a Plugin by id. The id is either:
|
|
61
|
+
* - a built-in id (e.g. "openbot", "bash"), or
|
|
62
|
+
* - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
|
|
63
|
+
* in which case the folder layout is `plugins/<id>/dist/index.js`.
|
|
64
|
+
*/
|
|
65
|
+
export async function resolvePlugin(id) {
|
|
66
|
+
if (resolvedPluginCache.has(id))
|
|
67
|
+
return resolvedPluginCache.get(id);
|
|
68
|
+
if (BUILT_IN[id]) {
|
|
69
|
+
resolvedPluginCache.set(id, BUILT_IN[id]);
|
|
70
|
+
return BUILT_IN[id];
|
|
71
|
+
}
|
|
72
|
+
if (!pluginsDir) {
|
|
73
|
+
initPlugins();
|
|
74
|
+
}
|
|
75
|
+
if (!pluginsDir)
|
|
76
|
+
return null;
|
|
77
|
+
const distPath = path.join(pluginsDir, id, 'dist', 'index.js');
|
|
78
|
+
if (!fs.existsSync(distPath)) {
|
|
79
|
+
console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const module = await import(pathToFileURL(distPath).href);
|
|
84
|
+
const parsed = parsePluginModule(module);
|
|
85
|
+
if (!parsed) {
|
|
86
|
+
console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const plugin = { id, ...parsed, name: parsed.name || id };
|
|
90
|
+
resolvedPluginCache.set(id, plugin);
|
|
91
|
+
if (!loadedCommunityPlugins.has(id)) {
|
|
92
|
+
console.log(`[plugins] Loaded community plugin "${id}" from ${distPath}`);
|
|
93
|
+
loadedCommunityPlugins.add(id);
|
|
94
|
+
}
|
|
95
|
+
return plugin;
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Drop a single id from the in-memory cache (e.g. after fresh install). */
|
|
103
|
+
export function invalidatePlugin(id) {
|
|
104
|
+
clearResolvedPluginEntry(id);
|
|
105
|
+
}
|
|
106
|
+
/** List built-in plugins (for marketplace/registry views). */
|
|
107
|
+
export function listBuiltInPlugins() {
|
|
108
|
+
return Object.values(BUILT_IN);
|
|
109
|
+
}
|
|
110
|
+
export function getPluginsDir() {
|
|
111
|
+
return pluginsDir;
|
|
112
|
+
}
|