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.
Files changed (104) hide show
  1. package/README.md +15 -16
  2. package/dist/app/agent-ids.js +4 -0
  3. package/dist/app/cli.js +1 -1
  4. package/dist/app/config.js +10 -19
  5. package/dist/app/server.js +208 -17
  6. package/dist/bus/services.js +34 -124
  7. package/dist/harness/agent-invoke-run.js +44 -0
  8. package/dist/harness/agent-turn.js +99 -0
  9. package/dist/harness/channel-participants.js +40 -0
  10. package/dist/harness/constants.js +2 -0
  11. package/dist/harness/context-meter.js +97 -0
  12. package/dist/harness/context.js +95 -47
  13. package/dist/harness/dispatch.js +144 -0
  14. package/dist/harness/dispatcher.js +45 -156
  15. package/dist/harness/history.js +177 -0
  16. package/dist/harness/index.js +109 -0
  17. package/dist/harness/orchestration.js +88 -0
  18. package/dist/harness/participants.js +22 -0
  19. package/dist/harness/run-harness.js +154 -0
  20. package/dist/harness/run.js +98 -0
  21. package/dist/harness/runtime-factory.js +0 -34
  22. package/dist/harness/runtime.js +57 -0
  23. package/dist/harness/todo-dispatch.js +51 -0
  24. package/dist/harness/todos.js +5 -0
  25. package/dist/harness/turn.js +79 -0
  26. package/dist/plugins/approval/index.js +120 -149
  27. package/dist/plugins/bash/index.js +195 -0
  28. package/dist/plugins/delegation/index.js +121 -32
  29. package/dist/plugins/memory/index.js +103 -14
  30. package/dist/plugins/memory/service.js +152 -0
  31. package/dist/plugins/openbot/context.js +125 -0
  32. package/dist/plugins/openbot/history.js +144 -0
  33. package/dist/plugins/openbot/index.js +71 -0
  34. package/dist/plugins/openbot/runtime.js +381 -0
  35. package/dist/plugins/openbot/system-prompt.js +25 -0
  36. package/dist/plugins/plugin-manager/index.js +189 -0
  37. package/dist/plugins/shell/index.js +2 -1
  38. package/dist/plugins/storage/files.js +67 -0
  39. package/dist/plugins/storage/index.js +750 -0
  40. package/dist/plugins/storage/service.js +1316 -0
  41. package/dist/plugins/storage-tools/index.js +2 -2
  42. package/dist/plugins/thread-namer/index.js +72 -0
  43. package/dist/plugins/thread-naming/generate-title.js +44 -0
  44. package/dist/plugins/thread-naming/index.js +103 -0
  45. package/dist/plugins/threads/index.js +114 -0
  46. package/dist/plugins/todo/index.js +24 -25
  47. package/dist/plugins/ui/index.js +109 -180
  48. package/dist/registry/plugins.js +3 -9
  49. package/dist/services/abort.js +43 -0
  50. package/dist/services/plugins/domain.js +1 -0
  51. package/dist/services/plugins/plugin-cache.js +9 -0
  52. package/dist/services/plugins/registry.js +112 -0
  53. package/dist/services/plugins/service.js +232 -0
  54. package/dist/services/plugins/types.js +1 -0
  55. package/dist/services/process.js +29 -0
  56. package/dist/services/storage.js +11 -10
  57. package/dist/services/thread-naming.js +81 -0
  58. package/docs/agents.md +15 -12
  59. package/docs/architecture.md +2 -2
  60. package/docs/plugins.md +29 -17
  61. package/docs/templates/AGENT.example.md +8 -14
  62. package/package.json +1 -2
  63. package/src/app/agent-ids.ts +5 -0
  64. package/src/app/cli.ts +1 -1
  65. package/src/app/config.ts +14 -31
  66. package/src/app/server.ts +243 -19
  67. package/src/app/types.ts +331 -187
  68. package/src/harness/index.ts +166 -0
  69. package/src/plugins/approval/index.ts +107 -188
  70. package/src/plugins/bash/index.ts +232 -0
  71. package/src/plugins/delegation/index.ts +139 -39
  72. package/src/plugins/memory/index.ts +112 -15
  73. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  74. package/src/plugins/openbot/context.ts +140 -0
  75. package/src/plugins/openbot/history.ts +158 -0
  76. package/src/plugins/openbot/index.ts +79 -0
  77. package/src/plugins/openbot/runtime.ts +478 -0
  78. package/src/plugins/openbot/system-prompt.ts +27 -0
  79. package/src/plugins/plugin-manager/index.ts +224 -0
  80. package/src/plugins/storage/files.ts +81 -0
  81. package/src/plugins/storage/index.ts +823 -0
  82. package/src/{services/storage.ts → plugins/storage/service.ts} +485 -105
  83. package/src/plugins/ui/index.ts +117 -221
  84. package/src/services/abort.ts +46 -0
  85. package/src/{bus/types.ts → services/plugins/domain.ts} +50 -8
  86. package/src/services/plugins/plugin-cache.ts +13 -0
  87. package/src/{registry/plugins.ts → services/plugins/registry.ts} +28 -28
  88. package/src/services/plugins/service.ts +318 -0
  89. package/src/{bus/plugin.ts → services/plugins/types.ts} +7 -3
  90. package/src/bus/services.ts +0 -954
  91. package/src/harness/context.ts +0 -365
  92. package/src/harness/dispatcher.ts +0 -379
  93. package/src/harness/mcp.ts +0 -78
  94. package/src/harness/runtime-factory.ts +0 -129
  95. package/src/harness/todo-advance.ts +0 -128
  96. package/src/plugins/ai-sdk/index.ts +0 -41
  97. package/src/plugins/ai-sdk/runtime.ts +0 -468
  98. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  99. package/src/plugins/mcp/index.ts +0 -128
  100. package/src/plugins/shell/index.ts +0 -123
  101. package/src/plugins/storage-tools/index.ts +0 -90
  102. package/src/plugins/todo/index.ts +0 -64
  103. package/src/services/plugins.ts +0 -133
  104. /package/src/{harness → services}/process.ts +0 -0
@@ -1,227 +1,123 @@
1
- import { MelonyPlugin } from 'melony';
2
- import z from 'zod';
3
- import type { Plugin } from '../../bus/plugin.js';
4
- import {
5
- OpenBotEvent,
6
- OpenBotState,
7
- RenderUIWidgetData,
8
- UIWidgetField,
9
- UIWidgetListItem,
10
- UIWidgetSpec,
11
- } from '../../app/types.js';
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 Widgets',
222
- description: 'Render server-driven UI widgets (messages, choices, forms, lists) in the conversation.',
223
- toolDefinitions: uiToolDefinitions,
224
- factory: () => uiPluginRuntime(),
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 '../app/types.js';
2
- import type { PluginRef } from './plugin.js';
3
- import type { MemoryRecord, ListMemoriesArgs } from '../services/memory.js';
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 bus.
6
+ * Public data types exposed by the OpenBot platform.
7
7
  *
8
- * The bus is the platform layer that owns channels, threads, the agent registry,
9
- * and the event stream. Agents are composed entirely of Plugins (see
10
- * `bus/plugin.ts`); their internal implementation is opaque to the bus.
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 '../bus/plugin.js';
5
- import { aiSdkPlugin } from '../plugins/ai-sdk/index.js';
6
- import { shellPlugin } from '../plugins/shell/index.js';
7
- import { mcpPlugin } from '../plugins/mcp/index.js';
8
- import { delegationPlugin } from '../plugins/delegation/index.js';
9
- import { storageToolsPlugin } from '../plugins/storage-tools/index.js';
10
- import { uiPlugin } from '../plugins/ui/index.js';
11
- import { approvalPlugin } from '../plugins/approval/index.js';
12
- import { memoryPlugin } from '../plugins/memory/index.js';
13
- import { todoPlugin } from '../plugins/todo/index.js';
14
- import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
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
- [aiSdkPlugin.id]: aiSdkPlugin,
22
- [shellPlugin.id]: shellPlugin,
23
- [mcpPlugin.id]: mcpPlugin,
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
- [todoPlugin.id]: todoPlugin,
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. "ai-sdk", "shell"), or
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 (cache.has(id)) return cache.get(id)!;
87
+ if (resolvedPluginCache.has(id)) return resolvedPluginCache.get(id)!;
87
88
  if (BUILT_IN[id]) {
88
- cache.set(id, BUILT_IN[id]);
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
- cache.set(id, plugin);
113
- if (!loadedPlugins.has(id)) {
113
+ resolvedPluginCache.set(id, plugin);
114
+ if (!loadedCommunityPlugins.has(id)) {
114
115
  console.log(`[plugins] Loaded community plugin "${id}" from ${distPath}`);
115
- loadedPlugins.add(id);
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
- cache.delete(id);
127
- loadedPlugins.delete(id);
127
+ clearResolvedPluginEntry(id);
128
128
  }
129
129
 
130
130
  /** List built-in plugins (for marketplace/registry views). */