openbot 0.4.6 → 0.5.4
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/.claude/agents/runtime-security-reviewer.md +32 -0
- package/.claude/hooks/format.sh +18 -0
- package/.claude/hooks/protect-secrets.sh +23 -0
- package/.claude/settings.json +26 -0
- package/.claude/skills/deploy/SKILL.md +26 -0
- package/.claude/skills/new-plugin/SKILL.md +46 -0
- package/.dockerignore +9 -0
- package/.mcp.json +8 -0
- package/AGENTS.md +2 -2
- package/CLAUDE.md +50 -0
- package/Dockerfile +41 -0
- package/deploy/README.md +61 -0
- package/deploy/entrypoint.sh +6 -0
- package/deploy/fly.toml +36 -0
- package/dist/app/bootstrap.js +29 -0
- package/dist/app/cli.js +3 -1
- package/dist/app/cloud-mode.js +26 -0
- package/dist/app/config.js +61 -16
- package/dist/app/responding-agent.js +32 -0
- package/dist/app/server.js +47 -9
- package/dist/plugins/approval/index.js +1 -1
- package/dist/plugins/bash/index.js +386 -164
- package/dist/plugins/memory/service.js +2 -5
- package/dist/plugins/openbot/context.js +17 -1
- package/dist/plugins/openbot/index.js +25 -9
- package/dist/plugins/openbot/model.js +59 -0
- package/dist/plugins/openbot/runtime.js +39 -56
- package/dist/plugins/openbot/system-prompt.js +44 -9
- package/dist/plugins/preview/index.js +267 -0
- package/dist/plugins/storage/index.js +3 -3
- package/dist/plugins/storage/service.js +58 -12
- package/dist/plugins/todo/index.js +131 -38
- package/dist/plugins/todo/service.js +93 -0
- package/dist/plugins/ui/index.js +8 -1
- package/dist/services/plugins/model-registry.js +101 -0
- package/dist/services/plugins/registry.js +6 -4
- package/dist/services/plugins/service.js +2 -6
- package/docs/agents.md +20 -2
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +13 -3
- package/docs/templates/AGENT.example.md +1 -1
- package/package.json +5 -2
- package/pnpm-workspace.yaml +2 -0
- package/src/app/bootstrap.ts +43 -0
- package/src/app/cli.ts +3 -1
- package/src/app/cloud-mode.ts +41 -0
- package/src/app/config.ts +71 -19
- package/src/app/responding-agent.ts +46 -0
- package/src/app/server.ts +55 -9
- package/src/app/types.ts +167 -31
- package/src/plugins/approval/index.ts +1 -1
- package/src/plugins/bash/index.ts +498 -185
- package/src/plugins/memory/service.ts +2 -5
- package/src/plugins/openbot/context.ts +17 -1
- package/src/plugins/openbot/index.ts +27 -11
- package/src/plugins/openbot/model.ts +76 -0
- package/src/plugins/openbot/runtime.ts +47 -69
- package/src/plugins/openbot/system-prompt.ts +44 -9
- package/src/plugins/preview/index.ts +323 -0
- package/src/plugins/storage/index.ts +5 -8
- package/src/plugins/storage/service.ts +79 -15
- package/src/plugins/todo/index.ts +166 -0
- package/src/plugins/todo/service.ts +123 -0
- package/src/plugins/ui/index.ts +8 -1
- package/src/services/plugins/domain.ts +2 -0
- package/src/services/plugins/model-registry.ts +146 -0
- package/src/services/plugins/registry.ts +6 -4
- package/src/services/plugins/service.ts +2 -6
- package/dist/agents/openbot/index.js +0 -76
- package/dist/agents/openbot/middleware/approval.js +0 -132
- package/dist/agents/openbot/runtime.js +0 -289
- package/dist/agents/openbot/system-prompt.js +0 -32
- package/dist/agents/openbot/tools/delegation.js +0 -78
- package/dist/agents/openbot/tools/mcp.js +0 -99
- package/dist/agents/openbot/tools/shell.js +0 -91
- package/dist/agents/openbot/tools/storage.js +0 -75
- package/dist/agents/openbot/tools/ui.js +0 -176
- package/dist/agents/system.js +0 -33
- package/dist/bus/agent-package.js +0 -1
- package/dist/bus/plugin.js +0 -1
- package/dist/bus/services.js +0 -754
- package/dist/bus/types.js +0 -1
- package/dist/harness/agent-harness.js +0 -45
- package/dist/harness/agent-invoke-run.js +0 -44
- package/dist/harness/agent-turn.js +0 -99
- package/dist/harness/channel-participants.js +0 -40
- package/dist/harness/constants.js +0 -2
- package/dist/harness/context-meter.js +0 -97
- package/dist/harness/context.js +0 -363
- package/dist/harness/dispatch.js +0 -144
- package/dist/harness/dispatcher.js +0 -156
- package/dist/harness/event-normalizer.js +0 -59
- package/dist/harness/history.js +0 -177
- package/dist/harness/mcp.js +0 -61
- package/dist/harness/orchestration.js +0 -88
- package/dist/harness/orchestrator.js +0 -149
- package/dist/harness/participants.js +0 -22
- package/dist/harness/process.js +0 -29
- package/dist/harness/queue-processor.js +0 -187
- package/dist/harness/run-harness.js +0 -154
- package/dist/harness/run.js +0 -98
- package/dist/harness/runtime-factory.js +0 -73
- package/dist/harness/runtime.js +0 -57
- package/dist/harness/todo-advance.js +0 -93
- package/dist/harness/todo-dispatch.js +0 -51
- package/dist/harness/todos.js +0 -5
- package/dist/harness/turn.js +0 -79
- package/dist/harness/types.js +0 -1
- package/dist/plugins/ai-sdk/index.js +0 -34
- package/dist/plugins/ai-sdk/runtime.js +0 -395
- package/dist/plugins/ai-sdk/system-prompt.js +0 -18
- package/dist/plugins/ai-sdk.js +0 -331
- package/dist/plugins/approval.js +0 -163
- package/dist/plugins/delegation.js +0 -108
- package/dist/plugins/mcp/index.js +0 -108
- package/dist/plugins/mcp.js +0 -140
- package/dist/plugins/shell/index.js +0 -100
- package/dist/plugins/shell.js +0 -123
- package/dist/plugins/storage-tools/index.js +0 -76
- package/dist/plugins/storage.js +0 -737
- package/dist/plugins/thread-namer/index.js +0 -72
- package/dist/plugins/threads/index.js +0 -114
- package/dist/plugins/ui.js +0 -211
- package/dist/plugins/workflow/index.js +0 -65
- package/dist/registry/agents.js +0 -138
- package/dist/registry/plugins.js +0 -110
- package/dist/services/agent-packages.js +0 -103
- package/dist/services/memory.js +0 -152
- package/dist/services/plugins.js +0 -98
- package/dist/services/storage.js +0 -1028
- package/dist/workflow/service.js +0 -106
- package/dist/workflow/types.js +0 -3
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { isCloudSystemAgent, resolveCloudSystemModel } from '../../app/cloud-mode.js';
|
|
1
2
|
import { openbotRuntime } from './runtime.js';
|
|
2
3
|
import { bashPlugin } from '../bash/index.js';
|
|
3
4
|
import { memoryPlugin } from '../memory/index.js';
|
|
5
|
+
import { todoPlugin } from '../todo/index.js';
|
|
4
6
|
import { approvalPlugin } from '../approval/index.js';
|
|
5
7
|
import { storagePlugin } from '../storage/index.js';
|
|
6
8
|
import { delegationPlugin } from '../delegation/index.js';
|
|
7
9
|
import { uiPlugin } from '../ui/index.js';
|
|
10
|
+
import { previewPlugin } from '../preview/index.js';
|
|
8
11
|
/**
|
|
9
12
|
* `openbot` — the standard, opinionated OpenBot agent runtime.
|
|
10
13
|
*
|
|
@@ -12,20 +15,19 @@ import { uiPlugin } from '../ui/index.js';
|
|
|
12
15
|
* `agent:invoke`, manages short-term memory, assembles context, and
|
|
13
16
|
* orchestrates tool calls.
|
|
14
17
|
*
|
|
15
|
-
* It comes with a "batteries-included" set of inbuilt tools:
|
|
16
|
-
* storage, delegation, and approval.
|
|
18
|
+
* It comes with a "batteries-included" set of inbuilt tools: shell, memory,
|
|
19
|
+
* todo, storage, delegation, and approval.
|
|
17
20
|
*/
|
|
18
21
|
export const openbotPlugin = {
|
|
19
22
|
id: 'openbot',
|
|
20
23
|
name: 'OpenBot Agent',
|
|
21
|
-
description: 'The standard OpenBot agent runtime with inbuilt tools (
|
|
24
|
+
description: 'The standard OpenBot agent runtime with inbuilt tools (shell, memory, todo, storage, delegation, and approval).',
|
|
22
25
|
configSchema: {
|
|
23
26
|
type: 'object',
|
|
24
27
|
properties: {
|
|
25
28
|
model: {
|
|
26
29
|
type: 'string',
|
|
27
|
-
description: '
|
|
28
|
-
default: 'openai/gpt-4o-mini',
|
|
30
|
+
description: 'Model from the hosted marketplace registry.',
|
|
29
31
|
},
|
|
30
32
|
approval: {
|
|
31
33
|
type: 'object',
|
|
@@ -43,25 +45,39 @@ export const openbotPlugin = {
|
|
|
43
45
|
toolDefinitions: {
|
|
44
46
|
...bashPlugin.toolDefinitions,
|
|
45
47
|
...memoryPlugin.toolDefinitions,
|
|
48
|
+
...todoPlugin.toolDefinitions,
|
|
46
49
|
...storagePlugin.toolDefinitions,
|
|
47
50
|
...delegationPlugin.toolDefinitions,
|
|
48
|
-
...
|
|
51
|
+
...previewPlugin.toolDefinitions,
|
|
52
|
+
// this is the capability to render UI widgets to the user. We dont need it for now.
|
|
53
|
+
// ...uiPlugin.toolDefinitions,
|
|
49
54
|
},
|
|
50
55
|
factory: (context) => (builder) => {
|
|
51
|
-
const { config, storage, tools, abortSignal } = context;
|
|
56
|
+
const { agentId, config, storage, tools, abortSignal } = context;
|
|
52
57
|
// Register inbuilt plugins
|
|
53
58
|
bashPlugin.factory(context)(builder);
|
|
54
59
|
memoryPlugin.factory(context)(builder);
|
|
60
|
+
todoPlugin.factory(context)(builder);
|
|
55
61
|
storagePlugin.factory(context)(builder);
|
|
56
62
|
delegationPlugin.factory(context)(builder);
|
|
57
63
|
uiPlugin.factory(context)(builder);
|
|
64
|
+
previewPlugin.factory(context)(builder);
|
|
58
65
|
// Approval plugin configuration
|
|
59
66
|
const approvalConfig = config?.approval || {
|
|
60
|
-
actions: [
|
|
67
|
+
actions: [
|
|
68
|
+
'action:shell_exec',
|
|
69
|
+
'action:expose_port',
|
|
70
|
+
'action:create_channel',
|
|
71
|
+
'action:delete_channel',
|
|
72
|
+
],
|
|
61
73
|
};
|
|
62
74
|
approvalPlugin.factory({ ...context, config: approvalConfig })(builder);
|
|
75
|
+
const model = isCloudSystemAgent(agentId)
|
|
76
|
+
? resolveCloudSystemModel(config?.model)
|
|
77
|
+
: config?.model;
|
|
63
78
|
return openbotRuntime({
|
|
64
|
-
model
|
|
79
|
+
model,
|
|
80
|
+
agentId,
|
|
65
81
|
storage,
|
|
66
82
|
toolDefinitions: tools,
|
|
67
83
|
abortSignal,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createOpenAI, openai as defaultOpenai } from '@ai-sdk/openai';
|
|
2
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
3
|
+
import { google } from '@ai-sdk/google';
|
|
4
|
+
import { getCloudIntegrationsConfig, isCloudSystemAgent, } from '../../app/cloud-mode.js';
|
|
5
|
+
let cloudOpenAIProvider;
|
|
6
|
+
/** Strip credentials — the integrations gateway injects the provider master key. */
|
|
7
|
+
const integrationsFetch = async (input, init) => {
|
|
8
|
+
const headers = new Headers(init?.headers);
|
|
9
|
+
headers.delete('Authorization');
|
|
10
|
+
headers.delete('x-api-key');
|
|
11
|
+
return globalThis.fetch(input, { ...init, headers });
|
|
12
|
+
};
|
|
13
|
+
function getCloudOpenAIProvider() {
|
|
14
|
+
if (cloudOpenAIProvider !== undefined)
|
|
15
|
+
return cloudOpenAIProvider;
|
|
16
|
+
const config = getCloudIntegrationsConfig();
|
|
17
|
+
if (!config) {
|
|
18
|
+
cloudOpenAIProvider = null;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
cloudOpenAIProvider = createOpenAI({
|
|
22
|
+
baseURL: `${config.baseUrl}/openai/v1`,
|
|
23
|
+
apiKey: 'unused',
|
|
24
|
+
headers: {
|
|
25
|
+
'x-openbot-integrations-token': config.token,
|
|
26
|
+
},
|
|
27
|
+
fetch: integrationsFetch,
|
|
28
|
+
});
|
|
29
|
+
return cloudOpenAIProvider;
|
|
30
|
+
}
|
|
31
|
+
function resolveOpenbotModel(modelId, agentId) {
|
|
32
|
+
if (!agentId || !isCloudSystemAgent(agentId)) {
|
|
33
|
+
throw new Error(`OpenBot models are only available for the cloud system agent. Use openai/..., anthropic/..., etc.`);
|
|
34
|
+
}
|
|
35
|
+
const cloudOpenai = getCloudOpenAIProvider();
|
|
36
|
+
if (!cloudOpenai) {
|
|
37
|
+
throw new Error('OpenBot cloud model requires OPENBOT_INTEGRATIONS_BASE_URL and OPENBOT_INTEGRATIONS_TOKEN.');
|
|
38
|
+
}
|
|
39
|
+
return cloudOpenai.chat(modelId);
|
|
40
|
+
}
|
|
41
|
+
export function resolveModel(modelString, agentId) {
|
|
42
|
+
const [provider, ...rest] = modelString.split('/');
|
|
43
|
+
const modelId = rest.join('/');
|
|
44
|
+
if (!modelId) {
|
|
45
|
+
throw new Error(`Invalid model string: "${modelString}". Expected "provider/model-id".`);
|
|
46
|
+
}
|
|
47
|
+
switch (provider) {
|
|
48
|
+
case 'openbot':
|
|
49
|
+
return resolveOpenbotModel(modelId, agentId);
|
|
50
|
+
case 'openai':
|
|
51
|
+
return defaultOpenai(modelId);
|
|
52
|
+
case 'anthropic':
|
|
53
|
+
return anthropic(modelId);
|
|
54
|
+
case 'google':
|
|
55
|
+
return google(modelId);
|
|
56
|
+
default:
|
|
57
|
+
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,44 +1,11 @@
|
|
|
1
1
|
import { generateText } from 'ai';
|
|
2
|
-
import { openai } from '@ai-sdk/openai';
|
|
3
|
-
import { anthropic } from '@ai-sdk/anthropic';
|
|
4
|
-
import { google } from '@ai-sdk/google';
|
|
5
2
|
import { eventsToModelMessages } from './history.js';
|
|
6
|
-
import { buildContext
|
|
7
|
-
import { saveConfig
|
|
3
|
+
import { buildContext } from './context.js';
|
|
4
|
+
import { saveConfig } from '../../app/config.js';
|
|
5
|
+
import { isCloudSystemAgent } from '../../app/cloud-mode.js';
|
|
8
6
|
import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (cachedRegistry)
|
|
12
|
-
return cachedRegistry;
|
|
13
|
-
try {
|
|
14
|
-
const response = await fetch(DEFAULT_MARKETPLACE_REGISTRY_URL);
|
|
15
|
-
if (!response.ok)
|
|
16
|
-
throw new Error(`Failed to fetch registry: ${response.statusText}`);
|
|
17
|
-
cachedRegistry = (await response.json());
|
|
18
|
-
return cachedRegistry;
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
console.error('[openbot] Failed to fetch model registry:', error);
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function resolveModel(modelString) {
|
|
26
|
-
const [provider, ...rest] = modelString.split('/');
|
|
27
|
-
const modelId = rest.join('/');
|
|
28
|
-
if (!modelId) {
|
|
29
|
-
throw new Error(`Invalid model string: "${modelString}". Expected "provider/model-id".`);
|
|
30
|
-
}
|
|
31
|
-
switch (provider) {
|
|
32
|
-
case 'openai':
|
|
33
|
-
return openai(modelId);
|
|
34
|
-
case 'anthropic':
|
|
35
|
-
return anthropic(modelId);
|
|
36
|
-
case 'google':
|
|
37
|
-
return google(modelId);
|
|
38
|
-
default:
|
|
39
|
-
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
7
|
+
import { resolveModel } from './model.js';
|
|
8
|
+
import { listApiKeyProvidersFromRegistry, resolveModelRegistry, } from '../../services/plugins/model-registry.js';
|
|
42
9
|
async function buildSystemPrompt(state, storage) {
|
|
43
10
|
const context = await buildContext(state, storage);
|
|
44
11
|
const sections = [OPENBOT_SYSTEM_PROMPT, '', context];
|
|
@@ -102,9 +69,9 @@ function createToolBatchTracker(state, storage, channelId, threadId) {
|
|
|
102
69
|
* - When a full batch of results is in, `runLLM` runs again with updated history.
|
|
103
70
|
*/
|
|
104
71
|
export const openbotRuntime = (options) => (builder) => {
|
|
105
|
-
const { model: modelString = 'openai/gpt-4o-mini', storage, toolDefinitions = {}, abortSignal, } = options;
|
|
72
|
+
const { model: modelString = 'openai/gpt-4o-mini', agentId, storage, toolDefinitions = {}, abortSignal, } = options;
|
|
106
73
|
let currentModelString = modelString;
|
|
107
|
-
let model = resolveModel(currentModelString);
|
|
74
|
+
let model = resolveModel(currentModelString, agentId);
|
|
108
75
|
const runLLM = async function* (context, threadId, trigger) {
|
|
109
76
|
if (!storage)
|
|
110
77
|
return;
|
|
@@ -200,7 +167,13 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
200
167
|
errorMessage.includes('401') ||
|
|
201
168
|
errorMessage.includes('Unauthorized') ||
|
|
202
169
|
errorMessage.includes('authentication');
|
|
203
|
-
if (isApiKeyError) {
|
|
170
|
+
if (isApiKeyError && !isCloudSystemAgent(context.state.agentId)) {
|
|
171
|
+
const registry = await resolveModelRegistry();
|
|
172
|
+
const providerActions = listApiKeyProvidersFromRegistry(registry).map((provider) => ({
|
|
173
|
+
id: provider.id,
|
|
174
|
+
label: provider.label,
|
|
175
|
+
variant: 'primary',
|
|
176
|
+
}));
|
|
204
177
|
yield {
|
|
205
178
|
type: 'client:ui:widget',
|
|
206
179
|
data: {
|
|
@@ -208,11 +181,13 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
208
181
|
widgetId: `api_provider_selection_${Date.now()}`,
|
|
209
182
|
title: `Setup AI Provider`,
|
|
210
183
|
description: `Select a provider to continue.`,
|
|
211
|
-
actions:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
184
|
+
actions: providerActions.length > 0
|
|
185
|
+
? providerActions
|
|
186
|
+
: [
|
|
187
|
+
{ id: 'openai', label: 'OpenAI', variant: 'primary' },
|
|
188
|
+
{ id: 'anthropic', label: 'Anthropic', variant: 'primary' },
|
|
189
|
+
{ id: 'google', label: 'Google', variant: 'primary' },
|
|
190
|
+
],
|
|
216
191
|
metadata: {
|
|
217
192
|
type: 'api_provider_selection',
|
|
218
193
|
},
|
|
@@ -259,12 +234,14 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
259
234
|
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
260
235
|
const { metadata, values, actionId } = event.data;
|
|
261
236
|
const threadId = event.meta?.threadId || context.state.threadId;
|
|
237
|
+
if (isCloudSystemAgent(context.state.agentId))
|
|
238
|
+
return;
|
|
262
239
|
if (metadata?.type === 'api_provider_selection') {
|
|
263
240
|
const provider = actionId;
|
|
264
241
|
const [_, ...rest] = currentModelString.split('/');
|
|
265
242
|
const currentModelId = rest.join('/');
|
|
266
|
-
const registry = await
|
|
267
|
-
const providerData = registry?.
|
|
243
|
+
const registry = await resolveModelRegistry();
|
|
244
|
+
const providerData = registry.providers?.[provider];
|
|
268
245
|
const providerLinks = {
|
|
269
246
|
openai: 'https://platform.openai.com/api-keys',
|
|
270
247
|
anthropic: 'https://console.anthropic.com/settings/keys',
|
|
@@ -276,10 +253,18 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
276
253
|
label: m.label,
|
|
277
254
|
value: m.id,
|
|
278
255
|
}));
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
256
|
+
if (!modelOptions || modelOptions.length === 0) {
|
|
257
|
+
yield {
|
|
258
|
+
type: 'agent:output',
|
|
259
|
+
data: {
|
|
260
|
+
content: `No models are listed for **${label}** in the marketplace registry.`,
|
|
261
|
+
},
|
|
262
|
+
meta: { agentId: context.state.agentId },
|
|
263
|
+
};
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const defaultModel = modelOptions[0].value;
|
|
267
|
+
const defaultValue = modelOptions.find((m) => m.value === currentModelId)?.value || defaultModel;
|
|
283
268
|
yield {
|
|
284
269
|
type: 'client:ui:widget',
|
|
285
270
|
data: {
|
|
@@ -305,10 +290,8 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
305
290
|
{
|
|
306
291
|
id: 'model',
|
|
307
292
|
label: 'Model',
|
|
308
|
-
type:
|
|
309
|
-
description: modelOptions ? undefined : `Model name (e.g. \`${defaultModel}\`).`,
|
|
293
|
+
type: 'select',
|
|
310
294
|
options: modelOptions,
|
|
311
|
-
placeholder: defaultModel,
|
|
312
295
|
required: true,
|
|
313
296
|
defaultValue,
|
|
314
297
|
},
|
|
@@ -358,7 +341,7 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
358
341
|
await storage.createVariable({ key: envVar, value: apiKey, secret: true });
|
|
359
342
|
process.env[envVar] = apiKey;
|
|
360
343
|
currentModelString = newModelString;
|
|
361
|
-
model = resolveModel(currentModelString);
|
|
344
|
+
model = resolveModel(currentModelString, agentId);
|
|
362
345
|
try {
|
|
363
346
|
saveConfig({ model: currentModelString });
|
|
364
347
|
// Also update the agent's AGENT.md if it has an openbot plugin config
|
|
@@ -1,22 +1,57 @@
|
|
|
1
1
|
export const OPENBOT_SYSTEM_PROMPT = [
|
|
2
2
|
'# ROLE',
|
|
3
|
-
'You are an OpenBot, the main coordinator and router agent. Your primary role is to orchestrate specialized agents to help the human achieve their goals.',
|
|
3
|
+
'You are an OpenBot, the main coordinator and router agent. Your primary role is to orchestrate specialized agents and manage tasks methodically to help the human achieve their goals.',
|
|
4
4
|
'',
|
|
5
5
|
'# SECURITY POLICY',
|
|
6
6
|
'- **CRITICAL**: Never request API keys, passwords, or sensitive credentials via text or UI widgets; these are managed deterministically via secure forms and must never enter your context.',
|
|
7
7
|
'- **Credential Guidance**: If an agent or tool requires credentials, inform the user they can be managed under "Settings > Variables".',
|
|
8
8
|
'',
|
|
9
9
|
'# CORE MISSION',
|
|
10
|
-
'You
|
|
10
|
+
'You act as a high-level manager, ensuring the right specialized agent is working on the right task. However, when orchestrating or when specialized agents are not available, you are capable of executing complex steps yourself using a highly structured, stateful, and disciplined approach.',
|
|
11
|
+
'',
|
|
12
|
+
'# THE AGENT LOOP',
|
|
13
|
+
'You operate in an iterative agent loop to complete user-assigned tasks step-by-step:',
|
|
14
|
+
'1. **Analyze Events**: Understand user needs and the current state through the chronological event stream (focusing on latest user messages and execution/observation results).',
|
|
15
|
+
'2. **Select Tools**: Choose the next tool call based on current state, todos, and environment constraints. To maximize precision, choose only one tool call per iteration for complex tasks, or parallel calls if they are independent.',
|
|
16
|
+
'3. **Wait for Execution**: Let the system execute the tool action and add observations/results back into the event stream.',
|
|
17
|
+
'4. **Iterate**: Patiently repeat the above steps, analyzing results at each turn, until all steps of the task are completed.',
|
|
18
|
+
'5. **Submit Results**: Message the human with clear, polished, and detailed outcomes, attaching any relevant files or deliverables.',
|
|
19
|
+
'6. **Enter Standby**: Enter an idle state and wait for new tasks.',
|
|
20
|
+
'',
|
|
21
|
+
'# TASK TRACKING (TODOS)',
|
|
22
|
+
'- For any complex multi-step task (3+ steps), you **MUST** maintain a todo list with `todo_write`.',
|
|
23
|
+
'- Write the full intended list at task start. Each call replaces the previous list (not a partial patch).',
|
|
24
|
+
'- Keep exactly one item `in_progress` while working. Mark items `completed` immediately after finishing them.',
|
|
25
|
+
'- Use `cancelled` for steps that are no longer needed. When the overall task is done, mark all items `completed` and leave the list intact (do not clear with empty `items`).',
|
|
26
|
+
'- The current list is injected into context each turn as `## TODOS`; call `todo_read` only if you need an explicit refresh.',
|
|
27
|
+
'- Do not stop with open `pending` or `in_progress` items unless you are blocked and have told the user why.',
|
|
11
28
|
'',
|
|
12
29
|
'# OPERATIONAL GUIDELINES',
|
|
13
|
-
'- **Channel and Threads**: The main
|
|
14
|
-
'- **Delegation**: You can delegate tasks to any specialized agent in the `INSTALLED AGENTS` list.',
|
|
15
|
-
'- **Bash Tool Usage**: You should use the `bash` tool very rarely. Only use it when the user explicitly requests a command to be run or when it is absolutely necessary for a task that no other agent can handle.',
|
|
16
|
-
'- **Context Awareness**: Use the provided ENVIRONMENT, CHANNEL SPECIFICATION, and MEMORIES to maintain continuity. Do not ask for information already present in these sections.',
|
|
30
|
+
'- **Channel and Threads**: The main way to communicate and act is through channels and threads. There might be a channel called "uncategorized" for general purpose communication.',
|
|
31
|
+
'- **Delegation**: You can delegate tasks to any specialized agent in the `INSTALLED AGENTS` list using the `delegate` tool.',
|
|
17
32
|
'- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.',
|
|
18
|
-
'- **
|
|
33
|
+
'- **Hub-and-Spoke**: Specialized agents cannot communicate directly; as coordinator, you must pass relevant data from one agent to another.',
|
|
34
|
+
'',
|
|
35
|
+
'# SHELL & FILE EXECUTION RULES',
|
|
36
|
+
'- **Stateful Sessions**: Use `shell_exec` with a session `id` (e.g. `default`, `server`). Reuse the same id to keep shell state (cwd, env) across commands.',
|
|
37
|
+
'- **Working Directory**: Always pass an absolute `exec_dir` (use the channel workspace path from ENVIRONMENT).',
|
|
38
|
+
'- **Command Discipline**: Avoid interactive prompts when possible; use `-y`, `-f`, or non-interactive flags. For prompts, use `shell_write_to_process`.',
|
|
39
|
+
'- **Long-Running Processes**: Start dev servers with `shell_exec` using `&` at the end (e.g. `pnpm dev &`). If you forget `&`, `shell_exec` returns after ~15s with partial logs — the server may still be running.',
|
|
40
|
+
'- **Polling dev servers**: After starting (or after a timeout), poll with `shell_wait` (2–5s) then `shell_view` until logs show ready (URL/port). Do not start a duplicate server; reuse the same session id.',
|
|
41
|
+
'- **Stop dev servers**: Use `shell_kill_process` before restarting or when finished.',
|
|
42
|
+
'- **Preview URLs**: After a dev server is ready, use `expose_port` with its port to generate a public preview URL (stored on channel details as `previewUrl`). Expose tunnels are temporary; close with `unexpose_port`.',
|
|
43
|
+
'- **Calculations**: Use `bc` or Python for math. Do not calculate complex math mentally.',
|
|
44
|
+
'- **Chaining**: Chain related commands with `&&` in a single `shell_exec` when they must run sequentially.',
|
|
45
|
+
'',
|
|
46
|
+
'# COMMUNICATION & WRITING STYLE',
|
|
47
|
+
'- Be concise, professional, proactive, and polite.',
|
|
48
|
+
'- Confirm receipt of user messages quickly and outline your high-level strategy brief before executing a long series of steps.',
|
|
49
|
+
'- Inform the user via messages when you change methods or find strategic issues.',
|
|
50
|
+
'- Write in clean, continuous prose. Avoid excessive bullet points or lists in final responses unless explicitly requested.',
|
|
51
|
+
'- All major deliverables and documents must be highly detailed and complete; do not summarize or truncate final outputs unless specified.',
|
|
19
52
|
'',
|
|
20
|
-
'#
|
|
21
|
-
'-
|
|
53
|
+
'# ERROR HANDLING',
|
|
54
|
+
'- Tool failures are provided as events. When an error occurs, do not panic or immediately ask the user for help.',
|
|
55
|
+
'- Carefully analyze the stderr or failure logs, verify tool/argument names, and attempt alternative approaches or parameter fixes.',
|
|
56
|
+
'- Only report failure reasons and ask the human for help after multiple alternative approaches have failed.',
|
|
22
57
|
].join('\n');
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
const TUNNEL_URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
5
|
+
const TUNNEL_READY_TIMEOUT_MS = 60000;
|
|
6
|
+
const MAX_LOG_CHARS = 8000;
|
|
7
|
+
const previewToolDefinitions = {
|
|
8
|
+
expose_port: {
|
|
9
|
+
description: 'Expose a local dev server port via a temporary public Cloudflare quick tunnel. Returns a previewUrl stored on the channel. Dev servers must listen on 0.0.0.0 or 127.0.0.1. Call after shell_exec when the server is ready.',
|
|
10
|
+
inputSchema: z.object({
|
|
11
|
+
port: z
|
|
12
|
+
.number()
|
|
13
|
+
.int()
|
|
14
|
+
.min(1024)
|
|
15
|
+
.max(65535)
|
|
16
|
+
.describe('Local port of the running dev server (e.g. 5173).'),
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
unexpose_port: {
|
|
20
|
+
description: 'Stop the active Cloudflare preview tunnel for this channel and clear previewUrl from channel state.',
|
|
21
|
+
inputSchema: z.object({}),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const tunnels = new Map();
|
|
25
|
+
const tunnelByChannel = new Map();
|
|
26
|
+
const blockedPorts = () => {
|
|
27
|
+
const openbotPort = Number(process.env.PORT ?? 4132);
|
|
28
|
+
return new Set([22, 80, 443, openbotPort]);
|
|
29
|
+
};
|
|
30
|
+
const appendLog = (tunnel, chunk) => {
|
|
31
|
+
tunnel.logs += chunk;
|
|
32
|
+
if (tunnel.logs.length > MAX_LOG_CHARS) {
|
|
33
|
+
tunnel.logs = tunnel.logs.slice(-MAX_LOG_CHARS);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const killTunnelProcess = (tunnel) => {
|
|
37
|
+
const { process: child } = tunnel;
|
|
38
|
+
if (!child.pid) {
|
|
39
|
+
try {
|
|
40
|
+
child.kill();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* ignore */
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
child.kill('SIGTERM');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
try {
|
|
52
|
+
child.kill();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* ignore */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const removeTunnel = (tunnelId) => {
|
|
60
|
+
const tunnel = tunnels.get(tunnelId);
|
|
61
|
+
if (!tunnel)
|
|
62
|
+
return;
|
|
63
|
+
killTunnelProcess(tunnel);
|
|
64
|
+
tunnels.delete(tunnelId);
|
|
65
|
+
if (tunnelByChannel.get(tunnel.channelId) === tunnelId) {
|
|
66
|
+
tunnelByChannel.delete(tunnel.channelId);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
export const stopPreviewForChannel = (channelId) => {
|
|
70
|
+
const tunnelId = tunnelByChannel.get(channelId);
|
|
71
|
+
if (tunnelId) {
|
|
72
|
+
removeTunnel(tunnelId);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const waitForTunnelUrl = (child, timeoutMs) => new Promise((resolve, reject) => {
|
|
76
|
+
let buffer = '';
|
|
77
|
+
let settled = false;
|
|
78
|
+
const cleanup = () => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
child.stdout?.off('data', onData);
|
|
81
|
+
child.stderr?.off('data', onData);
|
|
82
|
+
child.off('exit', onExit);
|
|
83
|
+
child.off('error', onError);
|
|
84
|
+
};
|
|
85
|
+
const tryParse = () => {
|
|
86
|
+
const match = buffer.match(TUNNEL_URL_PATTERN);
|
|
87
|
+
if (match) {
|
|
88
|
+
settled = true;
|
|
89
|
+
cleanup();
|
|
90
|
+
resolve(match[0]);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const onData = (chunk) => {
|
|
94
|
+
buffer += chunk.toString();
|
|
95
|
+
if (buffer.length > 16000) {
|
|
96
|
+
buffer = buffer.slice(-16000);
|
|
97
|
+
}
|
|
98
|
+
tryParse();
|
|
99
|
+
};
|
|
100
|
+
const onExit = (code) => {
|
|
101
|
+
if (settled)
|
|
102
|
+
return;
|
|
103
|
+
settled = true;
|
|
104
|
+
cleanup();
|
|
105
|
+
reject(new Error(`cloudflared exited before providing a tunnel URL (code ${code ?? 'unknown'})`));
|
|
106
|
+
};
|
|
107
|
+
const onError = (err) => {
|
|
108
|
+
if (settled)
|
|
109
|
+
return;
|
|
110
|
+
settled = true;
|
|
111
|
+
cleanup();
|
|
112
|
+
reject(err);
|
|
113
|
+
};
|
|
114
|
+
const timer = setTimeout(() => {
|
|
115
|
+
if (settled)
|
|
116
|
+
return;
|
|
117
|
+
settled = true;
|
|
118
|
+
cleanup();
|
|
119
|
+
reject(new Error('Timed out waiting for Cloudflare tunnel URL'));
|
|
120
|
+
}, timeoutMs);
|
|
121
|
+
child.stdout?.on('data', onData);
|
|
122
|
+
child.stderr?.on('data', onData);
|
|
123
|
+
child.on('exit', onExit);
|
|
124
|
+
child.on('error', onError);
|
|
125
|
+
tryParse();
|
|
126
|
+
});
|
|
127
|
+
const startCloudflaredTunnel = async (channelId, port) => {
|
|
128
|
+
const child = spawn('cloudflared', ['tunnel', '--url', `http://127.0.0.1:${port}`, '--no-autoupdate'], {
|
|
129
|
+
env: process.env,
|
|
130
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
131
|
+
});
|
|
132
|
+
const tunnel = {
|
|
133
|
+
id: randomUUID(),
|
|
134
|
+
channelId,
|
|
135
|
+
port,
|
|
136
|
+
url: '',
|
|
137
|
+
process: child,
|
|
138
|
+
startedAt: Date.now(),
|
|
139
|
+
logs: '',
|
|
140
|
+
};
|
|
141
|
+
child.stdout?.on('data', (data) => appendLog(tunnel, data.toString()));
|
|
142
|
+
child.stderr?.on('data', (data) => appendLog(tunnel, data.toString()));
|
|
143
|
+
child.on('exit', () => {
|
|
144
|
+
tunnels.delete(tunnel.id);
|
|
145
|
+
if (tunnelByChannel.get(channelId) === tunnel.id) {
|
|
146
|
+
tunnelByChannel.delete(channelId);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
const url = await waitForTunnelUrl(child, TUNNEL_READY_TIMEOUT_MS);
|
|
150
|
+
tunnel.url = url;
|
|
151
|
+
tunnels.set(tunnel.id, tunnel);
|
|
152
|
+
tunnelByChannel.set(channelId, tunnel.id);
|
|
153
|
+
return tunnel;
|
|
154
|
+
};
|
|
155
|
+
const clearPreviewChannelState = async (storage, channelId) => {
|
|
156
|
+
await storage.patchChannelState({
|
|
157
|
+
channelId,
|
|
158
|
+
state: {
|
|
159
|
+
previewUrl: null,
|
|
160
|
+
previewPort: null,
|
|
161
|
+
previewExposedAt: null,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
const previewPluginRuntime = (storage) => (builder) => {
|
|
166
|
+
builder.on('action:expose_port', async function* (event, context) {
|
|
167
|
+
const channelId = context.state.channelId;
|
|
168
|
+
const port = event.data?.port;
|
|
169
|
+
if (!Number.isInteger(port)) {
|
|
170
|
+
yield {
|
|
171
|
+
type: 'action:expose_port:result',
|
|
172
|
+
data: {
|
|
173
|
+
success: false,
|
|
174
|
+
output: 'port must be an integer between 1024 and 65535.',
|
|
175
|
+
},
|
|
176
|
+
meta: event.meta,
|
|
177
|
+
};
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (blockedPorts().has(port)) {
|
|
181
|
+
yield {
|
|
182
|
+
type: 'action:expose_port:result',
|
|
183
|
+
data: {
|
|
184
|
+
success: false,
|
|
185
|
+
output: `Port ${port} is reserved and cannot be exposed.`,
|
|
186
|
+
},
|
|
187
|
+
meta: event.meta,
|
|
188
|
+
};
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const existingTunnelId = tunnelByChannel.get(channelId);
|
|
192
|
+
if (existingTunnelId) {
|
|
193
|
+
removeTunnel(existingTunnelId);
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const tunnel = await startCloudflaredTunnel(channelId, port);
|
|
197
|
+
await storage.patchChannelState({
|
|
198
|
+
channelId,
|
|
199
|
+
state: {
|
|
200
|
+
previewUrl: tunnel.url,
|
|
201
|
+
previewPort: port,
|
|
202
|
+
previewExposedAt: tunnel.startedAt,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
if (context.state.channelDetails) {
|
|
206
|
+
context.state.channelDetails = await storage.getChannelDetails({ channelId });
|
|
207
|
+
}
|
|
208
|
+
yield {
|
|
209
|
+
type: 'action:expose_port:result',
|
|
210
|
+
data: {
|
|
211
|
+
success: true,
|
|
212
|
+
previewUrl: tunnel.url,
|
|
213
|
+
port,
|
|
214
|
+
temporary: true,
|
|
215
|
+
output: `Preview available at ${tunnel.url} (temporary Cloudflare quick tunnel).`,
|
|
216
|
+
},
|
|
217
|
+
meta: event.meta,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
const message = error instanceof Error ? error.message : 'Failed to start Cloudflare tunnel';
|
|
222
|
+
const needsCloudflared = message.includes('ENOENT') || message.toLowerCase().includes('cloudflared');
|
|
223
|
+
const hint = needsCloudflared
|
|
224
|
+
? ' Install cloudflared and ensure it is on PATH (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/downloads/).'
|
|
225
|
+
: '';
|
|
226
|
+
yield {
|
|
227
|
+
type: 'action:expose_port:result',
|
|
228
|
+
data: {
|
|
229
|
+
success: false,
|
|
230
|
+
error: message,
|
|
231
|
+
output: `${message}${hint}`,
|
|
232
|
+
},
|
|
233
|
+
meta: event.meta,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
builder.on('action:unexpose_port', async function* (event, context) {
|
|
238
|
+
const channelId = context.state.channelId;
|
|
239
|
+
stopPreviewForChannel(channelId);
|
|
240
|
+
await clearPreviewChannelState(storage, channelId);
|
|
241
|
+
if (context.state.channelDetails) {
|
|
242
|
+
context.state.channelDetails = await storage.getChannelDetails({ channelId });
|
|
243
|
+
}
|
|
244
|
+
yield {
|
|
245
|
+
type: 'action:unexpose_port:result',
|
|
246
|
+
data: {
|
|
247
|
+
success: true,
|
|
248
|
+
output: 'Preview tunnel stopped and previewUrl cleared from channel state.',
|
|
249
|
+
},
|
|
250
|
+
meta: event.meta,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
builder.on('action:delete_channel', async function* (event) {
|
|
254
|
+
const channelId = event.data?.channelId;
|
|
255
|
+
if (channelId) {
|
|
256
|
+
stopPreviewForChannel(channelId);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
export const previewPlugin = {
|
|
261
|
+
id: 'preview',
|
|
262
|
+
name: 'Preview',
|
|
263
|
+
description: 'Temporary public preview URLs via Cloudflare quick tunnels.',
|
|
264
|
+
toolDefinitions: previewToolDefinitions,
|
|
265
|
+
factory: ({ storage }) => previewPluginRuntime(storage),
|
|
266
|
+
};
|
|
267
|
+
export default previewPlugin;
|
|
@@ -32,7 +32,7 @@ const storageToolDefinitions = {
|
|
|
32
32
|
state: z
|
|
33
33
|
.record(z.string(), z.unknown())
|
|
34
34
|
.optional()
|
|
35
|
-
.describe('JSON state object for the channel.
|
|
35
|
+
.describe('JSON state object for the channel.'),
|
|
36
36
|
spec: z
|
|
37
37
|
.string()
|
|
38
38
|
.optional()
|
|
@@ -44,11 +44,11 @@ const storageToolDefinitions = {
|
|
|
44
44
|
value.cwd !== undefined, { message: 'Provide at least one of state, spec, or cwd.' }),
|
|
45
45
|
},
|
|
46
46
|
patch_thread_details: {
|
|
47
|
-
description: 'Patch current thread details (state).',
|
|
47
|
+
description: 'Patch current thread details (state). Use for thread metadata such as `name` or `isSmartNamed`. For multi-step task tracking, use `todo_write` instead.',
|
|
48
48
|
inputSchema: z.object({
|
|
49
49
|
state: z
|
|
50
50
|
.record(z.string(), z.unknown())
|
|
51
|
-
.describe('JSON state object for the thread.
|
|
51
|
+
.describe('JSON state object for the thread. Merges with existing state.'),
|
|
52
52
|
}),
|
|
53
53
|
},
|
|
54
54
|
create_variable: {
|