openbot 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +10 -0
- package/dist/app/server.js +200 -3
- package/dist/harness/index.js +18 -0
- package/dist/plugins/approval/index.js +35 -20
- package/dist/plugins/bash/index.js +195 -0
- package/dist/plugins/delegation/index.js +4 -2
- package/dist/plugins/openbot/context.js +54 -9
- package/dist/plugins/openbot/history.js +47 -1
- package/dist/plugins/openbot/index.js +43 -3
- package/dist/plugins/openbot/runtime.js +91 -27
- package/dist/plugins/openbot/system-prompt.js +21 -1
- package/dist/plugins/plugin-manager/index.js +87 -3
- package/dist/plugins/shell/index.js +2 -1
- package/dist/plugins/storage/files.js +67 -0
- package/dist/plugins/storage/index.js +184 -7
- package/dist/plugins/storage/service.js +201 -44
- package/dist/plugins/ui/index.js +109 -150
- package/dist/services/abort.js +43 -0
- package/dist/services/plugins/registry.js +5 -3
- package/dist/services/plugins/service.js +66 -11
- package/docs/agents.md +5 -8
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +28 -7
- package/docs/templates/AGENT.example.md +4 -4
- package/package.json +1 -1
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +13 -0
- package/src/app/server.ts +235 -3
- package/src/app/types.ts +284 -14
- package/src/harness/index.ts +21 -0
- package/src/plugins/approval/index.ts +37 -20
- package/src/plugins/bash/index.ts +232 -0
- package/src/plugins/delegation/index.ts +5 -2
- package/src/plugins/openbot/context.ts +58 -9
- package/src/plugins/openbot/history.ts +52 -1
- package/src/plugins/openbot/index.ts +45 -3
- package/src/plugins/openbot/runtime.ts +121 -27
- package/src/plugins/openbot/system-prompt.ts +21 -1
- package/src/plugins/plugin-manager/index.ts +105 -3
- package/src/plugins/storage/files.ts +81 -0
- package/src/plugins/storage/index.ts +198 -8
- package/src/plugins/storage/service.ts +267 -44
- package/src/plugins/ui/index.ts +123 -0
- package/src/services/abort.ts +46 -0
- package/src/services/plugins/domain.ts +34 -1
- package/src/services/plugins/registry.ts +5 -3
- package/src/services/plugins/service.ts +136 -45
- package/src/services/plugins/types.ts +5 -1
- package/src/plugins/shell/index.ts +0 -123
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { MelonyPlugin } from 'melony';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spawn, ChildProcess } from 'node:child_process';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import type { Plugin } from '../../services/plugins/types.js';
|
|
6
|
+
import { OpenBotEvent, OpenBotState } from '../../app/types.js';
|
|
7
|
+
import { resolvePath } from '../../app/config.js';
|
|
8
|
+
|
|
9
|
+
const bashToolDefinitions = {
|
|
10
|
+
bash: {
|
|
11
|
+
description:
|
|
12
|
+
'Execute a bash command in a stateful session. The working directory and environment variables persist between calls. Use this for all system tasks, file operations, and running development servers.',
|
|
13
|
+
inputSchema: z.object({
|
|
14
|
+
command: z.string().describe('The bash command to execute.'),
|
|
15
|
+
restart: z
|
|
16
|
+
.boolean()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Restart the bash session before running the command.'),
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
bash_stop: {
|
|
22
|
+
description: 'Stop the bash session for the current or specified channel.',
|
|
23
|
+
inputSchema: z.object({
|
|
24
|
+
channelId: z.string().optional().describe('The channel ID to stop the session for.'),
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
bash_list_sessions: {
|
|
28
|
+
description: 'List all active bash sessions.',
|
|
29
|
+
inputSchema: z.object({}),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
interface BashSession {
|
|
34
|
+
process: ChildProcess;
|
|
35
|
+
cwd: string;
|
|
36
|
+
lastActivity: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const sessions = new Map<string, BashSession>();
|
|
40
|
+
|
|
41
|
+
const getSession = (channelId: string, initialCwd: string): BashSession => {
|
|
42
|
+
let session = sessions.get(channelId);
|
|
43
|
+
if (!session) {
|
|
44
|
+
const childProcess = spawn('bash', ['--login'], {
|
|
45
|
+
cwd: initialCwd,
|
|
46
|
+
env: { ...process.env, PS1: '' },
|
|
47
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
session = {
|
|
51
|
+
process: childProcess,
|
|
52
|
+
cwd: initialCwd,
|
|
53
|
+
lastActivity: Date.now(),
|
|
54
|
+
};
|
|
55
|
+
sessions.set(channelId, session);
|
|
56
|
+
|
|
57
|
+
// Basic error handling for the process
|
|
58
|
+
childProcess.on('error', (err: Error) => {
|
|
59
|
+
console.error(`[bash] Session error for channel ${channelId}:`, err);
|
|
60
|
+
sessions.delete(channelId);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
childProcess.on('exit', () => {
|
|
64
|
+
sessions.delete(channelId);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return session;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const bashPluginRuntime = (): MelonyPlugin<OpenBotState, OpenBotEvent> => (builder) => {
|
|
71
|
+
builder.on('action:bash', async function* (event, context) {
|
|
72
|
+
const { command, restart } = event.data;
|
|
73
|
+
const channelId = context.state.channelId;
|
|
74
|
+
const initialCwd = resolvePath(context.state.channelDetails?.cwd || process.cwd());
|
|
75
|
+
|
|
76
|
+
if (restart) {
|
|
77
|
+
const oldSession = sessions.get(channelId);
|
|
78
|
+
if (oldSession) {
|
|
79
|
+
oldSession.process.kill();
|
|
80
|
+
sessions.delete(channelId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const session = getSession(channelId, initialCwd);
|
|
85
|
+
session.lastActivity = Date.now();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const result = await new Promise<{
|
|
89
|
+
exitCode: number | null;
|
|
90
|
+
stdout: string;
|
|
91
|
+
stderr: string;
|
|
92
|
+
timedOut: boolean;
|
|
93
|
+
}>((resolve) => {
|
|
94
|
+
let stdout = '';
|
|
95
|
+
let stderr = '';
|
|
96
|
+
let timedOut = false;
|
|
97
|
+
const sentinel = `__OPENBOT_BASH_DONE_${Math.random().toString(36).substring(7)}__`;
|
|
98
|
+
|
|
99
|
+
const timeoutMs = 60000; // 1 minute timeout for tool calls
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
timedOut = true;
|
|
102
|
+
// We don't kill the session on timeout, just return what we have
|
|
103
|
+
resolve({ exitCode: null, stdout, stderr, timedOut });
|
|
104
|
+
}, timeoutMs);
|
|
105
|
+
|
|
106
|
+
const onStdout = (data: Buffer) => {
|
|
107
|
+
const str = data.toString();
|
|
108
|
+
if (str.includes(sentinel)) {
|
|
109
|
+
const parts = str.split(sentinel);
|
|
110
|
+
stdout += parts[0];
|
|
111
|
+
const exitCodeMatch = parts[1].match(/EXIT:(\d+)/);
|
|
112
|
+
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 0;
|
|
113
|
+
|
|
114
|
+
cleanup();
|
|
115
|
+
resolve({ exitCode, stdout, stderr, timedOut: false });
|
|
116
|
+
} else {
|
|
117
|
+
stdout += str;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const onStderr = (data: Buffer) => {
|
|
122
|
+
stderr += data.toString();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const cleanup = () => {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
session.process.stdout?.removeListener('data', onStdout);
|
|
128
|
+
session.process.stderr?.removeListener('data', onStderr);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
session.process.stdout?.on('data', onStdout);
|
|
132
|
+
session.process.stderr?.on('data', onStderr);
|
|
133
|
+
|
|
134
|
+
// Execute command and then echo the sentinel with exit code
|
|
135
|
+
session.process.stdin?.write(`${command}\necho "${sentinel}EXIT:$?"\n`);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
yield {
|
|
139
|
+
type: 'action:bash:result',
|
|
140
|
+
data: {
|
|
141
|
+
success: result.exitCode === 0 && !result.timedOut,
|
|
142
|
+
exitCode: result.exitCode,
|
|
143
|
+
stdout: result.stdout.trim(),
|
|
144
|
+
stderr: result.stderr.trim(),
|
|
145
|
+
timedOut: result.timedOut,
|
|
146
|
+
output: result.stderr.trim() ? result.stderr.trim() : result.stdout.trim(),
|
|
147
|
+
},
|
|
148
|
+
meta: event.meta,
|
|
149
|
+
} as OpenBotEvent;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const message = error instanceof Error ? error.message : 'Unknown bash error';
|
|
152
|
+
yield {
|
|
153
|
+
type: 'action:bash:result',
|
|
154
|
+
data: {
|
|
155
|
+
success: false,
|
|
156
|
+
exitCode: -1,
|
|
157
|
+
stdout: '',
|
|
158
|
+
stderr: message,
|
|
159
|
+
timedOut: false,
|
|
160
|
+
error: message,
|
|
161
|
+
output: message,
|
|
162
|
+
},
|
|
163
|
+
meta: event.meta,
|
|
164
|
+
} as OpenBotEvent;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Add a tool to stop/kill the session
|
|
169
|
+
builder.on('action:bash_stop', async function* (event, context) {
|
|
170
|
+
const channelId = event.data?.channelId || context.state.channelId;
|
|
171
|
+
const session = sessions.get(channelId);
|
|
172
|
+
if (session) {
|
|
173
|
+
session.process.kill();
|
|
174
|
+
sessions.delete(channelId);
|
|
175
|
+
}
|
|
176
|
+
yield {
|
|
177
|
+
type: 'action:bash_stop:result',
|
|
178
|
+
data: { success: true, output: `Bash session for channel ${channelId} stopped.` },
|
|
179
|
+
meta: event.meta,
|
|
180
|
+
} as OpenBotEvent;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Add a tool to list all active sessions
|
|
184
|
+
builder.on('action:bash_list_sessions', async function* (event, context) {
|
|
185
|
+
const activeSessions = Array.from(sessions.entries()).map(([channelId, session]) => ({
|
|
186
|
+
channelId,
|
|
187
|
+
cwd: session.cwd,
|
|
188
|
+
lastActivity: session.lastActivity,
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
yield {
|
|
192
|
+
type: 'client:ui:widget',
|
|
193
|
+
data: {
|
|
194
|
+
widgetId: randomUUID(),
|
|
195
|
+
kind: 'list',
|
|
196
|
+
title: 'Active Bash Sessions',
|
|
197
|
+
description: `Found ${activeSessions.length} active bash session${activeSessions.length === 1 ? '' : 's'}.`,
|
|
198
|
+
items: activeSessions.map((s) => ({
|
|
199
|
+
id: s.channelId,
|
|
200
|
+
label: s.channelId,
|
|
201
|
+
description: `CWD: ${s.cwd}`,
|
|
202
|
+
status: 'done',
|
|
203
|
+
metadata: {
|
|
204
|
+
cwd: s.cwd,
|
|
205
|
+
lastActivity: s.lastActivity,
|
|
206
|
+
},
|
|
207
|
+
})),
|
|
208
|
+
},
|
|
209
|
+
meta: event.meta,
|
|
210
|
+
} as OpenBotEvent;
|
|
211
|
+
|
|
212
|
+
yield {
|
|
213
|
+
type: 'action:bash_list_sessions:result',
|
|
214
|
+
data: {
|
|
215
|
+
success: true,
|
|
216
|
+
sessions: activeSessions,
|
|
217
|
+
output: JSON.stringify(activeSessions),
|
|
218
|
+
},
|
|
219
|
+
meta: event.meta,
|
|
220
|
+
} as OpenBotEvent;
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const bashPlugin: Plugin = {
|
|
225
|
+
id: 'bash',
|
|
226
|
+
name: 'Bash',
|
|
227
|
+
description: 'Stateful bash session for the channel.',
|
|
228
|
+
toolDefinitions: bashToolDefinitions,
|
|
229
|
+
factory: () => bashPluginRuntime(),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export default bashPlugin;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { generateId } from 'melony';
|
|
3
3
|
import type { Plugin } from '../../services/plugins/types.js';
|
|
4
|
-
import { runAgent } from '../../harness/index.js';
|
|
5
4
|
import {
|
|
6
5
|
OpenBotEvent,
|
|
7
6
|
DelegateTaskEvent,
|
|
@@ -30,7 +29,7 @@ export const delegationPlugin: Plugin = {
|
|
|
30
29
|
name: 'Delegation',
|
|
31
30
|
description: 'Allows agents to call upon other agents to solve sub-tasks.',
|
|
32
31
|
toolDefinitions: delegationToolDefinitions,
|
|
33
|
-
factory: () => (builder) => {
|
|
32
|
+
factory: (pluginContext) => (builder) => {
|
|
34
33
|
|
|
35
34
|
// Handle the tool execution
|
|
36
35
|
builder.on('action:delegate_task', async function* (event, context) {
|
|
@@ -54,6 +53,9 @@ export const delegationPlugin: Plugin = {
|
|
|
54
53
|
|
|
55
54
|
if (!toolCallId) return;
|
|
56
55
|
|
|
56
|
+
// Break circular dependency by dynamic import
|
|
57
|
+
const { runAgent } = await import('../../harness/index.js');
|
|
58
|
+
|
|
57
59
|
const runId = `dg_${generateId()}`;
|
|
58
60
|
let lastAgentOutput = '';
|
|
59
61
|
|
|
@@ -82,6 +84,7 @@ export const delegationPlugin: Plugin = {
|
|
|
82
84
|
} as OpenBotEvent,
|
|
83
85
|
channelId: context.state.channelId,
|
|
84
86
|
threadId: context.state.threadId,
|
|
87
|
+
publicBaseUrl: pluginContext.publicBaseUrl,
|
|
85
88
|
onEvent: async (outEvent) => {
|
|
86
89
|
// Enrich events with parent metadata so the UI can track the hierarchy
|
|
87
90
|
const enrichedEvent = {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { OpenBotState } from '../../app/types.js';
|
|
2
2
|
import { Storage } from '../../services/plugins/domain.js';
|
|
3
|
+
import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
|
|
3
4
|
|
|
4
5
|
export const DEFAULT_CONTEXT_BUDGET = 8000;
|
|
6
|
+
export const MAX_CONTEXT_FILES = 50;
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Returns the known context window budget (in tokens) for a given model string.
|
|
@@ -42,35 +44,82 @@ export async function buildContext(state: OpenBotState, storage?: Storage): Prom
|
|
|
42
44
|
|
|
43
45
|
const sections: string[] = [];
|
|
44
46
|
|
|
45
|
-
//
|
|
47
|
+
// Fetch agents once if storage is available
|
|
48
|
+
const allAgents = storage?.getAgents ? await storage.getAgents().catch(() => []) : [];
|
|
49
|
+
|
|
50
|
+
// 1. User
|
|
51
|
+
if (state.currentUser?.userName) {
|
|
52
|
+
sections.push(`## HUMAN\n- Name: ${state.currentUser.userName}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. Environment
|
|
46
56
|
let env = '## ENVIRONMENT\n';
|
|
47
57
|
if (isDm) {
|
|
48
58
|
env += '- Mode: Direct Message (Solo)\n';
|
|
49
59
|
} else {
|
|
50
60
|
const channelName = channelDetails?.name || channelId;
|
|
51
61
|
env += `- Mode: Channel (#${channelName})\n`;
|
|
62
|
+
if (channelDetails?.cwd) {
|
|
63
|
+
env += `- Workspace: ${channelDetails.cwd}\n`;
|
|
64
|
+
}
|
|
52
65
|
if (threadId) {
|
|
53
66
|
env += `- Thread: ${threadDetails?.name || threadId}\n`;
|
|
54
67
|
}
|
|
55
68
|
const peerIds = participants.filter((id: string) => id !== agentId);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
const participantLabels = peerIds.map((id) => {
|
|
70
|
+
const agent = allAgents.find((a) => a.id === id);
|
|
71
|
+
return agent ? `${agent.name} (${id})` : id;
|
|
72
|
+
});
|
|
73
|
+
env += `- Participants: ${participantLabels.length > 0 ? participantLabels.join(', ') : 'None'}\n`;
|
|
59
74
|
}
|
|
60
75
|
sections.push(env);
|
|
61
76
|
|
|
62
|
-
// 2.
|
|
77
|
+
// 2.5 Installed Agents
|
|
78
|
+
if (allAgents.length > 0) {
|
|
79
|
+
const formatted = allAgents
|
|
80
|
+
.map((a) => `- ${a.id}: ${a.name}${a.description ? ` - ${a.description}` : ''}`)
|
|
81
|
+
.join('\n');
|
|
82
|
+
sections.push(`## INSTALLED AGENTS\n${formatted}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. Channel Spec
|
|
63
86
|
const spec = channelDetails?.spec?.trim();
|
|
64
87
|
if (spec) {
|
|
65
88
|
sections.push(`## CHANNEL SPECIFICATION\n${spec}`);
|
|
66
89
|
}
|
|
67
90
|
|
|
68
|
-
//
|
|
69
|
-
if (
|
|
70
|
-
|
|
91
|
+
// 4. Files
|
|
92
|
+
if (storage?.listFiles && channelId && channelDetails?.cwd) {
|
|
93
|
+
try {
|
|
94
|
+
const files = await storage.listFiles({ channelId });
|
|
95
|
+
if (files.length > 0) {
|
|
96
|
+
const limited = files.slice(0, MAX_CONTEXT_FILES);
|
|
97
|
+
const formatted = limited
|
|
98
|
+
.map((f) => `- ${f.name}${f.isDirectory ? '/' : ''}`)
|
|
99
|
+
.join('\n');
|
|
100
|
+
let fileSection = `## FILES\n${formatted}`;
|
|
101
|
+
if (files.length > MAX_CONTEXT_FILES) {
|
|
102
|
+
fileSection += `\n- ... and ${files.length - MAX_CONTEXT_FILES} more files`;
|
|
103
|
+
}
|
|
104
|
+
sections.push(fileSection);
|
|
105
|
+
} else {
|
|
106
|
+
sections.push('## FILES\n- (No files in workspace)');
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.warn('[context] Failed to fetch files:', error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 5. Agent Instructions
|
|
114
|
+
const rawInstructions = agentDetails?.instructions?.trim();
|
|
115
|
+
if (
|
|
116
|
+
rawInstructions &&
|
|
117
|
+
rawInstructions !== OPENBOT_SYSTEM_PROMPT.trim()
|
|
118
|
+
) {
|
|
119
|
+
sections.push(`## Instructions\n${rawInstructions}`);
|
|
71
120
|
}
|
|
72
121
|
|
|
73
|
-
//
|
|
122
|
+
// 6. Memories
|
|
74
123
|
if (storage?.listMemories) {
|
|
75
124
|
try {
|
|
76
125
|
const scopes = ['global', `agent:${agentId}`];
|
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
import { OpenBotEvent } from '../../app/types.js';
|
|
2
2
|
import { ToolResultPart, type ModelMessage } from 'ai';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Ensures every tool-call has a matching tool-result before calling the LLM.
|
|
6
|
+
* Orphaned calls (interrupted run, missing :result event, etc.) get an empty
|
|
7
|
+
* result so the conversation can resume instead of failing validation.
|
|
8
|
+
*/
|
|
9
|
+
function fillMissingToolResults(messages: ModelMessage[]): ModelMessage[] {
|
|
10
|
+
const filled: ModelMessage[] = [];
|
|
11
|
+
const pending = new Map<string, string>();
|
|
12
|
+
|
|
13
|
+
const flushPending = () => {
|
|
14
|
+
if (pending.size === 0) return;
|
|
15
|
+
filled.push({
|
|
16
|
+
role: 'tool',
|
|
17
|
+
content: [...pending.entries()].map(([toolCallId, toolName]) => ({
|
|
18
|
+
type: 'tool-result' as const,
|
|
19
|
+
toolCallId,
|
|
20
|
+
toolName,
|
|
21
|
+
output: { type: 'text' as const, value: '' },
|
|
22
|
+
})),
|
|
23
|
+
});
|
|
24
|
+
pending.clear();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
for (const message of messages) {
|
|
28
|
+
if (message.role === 'tool' && Array.isArray(message.content)) {
|
|
29
|
+
filled.push(message);
|
|
30
|
+
for (const part of message.content) {
|
|
31
|
+
if ((part as ToolResultPart).type === 'tool-result') {
|
|
32
|
+
pending.delete((part as ToolResultPart).toolCallId);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
flushPending();
|
|
39
|
+
filled.push(message);
|
|
40
|
+
|
|
41
|
+
if (message.role === 'assistant' && Array.isArray(message.content)) {
|
|
42
|
+
for (const part of message.content) {
|
|
43
|
+
if ((part as { type?: string; toolCallId?: string; toolName?: string }).type === 'tool-call') {
|
|
44
|
+
const toolCall = part as { toolCallId: string; toolName: string };
|
|
45
|
+
pending.set(toolCall.toolCallId, toolCall.toolName);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
flushPending();
|
|
52
|
+
return filled;
|
|
53
|
+
}
|
|
54
|
+
|
|
4
55
|
/**
|
|
5
56
|
* Converts a raw event log into a valid chain of ModelMessages for the AI SDK.
|
|
6
57
|
*
|
|
@@ -103,5 +154,5 @@ export function eventsToModelMessages(events: OpenBotEvent[]): ModelMessage[] {
|
|
|
103
154
|
}
|
|
104
155
|
}
|
|
105
156
|
|
|
106
|
-
return messages;
|
|
157
|
+
return fillMissingToolResults(messages);
|
|
107
158
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { Plugin } from '../../services/plugins/types.js';
|
|
2
2
|
import { openbotRuntime } from './runtime.js';
|
|
3
|
+
import { bashPlugin } from '../bash/index.js';
|
|
4
|
+
import { memoryPlugin } from '../memory/index.js';
|
|
5
|
+
import { approvalPlugin } from '../approval/index.js';
|
|
6
|
+
import { storagePlugin } from '../storage/index.js';
|
|
7
|
+
import { delegationPlugin } from '../delegation/index.js';
|
|
8
|
+
import { uiPlugin } from '../ui/index.js';
|
|
3
9
|
|
|
4
10
|
/**
|
|
5
11
|
* `openbot` — the standard, opinionated OpenBot agent runtime.
|
|
@@ -7,12 +13,15 @@ import { openbotRuntime } from './runtime.js';
|
|
|
7
13
|
* This is the canonical execution loop for OpenBot agents. It handles
|
|
8
14
|
* `agent:invoke`, manages short-term memory, assembles context, and
|
|
9
15
|
* orchestrates tool calls.
|
|
16
|
+
*
|
|
17
|
+
* It comes with a "batteries-included" set of inbuilt tools: bash, memory,
|
|
18
|
+
* storage, delegation, and approval.
|
|
10
19
|
*/
|
|
11
20
|
export const openbotPlugin: Plugin = {
|
|
12
21
|
id: 'openbot',
|
|
13
22
|
name: 'OpenBot Agent',
|
|
14
23
|
description:
|
|
15
|
-
'The standard
|
|
24
|
+
'The standard OpenBot agent runtime with inbuilt tools (bash, memory, storage, delegation, and approval).',
|
|
16
25
|
configSchema: {
|
|
17
26
|
type: 'object',
|
|
18
27
|
properties: {
|
|
@@ -22,15 +31,48 @@ export const openbotPlugin: Plugin = {
|
|
|
22
31
|
'Provider model string, e.g. openai/gpt-4o-mini, anthropic/claude-3-5-sonnet-20240620',
|
|
23
32
|
default: 'openai/gpt-4o-mini',
|
|
24
33
|
},
|
|
34
|
+
approval: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
description: 'Configuration for the inbuilt approval plugin.',
|
|
37
|
+
properties: {
|
|
38
|
+
actions: {
|
|
39
|
+
type: 'array',
|
|
40
|
+
items: { type: 'string' },
|
|
41
|
+
description: 'List of actions that require manual approval.',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
25
45
|
},
|
|
26
46
|
},
|
|
27
|
-
|
|
47
|
+
toolDefinitions: {
|
|
48
|
+
...bashPlugin.toolDefinitions,
|
|
49
|
+
...memoryPlugin.toolDefinitions,
|
|
50
|
+
...storagePlugin.toolDefinitions,
|
|
51
|
+
...delegationPlugin.toolDefinitions,
|
|
52
|
+
...uiPlugin.toolDefinitions,
|
|
53
|
+
},
|
|
54
|
+
factory: (context) => (builder) => {
|
|
55
|
+
const { config, storage, tools, abortSignal } = context;
|
|
56
|
+
|
|
57
|
+
// Register inbuilt plugins
|
|
58
|
+
bashPlugin.factory(context)(builder);
|
|
59
|
+
memoryPlugin.factory(context)(builder);
|
|
60
|
+
storagePlugin.factory(context)(builder);
|
|
61
|
+
delegationPlugin.factory(context)(builder);
|
|
62
|
+
uiPlugin.factory(context)(builder);
|
|
63
|
+
|
|
64
|
+
// Approval plugin configuration
|
|
65
|
+
const approvalConfig = (config?.approval as any) || {
|
|
66
|
+
actions: ['action:bash', 'action:create_channel', 'action:delete_channel'],
|
|
67
|
+
};
|
|
68
|
+
approvalPlugin.factory({ ...context, config: approvalConfig })(builder);
|
|
28
69
|
|
|
29
70
|
return openbotRuntime({
|
|
30
71
|
model: config?.model as string,
|
|
31
72
|
storage,
|
|
32
73
|
toolDefinitions: tools,
|
|
33
|
-
|
|
74
|
+
abortSignal,
|
|
75
|
+
})(builder);
|
|
34
76
|
},
|
|
35
77
|
};
|
|
36
78
|
|