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/src/plugins/ui/index.ts
CHANGED
|
@@ -1,227 +1,123 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import z from 'zod';
|
|
3
|
-
import type { Plugin } from '../../
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const actionSchema = z.object({
|
|
14
|
-
id: z.string().describe('Stable action ID returned by client:ui:widget:response.'),
|
|
15
|
-
label: z.string().describe('Human-readable button label.'),
|
|
16
|
-
value: z.unknown().optional().describe('Optional machine-readable value for this action.'),
|
|
17
|
-
variant: z.enum(['primary', 'secondary', 'danger']).optional(),
|
|
18
|
-
disabled: z.boolean().optional(),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const optionSchema = z.object({
|
|
22
|
-
label: z.string(),
|
|
23
|
-
value: z.string(),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const fieldSchema = z.object({
|
|
27
|
-
id: z.string().describe('Stable field ID used as the submitted value key.'),
|
|
28
|
-
label: z.string(),
|
|
29
|
-
type: z.enum(['text', 'textarea', 'number', 'boolean', 'select', 'multiselect', 'date']),
|
|
30
|
-
description: z.string().optional(),
|
|
31
|
-
placeholder: z.string().optional(),
|
|
32
|
-
required: z.boolean().optional(),
|
|
33
|
-
options: z.array(optionSchema).optional(),
|
|
34
|
-
defaultValue: z.unknown().optional(),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const listItemSchema = z.object({
|
|
38
|
-
id: z.string(),
|
|
39
|
-
label: z.string(),
|
|
40
|
-
description: z.string().optional(),
|
|
41
|
-
status: z.enum(['pending', 'in_progress', 'done', 'error', 'cancelled']).optional(),
|
|
42
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const widgetBaseSchema = {
|
|
46
|
-
widgetId: z.string().optional().describe('Stable widget ID. Defaults from toolCallId.'),
|
|
47
|
-
title: z.string().optional(),
|
|
48
|
-
description: z.string().optional(),
|
|
49
|
-
body: z.string().optional(),
|
|
50
|
-
state: z.enum(['open', 'submitted', 'cancelled', 'error']).optional(),
|
|
51
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const renderWidgetSchema = z.union([
|
|
55
|
-
z.object({
|
|
56
|
-
...widgetBaseSchema,
|
|
57
|
-
kind: z.literal('message'),
|
|
58
|
-
actions: z.array(actionSchema).optional(),
|
|
59
|
-
}),
|
|
60
|
-
z.object({
|
|
61
|
-
...widgetBaseSchema,
|
|
62
|
-
kind: z.literal('choice'),
|
|
63
|
-
actions: z.array(actionSchema).min(1),
|
|
64
|
-
}),
|
|
65
|
-
z.object({
|
|
66
|
-
...widgetBaseSchema,
|
|
67
|
-
kind: z.literal('form'),
|
|
68
|
-
fields: z.array(fieldSchema).optional(),
|
|
69
|
-
submitLabel: z.string().optional(),
|
|
70
|
-
actions: z.array(actionSchema).optional(),
|
|
71
|
-
props: z.record(z.string(), z.unknown()).optional(),
|
|
72
|
-
}),
|
|
73
|
-
z.object({
|
|
74
|
-
...widgetBaseSchema,
|
|
75
|
-
kind: z.literal('list'),
|
|
76
|
-
items: z.array(listItemSchema).optional(),
|
|
77
|
-
actions: z.array(actionSchema).optional(),
|
|
78
|
-
}),
|
|
79
|
-
z.object({
|
|
80
|
-
kind: z.enum(['approval', 'todo_list']).describe('Legacy preset. Prefer choice or list.'),
|
|
81
|
-
widgetId: z.string().optional(),
|
|
82
|
-
title: z.string().optional(),
|
|
83
|
-
props: z.record(z.string(), z.unknown()).optional(),
|
|
84
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
85
|
-
}),
|
|
86
|
-
]);
|
|
87
|
-
|
|
88
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
89
|
-
!!value && typeof value === 'object' && !Array.isArray(value);
|
|
90
|
-
|
|
91
|
-
const readString = (value: unknown): string | undefined =>
|
|
92
|
-
typeof value === 'string' && value.trim() ? value : undefined;
|
|
93
|
-
|
|
94
|
-
const asFields = (value: unknown): UIWidgetField[] | undefined =>
|
|
95
|
-
Array.isArray(value) ? (value as UIWidgetField[]) : undefined;
|
|
96
|
-
|
|
97
|
-
const asListItems = (value: unknown): UIWidgetListItem[] | undefined =>
|
|
98
|
-
Array.isArray(value) ? (value as UIWidgetListItem[]) : undefined;
|
|
99
|
-
|
|
100
|
-
const todoToListItem = (todo: unknown, index: number): UIWidgetListItem => {
|
|
101
|
-
if (!isRecord(todo)) {
|
|
102
|
-
return { id: `todo_${index + 1}`, label: String(todo) };
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
id: readString(todo.id) || `todo_${index + 1}`,
|
|
106
|
-
label:
|
|
107
|
-
readString(todo.label) ||
|
|
108
|
-
readString(todo.task) ||
|
|
109
|
-
readString(todo.title) ||
|
|
110
|
-
`Todo ${index + 1}`,
|
|
111
|
-
description: readString(todo.description),
|
|
112
|
-
status: readString(todo.status) as UIWidgetListItem['status'],
|
|
113
|
-
metadata: todo,
|
|
114
|
-
};
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const createWidgetId = (data: RenderUIWidgetData, toolCallId?: string): string => {
|
|
118
|
-
if ('widgetId' in data && data.widgetId) return data.widgetId;
|
|
119
|
-
if (toolCallId) return `widget_${toolCallId}`;
|
|
120
|
-
return `widget_${Date.now()}`;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const normalizeWidget = (
|
|
124
|
-
data: RenderUIWidgetData,
|
|
125
|
-
state: OpenBotState,
|
|
126
|
-
toolCallId?: string,
|
|
127
|
-
): UIWidgetSpec => {
|
|
128
|
-
const widgetId = createWidgetId(data, toolCallId);
|
|
129
|
-
|
|
130
|
-
if (data.kind === 'approval') {
|
|
131
|
-
const props = data.props || {};
|
|
132
|
-
return {
|
|
133
|
-
widgetId,
|
|
134
|
-
kind: 'choice',
|
|
135
|
-
title: data.title || 'Approval Required',
|
|
136
|
-
body:
|
|
137
|
-
readString(props.message) ||
|
|
138
|
-
readString(props.summary) ||
|
|
139
|
-
'Please approve or deny this action.',
|
|
140
|
-
metadata: {
|
|
141
|
-
...(data.metadata || {}),
|
|
142
|
-
legacyKind: 'approval',
|
|
143
|
-
actionId: props.actionId,
|
|
144
|
-
},
|
|
145
|
-
actions: [
|
|
146
|
-
{ id: 'approve', label: 'Approve', value: props.actionId || 'approve', variant: 'primary' },
|
|
147
|
-
{ id: 'deny', label: 'Deny', value: props.actionId || 'deny', variant: 'danger' },
|
|
148
|
-
],
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (data.kind === 'todo_list') {
|
|
153
|
-
const props = data.props || {};
|
|
154
|
-
const stateTodos = isRecord(state.threadDetails?.state)
|
|
155
|
-
? (state.threadDetails.state as Record<string, unknown>).todos
|
|
156
|
-
: undefined;
|
|
157
|
-
const todos = asListItems(props.todos) || asListItems(stateTodos) || [];
|
|
158
|
-
return {
|
|
159
|
-
widgetId,
|
|
160
|
-
kind: 'list',
|
|
161
|
-
title: data.title || readString(props.title) || 'Task List',
|
|
162
|
-
description: readString(props.description),
|
|
163
|
-
metadata: { ...(data.metadata || {}), legacyKind: 'todo_list' },
|
|
164
|
-
items: todos.map(todoToListItem),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (data.kind === 'form') {
|
|
169
|
-
const propsSource = (data as unknown as { props?: unknown }).props;
|
|
170
|
-
const props = isRecord(propsSource) ? propsSource : {};
|
|
171
|
-
return {
|
|
172
|
-
widgetId,
|
|
173
|
-
kind: 'form',
|
|
174
|
-
title: data.title || 'Details Required',
|
|
175
|
-
description: data.description,
|
|
176
|
-
body: data.body,
|
|
177
|
-
state: data.state,
|
|
178
|
-
metadata: data.metadata,
|
|
179
|
-
fields: data.fields || asFields(props.schema) || [],
|
|
180
|
-
submitLabel: data.submitLabel || readString(props.submitLabel),
|
|
181
|
-
actions: data.actions,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (data.kind === 'list') {
|
|
186
|
-
return { ...data, widgetId, title: data.title || 'Task List', items: data.items || [] };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (data.kind === 'choice') {
|
|
190
|
-
return { ...data, widgetId, title: data.title || 'Choose an Option' };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (data.kind === 'message') {
|
|
194
|
-
return { ...data, widgetId, title: data.title || 'Message' };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
throw new Error(`Unsupported UI widget kind: ${(data as { kind?: string }).kind || 'unknown'}`);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const uiToolDefinitions = {
|
|
201
|
-
render_ui_widget: {
|
|
202
|
-
description:
|
|
203
|
-
'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.',
|
|
204
|
-
inputSchema: renderWidgetSchema,
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const uiPluginRuntime = (): MelonyPlugin<OpenBotState, OpenBotEvent> => (builder) => {
|
|
209
|
-
builder.on('action:render_ui_widget', async function* (event, context) {
|
|
210
|
-
const widget = normalizeWidget(event.data, context.state, event.meta?.toolCallId);
|
|
211
|
-
yield {
|
|
212
|
-
type: 'client:ui:widget',
|
|
213
|
-
data: widget,
|
|
214
|
-
meta: event.meta,
|
|
215
|
-
};
|
|
216
|
-
});
|
|
217
|
-
};
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { Plugin } from '../../services/plugins/types.js';
|
|
4
|
+
import { OpenBotEvent, RenderWidgetEvent, UIWidgetResponseEvent } from '../../app/types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `ui` — provides a tool for the agent to render interactive UI widgets.
|
|
8
|
+
*
|
|
9
|
+
* The model can choose which widget to render (form, choice, list, message)
|
|
10
|
+
* depending on the situation.
|
|
11
|
+
*/
|
|
218
12
|
|
|
219
13
|
export const uiPlugin: Plugin = {
|
|
220
14
|
id: 'ui',
|
|
221
|
-
name: 'UI
|
|
222
|
-
description: 'Render
|
|
223
|
-
toolDefinitions:
|
|
224
|
-
|
|
15
|
+
name: 'UI',
|
|
16
|
+
description: 'Render interactive UI widgets to interact with the user.',
|
|
17
|
+
toolDefinitions: {
|
|
18
|
+
render_widget: {
|
|
19
|
+
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.',
|
|
20
|
+
inputSchema: z.object({
|
|
21
|
+
kind: z.enum(['message', 'choice', 'form', 'list']).describe('The type of widget to render.'),
|
|
22
|
+
title: z.string().describe('The title of the widget.'),
|
|
23
|
+
description: z.string().optional().describe('A description or body text.'),
|
|
24
|
+
fields: z.array(z.object({
|
|
25
|
+
id: z.string().describe('Unique ID for the field.'),
|
|
26
|
+
label: z.string().describe('Label shown to the user.'),
|
|
27
|
+
type: z.enum(['text', 'textarea', 'number', 'boolean', 'select', 'multiselect', 'date']),
|
|
28
|
+
description: z.string().optional(),
|
|
29
|
+
placeholder: z.string().optional(),
|
|
30
|
+
required: z.boolean().optional(),
|
|
31
|
+
options: z.array(z.object({ label: z.string(), value: z.string() })).optional(),
|
|
32
|
+
defaultValue: z.any().optional()
|
|
33
|
+
})).optional().describe('Required for kind="form". List of form fields.'),
|
|
34
|
+
actions: z.array(z.object({
|
|
35
|
+
id: z.string(),
|
|
36
|
+
label: z.string(),
|
|
37
|
+
variant: z.enum(['primary', 'secondary', 'danger']).optional(),
|
|
38
|
+
})).optional().describe('Buttons or actions available on the widget.'),
|
|
39
|
+
items: z.array(z.object({
|
|
40
|
+
id: z.string(),
|
|
41
|
+
label: z.string(),
|
|
42
|
+
description: z.string().optional(),
|
|
43
|
+
status: z.enum(['pending', 'in_progress', 'done', 'error', 'cancelled']).optional(),
|
|
44
|
+
metadata: z.record(z.string(), z.any()).optional()
|
|
45
|
+
})).optional().describe('Required for kind="list". List of items to display.'),
|
|
46
|
+
submitLabel: z.string().optional().describe('Label for the primary action button (e.g. "Submit", "Save").')
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
factory: () => (builder) => {
|
|
51
|
+
// Handle the tool call from the agent
|
|
52
|
+
builder.on('action:render_widget', async function* (event, context) {
|
|
53
|
+
const widgetEvent = event as RenderWidgetEvent;
|
|
54
|
+
const toolCallId = widgetEvent.meta?.toolCallId;
|
|
55
|
+
const threadId = widgetEvent.meta?.threadId || context.state.threadId;
|
|
56
|
+
|
|
57
|
+
if (!toolCallId) return;
|
|
58
|
+
|
|
59
|
+
const widgetId = randomUUID();
|
|
60
|
+
|
|
61
|
+
// Emit the UI widget event to the client
|
|
62
|
+
yield {
|
|
63
|
+
type: 'client:ui:widget',
|
|
64
|
+
data: {
|
|
65
|
+
...widgetEvent.data,
|
|
66
|
+
widgetId,
|
|
67
|
+
metadata: {
|
|
68
|
+
type: 'ui:request',
|
|
69
|
+
originalEvent: widgetEvent
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
meta: { agentId: context.state.agentId, threadId }
|
|
73
|
+
} as OpenBotEvent;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Handle the user's response from the UI widget
|
|
77
|
+
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
78
|
+
const responseEvent = event as UIWidgetResponseEvent;
|
|
79
|
+
const { widgetId, actionId, values, metadata } = responseEvent.data;
|
|
80
|
+
if (metadata?.type !== 'ui:request') return;
|
|
81
|
+
|
|
82
|
+
const originalEvent = metadata.originalEvent as RenderWidgetEvent;
|
|
83
|
+
const toolCallId = originalEvent?.meta?.toolCallId;
|
|
84
|
+
const threadId = originalEvent?.meta?.threadId || context.state.threadId;
|
|
85
|
+
|
|
86
|
+
if (!toolCallId) return;
|
|
87
|
+
|
|
88
|
+
// Yield a "submitted" widget update to the UI to collapse/disable it
|
|
89
|
+
yield {
|
|
90
|
+
type: 'client:ui:widget',
|
|
91
|
+
data: {
|
|
92
|
+
widgetId,
|
|
93
|
+
title: originalEvent.data.title,
|
|
94
|
+
kind: originalEvent.data.kind as any,
|
|
95
|
+
state: 'submitted',
|
|
96
|
+
body: "Thank you for your response. We will process it and get back to you soon.",
|
|
97
|
+
display: 'collapsed',
|
|
98
|
+
disabled: true,
|
|
99
|
+
actions: [], // Clear actions to disable buttons in UI
|
|
100
|
+
},
|
|
101
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
102
|
+
} as OpenBotEvent;
|
|
103
|
+
|
|
104
|
+
// Emit the tool result event so the agent runtime can resume
|
|
105
|
+
yield {
|
|
106
|
+
type: 'action:render_widget:result',
|
|
107
|
+
data: {
|
|
108
|
+
success: true,
|
|
109
|
+
actionId,
|
|
110
|
+
values,
|
|
111
|
+
output: JSON.stringify(values)
|
|
112
|
+
},
|
|
113
|
+
meta: {
|
|
114
|
+
agentId: context.state.agentId,
|
|
115
|
+
threadId,
|
|
116
|
+
toolCallId
|
|
117
|
+
}
|
|
118
|
+
} as any as OpenBotEvent;
|
|
119
|
+
});
|
|
120
|
+
},
|
|
225
121
|
};
|
|
226
122
|
|
|
227
123
|
export default uiPlugin;
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
|
|
9
|
+
export const abortKey = (channelId: string, threadId?: string): string =>
|
|
10
|
+
`${channelId}:${threadId || ''}`;
|
|
11
|
+
|
|
12
|
+
class AbortRegistry {
|
|
13
|
+
private entries = new Map<string, { controller: AbortController; refs: number }>();
|
|
14
|
+
|
|
15
|
+
/** Register interest in a run. Returns a shared signal for the key. */
|
|
16
|
+
acquire(key: string): AbortSignal {
|
|
17
|
+
let entry = this.entries.get(key);
|
|
18
|
+
if (!entry) {
|
|
19
|
+
entry = { controller: new AbortController(), refs: 0 };
|
|
20
|
+
this.entries.set(key, entry);
|
|
21
|
+
}
|
|
22
|
+
entry.refs += 1;
|
|
23
|
+
return entry.controller.signal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Release interest. Removes the entry once no runs reference it. */
|
|
27
|
+
release(key: string): void {
|
|
28
|
+
const entry = this.entries.get(key);
|
|
29
|
+
if (!entry) return;
|
|
30
|
+
entry.refs -= 1;
|
|
31
|
+
if (entry.refs <= 0) {
|
|
32
|
+
this.entries.delete(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Abort all runs for the key. Returns true if something was active. */
|
|
37
|
+
abort(key: string): boolean {
|
|
38
|
+
const entry = this.entries.get(key);
|
|
39
|
+
if (!entry) return false;
|
|
40
|
+
entry.controller.abort();
|
|
41
|
+
this.entries.delete(key);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const abortRegistry = new AbortRegistry();
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { OpenBotEvent } from '
|
|
2
|
-
import type { PluginRef } from './
|
|
3
|
-
import type { MemoryRecord, ListMemoriesArgs } from '
|
|
1
|
+
import type { OpenBotEvent } from '../../app/types.js';
|
|
2
|
+
import type { PluginRef } from './types.js';
|
|
3
|
+
import type { MemoryRecord, ListMemoriesArgs } from '../../plugins/memory/service.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Public data types exposed by the OpenBot
|
|
6
|
+
* Public data types exposed by the OpenBot platform.
|
|
7
7
|
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The platform layer owns channels, threads, the agent registry, and the event
|
|
9
|
+
* stream. Agents are composed entirely of Plugins (see `./types.ts`); their
|
|
10
|
+
* internal implementation is opaque to the platform.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
export type Agent = {
|
|
@@ -17,6 +17,8 @@ export type Agent = {
|
|
|
17
17
|
image?: string;
|
|
18
18
|
/** Plugin ids that compose this agent (mirrors AGENT.md `plugins[].id`). */
|
|
19
19
|
plugins: string[];
|
|
20
|
+
/** When true, omitted from `action:storage:get-agents` (still available via get-agent-details). */
|
|
21
|
+
hidden?: boolean;
|
|
20
22
|
createdAt: Date;
|
|
21
23
|
updatedAt: Date;
|
|
22
24
|
};
|
|
@@ -43,13 +45,15 @@ export type ConfigSchema = {
|
|
|
43
45
|
type: 'object';
|
|
44
46
|
properties: {
|
|
45
47
|
[key: string]: {
|
|
46
|
-
type: 'string' | 'number' | 'boolean' | 'integer';
|
|
48
|
+
type: 'string' | 'number' | 'boolean' | 'integer' | 'object' | 'array';
|
|
47
49
|
description?: string;
|
|
48
50
|
default?: unknown;
|
|
49
51
|
enum?: unknown[];
|
|
50
52
|
minimum?: number;
|
|
51
53
|
maximum?: number;
|
|
52
54
|
format?: 'password' | 'url' | 'email';
|
|
55
|
+
properties?: ConfigSchema['properties'];
|
|
56
|
+
items?: ConfigSchema['properties'][string];
|
|
53
57
|
};
|
|
54
58
|
};
|
|
55
59
|
required?: string[];
|
|
@@ -74,6 +78,7 @@ export type Thread = {
|
|
|
74
78
|
channelId: string;
|
|
75
79
|
createdAt: Date;
|
|
76
80
|
updatedAt: Date;
|
|
81
|
+
hasUnseenMessages?: boolean;
|
|
77
82
|
};
|
|
78
83
|
|
|
79
84
|
export type ThreadDetails = {
|
|
@@ -102,6 +107,8 @@ export interface Storage {
|
|
|
102
107
|
initialState?: Record<string, unknown>;
|
|
103
108
|
cwd?: string;
|
|
104
109
|
}) => Promise<void>;
|
|
110
|
+
/** Removes the channel directory and cleans up `_meta/last-read.json`. */
|
|
111
|
+
deleteChannel: (args: { channelId: string }) => Promise<void>;
|
|
105
112
|
createThread: (args: {
|
|
106
113
|
channelId: string;
|
|
107
114
|
threadId: string;
|
|
@@ -110,29 +117,42 @@ export interface Storage {
|
|
|
110
117
|
}) => Promise<void>;
|
|
111
118
|
getThreads: (args: { channelId: string }) => Promise<Thread[]>;
|
|
112
119
|
getThreadDetails: (args: { channelId: string; threadId: string }) => Promise<ThreadDetails>;
|
|
120
|
+
setLastRead: (args: { channelId: string; threadId?: string; lastReadEventId: string }) => Promise<void>;
|
|
121
|
+
/** User-facing agent list; excludes agents with `hidden: true` (e.g. built-in `state`). */
|
|
113
122
|
getAgents: () => Promise<Agent[]>;
|
|
114
123
|
getPlugins: () => Promise<PluginDescriptor[]>;
|
|
115
124
|
getAgentDetails: (args: { agentId: string }) => Promise<AgentDetails>;
|
|
125
|
+
/** Includes built-in `system` / `state` agents as optional AGENT.md overlays. */
|
|
116
126
|
createAgent: (args: {
|
|
117
127
|
agentId: string;
|
|
118
128
|
name: string;
|
|
119
129
|
description?: string;
|
|
120
130
|
/** Avatar/logo URL or data URI; persisted in AGENT.md frontmatter. */
|
|
121
131
|
image?: string;
|
|
132
|
+
/** When true, agent is omitted from `getAgents` / `action:storage:get-agents`. */
|
|
133
|
+
hidden?: boolean;
|
|
122
134
|
instructions: string;
|
|
123
135
|
plugins: PluginRef[];
|
|
124
136
|
}) => Promise<void>;
|
|
137
|
+
/** Partial update; for `system` / `state`, creates overlay file if missing. */
|
|
125
138
|
updateAgent: (args: {
|
|
126
139
|
agentId: string;
|
|
127
140
|
name?: string;
|
|
128
141
|
description?: string;
|
|
129
142
|
/** Omit to leave unchanged; empty string removes stored image. */
|
|
130
143
|
image?: string;
|
|
144
|
+
hidden?: boolean;
|
|
131
145
|
instructions?: string;
|
|
132
146
|
plugins?: PluginRef[];
|
|
133
147
|
}) => Promise<void>;
|
|
148
|
+
/** For `system` / `state`, removes only `AGENT.md` (reverts to code defaults). */
|
|
134
149
|
deleteAgent: (args: { agentId: string }) => Promise<void>;
|
|
135
150
|
getEvents: (args: { channelId: string; threadId?: string }) => Promise<OpenBotEvent[]>;
|
|
151
|
+
storeEvent: (args: {
|
|
152
|
+
channelId: string;
|
|
153
|
+
threadId?: string;
|
|
154
|
+
event: OpenBotEvent;
|
|
155
|
+
}) => Promise<void>;
|
|
136
156
|
getChannelDetails: (args: { channelId: string }) => Promise<ChannelDetails>;
|
|
137
157
|
patchChannelState: (args: { channelId: string; state: unknown }) => Promise<void>;
|
|
138
158
|
patchThreadState: (args: {
|
|
@@ -149,6 +169,28 @@ export interface Storage {
|
|
|
149
169
|
path?: string;
|
|
150
170
|
}) => Promise<Array<{ name: string; isDirectory: boolean }>>;
|
|
151
171
|
readFile: (args: { channelId: string; path: string }) => Promise<string>;
|
|
172
|
+
readChannelFile: (args: {
|
|
173
|
+
channelId: string;
|
|
174
|
+
path: string;
|
|
175
|
+
encoding?: 'utf8' | 'base64';
|
|
176
|
+
}) => Promise<{ content: string; mimeType: string; size: number }>;
|
|
177
|
+
writeChannelFile: (args: {
|
|
178
|
+
channelId: string;
|
|
179
|
+
path: string;
|
|
180
|
+
content: string;
|
|
181
|
+
encoding?: 'utf8' | 'base64';
|
|
182
|
+
overwrite?: boolean;
|
|
183
|
+
}) => Promise<{ path: string; size: number; mimeType: string }>;
|
|
184
|
+
uploadChannelFile: (args: {
|
|
185
|
+
channelId: string;
|
|
186
|
+
path: string;
|
|
187
|
+
body: Buffer;
|
|
188
|
+
overwrite?: boolean;
|
|
189
|
+
}) => Promise<{ path: string; size: number; mimeType: string }>;
|
|
190
|
+
getChannelFileStat: (args: {
|
|
191
|
+
channelId: string;
|
|
192
|
+
path: string;
|
|
193
|
+
}) => Promise<{ abs: string; size: number; mimeType: string }>;
|
|
152
194
|
/** Persist a memory record into the global memory log. */
|
|
153
195
|
appendMemory: (args: {
|
|
154
196
|
scope: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Plugin } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** Resolved plugins (built-in + community); shared with registry resolution. */
|
|
4
|
+
export const resolvedPluginCache = new Map<string, Plugin>();
|
|
5
|
+
|
|
6
|
+
/** Community plugin ids that have already been logged as loaded once. */
|
|
7
|
+
export const loadedCommunityPlugins = new Set<string>();
|
|
8
|
+
|
|
9
|
+
/** Drop a single id from the in-memory resolver cache (e.g. after install/uninstall). */
|
|
10
|
+
export function invalidatePlugin(id: string): void {
|
|
11
|
+
resolvedPluginCache.delete(id);
|
|
12
|
+
loadedCommunityPlugins.delete(id);
|
|
13
|
+
}
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import type { Plugin } from '
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
4
|
+
import type { Plugin } from './types.js';
|
|
5
|
+
import { openbotPlugin } from '../../plugins/openbot/index.js';
|
|
6
|
+
import { bashPlugin } from '../../plugins/bash/index.js';
|
|
7
|
+
import { storagePlugin } from '../../plugins/storage/index.js';
|
|
8
|
+
import { approvalPlugin } from '../../plugins/approval/index.js';
|
|
9
|
+
import { memoryPlugin } from '../../plugins/memory/index.js';
|
|
10
|
+
import { delegationPlugin } from '../../plugins/delegation/index.js';
|
|
11
|
+
import { uiPlugin } from '../../plugins/ui/index.js';
|
|
12
|
+
import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
|
|
13
|
+
import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../../app/config.js';
|
|
14
|
+
import {
|
|
15
|
+
invalidatePlugin as clearResolvedPluginEntry,
|
|
16
|
+
loadedCommunityPlugins,
|
|
17
|
+
resolvedPluginCache,
|
|
18
|
+
} from './plugin-cache.js';
|
|
15
19
|
|
|
16
20
|
let pluginsDir: string | null = null;
|
|
17
|
-
const loadedPlugins = new Set<string>();
|
|
18
|
-
const cache = new Map<string, Plugin>();
|
|
19
21
|
|
|
20
22
|
const BUILT_IN: Record<string, Plugin> = {
|
|
21
|
-
[
|
|
22
|
-
[
|
|
23
|
-
[
|
|
24
|
-
[delegationPlugin.id]: delegationPlugin,
|
|
25
|
-
[storageToolsPlugin.id]: storageToolsPlugin,
|
|
26
|
-
[uiPlugin.id]: uiPlugin,
|
|
23
|
+
[openbotPlugin.id]: openbotPlugin,
|
|
24
|
+
[bashPlugin.id]: bashPlugin,
|
|
25
|
+
[storagePlugin.id]: storagePlugin,
|
|
27
26
|
[approvalPlugin.id]: approvalPlugin,
|
|
28
27
|
[memoryPlugin.id]: memoryPlugin,
|
|
29
|
-
[
|
|
28
|
+
[delegationPlugin.id]: delegationPlugin,
|
|
29
|
+
[uiPlugin.id]: uiPlugin,
|
|
30
|
+
[pluginManagerPlugin.id]: pluginManagerPlugin,
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -78,14 +79,14 @@ export function initPlugins(dir?: string) {
|
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
81
|
* Resolve a Plugin by id. The id is either:
|
|
81
|
-
* - a built-in id (e.g. "
|
|
82
|
+
* - a built-in id (e.g. "openbot", "bash"), or
|
|
82
83
|
* - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
|
|
83
84
|
* in which case the folder layout is `plugins/<id>/dist/index.js`.
|
|
84
85
|
*/
|
|
85
86
|
export async function resolvePlugin(id: string): Promise<Plugin | null> {
|
|
86
|
-
if (
|
|
87
|
+
if (resolvedPluginCache.has(id)) return resolvedPluginCache.get(id)!;
|
|
87
88
|
if (BUILT_IN[id]) {
|
|
88
|
-
|
|
89
|
+
resolvedPluginCache.set(id, BUILT_IN[id]);
|
|
89
90
|
return BUILT_IN[id];
|
|
90
91
|
}
|
|
91
92
|
|
|
@@ -109,10 +110,10 @@ export async function resolvePlugin(id: string): Promise<Plugin | null> {
|
|
|
109
110
|
return null;
|
|
110
111
|
}
|
|
111
112
|
const plugin: Plugin = { id, ...parsed, name: parsed.name || id };
|
|
112
|
-
|
|
113
|
-
if (!
|
|
113
|
+
resolvedPluginCache.set(id, plugin);
|
|
114
|
+
if (!loadedCommunityPlugins.has(id)) {
|
|
114
115
|
console.log(`[plugins] Loaded community plugin "${id}" from ${distPath}`);
|
|
115
|
-
|
|
116
|
+
loadedCommunityPlugins.add(id);
|
|
116
117
|
}
|
|
117
118
|
return plugin;
|
|
118
119
|
} catch (e) {
|
|
@@ -123,8 +124,7 @@ export async function resolvePlugin(id: string): Promise<Plugin | null> {
|
|
|
123
124
|
|
|
124
125
|
/** Drop a single id from the in-memory cache (e.g. after fresh install). */
|
|
125
126
|
export function invalidatePlugin(id: string): void {
|
|
126
|
-
|
|
127
|
-
loadedPlugins.delete(id);
|
|
127
|
+
clearResolvedPluginEntry(id);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/** List built-in plugins (for marketplace/registry views). */
|