openbot 0.2.14 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/openbot/index.js +76 -0
- package/dist/agents/openbot/middleware/approval.js +132 -0
- package/dist/agents/openbot/runtime.js +289 -0
- package/dist/agents/openbot/system-prompt.js +32 -0
- package/dist/agents/openbot/tools/delegation.js +78 -0
- package/dist/agents/openbot/tools/mcp.js +99 -0
- package/dist/agents/openbot/tools/shell.js +91 -0
- package/dist/agents/openbot/tools/storage.js +75 -0
- package/dist/agents/openbot/tools/ui.js +176 -0
- package/dist/agents/system.js +20 -93
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +4 -1
- package/dist/app/server.js +15 -8
- package/dist/bus/agent-package.js +1 -0
- package/dist/bus/plugin.js +1 -0
- package/dist/bus/services.js +711 -0
- package/dist/bus/types.js +1 -0
- package/dist/harness/context.js +250 -0
- package/dist/harness/event-normalizer.js +59 -0
- package/dist/harness/orchestrator.js +27 -227
- package/dist/harness/process.js +25 -3
- package/dist/harness/queue-processor.js +227 -0
- package/dist/harness/runtime-factory.js +103 -0
- package/dist/plugins/ai-sdk/index.js +37 -0
- package/dist/plugins/ai-sdk/runtime.js +402 -0
- package/dist/plugins/ai-sdk/system-prompt.js +3 -0
- package/dist/plugins/ai-sdk.js +277 -87
- package/dist/plugins/approval/index.js +159 -0
- package/dist/plugins/approval.js +163 -0
- package/dist/plugins/delegation/index.js +79 -0
- package/dist/plugins/delegation.js +67 -11
- package/dist/plugins/mcp/index.js +108 -0
- package/dist/plugins/memory/index.js +71 -0
- package/dist/plugins/shell/index.js +99 -0
- package/dist/plugins/shell.js +123 -0
- package/dist/plugins/storage-tools/index.js +85 -0
- package/dist/plugins/storage.js +240 -5
- package/dist/plugins/ui/index.js +184 -0
- package/dist/plugins/ui.js +185 -21
- package/dist/registry/agents.js +138 -0
- package/dist/registry/plugins.js +93 -50
- package/dist/services/agent-packages.js +103 -0
- package/dist/services/memory.js +152 -0
- package/dist/services/plugins.js +98 -0
- package/dist/services/storage.js +366 -94
- package/docs/agents.md +52 -65
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +70 -58
- package/docs/templates/AGENT.example.md +57 -0
- package/package.json +8 -7
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +14 -4
- package/src/app/server.ts +23 -10
- package/src/app/types.ts +445 -16
- package/src/assets/icon.svg +4 -1
- package/src/bus/plugin.ts +67 -0
- package/src/bus/services.ts +786 -0
- package/src/bus/types.ts +160 -0
- package/src/harness/context.ts +293 -0
- package/src/harness/event-normalizer.ts +82 -0
- package/src/harness/orchestrator.ts +35 -273
- package/src/harness/process.ts +28 -4
- package/src/harness/queue-processor.ts +309 -0
- package/src/harness/runtime-factory.ts +125 -0
- package/src/plugins/ai-sdk/index.ts +44 -0
- package/src/plugins/ai-sdk/runtime.ts +484 -0
- package/src/plugins/ai-sdk/system-prompt.ts +4 -0
- package/src/plugins/approval/index.ts +228 -0
- package/src/plugins/delegation/index.ts +94 -0
- package/src/plugins/mcp/index.ts +128 -0
- package/src/plugins/memory/index.ts +85 -0
- package/src/plugins/shell/index.ts +123 -0
- package/src/plugins/storage-tools/index.ts +101 -0
- package/src/plugins/ui/index.ts +227 -0
- package/src/registry/plugins.ts +108 -55
- package/src/services/memory.ts +213 -0
- package/src/services/plugins.ts +133 -0
- package/src/services/storage.ts +472 -137
- package/src/agents/system.ts +0 -112
- package/src/plugins/ai-sdk.ts +0 -197
- package/src/plugins/delegation.ts +0 -60
- package/src/plugins/mcp.ts +0 -154
- package/src/plugins/storage.ts +0 -725
- package/src/plugins/ui.ts +0 -57
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { openBotRuntime } from './runtime.js';
|
|
3
|
+
import { shellPlugin, shellToolDefinitions } from './tools/shell.js';
|
|
4
|
+
import { mcpPlugin, mcpToolDefinitions } from './tools/mcp.js';
|
|
5
|
+
import { uiPlugin, uiToolDefinitions } from './tools/ui.js';
|
|
6
|
+
import { delegationPlugin, delegationToolDefinitions } from './tools/delegation.js';
|
|
7
|
+
import { storageToolDefinitions } from './tools/storage.js';
|
|
8
|
+
import { approvalPlugin } from './middleware/approval.js';
|
|
9
|
+
import { DEFAULT_OPENBOT_APPROVAL_RULES, OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
|
|
10
|
+
const OPENBOT_ICON_DATA_URL = (() => {
|
|
11
|
+
try {
|
|
12
|
+
const svg = readFileSync(new URL('../../assets/icon.svg', import.meta.url), 'utf-8').trim();
|
|
13
|
+
if (!svg.startsWith('<svg'))
|
|
14
|
+
return undefined;
|
|
15
|
+
return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf-8').toString('base64')}`;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
})();
|
|
21
|
+
const composeMelonyPlugin = (...plugins) => {
|
|
22
|
+
return (builder) => {
|
|
23
|
+
for (const plugin of plugins) {
|
|
24
|
+
plugin(builder);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* OpenBot — the first-party orchestrator agent package.
|
|
30
|
+
*
|
|
31
|
+
* Treats the bus as a peer environment: registers a runtime that owns
|
|
32
|
+
* `agent:invoke`, exposes a curated tool set (delegation, storage, MCP, shell,
|
|
33
|
+
* UI widgets), and wires approval middleware for protected actions.
|
|
34
|
+
*
|
|
35
|
+
* Other agents (Codex, Claude Code, Gemini, custom Coder/Researcher) are
|
|
36
|
+
* separate AgentPackages with their own runtime + tool composition.
|
|
37
|
+
*/
|
|
38
|
+
export const openBotAgentPackage = {
|
|
39
|
+
id: 'openbot',
|
|
40
|
+
name: 'OpenBot',
|
|
41
|
+
description: 'First-party orchestration agent for OpenBot. Coordinates other agents via handoff and delegation.',
|
|
42
|
+
image: OPENBOT_ICON_DATA_URL,
|
|
43
|
+
defaultInstructions: OPENBOT_SYSTEM_PROMPT,
|
|
44
|
+
configSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
model: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Provider model string, e.g. openai/gpt-4o-mini, anthropic/claude-3-5-sonnet-20240620',
|
|
50
|
+
default: 'openai/gpt-4o-mini',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
factory: ({ agentDetails, config, storage }) => {
|
|
55
|
+
const model = typeof config.model === 'string' && config.model
|
|
56
|
+
? config.model
|
|
57
|
+
: 'openai/gpt-4o-mini';
|
|
58
|
+
const toolDefinitions = {
|
|
59
|
+
...delegationToolDefinitions,
|
|
60
|
+
...storageToolDefinitions,
|
|
61
|
+
...mcpToolDefinitions,
|
|
62
|
+
...shellToolDefinitions,
|
|
63
|
+
// Re-enable when the dashboard renders widgets:
|
|
64
|
+
// ...uiToolDefinitions,
|
|
65
|
+
};
|
|
66
|
+
const systemPrompt = agentDetails.instructions || OPENBOT_SYSTEM_PROMPT;
|
|
67
|
+
return composeMelonyPlugin(openBotRuntime({
|
|
68
|
+
model,
|
|
69
|
+
system: systemPrompt,
|
|
70
|
+
storage,
|
|
71
|
+
toolDefinitions,
|
|
72
|
+
}), approvalPlugin({ rules: DEFAULT_OPENBOT_APPROVAL_RULES }), delegationPlugin(), shellPlugin(), mcpPlugin(), uiPlugin());
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
// Suppress unused warning while UI widget tools are not wired into the LLM tool list.
|
|
76
|
+
void uiToolDefinitions;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { storageService } from '../../../services/storage.js';
|
|
2
|
+
const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
3
|
+
? value
|
|
4
|
+
: {};
|
|
5
|
+
const getApprovalsFromState = (state) => {
|
|
6
|
+
const source = state.threadDetails?.state ?? state.channelDetails?.state;
|
|
7
|
+
const stateRecord = asRecord(source);
|
|
8
|
+
return asRecord(stateRecord.approvals);
|
|
9
|
+
};
|
|
10
|
+
const persistApprovals = async (state, approvals) => {
|
|
11
|
+
if (state.threadId) {
|
|
12
|
+
await storageService.patchThreadState({
|
|
13
|
+
channelId: state.channelId,
|
|
14
|
+
threadId: state.threadId,
|
|
15
|
+
state: { approvals },
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
await storageService.patchChannelState({
|
|
20
|
+
channelId: state.channelId,
|
|
21
|
+
state: { approvals },
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
export const approvalPlugin = (options = {}) => (builder) => {
|
|
25
|
+
const rules = options.rules || [];
|
|
26
|
+
for (const rule of rules) {
|
|
27
|
+
builder.on(rule.action, async function* (event, context) {
|
|
28
|
+
const meta = asRecord(event.meta);
|
|
29
|
+
if (meta.approvalStatus === 'approved')
|
|
30
|
+
return;
|
|
31
|
+
const eventData = asRecord(event.data);
|
|
32
|
+
const eventMeta = meta;
|
|
33
|
+
const approvalId = `approval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
34
|
+
const widgetId = `widget_${approvalId}`;
|
|
35
|
+
const executeEvent = rule.executeEvent || rule.action;
|
|
36
|
+
const denyEvent = rule.denyEvent || `${rule.action}:result`;
|
|
37
|
+
const denyData = rule.denyData || {};
|
|
38
|
+
const hiddenKeys = new Set(rule.hiddenKeys || []);
|
|
39
|
+
const detailKeys = rule.detailKeys || Object.keys(eventData);
|
|
40
|
+
const details = detailKeys
|
|
41
|
+
.filter((key) => !hiddenKeys.has(key))
|
|
42
|
+
.map((key) => `- ${key}: ${String(eventData[key] ?? '')}`)
|
|
43
|
+
.join('\n');
|
|
44
|
+
const pendingApprovals = getApprovalsFromState(context.state);
|
|
45
|
+
pendingApprovals[approvalId] = {
|
|
46
|
+
id: approvalId,
|
|
47
|
+
action: rule.action,
|
|
48
|
+
executeEvent,
|
|
49
|
+
denyEvent,
|
|
50
|
+
denyData,
|
|
51
|
+
payload: eventData,
|
|
52
|
+
meta: eventMeta,
|
|
53
|
+
message: rule.message || `Approval required for ${rule.action}.`,
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
status: 'pending',
|
|
56
|
+
};
|
|
57
|
+
await persistApprovals(context.state, pendingApprovals);
|
|
58
|
+
yield {
|
|
59
|
+
type: 'client:ui:widget',
|
|
60
|
+
data: {
|
|
61
|
+
kind: 'choice',
|
|
62
|
+
widgetId,
|
|
63
|
+
title: 'Approval Required',
|
|
64
|
+
body: `${rule.message || 'A protected action requires approval.'}${details ? `\n\n${details}` : ''}`,
|
|
65
|
+
metadata: { type: 'approval:request', approvalId, action: rule.action },
|
|
66
|
+
actions: [
|
|
67
|
+
{ id: 'approve', label: 'Approve', variant: 'primary' },
|
|
68
|
+
{ id: 'deny', label: 'Deny', variant: 'danger' },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
72
|
+
};
|
|
73
|
+
yield {
|
|
74
|
+
type: 'agent:output',
|
|
75
|
+
data: { content: `Waiting for approval before running \`${rule.action}\`.` },
|
|
76
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
77
|
+
};
|
|
78
|
+
context.suspend();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
82
|
+
const metadata = asRecord(event.data?.metadata);
|
|
83
|
+
if (metadata.type !== 'approval:request')
|
|
84
|
+
return;
|
|
85
|
+
const approvalId = String(metadata.approvalId || '');
|
|
86
|
+
if (!approvalId)
|
|
87
|
+
return;
|
|
88
|
+
const approvals = getApprovalsFromState(context.state);
|
|
89
|
+
const approval = approvals[approvalId];
|
|
90
|
+
if (!approval || approval.status !== 'pending') {
|
|
91
|
+
yield {
|
|
92
|
+
type: 'agent:output',
|
|
93
|
+
data: { content: 'Approval request not found or already resolved.' },
|
|
94
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
95
|
+
};
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const approved = event.data.actionId === 'approve';
|
|
99
|
+
approvals[approvalId] = {
|
|
100
|
+
...approval,
|
|
101
|
+
status: approved ? 'approved' : 'denied',
|
|
102
|
+
};
|
|
103
|
+
await persistApprovals(context.state, approvals);
|
|
104
|
+
if (approved) {
|
|
105
|
+
yield {
|
|
106
|
+
type: approval.executeEvent,
|
|
107
|
+
data: approval.payload,
|
|
108
|
+
meta: {
|
|
109
|
+
...(approval.meta || {}),
|
|
110
|
+
approvalId,
|
|
111
|
+
approvalStatus: 'approved',
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
yield {
|
|
117
|
+
type: approval.denyEvent,
|
|
118
|
+
data: {
|
|
119
|
+
success: false,
|
|
120
|
+
approved: false,
|
|
121
|
+
error: 'Action denied by user approval.',
|
|
122
|
+
...approval.denyData,
|
|
123
|
+
},
|
|
124
|
+
meta: { ...(approval.meta || {}), approvalId },
|
|
125
|
+
};
|
|
126
|
+
yield {
|
|
127
|
+
type: 'agent:output',
|
|
128
|
+
data: { content: 'Action denied by user approval.' },
|
|
129
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
};
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { generateText } from 'ai';
|
|
2
|
+
import { openai } from '@ai-sdk/openai';
|
|
3
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
4
|
+
import { createDefaultContextEngine } from '../../harness/context.js';
|
|
5
|
+
function resolveModel(modelString) {
|
|
6
|
+
const [provider, ...rest] = modelString.split('/');
|
|
7
|
+
const modelId = rest.join('/');
|
|
8
|
+
if (!modelId) {
|
|
9
|
+
throw new Error(`Invalid model string: "${modelString}". Expected "provider/model-id".`);
|
|
10
|
+
}
|
|
11
|
+
switch (provider) {
|
|
12
|
+
case 'openai':
|
|
13
|
+
return openai(modelId);
|
|
14
|
+
case 'anthropic':
|
|
15
|
+
return anthropic(modelId);
|
|
16
|
+
default:
|
|
17
|
+
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
21
|
+
? value
|
|
22
|
+
: {};
|
|
23
|
+
const readPersistedShortTermMessages = (state) => {
|
|
24
|
+
const source = state.threadDetails?.state ?? state.channelDetails?.state;
|
|
25
|
+
const record = asRecord(source);
|
|
26
|
+
const raw = record.shortTermMessages;
|
|
27
|
+
return Array.isArray(raw) ? raw : [];
|
|
28
|
+
};
|
|
29
|
+
const persistShortTermMessages = async (state, storage) => {
|
|
30
|
+
if (!storage)
|
|
31
|
+
return;
|
|
32
|
+
const shortTermMessages = state.shortTermMessages ?? [];
|
|
33
|
+
if (state.threadId) {
|
|
34
|
+
await storage.patchThreadState({
|
|
35
|
+
channelId: state.channelId,
|
|
36
|
+
threadId: state.threadId,
|
|
37
|
+
state: { shortTermMessages },
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
await storage.patchChannelState({
|
|
42
|
+
channelId: state.channelId,
|
|
43
|
+
state: { shortTermMessages },
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
async function buildSystemPrompt(state, system, context, storage, contextEngine) {
|
|
47
|
+
const sections = [];
|
|
48
|
+
if (system && typeof system === 'string')
|
|
49
|
+
sections.push(system);
|
|
50
|
+
if (system && typeof system === 'function' && context)
|
|
51
|
+
sections.push(await system(context));
|
|
52
|
+
if (contextEngine)
|
|
53
|
+
sections.push(await contextEngine.buildContext(state, storage));
|
|
54
|
+
return sections.join('\n\n');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The ai-sdk based runtime that drives OpenBot's first-party agent.
|
|
58
|
+
*
|
|
59
|
+
* It owns `agent:invoke`, runs the LLM, emits tool-call events and stitches
|
|
60
|
+
* tool results back into the conversation. The runtime is intentionally
|
|
61
|
+
* agent-package-internal: it is not a generic OpenBot abstraction.
|
|
62
|
+
*/
|
|
63
|
+
export const openBotRuntime = (options) => (builder) => {
|
|
64
|
+
const { model: modelString = 'openai/gpt-4o-mini', system, storage, contextEngine = createDefaultContextEngine(), toolDefinitions = {}, } = options;
|
|
65
|
+
const model = resolveModel(modelString);
|
|
66
|
+
const ensureShortTermMessages = (state) => {
|
|
67
|
+
if (!state.shortTermMessages || state.shortTermMessages.length === 0) {
|
|
68
|
+
state.shortTermMessages = readPersistedShortTermMessages(state);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const mapToCoreMessages = (messages) => {
|
|
72
|
+
return messages.map((m) => {
|
|
73
|
+
if (m.role === 'assistant' && m.toolCalls) {
|
|
74
|
+
return {
|
|
75
|
+
role: 'assistant',
|
|
76
|
+
content: [
|
|
77
|
+
{ type: 'text', text: m.content || '' },
|
|
78
|
+
...m.toolCalls.map((tc) => ({
|
|
79
|
+
type: 'tool-call',
|
|
80
|
+
toolCallId: tc.id,
|
|
81
|
+
toolName: tc.function.name,
|
|
82
|
+
input: JSON.parse(tc.function.arguments),
|
|
83
|
+
})),
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (m.role === 'assistant') {
|
|
88
|
+
return { role: 'assistant', content: m.content || '' };
|
|
89
|
+
}
|
|
90
|
+
if (m.role === 'tool') {
|
|
91
|
+
return {
|
|
92
|
+
role: 'tool',
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: 'tool-result',
|
|
96
|
+
toolCallId: m.toolCallId,
|
|
97
|
+
toolName: m.toolName,
|
|
98
|
+
output: { type: 'text', value: JSON.stringify(m.content) },
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return m;
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const runLLM = async function* (context, threadId) {
|
|
107
|
+
ensureShortTermMessages(context.state);
|
|
108
|
+
const systemPrompt = await buildSystemPrompt(context.state, system, context, storage, contextEngine);
|
|
109
|
+
const coreMessages = mapToCoreMessages(context.state.shortTermMessages || []);
|
|
110
|
+
try {
|
|
111
|
+
const result = await generateText({
|
|
112
|
+
model,
|
|
113
|
+
system: systemPrompt,
|
|
114
|
+
messages: coreMessages,
|
|
115
|
+
tools: toolDefinitions,
|
|
116
|
+
});
|
|
117
|
+
const toolCalls = result.toolCalls ?? [];
|
|
118
|
+
if (toolCalls.length > 0) {
|
|
119
|
+
context.state.shortTermMessages = [
|
|
120
|
+
...(context.state.shortTermMessages ?? []),
|
|
121
|
+
{
|
|
122
|
+
role: 'assistant',
|
|
123
|
+
content: result.text || '',
|
|
124
|
+
toolCalls: toolCalls.map((tc) => ({
|
|
125
|
+
id: tc.toolCallId,
|
|
126
|
+
type: 'function',
|
|
127
|
+
function: {
|
|
128
|
+
name: tc.toolName,
|
|
129
|
+
arguments: JSON.stringify(tc.input),
|
|
130
|
+
},
|
|
131
|
+
})),
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
await persistShortTermMessages(context.state, storage);
|
|
135
|
+
for (const toolCall of toolCalls) {
|
|
136
|
+
yield {
|
|
137
|
+
type: `action:${toolCall.toolName}`,
|
|
138
|
+
data: toolCall.input,
|
|
139
|
+
meta: {
|
|
140
|
+
toolCallId: toolCall.toolCallId,
|
|
141
|
+
agentId: context.state.agentId,
|
|
142
|
+
threadId,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (result.text) {
|
|
148
|
+
if (toolCalls.length === 0) {
|
|
149
|
+
context.state.shortTermMessages = [
|
|
150
|
+
...(context.state.shortTermMessages ?? []),
|
|
151
|
+
{ role: 'assistant', content: result.text },
|
|
152
|
+
];
|
|
153
|
+
await persistShortTermMessages(context.state, storage);
|
|
154
|
+
}
|
|
155
|
+
yield {
|
|
156
|
+
type: 'agent:output',
|
|
157
|
+
data: { content: result.text },
|
|
158
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
const errorMessage = error?.message || String(error);
|
|
164
|
+
const isApiKeyError = errorMessage.includes('API key') ||
|
|
165
|
+
errorMessage.includes('401') ||
|
|
166
|
+
errorMessage.includes('Unauthorized') ||
|
|
167
|
+
errorMessage.includes('authentication');
|
|
168
|
+
if (isApiKeyError) {
|
|
169
|
+
const provider = modelString.split('/')[0];
|
|
170
|
+
const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
|
|
171
|
+
yield {
|
|
172
|
+
type: 'client:ui:widget',
|
|
173
|
+
data: {
|
|
174
|
+
kind: 'form',
|
|
175
|
+
widgetId: `api_key_request_${Date.now()}`,
|
|
176
|
+
title: `${provider.toUpperCase()} API Key Required`,
|
|
177
|
+
description: `The ${provider} API returned an authentication error. Please provide a valid API key to continue. The key never leaves your local runtime.`,
|
|
178
|
+
fields: [
|
|
179
|
+
{
|
|
180
|
+
id: 'apiKey',
|
|
181
|
+
label: 'API Key',
|
|
182
|
+
type: 'text',
|
|
183
|
+
placeholder: `sk-...`,
|
|
184
|
+
required: true,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
submitLabel: 'Save API Key',
|
|
188
|
+
metadata: {
|
|
189
|
+
type: 'api_key_request',
|
|
190
|
+
provider,
|
|
191
|
+
envVar,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
195
|
+
};
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
builder.on('agent:invoke', async function* (event, context) {
|
|
202
|
+
const routedTo = event.data?.agentId;
|
|
203
|
+
if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
207
|
+
ensureShortTermMessages(context.state);
|
|
208
|
+
context.state.shortTermMessages = [
|
|
209
|
+
...(context.state.shortTermMessages ?? []),
|
|
210
|
+
{
|
|
211
|
+
role: event.data?.role || 'user',
|
|
212
|
+
content: event?.data?.content || '',
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
await persistShortTermMessages(context.state, storage);
|
|
216
|
+
yield* runLLM(context, threadId);
|
|
217
|
+
});
|
|
218
|
+
builder.on('*', async function* (event, context) {
|
|
219
|
+
if (!event.type.endsWith(':result'))
|
|
220
|
+
return;
|
|
221
|
+
if (event.meta?.agentId !== context.state.agentId)
|
|
222
|
+
return;
|
|
223
|
+
const toolCallId = event.meta?.toolCallId;
|
|
224
|
+
if (!toolCallId)
|
|
225
|
+
return;
|
|
226
|
+
ensureShortTermMessages(context.state);
|
|
227
|
+
const toolName = event.type.replace(/^action:/, '').replace(/:result$/, '');
|
|
228
|
+
const resultData = event.data;
|
|
229
|
+
const content = typeof resultData === 'string' ? resultData : JSON.stringify(resultData);
|
|
230
|
+
context.state.shortTermMessages = [
|
|
231
|
+
...(context.state.shortTermMessages ?? []),
|
|
232
|
+
{ role: 'tool', content, toolCallId, toolName },
|
|
233
|
+
];
|
|
234
|
+
await persistShortTermMessages(context.state, storage);
|
|
235
|
+
const lastAssistant = [...(context.state.shortTermMessages ?? [])]
|
|
236
|
+
.reverse()
|
|
237
|
+
.find((m) => m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0);
|
|
238
|
+
if (lastAssistant && lastAssistant.toolCalls) {
|
|
239
|
+
const allFulfilled = lastAssistant.toolCalls.every((tc) => context.state.shortTermMessages?.some((m) => m.role === 'tool' && m.toolCallId === tc.id));
|
|
240
|
+
if (allFulfilled) {
|
|
241
|
+
// handoff terminates the current agent path; the orchestrator continues with the target agent.
|
|
242
|
+
if (toolName === 'handoff')
|
|
243
|
+
return;
|
|
244
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
245
|
+
yield* runLLM(context, threadId);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
250
|
+
const { metadata, values } = event.data;
|
|
251
|
+
if (metadata?.type === 'api_key_request' && values?.apiKey) {
|
|
252
|
+
const key = metadata.envVar;
|
|
253
|
+
const value = values.apiKey;
|
|
254
|
+
if (storage) {
|
|
255
|
+
try {
|
|
256
|
+
await storage.createVariable({ key, value, secret: true });
|
|
257
|
+
yield {
|
|
258
|
+
type: 'agent:output',
|
|
259
|
+
data: {
|
|
260
|
+
content: `Successfully saved ${metadata.provider} API key to workspace variables.`,
|
|
261
|
+
},
|
|
262
|
+
meta: { agentId: context.state.agentId },
|
|
263
|
+
};
|
|
264
|
+
yield {
|
|
265
|
+
type: 'client:ui:widget',
|
|
266
|
+
data: {
|
|
267
|
+
widgetId: event.data.widgetId,
|
|
268
|
+
kind: 'message',
|
|
269
|
+
title: 'API Key Saved',
|
|
270
|
+
body: `Successfully saved ${metadata.provider} API key. You can now continue your conversation.`,
|
|
271
|
+
state: 'submitted',
|
|
272
|
+
actions: [{ id: 'ok', label: 'Got it', variant: 'primary' }],
|
|
273
|
+
},
|
|
274
|
+
meta: { agentId: context.state.agentId },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
yield {
|
|
279
|
+
type: 'agent:output',
|
|
280
|
+
data: {
|
|
281
|
+
content: `Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
282
|
+
},
|
|
283
|
+
meta: { agentId: context.state.agentId },
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const OPENBOT_SYSTEM_PROMPT = 'You are OpenBot, the primary AI assistant and orchestrator of this workspace. ' +
|
|
2
|
+
'Your goal is to help users onboard, answer questions about the system, and ' +
|
|
3
|
+
'suggest specialized agents for specific tasks.\n\n' +
|
|
4
|
+
'### How to use OpenBot:\n' +
|
|
5
|
+
'1. **General Chat**: Just type your message here, and I will help you.\n' +
|
|
6
|
+
'2. **Specialized Agents**: Use `handoff` when you want another agent to take over. ' +
|
|
7
|
+
'Use `delegate` when you want another agent to return results so you can continue.\n' +
|
|
8
|
+
'3. **Channels**: Channels are shared spaces where multiple agents can participate. ' +
|
|
9
|
+
'You can create new channels for different topics.\n' +
|
|
10
|
+
'4. **Local-First**: OpenBot runs entirely on your machine. Your data stays private and local.\n\n' +
|
|
11
|
+
'### Workflow Guidelines:\n' +
|
|
12
|
+
'- **Todo Schema**: Keep todo items simple. Each item should have a short `id`, ' +
|
|
13
|
+
'a clear `task` description, and a `status` (e.g. "pending", "in_progress", "done").\n' +
|
|
14
|
+
'- **Handoff/Delegation**: Use handoff for ownership transfer and delegation for ' +
|
|
15
|
+
'subtask-return patterns. Reference the relevant Task ID from thread state and update ' +
|
|
16
|
+
'task status with `patch_thread_details` as progress is made.\n\n' +
|
|
17
|
+
'If you need to know what agents or packages are installed, I can help you find that information.';
|
|
18
|
+
export const DEFAULT_OPENBOT_APPROVAL_RULES = [
|
|
19
|
+
{
|
|
20
|
+
action: 'action:shell_exec',
|
|
21
|
+
denyEvent: 'action:shell_exec:result',
|
|
22
|
+
message: 'The agent wants to run a terminal command.',
|
|
23
|
+
detailKeys: ['command', 'cwd', 'shell', 'timeoutMs'],
|
|
24
|
+
hiddenKeys: ['env'],
|
|
25
|
+
denyData: {
|
|
26
|
+
exitCode: null,
|
|
27
|
+
stdout: '',
|
|
28
|
+
stderr: 'Command execution was denied by the user.',
|
|
29
|
+
timedOut: false,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Tool definitions for the OpenBot orchestrator's delegation/handoff capability.
|
|
4
|
+
*
|
|
5
|
+
* The actual cross-agent routing lives in the bus's queue processor: when this
|
|
6
|
+
* plugin yields `handoff:request` / `delegation:request` events, the orchestrator
|
|
7
|
+
* intercepts them and dispatches an `agent:invoke` to the target agent.
|
|
8
|
+
*/
|
|
9
|
+
export const delegationToolDefinitions = {
|
|
10
|
+
handoff: {
|
|
11
|
+
description: 'Transfer control to another agent. The target agent continues the task and you do not wait for a tool result.',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
agentId: z.string().describe('The ID of the target agent.'),
|
|
14
|
+
content: z.string().describe('The message or task to hand off.'),
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
delegate: {
|
|
18
|
+
description: 'Delegate a subtask to another agent and wait for its result so you can continue.',
|
|
19
|
+
inputSchema: z.object({
|
|
20
|
+
agentId: z.string().describe('The ID of the target agent.'),
|
|
21
|
+
content: z.string().describe('The subtask you want the target agent to execute.'),
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export const delegationPlugin = () => (builder) => {
|
|
26
|
+
builder.on('action:handoff', async function* (event, context) {
|
|
27
|
+
const { agentId, content } = event.data;
|
|
28
|
+
yield {
|
|
29
|
+
type: 'agent:output',
|
|
30
|
+
data: { content: `Handing off to **${agentId}**: ${content}` },
|
|
31
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
32
|
+
};
|
|
33
|
+
yield {
|
|
34
|
+
type: 'handoff:request',
|
|
35
|
+
data: { agentId, content },
|
|
36
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
37
|
+
};
|
|
38
|
+
if (event.meta?.toolCallId) {
|
|
39
|
+
yield {
|
|
40
|
+
type: 'action:handoff:result',
|
|
41
|
+
data: { success: true, agentId, accepted: true },
|
|
42
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
builder.on('action:delegate', async function* (event, context) {
|
|
47
|
+
const { agentId, content } = event.data;
|
|
48
|
+
const widgetId = event.meta?.toolCallId
|
|
49
|
+
? `delegate_${event.meta.toolCallId}`
|
|
50
|
+
: `delegate_${Date.now()}`;
|
|
51
|
+
yield {
|
|
52
|
+
type: 'client:ui:widget',
|
|
53
|
+
data: {
|
|
54
|
+
kind: 'message',
|
|
55
|
+
widgetId,
|
|
56
|
+
title: `Delegation started: ${agentId}`,
|
|
57
|
+
body: `Running delegated task in background.\n\n${content}`,
|
|
58
|
+
state: 'open',
|
|
59
|
+
metadata: {
|
|
60
|
+
type: 'delegation:status',
|
|
61
|
+
phase: 'started',
|
|
62
|
+
delegatedAgentId: agentId,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
66
|
+
};
|
|
67
|
+
yield {
|
|
68
|
+
type: 'delegation:request',
|
|
69
|
+
data: { agentId, content },
|
|
70
|
+
meta: {
|
|
71
|
+
...(event.meta || {}),
|
|
72
|
+
parentAgentId: context.state.agentId,
|
|
73
|
+
delegationWidgetId: widgetId,
|
|
74
|
+
agentId: context.state.agentId,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
};
|