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.
Files changed (130) hide show
  1. package/.claude/agents/runtime-security-reviewer.md +32 -0
  2. package/.claude/hooks/format.sh +18 -0
  3. package/.claude/hooks/protect-secrets.sh +23 -0
  4. package/.claude/settings.json +26 -0
  5. package/.claude/skills/deploy/SKILL.md +26 -0
  6. package/.claude/skills/new-plugin/SKILL.md +46 -0
  7. package/.dockerignore +9 -0
  8. package/.mcp.json +8 -0
  9. package/AGENTS.md +2 -2
  10. package/CLAUDE.md +50 -0
  11. package/Dockerfile +41 -0
  12. package/deploy/README.md +61 -0
  13. package/deploy/entrypoint.sh +6 -0
  14. package/deploy/fly.toml +36 -0
  15. package/dist/app/bootstrap.js +29 -0
  16. package/dist/app/cli.js +3 -1
  17. package/dist/app/cloud-mode.js +26 -0
  18. package/dist/app/config.js +61 -16
  19. package/dist/app/server.js +38 -6
  20. package/dist/plugins/approval/index.js +1 -1
  21. package/dist/plugins/bash/index.js +386 -164
  22. package/dist/plugins/memory/service.js +2 -5
  23. package/dist/plugins/openbot/context.js +17 -1
  24. package/dist/plugins/openbot/index.js +23 -8
  25. package/dist/plugins/openbot/model.js +59 -0
  26. package/dist/plugins/openbot/runtime.js +39 -56
  27. package/dist/plugins/openbot/system-prompt.js +43 -8
  28. package/dist/plugins/preview/index.js +267 -0
  29. package/dist/plugins/storage/index.js +3 -3
  30. package/dist/plugins/storage/service.js +58 -12
  31. package/dist/plugins/todo/index.js +131 -38
  32. package/dist/plugins/todo/service.js +93 -0
  33. package/dist/plugins/ui/index.js +8 -1
  34. package/dist/services/plugins/model-registry.js +101 -0
  35. package/dist/services/plugins/registry.js +6 -4
  36. package/dist/services/plugins/service.js +2 -6
  37. package/docs/agents.md +20 -2
  38. package/docs/architecture.md +1 -1
  39. package/docs/plugins.md +13 -3
  40. package/docs/templates/AGENT.example.md +1 -1
  41. package/package.json +5 -2
  42. package/pnpm-workspace.yaml +2 -0
  43. package/src/app/bootstrap.ts +43 -0
  44. package/src/app/cli.ts +3 -1
  45. package/src/app/cloud-mode.ts +41 -0
  46. package/src/app/config.ts +71 -19
  47. package/src/app/server.ts +45 -6
  48. package/src/app/types.ts +162 -31
  49. package/src/plugins/approval/index.ts +1 -1
  50. package/src/plugins/bash/index.ts +498 -185
  51. package/src/plugins/memory/service.ts +2 -5
  52. package/src/plugins/openbot/context.ts +17 -1
  53. package/src/plugins/openbot/index.ts +25 -10
  54. package/src/plugins/openbot/model.ts +76 -0
  55. package/src/plugins/openbot/runtime.ts +47 -69
  56. package/src/plugins/openbot/system-prompt.ts +43 -8
  57. package/src/plugins/preview/index.ts +323 -0
  58. package/src/plugins/storage/index.ts +5 -8
  59. package/src/plugins/storage/service.ts +79 -15
  60. package/src/plugins/todo/index.ts +166 -0
  61. package/src/plugins/todo/service.ts +123 -0
  62. package/src/plugins/ui/index.ts +8 -1
  63. package/src/services/plugins/domain.ts +2 -0
  64. package/src/services/plugins/model-registry.ts +146 -0
  65. package/src/services/plugins/registry.ts +6 -4
  66. package/src/services/plugins/service.ts +2 -6
  67. package/dist/agents/openbot/index.js +0 -76
  68. package/dist/agents/openbot/middleware/approval.js +0 -132
  69. package/dist/agents/openbot/runtime.js +0 -289
  70. package/dist/agents/openbot/system-prompt.js +0 -32
  71. package/dist/agents/openbot/tools/delegation.js +0 -78
  72. package/dist/agents/openbot/tools/mcp.js +0 -99
  73. package/dist/agents/openbot/tools/shell.js +0 -91
  74. package/dist/agents/openbot/tools/storage.js +0 -75
  75. package/dist/agents/openbot/tools/ui.js +0 -176
  76. package/dist/agents/system.js +0 -33
  77. package/dist/bus/agent-package.js +0 -1
  78. package/dist/bus/plugin.js +0 -1
  79. package/dist/bus/services.js +0 -754
  80. package/dist/bus/types.js +0 -1
  81. package/dist/harness/agent-harness.js +0 -45
  82. package/dist/harness/agent-invoke-run.js +0 -44
  83. package/dist/harness/agent-turn.js +0 -99
  84. package/dist/harness/channel-participants.js +0 -40
  85. package/dist/harness/constants.js +0 -2
  86. package/dist/harness/context-meter.js +0 -97
  87. package/dist/harness/context.js +0 -363
  88. package/dist/harness/dispatch.js +0 -144
  89. package/dist/harness/dispatcher.js +0 -156
  90. package/dist/harness/event-normalizer.js +0 -59
  91. package/dist/harness/history.js +0 -177
  92. package/dist/harness/mcp.js +0 -61
  93. package/dist/harness/orchestration.js +0 -88
  94. package/dist/harness/orchestrator.js +0 -149
  95. package/dist/harness/participants.js +0 -22
  96. package/dist/harness/process.js +0 -29
  97. package/dist/harness/queue-processor.js +0 -187
  98. package/dist/harness/run-harness.js +0 -154
  99. package/dist/harness/run.js +0 -98
  100. package/dist/harness/runtime-factory.js +0 -73
  101. package/dist/harness/runtime.js +0 -57
  102. package/dist/harness/todo-advance.js +0 -93
  103. package/dist/harness/todo-dispatch.js +0 -51
  104. package/dist/harness/todos.js +0 -5
  105. package/dist/harness/turn.js +0 -79
  106. package/dist/harness/types.js +0 -1
  107. package/dist/plugins/ai-sdk/index.js +0 -34
  108. package/dist/plugins/ai-sdk/runtime.js +0 -395
  109. package/dist/plugins/ai-sdk/system-prompt.js +0 -18
  110. package/dist/plugins/ai-sdk.js +0 -331
  111. package/dist/plugins/approval.js +0 -163
  112. package/dist/plugins/delegation.js +0 -108
  113. package/dist/plugins/mcp/index.js +0 -108
  114. package/dist/plugins/mcp.js +0 -140
  115. package/dist/plugins/shell/index.js +0 -100
  116. package/dist/plugins/shell.js +0 -123
  117. package/dist/plugins/storage-tools/index.js +0 -76
  118. package/dist/plugins/storage.js +0 -737
  119. package/dist/plugins/thread-namer/index.js +0 -72
  120. package/dist/plugins/threads/index.js +0 -114
  121. package/dist/plugins/ui.js +0 -211
  122. package/dist/plugins/workflow/index.js +0 -65
  123. package/dist/registry/agents.js +0 -138
  124. package/dist/registry/plugins.js +0 -110
  125. package/dist/services/agent-packages.js +0 -103
  126. package/dist/services/memory.js +0 -152
  127. package/dist/services/plugins.js +0 -98
  128. package/dist/services/storage.js +0 -1028
  129. package/dist/workflow/service.js +0 -106
  130. package/dist/workflow/types.js +0 -3
@@ -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
- };
@@ -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
- };