openbot 0.4.0 → 0.4.3
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 +6 -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 +215 -59
- 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 +7 -7
- 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 +7 -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 +282 -59
- 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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { generateId } from 'melony';
|
|
3
|
-
import { runAgent } from '../../harness/index.js';
|
|
4
3
|
/**
|
|
5
4
|
* `delegation` — allows agents to delegate tasks to other agents.
|
|
6
5
|
*
|
|
@@ -22,7 +21,7 @@ export const delegationPlugin = {
|
|
|
22
21
|
name: 'Delegation',
|
|
23
22
|
description: 'Allows agents to call upon other agents to solve sub-tasks.',
|
|
24
23
|
toolDefinitions: delegationToolDefinitions,
|
|
25
|
-
factory: () => (builder) => {
|
|
24
|
+
factory: (pluginContext) => (builder) => {
|
|
26
25
|
// Handle the tool execution
|
|
27
26
|
builder.on('action:delegate_task', async function* (event, context) {
|
|
28
27
|
const delegateEvent = event;
|
|
@@ -42,6 +41,8 @@ export const delegationPlugin = {
|
|
|
42
41
|
const toolCallId = delegateEvent.meta?.toolCallId;
|
|
43
42
|
if (!toolCallId)
|
|
44
43
|
return;
|
|
44
|
+
// Break circular dependency by dynamic import
|
|
45
|
+
const { runAgent } = await import('../../harness/index.js');
|
|
45
46
|
const runId = `dg_${generateId()}`;
|
|
46
47
|
let lastAgentOutput = '';
|
|
47
48
|
// Queue to bridge the async onEvent callback to this generator
|
|
@@ -68,6 +69,9 @@ export const delegationPlugin = {
|
|
|
68
69
|
},
|
|
69
70
|
channelId: context.state.channelId,
|
|
70
71
|
threadId: context.state.threadId,
|
|
72
|
+
publicBaseUrl: pluginContext.publicBaseUrl,
|
|
73
|
+
// Child events are re-yielded to the parent harness, which persists them once.
|
|
74
|
+
persistEvents: false,
|
|
71
75
|
onEvent: async (outEvent) => {
|
|
72
76
|
// Enrich events with parent metadata so the UI can track the hierarchy
|
|
73
77
|
const enrichedEvent = {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
|
|
1
2
|
export const DEFAULT_CONTEXT_BUDGET = 8000;
|
|
3
|
+
export const MAX_CONTEXT_FILES = 50;
|
|
2
4
|
/**
|
|
3
5
|
* Returns the known context window budget (in tokens) for a given model string.
|
|
4
6
|
*/
|
|
@@ -32,7 +34,13 @@ export async function buildContext(state, storage) {
|
|
|
32
34
|
const participants = channelDetails?.participants || [];
|
|
33
35
|
const isDm = isDmSoloChannel(participants, agentId);
|
|
34
36
|
const sections = [];
|
|
35
|
-
//
|
|
37
|
+
// Fetch agents once if storage is available
|
|
38
|
+
const allAgents = storage?.getAgents ? await storage.getAgents().catch(() => []) : [];
|
|
39
|
+
// 1. User
|
|
40
|
+
if (state.currentUser?.userName) {
|
|
41
|
+
sections.push(`## HUMAN\n- Name: ${state.currentUser.userName}`);
|
|
42
|
+
}
|
|
43
|
+
// 2. Environment
|
|
36
44
|
let env = '## ENVIRONMENT\n';
|
|
37
45
|
if (isDm) {
|
|
38
46
|
env += '- Mode: Direct Message (Solo)\n';
|
|
@@ -40,25 +48,62 @@ export async function buildContext(state, storage) {
|
|
|
40
48
|
else {
|
|
41
49
|
const channelName = channelDetails?.name || channelId;
|
|
42
50
|
env += `- Mode: Channel (#${channelName})\n`;
|
|
51
|
+
if (channelDetails?.cwd) {
|
|
52
|
+
env += `- Workspace: ${channelDetails.cwd}\n`;
|
|
53
|
+
}
|
|
43
54
|
if (threadId) {
|
|
44
55
|
env += `- Thread: ${threadDetails?.name || threadId}\n`;
|
|
45
56
|
}
|
|
46
57
|
const peerIds = participants.filter((id) => id !== agentId);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
const participantLabels = peerIds.map((id) => {
|
|
59
|
+
const agent = allAgents.find((a) => a.id === id);
|
|
60
|
+
return agent ? `${agent.name} (${id})` : id;
|
|
61
|
+
});
|
|
62
|
+
env += `- Participants: ${participantLabels.length > 0 ? participantLabels.join(', ') : 'None'}\n`;
|
|
50
63
|
}
|
|
51
64
|
sections.push(env);
|
|
52
|
-
// 2.
|
|
65
|
+
// 2.5 Installed Agents
|
|
66
|
+
if (allAgents.length > 0) {
|
|
67
|
+
const formatted = allAgents
|
|
68
|
+
.map((a) => `- ${a.id}: ${a.name}${a.description ? ` - ${a.description}` : ''}`)
|
|
69
|
+
.join('\n');
|
|
70
|
+
sections.push(`## INSTALLED AGENTS\n${formatted}`);
|
|
71
|
+
}
|
|
72
|
+
// 3. Channel Spec
|
|
53
73
|
const spec = channelDetails?.spec?.trim();
|
|
54
74
|
if (spec) {
|
|
55
75
|
sections.push(`## CHANNEL SPECIFICATION\n${spec}`);
|
|
56
76
|
}
|
|
57
|
-
//
|
|
58
|
-
if (
|
|
59
|
-
|
|
77
|
+
// 4. Files
|
|
78
|
+
if (storage?.listFiles && channelId && channelDetails?.cwd) {
|
|
79
|
+
try {
|
|
80
|
+
const files = await storage.listFiles({ channelId });
|
|
81
|
+
if (files.length > 0) {
|
|
82
|
+
const limited = files.slice(0, MAX_CONTEXT_FILES);
|
|
83
|
+
const formatted = limited
|
|
84
|
+
.map((f) => `- ${f.name}${f.isDirectory ? '/' : ''}`)
|
|
85
|
+
.join('\n');
|
|
86
|
+
let fileSection = `## FILES\n${formatted}`;
|
|
87
|
+
if (files.length > MAX_CONTEXT_FILES) {
|
|
88
|
+
fileSection += `\n- ... and ${files.length - MAX_CONTEXT_FILES} more files`;
|
|
89
|
+
}
|
|
90
|
+
sections.push(fileSection);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
sections.push('## FILES\n- (No files in workspace)');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.warn('[context] Failed to fetch files:', error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 5. Agent Instructions
|
|
101
|
+
const rawInstructions = agentDetails?.instructions?.trim();
|
|
102
|
+
if (rawInstructions &&
|
|
103
|
+
rawInstructions !== OPENBOT_SYSTEM_PROMPT.trim()) {
|
|
104
|
+
sections.push(`## Instructions\n${rawInstructions}`);
|
|
60
105
|
}
|
|
61
|
-
//
|
|
106
|
+
// 6. Memories
|
|
62
107
|
if (storage?.listMemories) {
|
|
63
108
|
try {
|
|
64
109
|
const scopes = ['global', `agent:${agentId}`];
|
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensures every tool-call has a matching tool-result before calling the LLM.
|
|
3
|
+
* Orphaned calls (interrupted run, missing :result event, etc.) get an empty
|
|
4
|
+
* result so the conversation can resume instead of failing validation.
|
|
5
|
+
*/
|
|
6
|
+
function fillMissingToolResults(messages) {
|
|
7
|
+
const filled = [];
|
|
8
|
+
const pending = new Map();
|
|
9
|
+
const flushPending = () => {
|
|
10
|
+
if (pending.size === 0)
|
|
11
|
+
return;
|
|
12
|
+
filled.push({
|
|
13
|
+
role: 'tool',
|
|
14
|
+
content: [...pending.entries()].map(([toolCallId, toolName]) => ({
|
|
15
|
+
type: 'tool-result',
|
|
16
|
+
toolCallId,
|
|
17
|
+
toolName,
|
|
18
|
+
output: { type: 'text', value: '' },
|
|
19
|
+
})),
|
|
20
|
+
});
|
|
21
|
+
pending.clear();
|
|
22
|
+
};
|
|
23
|
+
for (const message of messages) {
|
|
24
|
+
if (message.role === 'tool' && Array.isArray(message.content)) {
|
|
25
|
+
filled.push(message);
|
|
26
|
+
for (const part of message.content) {
|
|
27
|
+
if (part.type === 'tool-result') {
|
|
28
|
+
pending.delete(part.toolCallId);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
flushPending();
|
|
34
|
+
filled.push(message);
|
|
35
|
+
if (message.role === 'assistant' && Array.isArray(message.content)) {
|
|
36
|
+
for (const part of message.content) {
|
|
37
|
+
if (part.type === 'tool-call') {
|
|
38
|
+
const toolCall = part;
|
|
39
|
+
pending.set(toolCall.toolCallId, toolCall.toolName);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
flushPending();
|
|
45
|
+
return filled;
|
|
46
|
+
}
|
|
1
47
|
/**
|
|
2
48
|
* Converts a raw event log into a valid chain of ModelMessages for the AI SDK.
|
|
3
49
|
*
|
|
@@ -94,5 +140,5 @@ export function eventsToModelMessages(events) {
|
|
|
94
140
|
break;
|
|
95
141
|
}
|
|
96
142
|
}
|
|
97
|
-
return messages;
|
|
143
|
+
return fillMissingToolResults(messages);
|
|
98
144
|
}
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { openbotRuntime } from './runtime.js';
|
|
2
|
+
import { bashPlugin } from '../bash/index.js';
|
|
3
|
+
import { memoryPlugin } from '../memory/index.js';
|
|
4
|
+
import { approvalPlugin } from '../approval/index.js';
|
|
5
|
+
import { storagePlugin } from '../storage/index.js';
|
|
6
|
+
import { delegationPlugin } from '../delegation/index.js';
|
|
7
|
+
import { uiPlugin } from '../ui/index.js';
|
|
2
8
|
/**
|
|
3
9
|
* `openbot` — the standard, opinionated OpenBot agent runtime.
|
|
4
10
|
*
|
|
5
11
|
* This is the canonical execution loop for OpenBot agents. It handles
|
|
6
12
|
* `agent:invoke`, manages short-term memory, assembles context, and
|
|
7
13
|
* orchestrates tool calls.
|
|
14
|
+
*
|
|
15
|
+
* It comes with a "batteries-included" set of inbuilt tools: bash, memory,
|
|
16
|
+
* storage, delegation, and approval.
|
|
8
17
|
*/
|
|
9
18
|
export const openbotPlugin = {
|
|
10
19
|
id: 'openbot',
|
|
11
20
|
name: 'OpenBot Agent',
|
|
12
|
-
description: 'The standard
|
|
21
|
+
description: 'The standard OpenBot agent runtime with inbuilt tools (bash, memory, storage, delegation, and approval).',
|
|
13
22
|
configSchema: {
|
|
14
23
|
type: 'object',
|
|
15
24
|
properties: {
|
|
@@ -18,14 +27,45 @@ export const openbotPlugin = {
|
|
|
18
27
|
description: 'Provider model string, e.g. openai/gpt-4o-mini, anthropic/claude-3-5-sonnet-20240620',
|
|
19
28
|
default: 'openai/gpt-4o-mini',
|
|
20
29
|
},
|
|
30
|
+
approval: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
description: 'Configuration for the inbuilt approval plugin.',
|
|
33
|
+
properties: {
|
|
34
|
+
actions: {
|
|
35
|
+
type: 'array',
|
|
36
|
+
items: { type: 'string' },
|
|
37
|
+
description: 'List of actions that require manual approval.',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
21
41
|
},
|
|
22
42
|
},
|
|
23
|
-
|
|
43
|
+
toolDefinitions: {
|
|
44
|
+
...bashPlugin.toolDefinitions,
|
|
45
|
+
...memoryPlugin.toolDefinitions,
|
|
46
|
+
...storagePlugin.toolDefinitions,
|
|
47
|
+
...delegationPlugin.toolDefinitions,
|
|
48
|
+
...uiPlugin.toolDefinitions,
|
|
49
|
+
},
|
|
50
|
+
factory: (context) => (builder) => {
|
|
51
|
+
const { config, storage, tools, abortSignal } = context;
|
|
52
|
+
// Register inbuilt plugins
|
|
53
|
+
bashPlugin.factory(context)(builder);
|
|
54
|
+
memoryPlugin.factory(context)(builder);
|
|
55
|
+
storagePlugin.factory(context)(builder);
|
|
56
|
+
delegationPlugin.factory(context)(builder);
|
|
57
|
+
uiPlugin.factory(context)(builder);
|
|
58
|
+
// Approval plugin configuration
|
|
59
|
+
const approvalConfig = config?.approval || {
|
|
60
|
+
actions: ['action:bash', 'action:create_channel', 'action:delete_channel'],
|
|
61
|
+
};
|
|
62
|
+
approvalPlugin.factory({ ...context, config: approvalConfig })(builder);
|
|
24
63
|
return openbotRuntime({
|
|
25
64
|
model: config?.model,
|
|
26
65
|
storage,
|
|
27
66
|
toolDefinitions: tools,
|
|
28
|
-
|
|
67
|
+
abortSignal,
|
|
68
|
+
})(builder);
|
|
29
69
|
},
|
|
30
70
|
};
|
|
31
71
|
export default openbotPlugin;
|
|
@@ -2,7 +2,7 @@ import { generateText } from 'ai';
|
|
|
2
2
|
import { openai } from '@ai-sdk/openai';
|
|
3
3
|
import { anthropic } from '@ai-sdk/anthropic';
|
|
4
4
|
import { eventsToModelMessages } from './history.js';
|
|
5
|
-
import {
|
|
5
|
+
import { buildContext, } from './context.js';
|
|
6
6
|
import { saveConfig } from '../../app/config.js';
|
|
7
7
|
import { API_KEY_SETUP_MESSAGE, OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
|
|
8
8
|
function resolveModel(modelString) {
|
|
@@ -22,10 +22,7 @@ function resolveModel(modelString) {
|
|
|
22
22
|
}
|
|
23
23
|
async function buildSystemPrompt(state, storage) {
|
|
24
24
|
const context = await buildContext(state, storage);
|
|
25
|
-
const
|
|
26
|
-
? (state.agentDetails?.instructions?.trim() || OPENBOT_SYSTEM_PROMPT)
|
|
27
|
-
: OPENBOT_SYSTEM_PROMPT;
|
|
28
|
-
const sections = [instructions, '', context];
|
|
25
|
+
const sections = [OPENBOT_SYSTEM_PROMPT, '', context];
|
|
29
26
|
// Hardcoded naming hint logic
|
|
30
27
|
const threadState = state.threadDetails?.state;
|
|
31
28
|
if (!threadState?.isSmartNamed) {
|
|
@@ -40,24 +37,41 @@ async function buildSystemPrompt(state, storage) {
|
|
|
40
37
|
* a single `generateText` response execute one-by-one. We must wait for every ID
|
|
41
38
|
* in the batch before calling the LLM again — not after the first result.
|
|
42
39
|
*/
|
|
43
|
-
function createToolBatchTracker() {
|
|
44
|
-
|
|
40
|
+
function createToolBatchTracker(state, storage, channelId, threadId) {
|
|
41
|
+
const save = async (ids) => {
|
|
42
|
+
if (!storage || !channelId || !threadId)
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
await storage.patchThreadState({
|
|
46
|
+
channelId,
|
|
47
|
+
threadId,
|
|
48
|
+
state: { pendingToolCallIds: ids },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.error('[openbot] Failed to persist pendingToolCallIds:', error);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
45
55
|
return {
|
|
46
|
-
startBatch(toolCallIds) {
|
|
47
|
-
|
|
56
|
+
async startBatch(toolCallIds) {
|
|
57
|
+
state.pendingToolCallIds = [...toolCallIds];
|
|
58
|
+
await save(state.pendingToolCallIds);
|
|
48
59
|
},
|
|
49
|
-
clear() {
|
|
50
|
-
|
|
60
|
+
async clear() {
|
|
61
|
+
state.pendingToolCallIds = undefined;
|
|
62
|
+
await save(undefined);
|
|
51
63
|
},
|
|
52
64
|
/** Returns true when this result completes the batch (time to call the LLM again). */
|
|
53
|
-
recordResult(toolCallId) {
|
|
54
|
-
if (!
|
|
55
|
-
return false;
|
|
56
|
-
pending.delete(toolCallId);
|
|
57
|
-
if (pending.size > 0)
|
|
65
|
+
async recordResult(toolCallId) {
|
|
66
|
+
if (!state.pendingToolCallIds?.includes(toolCallId))
|
|
58
67
|
return false;
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
state.pendingToolCallIds = state.pendingToolCallIds.filter((id) => id !== toolCallId);
|
|
69
|
+
const done = state.pendingToolCallIds.length === 0;
|
|
70
|
+
if (done) {
|
|
71
|
+
state.pendingToolCallIds = undefined;
|
|
72
|
+
}
|
|
73
|
+
await save(state.pendingToolCallIds);
|
|
74
|
+
return done;
|
|
61
75
|
},
|
|
62
76
|
};
|
|
63
77
|
}
|
|
@@ -69,13 +83,15 @@ function createToolBatchTracker() {
|
|
|
69
83
|
* - When a full batch of results is in, `runLLM` runs again with updated history.
|
|
70
84
|
*/
|
|
71
85
|
export const openbotRuntime = (options) => (builder) => {
|
|
72
|
-
const { model: modelString = 'openai/gpt-4o-mini', storage, toolDefinitions = {}, } = options;
|
|
86
|
+
const { model: modelString = 'openai/gpt-4o-mini', storage, toolDefinitions = {}, abortSignal, } = options;
|
|
73
87
|
let currentModelString = modelString;
|
|
74
88
|
let model = resolveModel(currentModelString);
|
|
75
|
-
const toolBatch = createToolBatchTracker();
|
|
76
89
|
const runLLM = async function* (context, threadId, trigger) {
|
|
77
90
|
if (!storage)
|
|
78
91
|
return;
|
|
92
|
+
if (abortSignal?.aborted)
|
|
93
|
+
return;
|
|
94
|
+
const toolBatch = createToolBatchTracker(context.state, storage, context.state.channelId, threadId || context.state.threadId);
|
|
79
95
|
// Capture parent metadata for event enrichment
|
|
80
96
|
const triggerEvent = trigger || context.state.triggerEvent;
|
|
81
97
|
const parentAgentId = triggerEvent?.meta?.parentAgentId;
|
|
@@ -88,8 +104,8 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
88
104
|
});
|
|
89
105
|
const messages = eventsToModelMessages(events);
|
|
90
106
|
// console.log('systemPrompt:::::::\n', systemPrompt);
|
|
91
|
-
// console.log('messages:::::::\n', JSON.stringify(messages
|
|
92
|
-
// console.log('toolDefinitions:::::::\n', JSON.stringify(toolDefinitions
|
|
107
|
+
// console.log('messages:::::::\n', JSON.stringify(messages));
|
|
108
|
+
// console.log('toolDefinitions:::::::\n', JSON.stringify(toolDefinitions));
|
|
93
109
|
try {
|
|
94
110
|
// Single LLM request — tool execution happens externally via action:* handlers.
|
|
95
111
|
const result = await generateText({
|
|
@@ -99,6 +115,7 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
99
115
|
tools: toolDefinitions,
|
|
100
116
|
stopWhen: ({ steps }) => steps.length === 1,
|
|
101
117
|
allowSystemInMessages: true,
|
|
118
|
+
abortSignal,
|
|
102
119
|
});
|
|
103
120
|
const toolCalls = result.toolCalls ?? [];
|
|
104
121
|
// if (result.usage) {
|
|
@@ -138,7 +155,7 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
138
155
|
}
|
|
139
156
|
if (toolCalls.length > 0) {
|
|
140
157
|
// when multiple tool calls are made, Melony runtime handles them one by one, thats why we need to start a new batch
|
|
141
|
-
toolBatch.startBatch(toolCalls.map((tc) => tc.toolCallId));
|
|
158
|
+
await toolBatch.startBatch(toolCalls.map((tc) => tc.toolCallId));
|
|
142
159
|
for (const toolCall of toolCalls) {
|
|
143
160
|
yield {
|
|
144
161
|
type: `action:${toolCall.toolName}`,
|
|
@@ -152,10 +169,13 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
152
169
|
}
|
|
153
170
|
else {
|
|
154
171
|
// clear the tool batch if there are no tool calls
|
|
155
|
-
toolBatch.clear();
|
|
172
|
+
await toolBatch.clear();
|
|
156
173
|
}
|
|
157
174
|
}
|
|
158
175
|
catch (error) {
|
|
176
|
+
// Run was stopped — unwind quietly without surfacing an error.
|
|
177
|
+
if (abortSignal?.aborted)
|
|
178
|
+
return;
|
|
159
179
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
160
180
|
const isApiKeyError = errorMessage.includes('API key') ||
|
|
161
181
|
errorMessage.includes('401') ||
|
|
@@ -217,10 +237,53 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
217
237
|
if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
|
|
218
238
|
return;
|
|
219
239
|
}
|
|
240
|
+
// Capture user info from meta if available
|
|
241
|
+
if (event.meta?.userName) {
|
|
242
|
+
context.state.currentUser = {
|
|
243
|
+
userName: event.meta.userName,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
247
|
+
// Auto-add participants if tagged in the prompt
|
|
248
|
+
const content = event.data?.content;
|
|
249
|
+
if (content && storage) {
|
|
250
|
+
try {
|
|
251
|
+
const allAgents = await storage.getAgents();
|
|
252
|
+
const tags = content.match(/@([\w-]+)/g);
|
|
253
|
+
if (tags) {
|
|
254
|
+
const taggedAgentIds = tags.map((t) => t.slice(1));
|
|
255
|
+
const validAgentIds = taggedAgentIds.filter((id) => allAgents.some((a) => a.id === id));
|
|
256
|
+
const currentParticipants = context.state.channelDetails?.participants || [];
|
|
257
|
+
const newParticipants = [...new Set([...currentParticipants, ...validAgentIds])];
|
|
258
|
+
if (newParticipants.length > currentParticipants.length) {
|
|
259
|
+
// Update storage
|
|
260
|
+
await storage.patchChannelState({
|
|
261
|
+
channelId: context.state.channelId,
|
|
262
|
+
state: { participants: newParticipants },
|
|
263
|
+
});
|
|
264
|
+
// Refresh local state
|
|
265
|
+
context.state.channelDetails = await storage.getChannelDetails({
|
|
266
|
+
channelId: context.state.channelId,
|
|
267
|
+
});
|
|
268
|
+
// Notify UI/others about the change
|
|
269
|
+
yield {
|
|
270
|
+
type: 'action:patch_channel_details:result',
|
|
271
|
+
data: { success: true, updatedFields: ['participants'] },
|
|
272
|
+
meta: {
|
|
273
|
+
agentId: context.state.agentId,
|
|
274
|
+
threadId,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
console.warn('[openbot] Failed to auto-add participants from tags:', error);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
220
284
|
// clear the tool batch if the agent is invoked
|
|
221
285
|
// this is to prevent the tool batch from being used for a new agent invocation
|
|
222
|
-
|
|
223
|
-
const threadId = event.meta?.threadId || context.state.threadId;
|
|
286
|
+
await createToolBatchTracker(context.state, storage, context.state.channelId, threadId).clear();
|
|
224
287
|
yield* runLLM(context, threadId, event);
|
|
225
288
|
});
|
|
226
289
|
// this is to handle the tool results from the tool calls
|
|
@@ -232,7 +295,8 @@ export const openbotRuntime = (options) => (builder) => {
|
|
|
232
295
|
return;
|
|
233
296
|
const toolCallId = event.meta?.toolCallId;
|
|
234
297
|
// record the result of the tool call
|
|
235
|
-
if (!toolCallId ||
|
|
298
|
+
if (!toolCallId ||
|
|
299
|
+
!(await createToolBatchTracker(context.state, storage, context.state.channelId, event.meta?.threadId || context.state.threadId).recordResult(toolCallId)))
|
|
236
300
|
return;
|
|
237
301
|
const threadId = event.meta?.threadId || context.state.threadId;
|
|
238
302
|
yield* runLLM(context, threadId);
|
|
@@ -1,5 +1,25 @@
|
|
|
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.',
|
|
4
|
+
'',
|
|
5
|
+
'# SECURITY POLICY',
|
|
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
|
+
'- **Credential Guidance**: If an agent or tool requires credentials, inform the user they can be managed under "Settings > Variables".',
|
|
8
|
+
'',
|
|
9
|
+
'# CORE MISSION',
|
|
10
|
+
'You almost never execute tasks yourself. Instead, you delegate tasks to specialized agents (channel participants). You act as a high-level manager, ensuring the right agent is working on the right task.',
|
|
11
|
+
'',
|
|
12
|
+
'# OPERATIONAL GUIDELINES',
|
|
13
|
+
'- **Channel and Threads**: The main and only way to communicate and act is through channels and threads. There might be a channel called "uncategorized" for general purpose communication.',
|
|
14
|
+
'- **Agent Participation**: ONLY add an agent via `patch_channel_details` if the user manually tags them (e.g., `@name`) AND they are missing from the `Participants` list in `ENVIRONMENT`.',
|
|
15
|
+
'- **Delegation**: NEVER delegate to an agent who is not a participant. Only if existing participants clearly cannot handle a task should you suggest relevant agents from the `INSTALLED AGENTS` list.',
|
|
16
|
+
'- **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 participant can handle.',
|
|
17
|
+
'- **Context Awareness**: Use the provided ENVIRONMENT, CHANNEL SPECIFICATION, and MEMORIES to maintain continuity. Do not ask for information already present in these sections.',
|
|
18
|
+
'- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.',
|
|
19
|
+
'- **Structured Interaction**: Use the `render_widget` tool to collect information via forms, offer choices, or display lists. This is preferred over asking multiple separate questions in plain text.',
|
|
20
|
+
'',
|
|
21
|
+
'# COMMUNICATION STYLE',
|
|
22
|
+
'- Be always concise, professional, and proactive.',
|
|
3
23
|
].join('\n');
|
|
4
24
|
/** Shown in the API key setup form when no provider credentials are configured. */
|
|
5
25
|
export const API_KEY_SETUP_MESSAGE = 'OpenBot runs AI agents locally with tools, memory, and delegation. Bring your own OpenAI or Anthropic key — it stays on your machine. Use the form below to get started.';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { STATE_AGENT_ID } from '../../app/agent-ids.js';
|
|
2
|
-
import { pluginService,
|
|
2
|
+
import { pluginService, resolveMarketplaceRegistry, } from '../../services/plugins/service.js';
|
|
3
3
|
/**
|
|
4
4
|
* `plugin-manager` — marketplace listing, npm plugin install/uninstall, and
|
|
5
5
|
* installing agents from the registry. Wired on the **`state`** built-in agent
|
|
@@ -46,12 +46,96 @@ export const pluginManagerPlugin = {
|
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
builder.on('action:marketplace:list', async function* () {
|
|
49
|
-
const agents = await
|
|
49
|
+
const { agents, channels } = await resolveMarketplaceRegistry();
|
|
50
50
|
yield {
|
|
51
51
|
type: 'action:marketplace:list:result',
|
|
52
|
-
data: { success: true, agents },
|
|
52
|
+
data: { success: true, agents, channels },
|
|
53
53
|
};
|
|
54
54
|
});
|
|
55
|
+
builder.on('action:channel:install', async function* (event) {
|
|
56
|
+
try {
|
|
57
|
+
const { channelId: instanceId, name: templateName, participants: customParticipants, initialState: customInitialState, } = event.data;
|
|
58
|
+
const { agents: marketplaceAgents, channels } = await resolveMarketplaceRegistry();
|
|
59
|
+
// Try to find the template by ID or Name
|
|
60
|
+
const channelListing = channels.find((c) => c.id === instanceId) ||
|
|
61
|
+
channels.find((c) => c.name === templateName);
|
|
62
|
+
const channelId = instanceId;
|
|
63
|
+
const participants = customParticipants || channelListing?.participants || [];
|
|
64
|
+
const initialState = {
|
|
65
|
+
...(channelListing?.initialState || {}),
|
|
66
|
+
...(customInitialState || {}),
|
|
67
|
+
};
|
|
68
|
+
const spec = channelListing?.spec || '';
|
|
69
|
+
// 1. Auto-install participant agents if missing
|
|
70
|
+
for (const agentId of participants) {
|
|
71
|
+
const existingAgents = await storage.getAgents();
|
|
72
|
+
if (existingAgents.some((a) => a.id === agentId)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// Not found locally, look in marketplace
|
|
76
|
+
const agentListing = marketplaceAgents.find((a) => a.id === agentId);
|
|
77
|
+
if (agentListing) {
|
|
78
|
+
console.log(`[plugin-manager] Auto-installing agent ${agentId} for channel ${channelId}`);
|
|
79
|
+
// Install plugins for this agent
|
|
80
|
+
for (const ref of agentListing.plugins) {
|
|
81
|
+
const installed = await pluginService.isInstalled(ref.id);
|
|
82
|
+
if (!installed &&
|
|
83
|
+
ref.id.includes('/') === false &&
|
|
84
|
+
ref.id.includes('-plugin-') === false) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!installed) {
|
|
88
|
+
try {
|
|
89
|
+
await pluginService.install({ packageName: ref.id });
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
console.warn(`[plugins] Failed to pre-install plugin ${ref.id}`, err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Create the agent
|
|
97
|
+
await storage.createAgent({
|
|
98
|
+
agentId: agentListing.id,
|
|
99
|
+
name: agentListing.name,
|
|
100
|
+
description: agentListing.description,
|
|
101
|
+
image: agentListing.image,
|
|
102
|
+
instructions: agentListing.instructions,
|
|
103
|
+
plugins: agentListing.plugins,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// 2. Create the channel
|
|
108
|
+
await storage.createChannel({
|
|
109
|
+
channelId,
|
|
110
|
+
spec,
|
|
111
|
+
initialState: {
|
|
112
|
+
...initialState,
|
|
113
|
+
participants,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const channelUrl = `/channels/${channelId}`;
|
|
117
|
+
yield {
|
|
118
|
+
type: 'action:channel:install:result',
|
|
119
|
+
data: { success: true, channelId, channelUrl },
|
|
120
|
+
};
|
|
121
|
+
yield {
|
|
122
|
+
type: 'agent:output',
|
|
123
|
+
data: {
|
|
124
|
+
content: `Successfully installed channel **${channelListing?.name || templateName || channelId}** and created channel \`${channelId}\`.`,
|
|
125
|
+
},
|
|
126
|
+
meta: { agentId: 'system' },
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
yield {
|
|
131
|
+
type: 'action:channel:install:result',
|
|
132
|
+
data: {
|
|
133
|
+
success: false,
|
|
134
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
});
|
|
55
139
|
builder.on('action:agent:install', async function* (event) {
|
|
56
140
|
try {
|
|
57
141
|
const { agentId: newAgentId, name, description, image, instructions, plugins, } = event.data;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import { resolvePath } from '../../app/config.js';
|
|
3
4
|
const shellToolDefinitions = {
|
|
4
5
|
shell_exec: {
|
|
5
6
|
description: 'Execute a shell command in the terminal. Use this for file operations, running scripts, or system tasks.',
|
|
@@ -22,7 +23,7 @@ const shellPluginRuntime = () => (builder) => {
|
|
|
22
23
|
builder.on('action:shell_exec', async function* (event, context) {
|
|
23
24
|
const { command, cwd, shell = 'bash', timeoutMs = 30000 } = event.data;
|
|
24
25
|
const actualTimeout = Math.max(1000, Math.min(timeoutMs, 60000));
|
|
25
|
-
const actualCwd = cwd || context.state.channelDetails?.cwd || process.cwd();
|
|
26
|
+
const actualCwd = resolvePath(cwd || context.state.channelDetails?.cwd || process.cwd());
|
|
26
27
|
try {
|
|
27
28
|
const result = await new Promise((resolve) => {
|
|
28
29
|
const child = spawn(command, {
|