openbot 0.4.7 → 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/server.js +38 -6
- 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 +23 -8
- package/dist/plugins/openbot/model.js +59 -0
- package/dist/plugins/openbot/runtime.js +39 -56
- package/dist/plugins/openbot/system-prompt.js +43 -8
- 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/server.ts +45 -6
- package/src/app/types.ts +162 -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 +25 -10
- package/src/plugins/openbot/model.ts +76 -0
- package/src/plugins/openbot/runtime.ts +47 -69
- package/src/plugins/openbot/system-prompt.ts +43 -8
- 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
package/dist/plugins/ai-sdk.js
DELETED
|
@@ -1,331 +0,0 @@
|
|
|
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
|
-
/**
|
|
6
|
-
* Resolves a standardized model string to an AI SDK LanguageModel.
|
|
7
|
-
*/
|
|
8
|
-
function resolveModel(modelString) {
|
|
9
|
-
const [provider, ...rest] = modelString.split('/');
|
|
10
|
-
const modelId = rest.join('/');
|
|
11
|
-
if (!modelId) {
|
|
12
|
-
throw new Error(`Invalid model string: "${modelString}". Expected format: "provider/model-id"`);
|
|
13
|
-
}
|
|
14
|
-
switch (provider) {
|
|
15
|
-
case 'openai':
|
|
16
|
-
return openai(modelId);
|
|
17
|
-
case 'anthropic':
|
|
18
|
-
return anthropic(modelId);
|
|
19
|
-
default:
|
|
20
|
-
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
24
|
-
const readPersistedShortTermMessages = (state) => {
|
|
25
|
-
const source = state.threadDetails?.state ?? state.channelDetails?.state;
|
|
26
|
-
const record = asRecord(source);
|
|
27
|
-
const raw = record.shortTermMessages;
|
|
28
|
-
return Array.isArray(raw) ? raw : [];
|
|
29
|
-
};
|
|
30
|
-
const persistShortTermMessages = async (state, storage) => {
|
|
31
|
-
if (!storage)
|
|
32
|
-
return;
|
|
33
|
-
const shortTermMessages = state.shortTermMessages ?? [];
|
|
34
|
-
if (state.threadId) {
|
|
35
|
-
await storage.patchThreadState({
|
|
36
|
-
channelId: state.channelId,
|
|
37
|
-
threadId: state.threadId,
|
|
38
|
-
state: { shortTermMessages },
|
|
39
|
-
});
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
await storage.patchChannelState({
|
|
43
|
-
channelId: state.channelId,
|
|
44
|
-
state: { shortTermMessages },
|
|
45
|
-
});
|
|
46
|
-
};
|
|
47
|
-
async function buildSystemPrompt(state, system, context, storage, contextEngine) {
|
|
48
|
-
const sections = [];
|
|
49
|
-
if (system && typeof system === 'string') {
|
|
50
|
-
sections.push(system);
|
|
51
|
-
}
|
|
52
|
-
if (system && typeof system === 'function' && context) {
|
|
53
|
-
sections.push(await system(context));
|
|
54
|
-
}
|
|
55
|
-
if (contextEngine) {
|
|
56
|
-
sections.push(await contextEngine.buildContext(state, storage));
|
|
57
|
-
}
|
|
58
|
-
return sections.join('\n\n');
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* AI SDK Plugin for Melony.
|
|
62
|
-
* Automatically handles text events and routes them through an AI SDK using Vercel AI SDK.
|
|
63
|
-
* It can also automatically trigger events based on tool calls.
|
|
64
|
-
*/
|
|
65
|
-
export const aiSdkPlugin = (options) => (builder) => {
|
|
66
|
-
const { model: modelString = 'openai/gpt-4o-mini', system, storage, contextEngine = createDefaultContextEngine(), toolDefinitions = {}, } = options;
|
|
67
|
-
const model = resolveModel(modelString);
|
|
68
|
-
const ensureShortTermMessages = (state) => {
|
|
69
|
-
if (!state.shortTermMessages || state.shortTermMessages.length === 0) {
|
|
70
|
-
state.shortTermMessages = readPersistedShortTermMessages(state);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
const mapToCoreMessages = (messages) => {
|
|
74
|
-
return messages.map((m) => {
|
|
75
|
-
if (m.role === 'assistant' && m.toolCalls) {
|
|
76
|
-
const assistantContent = [
|
|
77
|
-
{
|
|
78
|
-
role: 'assistant',
|
|
79
|
-
content: [
|
|
80
|
-
{ type: 'text', text: m.content || '' },
|
|
81
|
-
...m.toolCalls.map((tc) => ({
|
|
82
|
-
type: 'tool-call',
|
|
83
|
-
toolCallId: tc.id,
|
|
84
|
-
toolName: tc.function.name,
|
|
85
|
-
input: JSON.parse(tc.function.arguments),
|
|
86
|
-
})),
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
];
|
|
90
|
-
return assistantContent[0];
|
|
91
|
-
}
|
|
92
|
-
if (m.role === 'assistant') {
|
|
93
|
-
return {
|
|
94
|
-
role: 'assistant',
|
|
95
|
-
content: m.content || '',
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
if (m.role === 'tool') {
|
|
99
|
-
return {
|
|
100
|
-
role: 'tool',
|
|
101
|
-
content: [
|
|
102
|
-
{
|
|
103
|
-
type: 'tool-result',
|
|
104
|
-
toolCallId: m.toolCallId,
|
|
105
|
-
toolName: m.toolName,
|
|
106
|
-
output: {
|
|
107
|
-
type: 'text',
|
|
108
|
-
value: JSON.stringify(m.content),
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return m;
|
|
115
|
-
});
|
|
116
|
-
};
|
|
117
|
-
const runLLM = async function* (context, threadId) {
|
|
118
|
-
ensureShortTermMessages(context.state);
|
|
119
|
-
const systemPrompt = await buildSystemPrompt(context.state, system, context, storage, contextEngine);
|
|
120
|
-
const coreMessages = mapToCoreMessages(context.state.shortTermMessages || []);
|
|
121
|
-
try {
|
|
122
|
-
const result = await generateText({
|
|
123
|
-
model,
|
|
124
|
-
system: systemPrompt,
|
|
125
|
-
messages: coreMessages,
|
|
126
|
-
tools: toolDefinitions,
|
|
127
|
-
});
|
|
128
|
-
const toolCalls = result.toolCalls ?? [];
|
|
129
|
-
if (toolCalls.length > 0) {
|
|
130
|
-
// Add assistant message with tool calls to state
|
|
131
|
-
context.state.shortTermMessages = [
|
|
132
|
-
...(context.state.shortTermMessages ?? []),
|
|
133
|
-
{
|
|
134
|
-
role: 'assistant',
|
|
135
|
-
content: result.text || '',
|
|
136
|
-
toolCalls: toolCalls.map((tc) => ({
|
|
137
|
-
id: tc.toolCallId,
|
|
138
|
-
type: 'function',
|
|
139
|
-
function: {
|
|
140
|
-
name: tc.toolName,
|
|
141
|
-
arguments: JSON.stringify(tc.input),
|
|
142
|
-
},
|
|
143
|
-
})),
|
|
144
|
-
},
|
|
145
|
-
];
|
|
146
|
-
await persistShortTermMessages(context.state, storage);
|
|
147
|
-
for (const toolCall of toolCalls) {
|
|
148
|
-
yield {
|
|
149
|
-
type: `action:${toolCall.toolName}`,
|
|
150
|
-
data: toolCall.input,
|
|
151
|
-
meta: {
|
|
152
|
-
toolCallId: toolCall.toolCallId,
|
|
153
|
-
agentId: context.state.agentId,
|
|
154
|
-
threadId,
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (result.text) {
|
|
160
|
-
if (toolCalls.length === 0) {
|
|
161
|
-
context.state.shortTermMessages = [
|
|
162
|
-
...(context.state.shortTermMessages ?? []),
|
|
163
|
-
{ role: 'assistant', content: result.text },
|
|
164
|
-
];
|
|
165
|
-
await persistShortTermMessages(context.state, storage);
|
|
166
|
-
}
|
|
167
|
-
yield {
|
|
168
|
-
type: 'agent:output',
|
|
169
|
-
data: {
|
|
170
|
-
content: result.text,
|
|
171
|
-
},
|
|
172
|
-
meta: {
|
|
173
|
-
agentId: context.state.agentId,
|
|
174
|
-
threadId,
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
const errorMessage = error?.message || String(error);
|
|
181
|
-
const isApiKeyError = errorMessage.includes('API key') ||
|
|
182
|
-
errorMessage.includes('401') ||
|
|
183
|
-
errorMessage.includes('Unauthorized') ||
|
|
184
|
-
errorMessage.includes('authentication');
|
|
185
|
-
if (isApiKeyError) {
|
|
186
|
-
const provider = modelString.split('/')[0];
|
|
187
|
-
const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
|
|
188
|
-
yield {
|
|
189
|
-
type: 'client:ui:widget',
|
|
190
|
-
data: {
|
|
191
|
-
kind: 'form',
|
|
192
|
-
widgetId: `api_key_request_${Date.now()}`,
|
|
193
|
-
title: `${provider.toUpperCase()} API Key Required`,
|
|
194
|
-
description: `The ${provider} API returned an authentication error. Please provide a valid API key to continue. The key provided here never leaves your runtime Computer.`,
|
|
195
|
-
fields: [
|
|
196
|
-
{
|
|
197
|
-
id: 'apiKey',
|
|
198
|
-
label: 'API Key',
|
|
199
|
-
type: 'text',
|
|
200
|
-
placeholder: `sk-...`,
|
|
201
|
-
required: true,
|
|
202
|
-
},
|
|
203
|
-
],
|
|
204
|
-
submitLabel: 'Save API Key',
|
|
205
|
-
metadata: {
|
|
206
|
-
type: 'api_key_request',
|
|
207
|
-
provider,
|
|
208
|
-
envVar,
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
meta: {
|
|
212
|
-
agentId: context.state.agentId,
|
|
213
|
-
threadId,
|
|
214
|
-
},
|
|
215
|
-
};
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
// Re-throw other errors
|
|
219
|
-
throw error;
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
builder.on('agent:invoke', async function* (event, context) {
|
|
223
|
-
// if the agent:invoke is routed to a different agent, don't process it, it prevents infinite loops
|
|
224
|
-
const routedTo = event.data?.agentId;
|
|
225
|
-
if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
// extract threadId if model decides to reply in a thread
|
|
229
|
-
const threadId = event.meta?.threadId || context.state.threadId;
|
|
230
|
-
ensureShortTermMessages(context.state);
|
|
231
|
-
context.state.shortTermMessages = [
|
|
232
|
-
...(context.state.shortTermMessages ?? []),
|
|
233
|
-
{
|
|
234
|
-
role: event.data?.role || 'user',
|
|
235
|
-
content: (event)?.data?.content || '',
|
|
236
|
-
},
|
|
237
|
-
];
|
|
238
|
-
await persistShortTermMessages(context.state, storage);
|
|
239
|
-
yield* runLLM(context, threadId);
|
|
240
|
-
});
|
|
241
|
-
builder.on('*', async function* (event, context) {
|
|
242
|
-
if (!event.type.endsWith(':result'))
|
|
243
|
-
return;
|
|
244
|
-
if (event.meta?.agentId !== context.state.agentId)
|
|
245
|
-
return;
|
|
246
|
-
const toolCallId = event.meta?.toolCallId;
|
|
247
|
-
if (!toolCallId)
|
|
248
|
-
return;
|
|
249
|
-
ensureShortTermMessages(context.state);
|
|
250
|
-
// Extract tool name from event type (e.g., action:shell_exec:result -> shell_exec)
|
|
251
|
-
const toolName = event.type.replace(/^action:/, '').replace(/:result$/, '');
|
|
252
|
-
// Add tool result to state
|
|
253
|
-
const resultData = event.data;
|
|
254
|
-
const content = typeof resultData === 'string' ? resultData : JSON.stringify(resultData);
|
|
255
|
-
context.state.shortTermMessages = [
|
|
256
|
-
...(context.state.shortTermMessages ?? []),
|
|
257
|
-
{ role: 'tool', content, toolCallId, toolName },
|
|
258
|
-
];
|
|
259
|
-
await persistShortTermMessages(context.state, storage);
|
|
260
|
-
// Check if we should re-trigger LLM
|
|
261
|
-
// We re-trigger if the last assistant message's tool calls are all fulfilled
|
|
262
|
-
const lastAssistant = [...(context.state.shortTermMessages ?? [])]
|
|
263
|
-
.reverse()
|
|
264
|
-
.find((m) => m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0);
|
|
265
|
-
if (lastAssistant && lastAssistant.toolCalls) {
|
|
266
|
-
const allFulfilled = lastAssistant.toolCalls.every((tc) => context.state.shortTermMessages?.some((m) => m.role === 'tool' && m.toolCallId === tc.id));
|
|
267
|
-
if (allFulfilled) {
|
|
268
|
-
// Some tools intentionally terminate the current agent path (e.g., handoff).
|
|
269
|
-
// We still persist their tool result for provider consistency, but do not continue this agent.
|
|
270
|
-
if (toolName === 'handoff')
|
|
271
|
-
return;
|
|
272
|
-
const threadId = event.meta?.threadId || context.state.threadId;
|
|
273
|
-
yield* runLLM(context, threadId);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
278
|
-
const { metadata, values } = event.data;
|
|
279
|
-
if (metadata?.type === 'api_key_request' && values?.apiKey) {
|
|
280
|
-
const key = metadata.envVar;
|
|
281
|
-
const value = values.apiKey;
|
|
282
|
-
if (storage) {
|
|
283
|
-
try {
|
|
284
|
-
await storage.createVariable({ key, value, secret: true });
|
|
285
|
-
yield {
|
|
286
|
-
type: 'agent:output',
|
|
287
|
-
data: {
|
|
288
|
-
content: `Successfully saved ${metadata.provider} API key to workspace variables.`,
|
|
289
|
-
},
|
|
290
|
-
meta: {
|
|
291
|
-
agentId: context.state.agentId,
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
// Update the widget to show success
|
|
295
|
-
yield {
|
|
296
|
-
type: 'client:ui:widget',
|
|
297
|
-
data: {
|
|
298
|
-
widgetId: event.data.widgetId,
|
|
299
|
-
kind: 'message',
|
|
300
|
-
title: 'API Key Saved',
|
|
301
|
-
body: `Successfully saved ${metadata.provider} API key. You can now continue your conversation.`,
|
|
302
|
-
state: 'submitted',
|
|
303
|
-
actions: [{ id: 'ok', label: 'Got it', variant: 'primary' }],
|
|
304
|
-
},
|
|
305
|
-
meta: {
|
|
306
|
-
agentId: context.state.agentId,
|
|
307
|
-
},
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
catch (error) {
|
|
311
|
-
yield {
|
|
312
|
-
type: 'agent:output',
|
|
313
|
-
data: {
|
|
314
|
-
content: `Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
315
|
-
},
|
|
316
|
-
meta: {
|
|
317
|
-
agentId: context.state.agentId,
|
|
318
|
-
},
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
};
|
|
325
|
-
export const plugin = {
|
|
326
|
-
id: 'ai-sdk',
|
|
327
|
-
name: 'AI SDK',
|
|
328
|
-
description: 'Built-in AI SDK plugin',
|
|
329
|
-
kind: 'runtime',
|
|
330
|
-
factory: () => aiSdkPlugin({}),
|
|
331
|
-
};
|
package/dist/plugins/approval.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { storageService } from '../services/storage.js';
|
|
2
|
-
const DEFAULT_RULES = [];
|
|
3
|
-
const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
4
|
-
const getApprovalsFromState = (state) => {
|
|
5
|
-
const source = state.threadDetails?.state ?? state.channelDetails?.state;
|
|
6
|
-
const stateRecord = asRecord(source);
|
|
7
|
-
return asRecord(stateRecord.approvals);
|
|
8
|
-
};
|
|
9
|
-
const persistApprovals = async (state, approvals) => {
|
|
10
|
-
if (state.threadId) {
|
|
11
|
-
await storageService.patchThreadState({
|
|
12
|
-
channelId: state.channelId,
|
|
13
|
-
threadId: state.threadId,
|
|
14
|
-
state: { approvals },
|
|
15
|
-
});
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
await storageService.patchChannelState({
|
|
19
|
-
channelId: state.channelId,
|
|
20
|
-
state: { approvals },
|
|
21
|
-
});
|
|
22
|
-
};
|
|
23
|
-
export const approvalPlugin = (options = {}) => (builder) => {
|
|
24
|
-
const rules = options.rules && options.rules.length > 0 ? options.rules : DEFAULT_RULES;
|
|
25
|
-
for (const rule of rules) {
|
|
26
|
-
builder.on(rule.action, async function* (event, context) {
|
|
27
|
-
const meta = asRecord(event.meta);
|
|
28
|
-
if (meta.approvalStatus === 'approved') {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
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: {
|
|
66
|
-
type: 'approval:request',
|
|
67
|
-
approvalId,
|
|
68
|
-
action: rule.action,
|
|
69
|
-
},
|
|
70
|
-
actions: [
|
|
71
|
-
{ id: 'approve', label: 'Approve', variant: 'primary' },
|
|
72
|
-
{ id: 'deny', label: 'Deny', variant: 'danger' },
|
|
73
|
-
],
|
|
74
|
-
},
|
|
75
|
-
meta: {
|
|
76
|
-
...(event.meta || {}),
|
|
77
|
-
agentId: context.state.agentId,
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
yield {
|
|
81
|
-
type: 'agent:output',
|
|
82
|
-
data: {
|
|
83
|
-
content: `Waiting for approval before running \`${rule.action}\`.`,
|
|
84
|
-
},
|
|
85
|
-
meta: {
|
|
86
|
-
...(event.meta || {}),
|
|
87
|
-
agentId: context.state.agentId,
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
context.suspend();
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
94
|
-
const metadata = asRecord(event.data?.metadata);
|
|
95
|
-
if (metadata.type !== 'approval:request') {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const approvalId = String(metadata.approvalId || '');
|
|
99
|
-
if (!approvalId) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const approvals = getApprovalsFromState(context.state);
|
|
103
|
-
const approval = approvals[approvalId];
|
|
104
|
-
if (!approval || approval.status !== 'pending') {
|
|
105
|
-
yield {
|
|
106
|
-
type: 'agent:output',
|
|
107
|
-
data: { content: 'Approval request not found or already resolved.' },
|
|
108
|
-
meta: {
|
|
109
|
-
...(event.meta || {}),
|
|
110
|
-
agentId: context.state.agentId,
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const approved = event.data.actionId === 'approve';
|
|
116
|
-
approvals[approvalId] = {
|
|
117
|
-
...approval,
|
|
118
|
-
status: approved ? 'approved' : 'denied',
|
|
119
|
-
};
|
|
120
|
-
await persistApprovals(context.state, approvals);
|
|
121
|
-
if (approved) {
|
|
122
|
-
yield {
|
|
123
|
-
type: approval.executeEvent,
|
|
124
|
-
data: approval.payload,
|
|
125
|
-
meta: {
|
|
126
|
-
...(approval.meta || {}),
|
|
127
|
-
approvalId,
|
|
128
|
-
approvalStatus: 'approved',
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
yield {
|
|
134
|
-
type: approval.denyEvent,
|
|
135
|
-
data: {
|
|
136
|
-
success: false,
|
|
137
|
-
approved: false,
|
|
138
|
-
error: 'Action denied by user approval.',
|
|
139
|
-
...approval.denyData,
|
|
140
|
-
},
|
|
141
|
-
meta: {
|
|
142
|
-
...(approval.meta || {}),
|
|
143
|
-
approvalId,
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
yield {
|
|
147
|
-
type: 'agent:output',
|
|
148
|
-
data: { content: 'Action denied by user approval.' },
|
|
149
|
-
meta: {
|
|
150
|
-
...(event.meta || {}),
|
|
151
|
-
agentId: context.state.agentId,
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
});
|
|
155
|
-
};
|
|
156
|
-
export const plugin = {
|
|
157
|
-
name: 'approval',
|
|
158
|
-
description: 'Approval workflow for protected actions',
|
|
159
|
-
version: '1.0.0',
|
|
160
|
-
author: 'OpenBot',
|
|
161
|
-
license: 'MIT',
|
|
162
|
-
factory: approvalPlugin,
|
|
163
|
-
};
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import z from 'zod';
|
|
2
|
-
/**
|
|
3
|
-
* Delegation Plugin for Melony.
|
|
4
|
-
* Handles handoff/delegation events and routes internal control events to the orchestrator.
|
|
5
|
-
*/
|
|
6
|
-
export const delegationPlugin = () => (builder) => {
|
|
7
|
-
builder.on('action:handoff', async function* (event, context) {
|
|
8
|
-
const { agentId, content } = event.data;
|
|
9
|
-
// 1. Show the handoff in the UI
|
|
10
|
-
yield {
|
|
11
|
-
type: 'agent:output',
|
|
12
|
-
data: {
|
|
13
|
-
content: `Handing off to **${agentId}**: ${content}`,
|
|
14
|
-
},
|
|
15
|
-
meta: {
|
|
16
|
-
...(event.meta || {}),
|
|
17
|
-
agentId: context.state.agentId,
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
// 2. Orchestrator turns this into a real agent:invoke for the target.
|
|
21
|
-
yield {
|
|
22
|
-
type: 'handoff:request',
|
|
23
|
-
data: { agentId, content },
|
|
24
|
-
meta: {
|
|
25
|
-
...(event.meta || {}),
|
|
26
|
-
agentId: context.state.agentId,
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
// 3. Acknowledge tool completion so providers requiring tool-result pairing stay consistent.
|
|
30
|
-
if (event.meta?.toolCallId) {
|
|
31
|
-
yield {
|
|
32
|
-
type: 'action:handoff:result',
|
|
33
|
-
data: {
|
|
34
|
-
success: true,
|
|
35
|
-
agentId,
|
|
36
|
-
accepted: true,
|
|
37
|
-
},
|
|
38
|
-
meta: {
|
|
39
|
-
...(event.meta || {}),
|
|
40
|
-
agentId: context.state.agentId,
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
builder.on('action:delegate', async function* (event, context) {
|
|
46
|
-
const { agentId, content } = event.data;
|
|
47
|
-
const widgetId = event.meta?.toolCallId
|
|
48
|
-
? `delegate_${event.meta.toolCallId}`
|
|
49
|
-
: `delegate_${Date.now()}`;
|
|
50
|
-
// 1. Show delegation progress in UI (child output stays hidden for delegate mode).
|
|
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: {
|
|
66
|
-
...(event.meta || {}),
|
|
67
|
-
agentId: context.state.agentId,
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
// 2. Orchestrator executes target agent and feeds result back as action:delegate:result.
|
|
71
|
-
yield {
|
|
72
|
-
type: 'delegation:request',
|
|
73
|
-
data: { agentId, content },
|
|
74
|
-
meta: {
|
|
75
|
-
...(event.meta || {}),
|
|
76
|
-
parentAgentId: context.state.agentId,
|
|
77
|
-
delegationWidgetId: widgetId,
|
|
78
|
-
agentId: context.state.agentId,
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
};
|
|
83
|
-
export const delegationToolDefinitions = {
|
|
84
|
-
handoff: {
|
|
85
|
-
description: 'Transfer control to another agent. The target agent continues the task and you do not wait for a tool result.',
|
|
86
|
-
inputSchema: z.object({
|
|
87
|
-
agentId: z.string().describe('The ID of the target agent (e.g. "os", "browser", "tavily").'),
|
|
88
|
-
content: z.string().describe('The message or task to hand off.'),
|
|
89
|
-
}),
|
|
90
|
-
},
|
|
91
|
-
delegate: {
|
|
92
|
-
description: 'Delegate a subtask to another agent and wait for its result so you can continue.',
|
|
93
|
-
inputSchema: z.object({
|
|
94
|
-
agentId: z.string().describe('The ID of the target agent (e.g. "os", "browser", "tavily").'),
|
|
95
|
-
content: z.string().describe('The subtask you want the target agent to execute.'),
|
|
96
|
-
}),
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
export const plugin = {
|
|
100
|
-
name: 'delegation',
|
|
101
|
-
description: 'Delegation plugin',
|
|
102
|
-
version: '1.0.0',
|
|
103
|
-
author: 'OpenBot',
|
|
104
|
-
license: 'MIT',
|
|
105
|
-
website: 'https://openbot.one',
|
|
106
|
-
factory: delegationPlugin,
|
|
107
|
-
toolDefinitions: delegationToolDefinitions,
|
|
108
|
-
};
|