openbot 0.2.14 → 0.3.0
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 +0 -0
- 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 +600 -0
- package/dist/bus/types.js +1 -0
- package/dist/harness/context.js +131 -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 +330 -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/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 +91 -50
- package/dist/services/agent-packages.js +103 -0
- package/dist/services/plugins.js +98 -0
- package/dist/services/storage.js +360 -94
- package/docs/agents.md +39 -66
- 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 +385 -16
- package/src/assets/icon.svg +4 -1
- package/src/bus/plugin.ts +67 -0
- package/src/bus/services.ts +666 -0
- package/src/bus/types.ts +147 -0
- package/src/harness/context.ts +160 -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 +410 -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/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 +106 -55
- package/src/services/plugins.ts +133 -0
- package/src/services/storage.ts +465 -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,99 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { mcpService } from '../../../harness/mcp.js';
|
|
3
|
+
function stringifyResult(value) {
|
|
4
|
+
if (typeof value === 'string')
|
|
5
|
+
return value;
|
|
6
|
+
try {
|
|
7
|
+
return JSON.stringify(value, null, 2);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return String(value);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export const mcpToolDefinitions = {
|
|
14
|
+
mcp_list_tools: {
|
|
15
|
+
description: 'List available tools from a configured MCP server. Use this first before calling tools on an unknown server.',
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
serverId: z.string().describe('Configured MCP server id (e.g. github, notion, linear).'),
|
|
18
|
+
}),
|
|
19
|
+
},
|
|
20
|
+
mcp_call: {
|
|
21
|
+
description: 'Call a tool on a configured MCP server. Provide tool arguments as a JSON object. Use mcp_list_tools first when uncertain.',
|
|
22
|
+
inputSchema: z.object({
|
|
23
|
+
serverId: z.string().describe('Configured MCP server id.'),
|
|
24
|
+
toolName: z.string().describe('Exact MCP tool name from mcp_list_tools.'),
|
|
25
|
+
args: z
|
|
26
|
+
.record(z.string(), z.unknown())
|
|
27
|
+
.default({})
|
|
28
|
+
.describe('Tool arguments as a JSON object.'),
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export const mcpPlugin = () => (builder) => {
|
|
33
|
+
builder.on('action:mcp_list_tools', async function* (event, context) {
|
|
34
|
+
const serverId = event.data?.serverId;
|
|
35
|
+
try {
|
|
36
|
+
const tools = await mcpService.listTools(serverId);
|
|
37
|
+
const toolNames = tools.map((tool) => `- ${tool.name}${tool.description ? `: ${tool.description}` : ''}`);
|
|
38
|
+
yield {
|
|
39
|
+
type: 'action:mcp_list_tools:result',
|
|
40
|
+
data: { success: true, serverId, tools },
|
|
41
|
+
meta: event.meta,
|
|
42
|
+
};
|
|
43
|
+
yield {
|
|
44
|
+
type: 'agent:output',
|
|
45
|
+
data: {
|
|
46
|
+
content: toolNames.length > 0
|
|
47
|
+
? `MCP tools available on \`${serverId}\`:\n${toolNames.join('\n')}`
|
|
48
|
+
: `MCP server \`${serverId}\` has no tools.`,
|
|
49
|
+
},
|
|
50
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const message = error instanceof Error ? error.message : 'Unknown MCP error';
|
|
55
|
+
yield {
|
|
56
|
+
type: 'action:mcp_list_tools:result',
|
|
57
|
+
data: { success: false, serverId, tools: [], error: message },
|
|
58
|
+
meta: event.meta,
|
|
59
|
+
};
|
|
60
|
+
yield {
|
|
61
|
+
type: 'agent:output',
|
|
62
|
+
data: { content: `Failed to list MCP tools for \`${serverId}\`: ${message}` },
|
|
63
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
builder.on('action:mcp_call', async function* (event, context) {
|
|
68
|
+
const serverId = event.data?.serverId;
|
|
69
|
+
const toolName = event.data?.toolName;
|
|
70
|
+
const args = (event.data?.args || {});
|
|
71
|
+
try {
|
|
72
|
+
const result = await mcpService.callTool(serverId, toolName, args);
|
|
73
|
+
const rendered = stringifyResult(result);
|
|
74
|
+
yield {
|
|
75
|
+
type: 'action:mcp_call:result',
|
|
76
|
+
data: { success: true, serverId, toolName, result },
|
|
77
|
+
meta: event.meta,
|
|
78
|
+
};
|
|
79
|
+
yield {
|
|
80
|
+
type: 'agent:output',
|
|
81
|
+
data: { content: `MCP \`${serverId}.${toolName}\` result:\n\n${rendered}` },
|
|
82
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : 'Unknown MCP error';
|
|
87
|
+
yield {
|
|
88
|
+
type: 'action:mcp_call:result',
|
|
89
|
+
data: { success: false, serverId, toolName, error: message },
|
|
90
|
+
meta: event.meta,
|
|
91
|
+
};
|
|
92
|
+
yield {
|
|
93
|
+
type: 'agent:output',
|
|
94
|
+
data: { content: `MCP call failed for \`${serverId}.${toolName}\`: ${message}` },
|
|
95
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Working directory. Defaults to the channel cwd or workspace root. Leave empty unless the user requests a specific directory.'),
|
|
12
|
+
shell: z.enum(['bash', 'sh', 'zsh']).optional().describe('Shell to use. Defaults to bash.'),
|
|
13
|
+
timeoutMs: z
|
|
14
|
+
.number()
|
|
15
|
+
.optional()
|
|
16
|
+
.default(30000)
|
|
17
|
+
.describe('Maximum execution time in milliseconds. Defaults to 30000 (30s).'),
|
|
18
|
+
}),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
export const shellPlugin = () => (builder) => {
|
|
22
|
+
builder.on('action:shell_exec', async function* (event, context) {
|
|
23
|
+
const { command, cwd, shell = 'bash', timeoutMs = 30000 } = event.data;
|
|
24
|
+
const actualTimeout = Math.max(1000, Math.min(timeoutMs, 60000));
|
|
25
|
+
const actualCwd = cwd || context.state.channelDetails?.cwd || process.cwd();
|
|
26
|
+
try {
|
|
27
|
+
const result = await new Promise((resolve) => {
|
|
28
|
+
const child = spawn(command, {
|
|
29
|
+
shell,
|
|
30
|
+
cwd: actualCwd,
|
|
31
|
+
env: { ...process.env },
|
|
32
|
+
});
|
|
33
|
+
let stdout = '';
|
|
34
|
+
let stderr = '';
|
|
35
|
+
let timedOut = false;
|
|
36
|
+
const timer = setTimeout(() => {
|
|
37
|
+
timedOut = true;
|
|
38
|
+
child.kill();
|
|
39
|
+
}, actualTimeout);
|
|
40
|
+
child.stdout.on('data', (data) => {
|
|
41
|
+
stdout += data.toString();
|
|
42
|
+
if (stdout.length > 100000) {
|
|
43
|
+
stdout = stdout.substring(0, 100000) + '\n... [output truncated]';
|
|
44
|
+
child.kill();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
child.stderr.on('data', (data) => {
|
|
48
|
+
stderr += data.toString();
|
|
49
|
+
if (stderr.length > 100000) {
|
|
50
|
+
stderr = stderr.substring(0, 100000) + '\n... [output truncated]';
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
child.on('close', (code) => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
resolve({ exitCode: code, stdout, stderr, timedOut });
|
|
56
|
+
});
|
|
57
|
+
child.on('error', (err) => {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
resolve({ exitCode: -1, stdout, stderr: stderr + err.message, timedOut: false });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
const success = result.exitCode === 0 && !result.timedOut;
|
|
63
|
+
yield {
|
|
64
|
+
type: 'action:shell_exec:result',
|
|
65
|
+
data: {
|
|
66
|
+
success,
|
|
67
|
+
exitCode: result.exitCode,
|
|
68
|
+
stdout: result.stdout,
|
|
69
|
+
stderr: result.stderr,
|
|
70
|
+
timedOut: result.timedOut,
|
|
71
|
+
},
|
|
72
|
+
meta: event.meta,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : 'Unknown shell error';
|
|
77
|
+
yield {
|
|
78
|
+
type: 'action:shell_exec:result',
|
|
79
|
+
data: {
|
|
80
|
+
success: false,
|
|
81
|
+
exitCode: -1,
|
|
82
|
+
stdout: '',
|
|
83
|
+
stderr: message,
|
|
84
|
+
timedOut: false,
|
|
85
|
+
error: message,
|
|
86
|
+
},
|
|
87
|
+
meta: event.meta,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Tool schemas the OpenBot orchestrator agent exposes to its LLM for talking to
|
|
4
|
+
* the bus's storage service. The actual handlers live in `src/bus/services.ts`
|
|
5
|
+
* since storage is platform infrastructure, not agent behaviour.
|
|
6
|
+
*/
|
|
7
|
+
export 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
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
export 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
|
+
export const uiPlugin = () => (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
|
+
};
|
package/dist/agents/system.js
CHANGED
|
@@ -1,106 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return undefined;
|
|
12
|
-
return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf-8').toString('base64')}`;
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
})();
|
|
1
|
+
import { openBotAgentPackage } from './openbot/index.js';
|
|
2
|
+
import { OPENBOT_SYSTEM_PROMPT } from './openbot/system-prompt.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default Agent record for the first-party OpenBot orchestrator.
|
|
5
|
+
*
|
|
6
|
+
* The OpenBot agent is a regular peer on the bus — it just happens to ship
|
|
7
|
+
* by default and to use the first-party `openbot` AgentPackage. Users (and the
|
|
8
|
+
* bus's storage layer) can override its instructions/config like any other agent.
|
|
9
|
+
*/
|
|
10
|
+
export const SYSTEM_AGENT_ID = 'system';
|
|
18
11
|
export const getSystemAgentDetails = (overrides) => {
|
|
19
12
|
const defaults = {
|
|
20
|
-
id:
|
|
21
|
-
name:
|
|
22
|
-
image:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
'3. **Channels**: Channels are shared spaces where multiple agents can participate. You can create new channels for different topics.\n' +
|
|
28
|
-
'4. **Local-First**: OpenBot runs entirely on your machine. Your data stays private and local.\n\n' +
|
|
29
|
-
'### Workflow Guidelines:\n' +
|
|
30
|
-
'- **Todo Schema**: Keep todo items simple. Each item should have a short `id`, a clear `task` description, and a `status` (e.g., "pending", "in_progress", "done").\n' +
|
|
31
|
-
'- **Interactive Widgets**: Use `render_ui_widget` to give the user a visual progress bar (`kind: "todo_list"`), request permissions (`kind: "approval"`), or gather structured data (`kind: "form"`).\n' +
|
|
32
|
-
'- **Delegation**: When delegating to another agent, reference the relevant Task ID from the thread state. Update the task status (e.g., using `patch_thread_details`) as progress is made.\n\n' +
|
|
33
|
-
'If you need to know what agents or plugins are installed, I can help you find that information.',
|
|
34
|
-
runtime: {
|
|
35
|
-
name: 'ai-sdk',
|
|
36
|
-
config: {
|
|
37
|
-
model: 'openai/gpt-4o-mini',
|
|
38
|
-
toolDefinitions: {
|
|
39
|
-
...delegationToolDefinitions,
|
|
40
|
-
...storageToolDefinitions,
|
|
41
|
-
...mcpToolDefinitions,
|
|
42
|
-
...uiToolDefinitions,
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
plugins: [
|
|
47
|
-
{ name: 'storage', config: { storage: storageService } },
|
|
48
|
-
{ name: 'delegation', config: {} },
|
|
49
|
-
{ name: 'mcp', config: {} },
|
|
50
|
-
{ name: 'ui', config: {} },
|
|
51
|
-
],
|
|
52
|
-
description: 'System coordinator agent',
|
|
13
|
+
id: SYSTEM_AGENT_ID,
|
|
14
|
+
name: openBotAgentPackage.name,
|
|
15
|
+
image: openBotAgentPackage.image,
|
|
16
|
+
description: openBotAgentPackage.description,
|
|
17
|
+
instructions: OPENBOT_SYSTEM_PROMPT,
|
|
18
|
+
packageId: openBotAgentPackage.id,
|
|
19
|
+
config: { model: 'openai/gpt-5.4-nano' },
|
|
53
20
|
createdAt: new Date(),
|
|
54
21
|
updatedAt: new Date(),
|
|
55
22
|
};
|
|
56
23
|
if (!overrides)
|
|
57
24
|
return defaults;
|
|
58
|
-
// Merge logic:
|
|
59
|
-
// - Simple fields: override if present
|
|
60
|
-
// - Runtime: merge config/model, but preserve toolDefinitions unless explicitly overridden
|
|
61
|
-
// - Plugins: merge by name (user plugins override defaults)
|
|
62
|
-
const mergedRuntime = {
|
|
63
|
-
...(typeof defaults.runtime === 'object' ? defaults.runtime : {}),
|
|
64
|
-
...(typeof overrides.runtime === 'object' ? overrides.runtime : {}),
|
|
65
|
-
name: (typeof overrides.runtime === 'object' ? overrides.runtime.name : overrides.runtime) ||
|
|
66
|
-
(typeof defaults.runtime === 'object' ? defaults.runtime.name : defaults.runtime) ||
|
|
67
|
-
'ai-sdk',
|
|
68
|
-
config: {
|
|
69
|
-
...(defaults.runtime && typeof defaults.runtime !== 'string' ? defaults.runtime.config : {}),
|
|
70
|
-
...(overrides.runtime && typeof overrides.runtime !== 'string' ? overrides.runtime.config : {}),
|
|
71
|
-
toolDefinitions: {
|
|
72
|
-
...(defaults.runtime && typeof defaults.runtime !== 'string'
|
|
73
|
-
? defaults.runtime.config?.toolDefinitions
|
|
74
|
-
: {}),
|
|
75
|
-
...(overrides.runtime && typeof overrides.runtime !== 'string'
|
|
76
|
-
? overrides.runtime.config?.toolDefinitions
|
|
77
|
-
: {}),
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
const mergedPlugins = [...(defaults.plugins || [])];
|
|
82
|
-
if (overrides.plugins) {
|
|
83
|
-
for (const p of overrides.plugins) {
|
|
84
|
-
const name = typeof p === 'string' ? p : p.name;
|
|
85
|
-
const index = mergedPlugins.findIndex((existing) => {
|
|
86
|
-
const existingName = typeof existing === 'string' ? existing : existing.name;
|
|
87
|
-
return existingName === name;
|
|
88
|
-
});
|
|
89
|
-
if (index !== -1) {
|
|
90
|
-
mergedPlugins[index] = p;
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
mergedPlugins.push(p);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
25
|
return {
|
|
98
26
|
...defaults,
|
|
99
27
|
...overrides,
|
|
100
|
-
id:
|
|
101
|
-
image: overrides.image || defaults.image,
|
|
102
|
-
|
|
103
|
-
plugins: mergedPlugins,
|
|
28
|
+
id: SYSTEM_AGENT_ID,
|
|
29
|
+
image: overrides.image || defaults.image,
|
|
30
|
+
config: { ...(defaults.config || {}), ...(overrides.config || {}) },
|
|
104
31
|
updatedAt: new Date(),
|
|
105
32
|
};
|
|
106
33
|
};
|
package/dist/app/cli.js
CHANGED
|
File without changes
|
package/dist/app/config.js
CHANGED
|
@@ -7,6 +7,8 @@ export const DEFAULT_AGENTS_DIR = 'agents';
|
|
|
7
7
|
export const DEFAULT_CHANNELS_DIR = 'channels';
|
|
8
8
|
export const CONFIG_FILE = 'config.json';
|
|
9
9
|
export const VARIABLES_FILE = 'variables.json';
|
|
10
|
+
/** Public agent registry used when `marketplaceRegistryUrl` is not set. */
|
|
11
|
+
export const DEFAULT_MARKETPLACE_REGISTRY_URL = 'https://raw.githubusercontent.com/meetopenbot/openbot-registry/main/registry.json';
|
|
10
12
|
export function resolvePath(p) {
|
|
11
13
|
return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : path.resolve(p);
|
|
12
14
|
}
|
|
@@ -37,7 +39,8 @@ export function isConfigured() {
|
|
|
37
39
|
return fs.existsSync(configPath);
|
|
38
40
|
}
|
|
39
41
|
export function loadVariables() {
|
|
40
|
-
const
|
|
42
|
+
const config = loadConfig();
|
|
43
|
+
const variablesPath = path.join(resolvePath(config.baseDir || DEFAULT_BASE_DIR), VARIABLES_FILE);
|
|
41
44
|
if (fs.existsSync(variablesPath)) {
|
|
42
45
|
return JSON.parse(fs.readFileSync(variablesPath, 'utf-8'));
|
|
43
46
|
}
|
package/dist/app/server.js
CHANGED
|
@@ -4,8 +4,11 @@ import cors from 'cors';
|
|
|
4
4
|
import z from 'zod';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import fs from 'fs/promises';
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const pkg = require('../../package.json');
|
|
7
10
|
import { generateId } from 'melony';
|
|
8
|
-
import { DEFAULT_BASE_DIR, loadConfig,
|
|
11
|
+
import { DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
|
|
9
12
|
import { processService } from '../harness/process.js';
|
|
10
13
|
import { storageService } from '../services/storage.js';
|
|
11
14
|
import { AgentHarness } from '../harness/agent-harness.js';
|
|
@@ -21,8 +24,7 @@ export async function startServer(options = {}) {
|
|
|
21
24
|
})
|
|
22
25
|
.passthrough();
|
|
23
26
|
const config = loadConfig();
|
|
24
|
-
|
|
25
|
-
processService.applyVariablesToProcessEnv(variables.variables);
|
|
27
|
+
processService.syncWorkspaceVariablesToProcessEnv();
|
|
26
28
|
const baseDir = config.baseDir || DEFAULT_BASE_DIR;
|
|
27
29
|
const openBotDir = resolvePath(baseDir);
|
|
28
30
|
const PORT = Number(options.port ?? config.port ?? process.env.PORT ?? 4132);
|
|
@@ -55,6 +57,7 @@ export async function startServer(options = {}) {
|
|
|
55
57
|
};
|
|
56
58
|
};
|
|
57
59
|
const getClientKey = (channelId, threadId) => threadId ? `${channelId}:${threadId}` : channelId;
|
|
60
|
+
const getRunKey = (runId, agentId, channelId, threadId) => `${runId}:${agentId}:${channelId}:${threadId || ''}`;
|
|
58
61
|
const sendToClientKey = (clientKey, chunk) => {
|
|
59
62
|
const threadClients = clients.get(clientKey);
|
|
60
63
|
if (!threadClients)
|
|
@@ -89,6 +92,9 @@ export async function startServer(options = {}) {
|
|
|
89
92
|
};
|
|
90
93
|
app.use(cors());
|
|
91
94
|
app.use(express.json({ limit: '20mb' }));
|
|
95
|
+
app.get('/api/health', (req, res) => {
|
|
96
|
+
res.json({ status: 'ok', version: pkg.version });
|
|
97
|
+
});
|
|
92
98
|
app.get('/api/events', (req, res) => {
|
|
93
99
|
const { channelId, threadId } = getContext(req);
|
|
94
100
|
const clientKey = getClientKey(channelId, threadId);
|
|
@@ -156,7 +162,7 @@ export async function startServer(options = {}) {
|
|
|
156
162
|
const targetThreadId = state?.threadId || threadId;
|
|
157
163
|
const targetClientKey = getClientKey(targetChannelId, targetThreadId);
|
|
158
164
|
if (chunk.type === 'agent:run:start') {
|
|
159
|
-
activeRuns.set(chunk.data.runId, {
|
|
165
|
+
activeRuns.set(getRunKey(chunk.data.runId, chunk.data.agentId, chunk.data.channelId, chunk.data.threadId), {
|
|
160
166
|
runId: chunk.data.runId,
|
|
161
167
|
channelId: chunk.data.channelId,
|
|
162
168
|
threadId: chunk.data.threadId,
|
|
@@ -164,7 +170,7 @@ export async function startServer(options = {}) {
|
|
|
164
170
|
});
|
|
165
171
|
}
|
|
166
172
|
else if (chunk.type === 'agent:run:end') {
|
|
167
|
-
activeRuns.delete(chunk.data.runId);
|
|
173
|
+
activeRuns.delete(getRunKey(chunk.data.runId, chunk.data.agentId, chunk.data.channelId, chunk.data.threadId));
|
|
168
174
|
}
|
|
169
175
|
await storageService.storeEvent({
|
|
170
176
|
channelId: targetChannelId,
|
|
@@ -230,8 +236,9 @@ export async function startServer(options = {}) {
|
|
|
230
236
|
});
|
|
231
237
|
app.listen(PORT, () => {
|
|
232
238
|
console.log(`\x1b[32mOpenBot server listening at http://localhost:${PORT}\x1b[0m`);
|
|
233
|
-
console.log(` -
|
|
234
|
-
console.log(` -
|
|
235
|
-
console.log(` -
|
|
239
|
+
console.log(` - Health endpoint: GET /health`);
|
|
240
|
+
console.log(` - Events endpoint: GET /api/events (SSE)`);
|
|
241
|
+
console.log(` - Publish endpoint: POST /api/publish`);
|
|
242
|
+
console.log(` - State endpoint: GET /api/state`);
|
|
236
243
|
});
|
|
237
244
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|