openbot 0.2.12 → 0.2.13
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/.prettierrc +8 -0
- package/AGENTS.md +68 -0
- package/CONTRIBUTING.md +74 -0
- package/LICENSE +21 -0
- package/README.md +117 -14
- package/dist/agents/system.js +106 -0
- package/dist/app/cli.js +27 -0
- package/dist/app/config.js +64 -0
- package/dist/app/server.js +237 -0
- package/dist/app/utils.js +35 -0
- package/dist/harness/agent-harness.js +45 -0
- package/dist/harness/mcp.js +61 -0
- package/dist/harness/orchestrator.js +273 -0
- package/dist/harness/process.js +7 -0
- package/dist/plugins/ai-sdk.js +141 -0
- package/dist/plugins/delegation.js +52 -0
- package/dist/plugins/mcp.js +140 -0
- package/dist/plugins/storage.js +502 -0
- package/dist/plugins/ui.js +47 -0
- package/dist/registry/plugins.js +73 -0
- package/dist/services/storage.js +724 -0
- package/docs/README.md +7 -0
- package/docs/agents.md +83 -0
- package/docs/architecture.md +34 -0
- package/docs/plugins.md +77 -0
- package/logo-black.png +0 -0
- package/{dist/assets/logo.js → logo-black.svg} +24 -24
- package/{dist/ui/sidebar.js → logo-white.svg} +23 -88
- package/package.json +10 -9
- package/src/agents/system.ts +112 -0
- package/src/app/cli.ts +38 -0
- package/src/app/config.ts +104 -0
- package/src/app/server.ts +284 -0
- package/src/app/types.ts +476 -0
- package/src/app/utils.ts +43 -0
- package/src/assets/icon.svg +1 -0
- package/src/harness/agent-harness.ts +58 -0
- package/src/harness/mcp.ts +78 -0
- package/src/harness/orchestrator.ts +342 -0
- package/src/harness/process.ts +9 -0
- package/src/harness/types.ts +34 -0
- package/src/plugins/ai-sdk.ts +197 -0
- package/src/plugins/delegation.ts +60 -0
- package/src/plugins/mcp.ts +154 -0
- package/src/plugins/storage.ts +725 -0
- package/src/plugins/ui.ts +57 -0
- package/src/registry/plugins.ts +85 -0
- package/src/services/storage.ts +957 -0
- package/tsconfig.json +18 -0
- package/dist/agents/agent-creator.js +0 -74
- package/dist/agents/browser-agent.js +0 -31
- package/dist/agents/os-agent.js +0 -32
- package/dist/agents/planner-agent.js +0 -32
- package/dist/agents/topic-agent.js +0 -46
- package/dist/architecture/execution-engine.js +0 -151
- package/dist/architecture/intent-classifier.js +0 -26
- package/dist/architecture/planner.js +0 -106
- package/dist/automation-worker.js +0 -121
- package/dist/automations.js +0 -52
- package/dist/cli.js +0 -279
- package/dist/config.js +0 -53
- package/dist/core/agents.js +0 -41
- package/dist/core/delegation.js +0 -230
- package/dist/core/manager.js +0 -96
- package/dist/core/plugins.js +0 -74
- package/dist/core/router.js +0 -191
- package/dist/handlers/init.js +0 -29
- package/dist/handlers/session-change.js +0 -21
- package/dist/handlers/settings.js +0 -47
- package/dist/handlers/tab-change.js +0 -14
- package/dist/installers.js +0 -156
- package/dist/marketplace.js +0 -80
- package/dist/model-catalog.js +0 -132
- package/dist/model-defaults.js +0 -25
- package/dist/models.js +0 -47
- package/dist/open-bot.js +0 -51
- package/dist/orchestrator/direct-invocation.js +0 -13
- package/dist/orchestrator/events.js +0 -36
- package/dist/orchestrator/state.js +0 -54
- package/dist/orchestrator.js +0 -422
- package/dist/plugins/agent/index.js +0 -81
- package/dist/plugins/approval/index.js +0 -100
- package/dist/plugins/brain/identity.js +0 -77
- package/dist/plugins/brain/index.js +0 -204
- package/dist/plugins/brain/memory.js +0 -120
- package/dist/plugins/brain/prompt.js +0 -46
- package/dist/plugins/brain/types.js +0 -45
- package/dist/plugins/brain/ui.js +0 -7
- package/dist/plugins/browser/index.js +0 -629
- package/dist/plugins/browser/ui.js +0 -13
- package/dist/plugins/file-system/index.js +0 -171
- package/dist/plugins/file-system/ui.js +0 -6
- package/dist/plugins/llm/context-budget.js +0 -139
- package/dist/plugins/llm/context-shaping.js +0 -177
- package/dist/plugins/llm/index.js +0 -380
- package/dist/plugins/memory/index.js +0 -220
- package/dist/plugins/memory/memory.js +0 -122
- package/dist/plugins/memory/prompt.js +0 -55
- package/dist/plugins/memory/types.js +0 -45
- package/dist/plugins/meta-agent/index.js +0 -570
- package/dist/plugins/meta-agent/ui.js +0 -11
- package/dist/plugins/shell/index.js +0 -100
- package/dist/plugins/shell/ui.js +0 -6
- package/dist/plugins/skills/index.js +0 -286
- package/dist/plugins/skills/types.js +0 -50
- package/dist/plugins/skills/ui.js +0 -12
- package/dist/registry/agent-registry.js +0 -35
- package/dist/registry/index.js +0 -2
- package/dist/registry/plugin-loader.js +0 -499
- package/dist/registry/plugin-registry.js +0 -44
- package/dist/registry/ts-agent-loader.js +0 -82
- package/dist/registry/yaml-agent-loader.js +0 -246
- package/dist/runtime/execution-trace.js +0 -41
- package/dist/runtime/intent-routing.js +0 -26
- package/dist/runtime/openbot-runtime.js +0 -354
- package/dist/server.js +0 -890
- package/dist/session.js +0 -179
- package/dist/ui/block.js +0 -12
- package/dist/ui/header.js +0 -52
- package/dist/ui/layout.js +0 -26
- package/dist/ui/navigation.js +0 -15
- package/dist/ui/settings.js +0 -106
- package/dist/ui/skills.js +0 -7
- package/dist/ui/thread.js +0 -16
- package/dist/ui/widgets/action-list.js +0 -2
- package/dist/ui/widgets/approval-card.js +0 -9
- package/dist/ui/widgets/code-snippet.js +0 -2
- package/dist/ui/widgets/data-block.js +0 -2
- package/dist/ui/widgets/data-table.js +0 -2
- package/dist/ui/widgets/delegation.js +0 -29
- package/dist/ui/widgets/empty-state.js +0 -2
- package/dist/ui/widgets/index.js +0 -23
- package/dist/ui/widgets/inquiry.js +0 -7
- package/dist/ui/widgets/key-value.js +0 -2
- package/dist/ui/widgets/progress-step.js +0 -2
- package/dist/ui/widgets/resource-card.js +0 -2
- package/dist/ui/widgets/status.js +0 -2
- package/dist/ui/widgets/todo-list.js +0 -2
- package/dist/version.js +0 -62
- /package/dist/{types.js → app/types.js} +0 -0
- /package/dist/{architecture/contracts.js → harness/types.js} +0 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { melony, Runtime } from 'melony';
|
|
2
|
+
import { AgentInvokeEvent, OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
3
|
+
import { resolvePlugin } from '../registry/plugins.js';
|
|
4
|
+
import { storageService } from '../services/storage.js';
|
|
5
|
+
import { ensureEventId } from '../app/utils.js';
|
|
6
|
+
import { loadConfig, PluginSpec } from '../app/config.js';
|
|
7
|
+
|
|
8
|
+
export interface ExecuteAgentOptions {
|
|
9
|
+
runId: string;
|
|
10
|
+
agentId: string;
|
|
11
|
+
event: OpenBotEvent;
|
|
12
|
+
channelId: string;
|
|
13
|
+
threadId?: string;
|
|
14
|
+
onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DispatchOptions {
|
|
18
|
+
runId: string;
|
|
19
|
+
agentId?: string;
|
|
20
|
+
event: OpenBotEvent;
|
|
21
|
+
channelId: string;
|
|
22
|
+
threadId?: string;
|
|
23
|
+
onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Enhances agent instructions with a list of other available agents.
|
|
28
|
+
*/
|
|
29
|
+
export async function enhanceInstructions(state: OpenBotState) {
|
|
30
|
+
const { agentId, agentDetails } = state;
|
|
31
|
+
if (!agentDetails) return;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const agents = await storageService.getAgents();
|
|
35
|
+
const otherAgents = agents.filter((a) => a.id !== agentId);
|
|
36
|
+
if (otherAgents.length === 0) return;
|
|
37
|
+
|
|
38
|
+
const agentsList = otherAgents
|
|
39
|
+
.map((a) => `- **${a.id}**${a.description ? `: ${a.description}` : ''}`)
|
|
40
|
+
.join('\n');
|
|
41
|
+
|
|
42
|
+
const header = '### Available Agents for Delegation:';
|
|
43
|
+
if (!agentDetails.instructions.includes(header)) {
|
|
44
|
+
agentDetails.instructions += `\n\n${header}\n${agentsList}\n\nYou can use the \`delegate\` tool to task these agents. Use their ID (the bold part) when delegating.`;
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.warn('[agent] Failed to enhance instructions', error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Factory for creating an OpenBot Melony Runtime.
|
|
53
|
+
*/
|
|
54
|
+
async function createAgentRuntime(
|
|
55
|
+
state: OpenBotState,
|
|
56
|
+
): Promise<Runtime<OpenBotState, OpenBotEvent>> {
|
|
57
|
+
// 1. Prepare instructions
|
|
58
|
+
await enhanceInstructions(state);
|
|
59
|
+
|
|
60
|
+
// 2. Initialize runtime with the agent plugin
|
|
61
|
+
const runtime = melony<OpenBotState, OpenBotEvent>({
|
|
62
|
+
initialState: state,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 3. Normalize plugin specs:
|
|
66
|
+
// - runtime can be a single spec or an array (for backward/forward compatibility)
|
|
67
|
+
// - plugins remains supported as additional specs
|
|
68
|
+
const runtimeSpecs = Array.isArray(state.agentDetails?.runtime)
|
|
69
|
+
? state.agentDetails.runtime
|
|
70
|
+
: state.agentDetails?.runtime
|
|
71
|
+
? [state.agentDetails.runtime]
|
|
72
|
+
: [];
|
|
73
|
+
const { globalPlugins = [] } = loadConfig();
|
|
74
|
+
const agentSpecs = [...runtimeSpecs, ...(state.agentDetails?.plugins || [])];
|
|
75
|
+
const pluginSpecs = mergePluginSpecs(globalPlugins, agentSpecs);
|
|
76
|
+
|
|
77
|
+
// 4. Load normalized plugins
|
|
78
|
+
for (const p of pluginSpecs) {
|
|
79
|
+
const name = typeof p === 'string' ? p : p?.name;
|
|
80
|
+
if (!name || typeof name !== 'string') {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const config = typeof p === 'string' ? {} : { ...(p.config || {}) };
|
|
85
|
+
const plugin = await resolvePlugin(name, config);
|
|
86
|
+
if (plugin) {
|
|
87
|
+
runtime.use(plugin);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return runtime.build();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mergePluginSpecs(globalSpecs: PluginSpec[], agentSpecs: PluginSpec[]): PluginSpec[] {
|
|
95
|
+
const specsByName = new Map<string, PluginSpec>();
|
|
96
|
+
|
|
97
|
+
for (const spec of globalSpecs) {
|
|
98
|
+
const name = typeof spec === 'string' ? spec : spec?.name;
|
|
99
|
+
if (!name || typeof name !== 'string') continue;
|
|
100
|
+
specsByName.set(name, spec);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Agent-defined plugins override global ones with the same name.
|
|
104
|
+
for (const spec of agentSpecs) {
|
|
105
|
+
const name = typeof spec === 'string' ? spec : spec?.name;
|
|
106
|
+
if (!name || typeof name !== 'string') continue;
|
|
107
|
+
specsByName.set(name, spec);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [...specsByName.values()];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const orchestratorService = {
|
|
114
|
+
/**
|
|
115
|
+
* The primary entry point for all events coming into the system (e.g. from the API).
|
|
116
|
+
* Handles routing and initial UI message creation.
|
|
117
|
+
*/
|
|
118
|
+
dispatch: async (options: DispatchOptions): Promise<void> => {
|
|
119
|
+
const { runId, agentId, event, channelId, threadId, onEvent } = options;
|
|
120
|
+
|
|
121
|
+
// 0. Ensure the incoming event has a unique ID immediately
|
|
122
|
+
ensureEventId(event);
|
|
123
|
+
|
|
124
|
+
let finalAgentId = agentId || 'system';
|
|
125
|
+
let finalEvent = event;
|
|
126
|
+
let currentThreadId = threadId;
|
|
127
|
+
|
|
128
|
+
// 1. Convert user:input (or other raw inputs) to agent:invoke
|
|
129
|
+
const rawContent = (event as any).data?.content || '';
|
|
130
|
+
if (event.type === 'user:input' || event.type === 'agent:invoke') {
|
|
131
|
+
const normalizedInvokeEvent: AgentInvokeEvent = {
|
|
132
|
+
type: 'agent:invoke',
|
|
133
|
+
id: event.id,
|
|
134
|
+
data: {
|
|
135
|
+
content: rawContent,
|
|
136
|
+
role: 'user',
|
|
137
|
+
},
|
|
138
|
+
meta: {
|
|
139
|
+
agentId: 'system',
|
|
140
|
+
userId: event.meta?.userId,
|
|
141
|
+
userName: event.meta?.userName,
|
|
142
|
+
userAvatarUrl: event.meta?.userAvatarUrl,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
finalEvent = normalizedInvokeEvent;
|
|
146
|
+
|
|
147
|
+
// 1. Store the user's input in the current context (main channel or existing thread)
|
|
148
|
+
const initialState = await storageService.getOpenBotState({
|
|
149
|
+
runId,
|
|
150
|
+
agentId: 'system',
|
|
151
|
+
channelId,
|
|
152
|
+
threadId: currentThreadId,
|
|
153
|
+
event: finalEvent,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// 2. Propagate the user's input to the event bus
|
|
157
|
+
await onEvent(finalEvent, initialState);
|
|
158
|
+
|
|
159
|
+
// 3. Prepare the event for the target agent
|
|
160
|
+
finalEvent = {
|
|
161
|
+
...event,
|
|
162
|
+
type: 'agent:invoke',
|
|
163
|
+
data: {
|
|
164
|
+
...((event as any).data || {}),
|
|
165
|
+
content: rawContent,
|
|
166
|
+
},
|
|
167
|
+
meta: {
|
|
168
|
+
...(event.meta || {}),
|
|
169
|
+
// The threadId in meta is the anchor for new threads (Slack-style)
|
|
170
|
+
threadId: currentThreadId || finalEvent.id,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 4. Linear Execution Loop
|
|
176
|
+
// Instead of recursion, we use a queue to process agents one after another.
|
|
177
|
+
const queue: { agentId: string; event: OpenBotEvent }[] = [
|
|
178
|
+
{ agentId: finalAgentId, event: finalEvent },
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
// Safety check to prevent infinite loops
|
|
182
|
+
let iterations = 0;
|
|
183
|
+
const MAX_ITERATIONS = 20;
|
|
184
|
+
|
|
185
|
+
while (queue.length > 0 && iterations < MAX_ITERATIONS) {
|
|
186
|
+
iterations++;
|
|
187
|
+
const { agentId, event: currentEvent } = queue.shift()!;
|
|
188
|
+
|
|
189
|
+
// Track agents queued in this step to avoid double-runs (e.g. from tool delegation)
|
|
190
|
+
const queuedAgents = new Set<string>();
|
|
191
|
+
const delegations: { agentId: string; event: OpenBotEvent }[] = [];
|
|
192
|
+
|
|
193
|
+
await orchestratorService.executeAgent({
|
|
194
|
+
runId,
|
|
195
|
+
agentId,
|
|
196
|
+
event: currentEvent,
|
|
197
|
+
channelId,
|
|
198
|
+
threadId: currentThreadId,
|
|
199
|
+
onEvent: async (chunk, state) => {
|
|
200
|
+
// 0. Filter out echoed input events to prevent duplication in the UI/storage
|
|
201
|
+
if (chunk.type === currentEvent.type && chunk.id === currentEvent.id) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 1. Detect if a new thread was created and update the context for the rest of the loop
|
|
206
|
+
if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
|
|
207
|
+
currentThreadId = chunk.data.threadId || currentThreadId;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 2. Detect delegations to queue them for the next iteration
|
|
211
|
+
let targetAgentId: string | null = null;
|
|
212
|
+
let targetEvent: OpenBotEvent | null = null;
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
chunk.type === 'agent:invoke' &&
|
|
216
|
+
chunk.data.agentId &&
|
|
217
|
+
chunk.data.agentId !== agentId
|
|
218
|
+
) {
|
|
219
|
+
targetAgentId = chunk.data.agentId;
|
|
220
|
+
targetEvent = {
|
|
221
|
+
...chunk,
|
|
222
|
+
meta: {
|
|
223
|
+
...(chunk.meta || {}),
|
|
224
|
+
threadId: currentThreadId,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 3. Queue only if not already queued in this step
|
|
230
|
+
if (targetAgentId && targetEvent && !queuedAgents.has(targetAgentId)) {
|
|
231
|
+
queuedAgents.add(targetAgentId);
|
|
232
|
+
delegations.push({
|
|
233
|
+
agentId: targetAgentId,
|
|
234
|
+
event: targetEvent,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Propagate all events
|
|
239
|
+
await onEvent(chunk, state);
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Add found delegations to the queue
|
|
244
|
+
queue.push(...delegations);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (iterations >= MAX_ITERATIONS) {
|
|
248
|
+
console.warn(`[orchestrator] Reached MAX_ITERATIONS (${MAX_ITERATIONS}). Stopping execution.`);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Executes a single agent runtime.
|
|
254
|
+
*/
|
|
255
|
+
executeAgent: async (options: ExecuteAgentOptions): Promise<void> => {
|
|
256
|
+
const { runId, agentId, event, channelId, threadId, onEvent } = options;
|
|
257
|
+
|
|
258
|
+
let agentState: OpenBotState;
|
|
259
|
+
try {
|
|
260
|
+
agentState = await storageService.getOpenBotState(options);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if ((error as Error & { code?: string }).code === 'AGENT_NOT_FOUND') {
|
|
263
|
+
const fallbackState = await storageService.getOpenBotState({
|
|
264
|
+
runId,
|
|
265
|
+
agentId: 'system',
|
|
266
|
+
channelId,
|
|
267
|
+
threadId,
|
|
268
|
+
event,
|
|
269
|
+
});
|
|
270
|
+
const warning = `⚠️ Agent **${agentId}** does not exist. Please check the agent ID and try again.`;
|
|
271
|
+
|
|
272
|
+
await onEvent(
|
|
273
|
+
{
|
|
274
|
+
type: 'agent:output',
|
|
275
|
+
data: { content: warning },
|
|
276
|
+
meta: { agentId: 'system', threadId },
|
|
277
|
+
},
|
|
278
|
+
fallbackState,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
const agentRuntime = await createAgentRuntime(agentState);
|
|
286
|
+
|
|
287
|
+
let hasProducedOutput = false;
|
|
288
|
+
|
|
289
|
+
await onEvent(
|
|
290
|
+
{
|
|
291
|
+
type: 'agent:run:start',
|
|
292
|
+
data: {
|
|
293
|
+
runId,
|
|
294
|
+
agentId,
|
|
295
|
+
channelId,
|
|
296
|
+
threadId,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
agentState,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// RUN the agent runtime
|
|
304
|
+
for await (const chunk of agentRuntime.run(event, { state: agentState, runId })) {
|
|
305
|
+
if (chunk.type === 'agent:output') {
|
|
306
|
+
hasProducedOutput = true;
|
|
307
|
+
chunk.meta = { ...chunk.meta, agentId };
|
|
308
|
+
} else if (chunk.type.startsWith('action:')) {
|
|
309
|
+
hasProducedOutput = true;
|
|
310
|
+
}
|
|
311
|
+
await onEvent(chunk, agentState);
|
|
312
|
+
}
|
|
313
|
+
} finally {
|
|
314
|
+
await onEvent(
|
|
315
|
+
{
|
|
316
|
+
type: 'agent:run:end',
|
|
317
|
+
data: {
|
|
318
|
+
runId,
|
|
319
|
+
agentId,
|
|
320
|
+
channelId,
|
|
321
|
+
threadId,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
agentState,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Fallback for agents that don't produce output (e.g. misconfigured or silent)
|
|
329
|
+
if (event.type === 'agent:invoke' && !hasProducedOutput) {
|
|
330
|
+
const warning = `⚠️ **${agentId}** is not configured to handle inputs. Please check its plugin configuration.`;
|
|
331
|
+
|
|
332
|
+
await onEvent(
|
|
333
|
+
{
|
|
334
|
+
type: 'agent:output',
|
|
335
|
+
data: { content: warning },
|
|
336
|
+
meta: { agentId },
|
|
337
|
+
},
|
|
338
|
+
agentState,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The Harness is the environment in which an agent operates.
|
|
5
|
+
* it provides the necessary "plumbing" (storage, communication, tools)
|
|
6
|
+
* so the agent can focus on reasoning.
|
|
7
|
+
*/
|
|
8
|
+
export interface Harness {
|
|
9
|
+
readonly runId: string;
|
|
10
|
+
readonly channelId: string;
|
|
11
|
+
readonly threadId?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Dispatches an event into the harness.
|
|
15
|
+
* This is the primary way to interact with the agent.
|
|
16
|
+
*/
|
|
17
|
+
dispatch(event: OpenBotEvent): Promise<void>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Observes events emitted by the harness.
|
|
21
|
+
*/
|
|
22
|
+
onEvent(callback: (event: OpenBotEvent, state: OpenBotState) => Promise<void>): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for creating a new Agent Harness instance.
|
|
27
|
+
*/
|
|
28
|
+
export interface HarnessOptions {
|
|
29
|
+
runId: string;
|
|
30
|
+
agentId: string;
|
|
31
|
+
channelId: string;
|
|
32
|
+
threadId?: string;
|
|
33
|
+
onEvent: (event: OpenBotEvent, state: OpenBotState) => Promise<void>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { MelonyPlugin, RuntimeContext } from 'melony';
|
|
2
|
+
import { generateText, ModelMessage, type LanguageModel } from 'ai';
|
|
3
|
+
import { openai } from '@ai-sdk/openai';
|
|
4
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { OpenBotEvent, OpenBotState, ShortTermMessage } from '../app/types.js';
|
|
7
|
+
import { Storage } from './storage.js';
|
|
8
|
+
|
|
9
|
+
export interface AISDKPluginOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Provider model as a standardized string (e.g. `openai/gpt-4o-mini`, `anthropic/claude-3-5-sonnet-20240620`).
|
|
12
|
+
* Default: `openai/gpt-4o-mini`
|
|
13
|
+
*/
|
|
14
|
+
model?: string;
|
|
15
|
+
system?: string | ((context: RuntimeContext) => string | Promise<string>);
|
|
16
|
+
storage?: Storage;
|
|
17
|
+
toolDefinitions?: Record<
|
|
18
|
+
string,
|
|
19
|
+
{
|
|
20
|
+
description: string;
|
|
21
|
+
inputSchema: z.ZodType<any>;
|
|
22
|
+
}
|
|
23
|
+
>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolves a standardized model string to an AI SDK LanguageModel.
|
|
28
|
+
*/
|
|
29
|
+
function resolveModel(modelString: string): LanguageModel {
|
|
30
|
+
const [provider, ...rest] = modelString.split('/');
|
|
31
|
+
const modelId = rest.join('/');
|
|
32
|
+
|
|
33
|
+
if (!modelId) {
|
|
34
|
+
throw new Error(`Invalid model string: "${modelString}". Expected format: "provider/model-id"`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
switch (provider) {
|
|
38
|
+
case 'openai':
|
|
39
|
+
return openai(modelId);
|
|
40
|
+
case 'anthropic':
|
|
41
|
+
return anthropic(modelId);
|
|
42
|
+
default:
|
|
43
|
+
throw new Error(`Unsupported AI provider: "${provider}"`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function buildSystemPrompt(
|
|
48
|
+
state: OpenBotState,
|
|
49
|
+
system?: string | ((context: RuntimeContext) => string | Promise<string>),
|
|
50
|
+
context?: RuntimeContext,
|
|
51
|
+
storage?: Storage,
|
|
52
|
+
): Promise<string> {
|
|
53
|
+
const sections: string[] = [];
|
|
54
|
+
|
|
55
|
+
if (state.agentDetails) {
|
|
56
|
+
sections.push(`## AGENT NAME\n${state.agentDetails.name}`);
|
|
57
|
+
sections.push(`## AGENT SPECIFICATION\n${state.agentDetails.instructions}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (state.channelDetails) {
|
|
61
|
+
sections.push(`## CHANNEL NAME\n${state.channelDetails.name}`);
|
|
62
|
+
sections.push(`## CHANNEL SPECIFICATION\n${state.channelDetails.spec}`);
|
|
63
|
+
// sections.push(`## CHANNEL STATE\n${JSON.stringify(state.channelDetails.state, null, 2)}`);
|
|
64
|
+
|
|
65
|
+
if (storage) {
|
|
66
|
+
try {
|
|
67
|
+
const channelEvents = await storage.getEvents({ channelId: state.channelId });
|
|
68
|
+
if (channelEvents.length > 0) {
|
|
69
|
+
const formattedEvents = channelEvents
|
|
70
|
+
.slice(-20)
|
|
71
|
+
.map((e) => `- ${e.type}: ${JSON.stringify((e as any).data || {})}`)
|
|
72
|
+
.join('\n');
|
|
73
|
+
sections.push(`## CHANNEL RECENT ACTIVITIES (events)\n${formattedEvents}`);
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.warn(`[ai-sdk] Failed to fetch channel events for ${state.channelId}`, error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (state.threadDetails) {
|
|
82
|
+
sections.push(`## THREAD NAME\n${state.threadDetails.name}`);
|
|
83
|
+
sections.push(`## THREAD SPECIFICATION\n${state.threadDetails.spec}`);
|
|
84
|
+
// sections.push(`## THREAD STATE\n${JSON.stringify(state.threadDetails.state, null, 2)}`);
|
|
85
|
+
|
|
86
|
+
if (storage && state.threadId) {
|
|
87
|
+
try {
|
|
88
|
+
const threadEvents = await storage.getEvents({
|
|
89
|
+
channelId: state.channelId,
|
|
90
|
+
threadId: state.threadId,
|
|
91
|
+
});
|
|
92
|
+
if (threadEvents.length > 0) {
|
|
93
|
+
const formattedEvents = threadEvents
|
|
94
|
+
.slice(-20)
|
|
95
|
+
.map((e) => `- ${e.type}: ${JSON.stringify((e as any).data || {})}`)
|
|
96
|
+
.join('\n');
|
|
97
|
+
sections.push(`## THREAD RECENT ACTIVITIES (events)\n${formattedEvents}`);
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.warn(
|
|
101
|
+
`[ai-sdk] Failed to fetch thread events for channel ${state.channelId} thread ${state.threadId}`,
|
|
102
|
+
error,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (system && typeof system === 'string') {
|
|
109
|
+
sections.push(`## SYSTEM INSTRUCTIONS\n${system}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (system && typeof system === 'function' && context) {
|
|
113
|
+
sections.push(await system(context));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return sections.join('\n\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* AI SDK Plugin for Melony.
|
|
121
|
+
* Automatically handles text events and routes them through an AI SDK using Vercel AI SDK.
|
|
122
|
+
* It can also automatically trigger events based on tool calls.
|
|
123
|
+
*/
|
|
124
|
+
export const aiSdkPlugin =
|
|
125
|
+
(options: AISDKPluginOptions): MelonyPlugin<OpenBotState, OpenBotEvent> =>
|
|
126
|
+
(builder) => {
|
|
127
|
+
const {
|
|
128
|
+
model: modelString = 'openai/gpt-4o-mini',
|
|
129
|
+
system,
|
|
130
|
+
storage,
|
|
131
|
+
toolDefinitions = {},
|
|
132
|
+
} = options;
|
|
133
|
+
|
|
134
|
+
const model = resolveModel(modelString);
|
|
135
|
+
|
|
136
|
+
builder.on('agent:invoke', async function* (event, context) {
|
|
137
|
+
// extract threadId if model decides to reply in a thread
|
|
138
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
139
|
+
const systemPrompt = await buildSystemPrompt(context.state, system, context, storage);
|
|
140
|
+
|
|
141
|
+
context.state.shortTermMessages = [
|
|
142
|
+
...(context.state.shortTermMessages ?? []),
|
|
143
|
+
{
|
|
144
|
+
role: event.data?.role || 'user',
|
|
145
|
+
content: (event as any)?.data?.content || '',
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const result = await generateText({
|
|
150
|
+
model,
|
|
151
|
+
system: systemPrompt,
|
|
152
|
+
messages: context.state.shortTermMessages,
|
|
153
|
+
tools: toolDefinitions,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const toolCalls = result.toolCalls ?? [];
|
|
157
|
+
|
|
158
|
+
if (toolCalls.length > 0) {
|
|
159
|
+
for (const toolCall of toolCalls) {
|
|
160
|
+
const toolEvent = {
|
|
161
|
+
type: `action:${toolCall.toolName}` as OpenBotEvent['type'],
|
|
162
|
+
data: toolCall.input,
|
|
163
|
+
meta: {
|
|
164
|
+
toolCallId: toolCall.toolCallId,
|
|
165
|
+
agentId: context.state.agentId,
|
|
166
|
+
threadId,
|
|
167
|
+
},
|
|
168
|
+
} as unknown as OpenBotEvent;
|
|
169
|
+
yield toolEvent;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (result.text) {
|
|
174
|
+
context.state.shortTermMessages = [
|
|
175
|
+
...(context.state.shortTermMessages ?? []),
|
|
176
|
+
{ role: 'assistant', content: result.text },
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
yield {
|
|
180
|
+
type: 'agent:output',
|
|
181
|
+
data: {
|
|
182
|
+
content: result.text,
|
|
183
|
+
},
|
|
184
|
+
meta: {
|
|
185
|
+
agentId: context.state.agentId,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const plugin = {
|
|
193
|
+
name: 'ai-sdk',
|
|
194
|
+
description: 'Built-in AI SDK plugin',
|
|
195
|
+
kind: 'runtime' as const,
|
|
196
|
+
factory: aiSdkPlugin,
|
|
197
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { MelonyPlugin } from 'melony';
|
|
2
|
+
import { OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
3
|
+
import z from 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Delegation Plugin for Melony.
|
|
7
|
+
* Automatically handles delegation events and routes them through the storage service.
|
|
8
|
+
*/
|
|
9
|
+
export const delegationPlugin = (): MelonyPlugin<OpenBotState, OpenBotEvent> => (builder) => {
|
|
10
|
+
builder.on('action:delegate', async function* (event, context) {
|
|
11
|
+
const { agentId, content } = event.data;
|
|
12
|
+
|
|
13
|
+
// 1. Show the delegation in the UI
|
|
14
|
+
yield {
|
|
15
|
+
type: 'agent:output',
|
|
16
|
+
data: {
|
|
17
|
+
content: `Delegating to **${agentId}**: ${content}`,
|
|
18
|
+
},
|
|
19
|
+
meta: {
|
|
20
|
+
...(event.meta || {}),
|
|
21
|
+
agentId: context.state.agentId,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// 2. Trigger the linear execution loop in orchestratorService
|
|
26
|
+
yield {
|
|
27
|
+
type: 'agent:invoke',
|
|
28
|
+
data: {
|
|
29
|
+
agentId,
|
|
30
|
+
content,
|
|
31
|
+
},
|
|
32
|
+
meta: {
|
|
33
|
+
...(event.meta || {}),
|
|
34
|
+
agentId: context.state.agentId,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const delegationToolDefinitions = {
|
|
41
|
+
delegate: {
|
|
42
|
+
description:
|
|
43
|
+
'Delegate a task to another agent. The agent will run independently and feed results back to you. Call at most once per step.',
|
|
44
|
+
inputSchema: z.object({
|
|
45
|
+
agentId: z.string().describe('The ID of the agent (e.g. "os", "browser", "tavily").'),
|
|
46
|
+
content: z.string().describe('The message or task for the agent.'),
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const plugin = {
|
|
52
|
+
name: 'delegation',
|
|
53
|
+
description: 'Delegation plugin',
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
author: 'OpenBot',
|
|
56
|
+
license: 'MIT',
|
|
57
|
+
website: 'https://openbot.one',
|
|
58
|
+
factory: delegationPlugin,
|
|
59
|
+
toolDefinitions: delegationToolDefinitions,
|
|
60
|
+
};
|