openbot 0.2.14 → 0.3.1
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/agents/openbot/index.js +76 -0
- package/dist/agents/openbot/middleware/approval.js +132 -0
- package/dist/agents/openbot/runtime.js +289 -0
- package/dist/agents/openbot/system-prompt.js +32 -0
- package/dist/agents/openbot/tools/delegation.js +78 -0
- package/dist/agents/openbot/tools/mcp.js +99 -0
- package/dist/agents/openbot/tools/shell.js +91 -0
- package/dist/agents/openbot/tools/storage.js +75 -0
- package/dist/agents/openbot/tools/ui.js +176 -0
- package/dist/agents/system.js +20 -93
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +4 -1
- package/dist/app/server.js +15 -8
- package/dist/bus/agent-package.js +1 -0
- package/dist/bus/plugin.js +1 -0
- package/dist/bus/services.js +711 -0
- package/dist/bus/types.js +1 -0
- package/dist/harness/context.js +250 -0
- package/dist/harness/event-normalizer.js +59 -0
- package/dist/harness/orchestrator.js +27 -227
- package/dist/harness/process.js +25 -3
- package/dist/harness/queue-processor.js +227 -0
- package/dist/harness/runtime-factory.js +103 -0
- package/dist/plugins/ai-sdk/index.js +37 -0
- package/dist/plugins/ai-sdk/runtime.js +402 -0
- package/dist/plugins/ai-sdk/system-prompt.js +3 -0
- package/dist/plugins/ai-sdk.js +277 -87
- package/dist/plugins/approval/index.js +159 -0
- package/dist/plugins/approval.js +163 -0
- package/dist/plugins/delegation/index.js +79 -0
- package/dist/plugins/delegation.js +67 -11
- package/dist/plugins/mcp/index.js +108 -0
- package/dist/plugins/memory/index.js +71 -0
- package/dist/plugins/shell/index.js +99 -0
- package/dist/plugins/shell.js +123 -0
- package/dist/plugins/storage-tools/index.js +85 -0
- package/dist/plugins/storage.js +240 -5
- package/dist/plugins/ui/index.js +184 -0
- package/dist/plugins/ui.js +185 -21
- package/dist/registry/agents.js +138 -0
- package/dist/registry/plugins.js +93 -50
- package/dist/services/agent-packages.js +103 -0
- package/dist/services/memory.js +152 -0
- package/dist/services/plugins.js +98 -0
- package/dist/services/storage.js +366 -94
- package/docs/agents.md +52 -65
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +70 -58
- package/docs/templates/AGENT.example.md +57 -0
- package/package.json +8 -7
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +14 -4
- package/src/app/server.ts +23 -10
- package/src/app/types.ts +445 -16
- package/src/assets/icon.svg +4 -1
- package/src/bus/plugin.ts +67 -0
- package/src/bus/services.ts +786 -0
- package/src/bus/types.ts +160 -0
- package/src/harness/context.ts +293 -0
- package/src/harness/event-normalizer.ts +82 -0
- package/src/harness/orchestrator.ts +35 -273
- package/src/harness/process.ts +28 -4
- package/src/harness/queue-processor.ts +309 -0
- package/src/harness/runtime-factory.ts +125 -0
- package/src/plugins/ai-sdk/index.ts +44 -0
- package/src/plugins/ai-sdk/runtime.ts +484 -0
- package/src/plugins/ai-sdk/system-prompt.ts +4 -0
- package/src/plugins/approval/index.ts +228 -0
- package/src/plugins/delegation/index.ts +94 -0
- package/src/plugins/mcp/index.ts +128 -0
- package/src/plugins/memory/index.ts +85 -0
- package/src/plugins/shell/index.ts +123 -0
- package/src/plugins/storage-tools/index.ts +101 -0
- package/src/plugins/ui/index.ts +227 -0
- package/src/registry/plugins.ts +108 -55
- package/src/services/memory.ts +213 -0
- package/src/services/plugins.ts +133 -0
- package/src/services/storage.ts +472 -137
- package/src/agents/system.ts +0 -112
- package/src/plugins/ai-sdk.ts +0 -197
- package/src/plugins/delegation.ts +0 -60
- package/src/plugins/mcp.ts +0 -154
- package/src/plugins/storage.ts +0 -725
- package/src/plugins/ui.ts +0 -57
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
export const shellToolDefinitions = {
|
|
4
|
+
shell_exec: {
|
|
5
|
+
description: 'Execute a shell command in the terminal. Use this for file operations, running scripts, or system tasks.',
|
|
6
|
+
inputSchema: z.object({
|
|
7
|
+
command: z.string().describe('The shell command to execute.'),
|
|
8
|
+
cwd: z.string().optional().describe('The working directory for the command. Defaults to the channel cwd or workspace root. Leave it empty unless user asks for a specific directory.'),
|
|
9
|
+
shell: z.enum(['bash', 'sh', 'zsh']).optional().describe('The shell to use. Defaults to bash.'),
|
|
10
|
+
timeoutMs: z.number().optional().default(30000).describe('Maximum execution time in milliseconds. Defaults to 30000 (30s).'),
|
|
11
|
+
}),
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export const shellPlugin = () => (builder) => {
|
|
15
|
+
builder.on('action:shell_exec', async function* (event, context) {
|
|
16
|
+
const { command, cwd, shell = 'bash', timeoutMs = 30000 } = event.data;
|
|
17
|
+
// Clamp timeout between 1s and 60s
|
|
18
|
+
const actualTimeout = Math.max(1000, Math.min(timeoutMs, 60000));
|
|
19
|
+
// Default CWD to channel CWD if not provided
|
|
20
|
+
const actualCwd = cwd || context.state.channelDetails?.cwd || process.cwd();
|
|
21
|
+
try {
|
|
22
|
+
const result = await new Promise((resolve) => {
|
|
23
|
+
const child = spawn(command, {
|
|
24
|
+
shell,
|
|
25
|
+
cwd: actualCwd,
|
|
26
|
+
env: { ...process.env },
|
|
27
|
+
});
|
|
28
|
+
let stdout = '';
|
|
29
|
+
let stderr = '';
|
|
30
|
+
let timedOut = false;
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
timedOut = true;
|
|
33
|
+
child.kill();
|
|
34
|
+
}, actualTimeout);
|
|
35
|
+
child.stdout.on('data', (data) => {
|
|
36
|
+
stdout += data.toString();
|
|
37
|
+
// Cap output at 100KB
|
|
38
|
+
if (stdout.length > 100000) {
|
|
39
|
+
stdout = stdout.substring(0, 100000) + '\n... [output truncated]';
|
|
40
|
+
child.kill();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
child.stderr.on('data', (data) => {
|
|
44
|
+
stderr += data.toString();
|
|
45
|
+
if (stderr.length > 100000) {
|
|
46
|
+
stderr = stderr.substring(0, 100000) + '\n... [output truncated]';
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
child.on('close', (code) => {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
resolve({ exitCode: code, stdout, stderr, timedOut });
|
|
52
|
+
});
|
|
53
|
+
child.on('error', (err) => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
resolve({ exitCode: -1, stdout, stderr: stderr + err.message, timedOut: false });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
const success = result.exitCode === 0 && !result.timedOut;
|
|
59
|
+
yield {
|
|
60
|
+
type: 'action:shell_exec:result',
|
|
61
|
+
data: {
|
|
62
|
+
success,
|
|
63
|
+
exitCode: result.exitCode,
|
|
64
|
+
stdout: result.stdout,
|
|
65
|
+
stderr: result.stderr,
|
|
66
|
+
timedOut: result.timedOut,
|
|
67
|
+
},
|
|
68
|
+
meta: event.meta,
|
|
69
|
+
};
|
|
70
|
+
// const output = [
|
|
71
|
+
// `Command: \`${command}\``,
|
|
72
|
+
// result.exitCode !== null ? `Exit code: ${result.exitCode}` : 'Exit code: unknown',
|
|
73
|
+
// result.timedOut ? '⚠️ Command timed out.' : '',
|
|
74
|
+
// result.stdout ? `\n**STDOUT**:\n${result.stdout}` : '',
|
|
75
|
+
// result.stderr ? `\n**STDERR**:\n${result.stderr}` : '',
|
|
76
|
+
// ].filter(Boolean).join('\n');
|
|
77
|
+
// yield {
|
|
78
|
+
// type: 'agent:output',
|
|
79
|
+
// data: {
|
|
80
|
+
// content: output,
|
|
81
|
+
// },
|
|
82
|
+
// meta: {
|
|
83
|
+
// ...(event.meta || {}),
|
|
84
|
+
// agentId: context.state.agentId,
|
|
85
|
+
// },
|
|
86
|
+
// } as any;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const message = error instanceof Error ? error.message : 'Unknown shell error';
|
|
90
|
+
yield {
|
|
91
|
+
type: 'action:shell_exec:result',
|
|
92
|
+
data: {
|
|
93
|
+
success: false,
|
|
94
|
+
exitCode: -1,
|
|
95
|
+
stdout: '',
|
|
96
|
+
stderr: message,
|
|
97
|
+
timedOut: false,
|
|
98
|
+
error: message,
|
|
99
|
+
},
|
|
100
|
+
meta: event.meta,
|
|
101
|
+
};
|
|
102
|
+
// yield {
|
|
103
|
+
// type: 'agent:output',
|
|
104
|
+
// data: {
|
|
105
|
+
// content: `Failed to execute shell command: ${message}`,
|
|
106
|
+
// },
|
|
107
|
+
// meta: {
|
|
108
|
+
// ...(event.meta || {}),
|
|
109
|
+
// agentId: context.state.agentId,
|
|
110
|
+
// },
|
|
111
|
+
// } as any;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
export const plugin = {
|
|
116
|
+
name: 'shell',
|
|
117
|
+
description: 'Execute shell commands in the terminal',
|
|
118
|
+
version: '1.0.0',
|
|
119
|
+
author: 'OpenBot',
|
|
120
|
+
license: 'MIT',
|
|
121
|
+
factory: shellPlugin,
|
|
122
|
+
toolDefinitions: shellToolDefinitions,
|
|
123
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* `storage-tools` — exposes channel/thread/variable mutation tools to runtime
|
|
4
|
+
* plugins. The actual handlers live in `bus/services.ts` because storage is
|
|
5
|
+
* platform infrastructure, not agent behaviour.
|
|
6
|
+
*/
|
|
7
|
+
const storageToolDefinitions = {
|
|
8
|
+
create_channel: {
|
|
9
|
+
description: 'Create a new channel. Use when the user intent is clearly different from the current channel and should be split. Always confirm before creating. Skip for simple Q&A.',
|
|
10
|
+
inputSchema: z.object({
|
|
11
|
+
channelId: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe('Unique channel ID (e.g. product-launch, backend-platform, channel_roadmap).'),
|
|
14
|
+
spec: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Optional initial markdown content for the channel spec.'),
|
|
18
|
+
initialState: z
|
|
19
|
+
.record(z.string(), z.unknown())
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Optional initial state object for the channel.'),
|
|
22
|
+
cwd: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe('Optional initial current working directory for the channel.'),
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
patch_channel_details: {
|
|
29
|
+
description: 'Patch current channel details (state, spec, cwd).',
|
|
30
|
+
inputSchema: z
|
|
31
|
+
.object({
|
|
32
|
+
state: z
|
|
33
|
+
.record(z.string(), z.unknown())
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('JSON state object for the channel. Use for structured data like `todos` or metadata.'),
|
|
36
|
+
spec: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('Markdown content for the channel specification (SPEC.md). Use for goals and rules.'),
|
|
40
|
+
cwd: z.string().optional().describe('Current working directory for the channel.'),
|
|
41
|
+
})
|
|
42
|
+
.refine((value) => value.state !== undefined || value.spec !== undefined || value.cwd !== undefined, { message: 'Provide at least one of state, spec, or cwd.' }),
|
|
43
|
+
},
|
|
44
|
+
patch_thread_details: {
|
|
45
|
+
description: 'Patch current thread details (state and/or spec).',
|
|
46
|
+
inputSchema: z
|
|
47
|
+
.object({
|
|
48
|
+
state: z
|
|
49
|
+
.record(z.string(), z.unknown())
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('JSON state object for the thread. Use for structured data like `todos` or progress.'),
|
|
52
|
+
spec: z
|
|
53
|
+
.string()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('Markdown content for the thread specification (SPEC.md). Use for plans and goals.'),
|
|
56
|
+
})
|
|
57
|
+
.refine((value) => value.state !== undefined || value.spec !== undefined, {
|
|
58
|
+
message: 'Provide at least one of state or spec.',
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
create_variable: {
|
|
62
|
+
description: 'Create or update a variable in the workspace storage.',
|
|
63
|
+
inputSchema: z.object({
|
|
64
|
+
key: z.string().describe('The key of the variable.'),
|
|
65
|
+
value: z.string().describe('The value of the variable.'),
|
|
66
|
+
secret: z.boolean().optional().describe('Whether the variable is a secret.'),
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
delete_variable: {
|
|
70
|
+
description: 'Delete a variable from the workspace storage.',
|
|
71
|
+
inputSchema: z.object({
|
|
72
|
+
key: z.string().describe('The key of the variable to delete.'),
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
export const storageToolsPlugin = {
|
|
77
|
+
id: 'storage-tools',
|
|
78
|
+
name: 'Storage Tools',
|
|
79
|
+
description: 'Tools for creating channels, patching state, and managing workspace variables.',
|
|
80
|
+
toolDefinitions: storageToolDefinitions,
|
|
81
|
+
factory: () => () => {
|
|
82
|
+
// Handlers live in bus/services.ts; this plugin only contributes tool definitions.
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
export default storageToolsPlugin;
|
package/dist/plugins/storage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { storageService } from '../services/storage.js';
|
|
2
|
+
import { pluginService } from '../services/plugins.js';
|
|
2
3
|
import z from 'zod';
|
|
3
4
|
export const storageToolDefinitions = {
|
|
4
5
|
create_channel: {
|
|
@@ -56,6 +57,20 @@ export const storageToolDefinitions = {
|
|
|
56
57
|
message: 'Provide at least one of state or spec.',
|
|
57
58
|
}),
|
|
58
59
|
},
|
|
60
|
+
create_variable: {
|
|
61
|
+
description: 'Create or update a variable in the workspace storage.',
|
|
62
|
+
inputSchema: z.object({
|
|
63
|
+
key: z.string().describe('The key of the variable.'),
|
|
64
|
+
value: z.string().describe('The value of the variable.'),
|
|
65
|
+
secret: z.boolean().optional().describe('Whether the variable is a secret.'),
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
delete_variable: {
|
|
69
|
+
description: 'Delete a variable from the workspace storage.',
|
|
70
|
+
inputSchema: z.object({
|
|
71
|
+
key: z.string().describe('The key of the variable to delete.'),
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
59
74
|
};
|
|
60
75
|
export const storagePlugin = (options) => (builder) => {
|
|
61
76
|
const { storage } = options;
|
|
@@ -174,6 +189,16 @@ export const storagePlugin = (options) => (builder) => {
|
|
|
174
189
|
data: { channelDetails },
|
|
175
190
|
};
|
|
176
191
|
});
|
|
192
|
+
builder.on('action:storage:get-thread-details', async function* (_, state) {
|
|
193
|
+
const threadId = state.state.threadId;
|
|
194
|
+
const threadDetails = threadId
|
|
195
|
+
? await storage.getThreadDetails({ channelId: state.state.channelId, threadId })
|
|
196
|
+
: null;
|
|
197
|
+
yield {
|
|
198
|
+
type: 'action:storage:get-thread-details-result',
|
|
199
|
+
data: { threadDetails },
|
|
200
|
+
};
|
|
201
|
+
});
|
|
177
202
|
builder.on('action:storage:get-agents', async function* () {
|
|
178
203
|
const agents = await storage.getAgents();
|
|
179
204
|
yield {
|
|
@@ -189,11 +214,94 @@ export const storagePlugin = (options) => (builder) => {
|
|
|
189
214
|
};
|
|
190
215
|
});
|
|
191
216
|
builder.on('action:storage:get-agent-details', async function* (event, state) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
217
|
+
try {
|
|
218
|
+
const agentDetails = await storage.getAgentDetails({ agentId: event.data.agentId });
|
|
219
|
+
yield {
|
|
220
|
+
type: 'action:storage:get-agent-details-result',
|
|
221
|
+
data: { agentDetails },
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
console.error(`[storage] Failed to get agent details for ${event.data.agentId}`, error);
|
|
226
|
+
yield {
|
|
227
|
+
type: 'action:storage:get-agent-details-result',
|
|
228
|
+
data: {
|
|
229
|
+
agentDetails: null,
|
|
230
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
builder.on('action:storage:create-agent', async function* (event) {
|
|
236
|
+
try {
|
|
237
|
+
const { agentId, name, description, instructions, plugins, runtime } = event.data;
|
|
238
|
+
await storage.createAgent({
|
|
239
|
+
agentId,
|
|
240
|
+
name,
|
|
241
|
+
description,
|
|
242
|
+
instructions,
|
|
243
|
+
plugins,
|
|
244
|
+
runtime,
|
|
245
|
+
});
|
|
246
|
+
yield {
|
|
247
|
+
type: 'action:storage:create-agent-result',
|
|
248
|
+
data: { success: true },
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
yield {
|
|
253
|
+
type: 'action:storage:create-agent-result',
|
|
254
|
+
data: {
|
|
255
|
+
success: false,
|
|
256
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
builder.on('action:storage:update-agent', async function* (event) {
|
|
262
|
+
try {
|
|
263
|
+
const { agentId, name, description, instructions, plugins, runtime } = event.data;
|
|
264
|
+
await storage.updateAgent({
|
|
265
|
+
agentId,
|
|
266
|
+
name,
|
|
267
|
+
description,
|
|
268
|
+
instructions,
|
|
269
|
+
plugins,
|
|
270
|
+
runtime,
|
|
271
|
+
});
|
|
272
|
+
yield {
|
|
273
|
+
type: 'action:storage:update-agent-result',
|
|
274
|
+
data: { success: true },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
yield {
|
|
279
|
+
type: 'action:storage:update-agent-result',
|
|
280
|
+
data: {
|
|
281
|
+
success: false,
|
|
282
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
builder.on('action:storage:delete-agent', async function* (event) {
|
|
288
|
+
try {
|
|
289
|
+
const { agentId } = event.data;
|
|
290
|
+
await storage.deleteAgent({ agentId });
|
|
291
|
+
yield {
|
|
292
|
+
type: 'action:storage:delete-agent-result',
|
|
293
|
+
data: { success: true },
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
yield {
|
|
298
|
+
type: 'action:storage:delete-agent-result',
|
|
299
|
+
data: {
|
|
300
|
+
success: false,
|
|
301
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
197
305
|
});
|
|
198
306
|
builder.on('action:storage:get-events', async function* (_, state) {
|
|
199
307
|
const events = await storage.getEvents(state.state);
|
|
@@ -229,6 +337,44 @@ export const storagePlugin = (options) => (builder) => {
|
|
|
229
337
|
data: { variables: maskedVariables },
|
|
230
338
|
};
|
|
231
339
|
});
|
|
340
|
+
builder.on('action:storage:create-variable', async function* (event) {
|
|
341
|
+
try {
|
|
342
|
+
const { key, value, secret } = event.data;
|
|
343
|
+
await storage.createVariable({ key, value, secret });
|
|
344
|
+
yield {
|
|
345
|
+
type: 'action:storage:create-variable-result',
|
|
346
|
+
data: { success: true },
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
yield {
|
|
351
|
+
type: 'action:storage:create-variable-result',
|
|
352
|
+
data: {
|
|
353
|
+
success: false,
|
|
354
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
builder.on('action:storage:delete-variable', async function* (event) {
|
|
360
|
+
try {
|
|
361
|
+
const { key } = event.data;
|
|
362
|
+
await storage.deleteVariable({ key });
|
|
363
|
+
yield {
|
|
364
|
+
type: 'action:storage:delete-variable-result',
|
|
365
|
+
data: { success: true },
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
yield {
|
|
370
|
+
type: 'action:storage:delete-variable-result',
|
|
371
|
+
data: {
|
|
372
|
+
success: false,
|
|
373
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
});
|
|
232
378
|
builder.on('action:storage:patch-channel-state', async function* (event, state) {
|
|
233
379
|
try {
|
|
234
380
|
await storage.patchChannelState({
|
|
@@ -493,6 +639,95 @@ export const storagePlugin = (options) => (builder) => {
|
|
|
493
639
|
};
|
|
494
640
|
}
|
|
495
641
|
});
|
|
642
|
+
builder.on('action:plugin:install', async function* (event) {
|
|
643
|
+
try {
|
|
644
|
+
const { name, version } = event.data;
|
|
645
|
+
const result = await pluginService.installPlugin({ packageName: name, version });
|
|
646
|
+
yield {
|
|
647
|
+
type: 'action:plugin:install:result',
|
|
648
|
+
data: { success: true, plugin: result },
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
yield {
|
|
653
|
+
type: 'action:plugin:install:result',
|
|
654
|
+
data: { success: false, error: error.message },
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
builder.on('action:plugin:uninstall', async function* (event) {
|
|
659
|
+
try {
|
|
660
|
+
const { id } = event.data;
|
|
661
|
+
await pluginService.uninstallPlugin(id);
|
|
662
|
+
yield {
|
|
663
|
+
type: 'action:plugin:uninstall:result',
|
|
664
|
+
data: { success: true },
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
yield {
|
|
669
|
+
type: 'action:plugin:uninstall:result',
|
|
670
|
+
data: { success: false, error: error.message },
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
builder.on('action:marketplace:list', async function* () {
|
|
675
|
+
// Mock marketplace agents for MVP
|
|
676
|
+
const agents = [
|
|
677
|
+
{
|
|
678
|
+
id: 'researcher',
|
|
679
|
+
name: 'Researcher',
|
|
680
|
+
description: 'Specialized in web research and information synthesis.',
|
|
681
|
+
// image: 'https://registry.openbot.local/agents/researcher/icon.svg',
|
|
682
|
+
instructions: 'You are a research assistant. Use available tools to find information.',
|
|
683
|
+
runtime: { name: 'ai-sdk', config: { model: 'openai/gpt-4o' } },
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
id: 'coder',
|
|
687
|
+
name: 'Coder',
|
|
688
|
+
description: 'Expert in multiple programming languages and software architecture.',
|
|
689
|
+
// image: 'https://registry.openbot.local/agents/coder/icon.svg',
|
|
690
|
+
instructions: 'You are an expert software engineer. Help the user with coding tasks.',
|
|
691
|
+
runtime: { name: 'ai-sdk', config: { model: 'openai/gpt-4o' } },
|
|
692
|
+
},
|
|
693
|
+
];
|
|
694
|
+
yield {
|
|
695
|
+
type: 'action:marketplace:list:result',
|
|
696
|
+
data: { success: true, agents },
|
|
697
|
+
};
|
|
698
|
+
});
|
|
699
|
+
builder.on('action:agent:install', async function* (event) {
|
|
700
|
+
try {
|
|
701
|
+
const { agentId, name, description, instructions, runtime, plugins } = event.data;
|
|
702
|
+
await storage.createAgent({
|
|
703
|
+
agentId,
|
|
704
|
+
name,
|
|
705
|
+
description,
|
|
706
|
+
instructions,
|
|
707
|
+
runtime,
|
|
708
|
+
plugins,
|
|
709
|
+
});
|
|
710
|
+
yield {
|
|
711
|
+
type: 'action:agent:install:result',
|
|
712
|
+
data: { success: true, agentId },
|
|
713
|
+
};
|
|
714
|
+
yield {
|
|
715
|
+
type: 'agent:output',
|
|
716
|
+
data: { content: `Successfully installed agent **${name}** (${agentId}) from marketplace.` },
|
|
717
|
+
meta: { agentId: 'system' },
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
yield {
|
|
722
|
+
type: 'action:agent:install:result',
|
|
723
|
+
data: {
|
|
724
|
+
success: false,
|
|
725
|
+
agentId: event.data.agentId,
|
|
726
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
});
|
|
496
731
|
};
|
|
497
732
|
export const plugin = {
|
|
498
733
|
name: 'storage',
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
const actionSchema = z.object({
|
|
3
|
+
id: z.string().describe('Stable action ID returned by client:ui:widget:response.'),
|
|
4
|
+
label: z.string().describe('Human-readable button label.'),
|
|
5
|
+
value: z.unknown().optional().describe('Optional machine-readable value for this action.'),
|
|
6
|
+
variant: z.enum(['primary', 'secondary', 'danger']).optional(),
|
|
7
|
+
disabled: z.boolean().optional(),
|
|
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
|
+
};
|
|
177
|
+
export const uiPlugin = {
|
|
178
|
+
id: 'ui',
|
|
179
|
+
name: 'UI Widgets',
|
|
180
|
+
description: 'Render server-driven UI widgets (messages, choices, forms, lists) in the conversation.',
|
|
181
|
+
toolDefinitions: uiToolDefinitions,
|
|
182
|
+
factory: () => uiPluginRuntime(),
|
|
183
|
+
};
|
|
184
|
+
export default uiPlugin;
|