openbot 0.3.5 → 0.4.0
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/README.md +15 -16
- package/dist/app/agent-ids.js +4 -0
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +0 -19
- package/dist/app/server.js +8 -14
- package/dist/assets/icon.svg +9 -3
- package/dist/bus/services.js +78 -132
- package/dist/harness/agent-invoke-run.js +44 -0
- package/dist/harness/agent-turn.js +99 -0
- package/dist/harness/channel-participants.js +40 -0
- package/dist/harness/constants.js +2 -0
- package/dist/harness/context-meter.js +97 -0
- package/dist/harness/context.js +98 -45
- package/dist/harness/dispatch.js +144 -0
- package/dist/harness/dispatcher.js +45 -156
- package/dist/harness/history.js +177 -0
- package/dist/harness/index.js +91 -0
- package/dist/harness/orchestration.js +88 -0
- package/dist/harness/participants.js +22 -0
- package/dist/harness/run-harness.js +154 -0
- package/dist/harness/run.js +98 -0
- package/dist/harness/runtime-factory.js +0 -34
- package/dist/harness/runtime.js +57 -0
- package/dist/harness/todo-dispatch.js +51 -0
- package/dist/harness/todos.js +5 -0
- package/dist/harness/turn.js +79 -0
- package/dist/plugins/approval/index.js +105 -149
- package/dist/plugins/delegation/index.js +119 -32
- package/dist/plugins/memory/index.js +103 -14
- package/dist/plugins/memory/service.js +152 -0
- package/dist/plugins/openbot/context.js +80 -0
- package/dist/plugins/openbot/history.js +98 -0
- package/dist/plugins/openbot/index.js +31 -0
- package/dist/plugins/openbot/runtime.js +317 -0
- package/dist/plugins/openbot/system-prompt.js +5 -0
- package/dist/plugins/plugin-manager/index.js +105 -0
- package/dist/plugins/storage/index.js +573 -0
- package/dist/plugins/storage/service.js +1159 -0
- package/dist/plugins/storage-tools/index.js +2 -2
- package/dist/plugins/thread-namer/index.js +72 -0
- package/dist/plugins/thread-naming/generate-title.js +44 -0
- package/dist/plugins/thread-naming/index.js +103 -0
- package/dist/plugins/threads/index.js +114 -0
- package/dist/plugins/todo/index.js +24 -25
- package/dist/plugins/ui/index.js +2 -32
- package/dist/registry/plugins.js +3 -9
- package/dist/services/plugins/domain.js +1 -0
- package/dist/services/plugins/plugin-cache.js +9 -0
- package/dist/services/plugins/registry.js +110 -0
- package/dist/services/plugins/service.js +177 -0
- package/dist/services/plugins/types.js +1 -0
- package/dist/services/process.js +29 -0
- package/dist/services/storage.js +41 -15
- package/dist/services/thread-naming.js +81 -0
- package/docs/agents.md +16 -10
- package/docs/architecture.md +2 -2
- package/docs/plugins.md +6 -15
- package/docs/templates/AGENT.example.md +7 -13
- package/package.json +1 -2
- package/src/app/agent-ids.ts +5 -0
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +1 -31
- package/src/app/server.ts +8 -16
- package/src/app/types.ts +70 -190
- package/src/assets/icon.svg +9 -3
- package/src/harness/index.ts +145 -0
- package/src/plugins/approval/index.ts +91 -189
- package/src/plugins/delegation/index.ts +136 -39
- package/src/plugins/memory/index.ts +112 -15
- package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
- package/src/plugins/openbot/context.ts +91 -0
- package/src/plugins/openbot/history.ts +107 -0
- package/src/plugins/openbot/index.ts +37 -0
- package/src/plugins/openbot/runtime.ts +384 -0
- package/src/plugins/openbot/system-prompt.ts +7 -0
- package/src/plugins/plugin-manager/index.ts +122 -0
- package/src/plugins/shell/index.ts +1 -1
- package/src/plugins/storage/index.ts +633 -0
- package/src/{services/storage.ts → plugins/storage/service.ts} +257 -72
- package/src/{bus/types.ts → services/plugins/domain.ts} +20 -7
- package/src/services/plugins/plugin-cache.ts +13 -0
- package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
- package/src/services/{plugins.ts → plugins/service.ts} +96 -2
- package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
- package/src/bus/services.ts +0 -908
- package/src/harness/context.ts +0 -356
- package/src/harness/dispatcher.ts +0 -379
- package/src/harness/mcp.ts +0 -78
- package/src/harness/runtime-factory.ts +0 -129
- package/src/harness/todo-advance.ts +0 -128
- package/src/plugins/ai-sdk/index.ts +0 -41
- package/src/plugins/ai-sdk/runtime.ts +0 -468
- package/src/plugins/ai-sdk/system-prompt.ts +0 -18
- package/src/plugins/mcp/index.ts +0 -128
- package/src/plugins/storage-tools/index.ts +0 -90
- package/src/plugins/todo/index.ts +0 -64
- package/src/plugins/ui/index.ts +0 -227
- /package/src/{harness → services}/process.ts +0 -0
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AgentInvokeEvent,
|
|
3
|
-
HandoffRequestEvent,
|
|
4
|
-
OpenBotEvent,
|
|
5
|
-
OpenBotState,
|
|
6
|
-
StopAgentRunEvent,
|
|
7
|
-
} from '../app/types.js';
|
|
8
|
-
import { ensureEventId } from '../app/utils.js';
|
|
9
|
-
import { storageService } from '../services/storage.js';
|
|
10
|
-
import { createAgentRuntime } from './runtime-factory.js';
|
|
11
|
-
import { advanceAfterRun } from './todo-advance.js';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Single entry point for every event arriving at the bus.
|
|
15
|
-
*
|
|
16
|
-
* Three flavors of dispatch:
|
|
17
|
-
*
|
|
18
|
-
* 1. `action:agent_run_stop` — record a stop signal, ack, done.
|
|
19
|
-
* 2. `user:input` / `agent:invoke` — *agent step*: normalize, emit user-facing
|
|
20
|
-
* copy, then run the target agent with `run:start`/`run:end` bracketing,
|
|
21
|
-
* handoff routing, and a single `advanceAfterRun` pass that can chain the
|
|
22
|
-
* next assignee. Recursive, depth-bounded.
|
|
23
|
-
* 3. Everything else — *bus pass-through*: run the event through the targeted
|
|
24
|
-
* agent's runtime once and forward emitted chunks. No `run:start`/`run:end`,
|
|
25
|
-
* no todo advance, no handoff. This is what backs `/api/state` queries and
|
|
26
|
-
* out-of-band action events posted to `/api/publish`.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
export interface DispatchOptions {
|
|
30
|
-
runId: string;
|
|
31
|
-
agentId?: string;
|
|
32
|
-
event: OpenBotEvent;
|
|
33
|
-
channelId: string;
|
|
34
|
-
threadId?: string;
|
|
35
|
-
onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<boolean | void>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface StepContext {
|
|
39
|
-
runId: string;
|
|
40
|
-
channelId: string;
|
|
41
|
-
/** Mutable: a `create_thread:result` mid-chain rebinds the rest of the chain. */
|
|
42
|
-
threadId?: string;
|
|
43
|
-
onEvent: DispatchOptions['onEvent'];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface FollowUp {
|
|
47
|
-
agentId: string;
|
|
48
|
-
event: AgentInvokeEvent;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const MAX_CHAIN_DEPTH = 20;
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Stop requests
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
type StopRequest = {
|
|
58
|
-
runId: string;
|
|
59
|
-
agentId?: string;
|
|
60
|
-
channelId?: string;
|
|
61
|
-
threadId?: string;
|
|
62
|
-
reason?: string;
|
|
63
|
-
requestedAt: number;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const stopRequests: StopRequest[] = [];
|
|
67
|
-
const STOP_REQUEST_TTL_MS = 30 * 60 * 1000;
|
|
68
|
-
|
|
69
|
-
const pruneStopRequests = () => {
|
|
70
|
-
const now = Date.now();
|
|
71
|
-
for (let i = stopRequests.length - 1; i >= 0; i -= 1) {
|
|
72
|
-
if (now - stopRequests[i].requestedAt > STOP_REQUEST_TTL_MS) {
|
|
73
|
-
stopRequests.splice(i, 1);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const findStopRequest = (target: {
|
|
79
|
-
runId: string;
|
|
80
|
-
agentId: string;
|
|
81
|
-
channelId: string;
|
|
82
|
-
threadId?: string;
|
|
83
|
-
}): StopRequest | undefined => {
|
|
84
|
-
pruneStopRequests();
|
|
85
|
-
return stopRequests.find((r) => {
|
|
86
|
-
if (r.runId !== target.runId) return false;
|
|
87
|
-
if (r.agentId && r.agentId !== target.agentId) return false;
|
|
88
|
-
if (r.channelId && r.channelId !== target.channelId) return false;
|
|
89
|
-
if (r.threadId && r.threadId !== target.threadId) return false;
|
|
90
|
-
return true;
|
|
91
|
-
});
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
// Public API
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
|
|
98
|
-
export async function dispatch(options: DispatchOptions): Promise<void> {
|
|
99
|
-
const { event } = options;
|
|
100
|
-
ensureEventId(event);
|
|
101
|
-
|
|
102
|
-
if (event.type === 'action:agent_run_stop') {
|
|
103
|
-
await handleStop(event as StopAgentRunEvent, options);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const ctx: StepContext = {
|
|
108
|
-
runId: options.runId,
|
|
109
|
-
channelId: options.channelId,
|
|
110
|
-
threadId: options.threadId,
|
|
111
|
-
onEvent: options.onEvent,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
if (event.type === 'user:input' || event.type === 'agent:invoke') {
|
|
115
|
-
const invoke = await normalizeUserInput(event, ctx);
|
|
116
|
-
await runStep({ agentId: options.agentId || 'system', event: invoke }, ctx, 0);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Bus pass-through: route to the targeted agent's runtime once. No agent step,
|
|
121
|
-
// no advance, no follow-ups. Keeps queries (`/api/state`) cheap and idempotent.
|
|
122
|
-
await runBusEvent(event, options.agentId || 'system', ctx);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
// Agent step: run:start -> runtime -> run:end -> advance -> chain
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
|
|
129
|
-
async function runStep(step: FollowUp, ctx: StepContext, depth: number): Promise<void> {
|
|
130
|
-
if (depth >= MAX_CHAIN_DEPTH) {
|
|
131
|
-
console.warn(`[dispatcher] Reached MAX_CHAIN_DEPTH (${MAX_CHAIN_DEPTH}); stopping chain.`);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const target = {
|
|
136
|
-
runId: ctx.runId,
|
|
137
|
-
agentId: step.agentId,
|
|
138
|
-
channelId: ctx.channelId,
|
|
139
|
-
threadId: ctx.threadId,
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const preStop = findStopRequest(target);
|
|
143
|
-
if (preStop) {
|
|
144
|
-
const state = await storageService.getOpenBotState({ ...target, event: step.event });
|
|
145
|
-
await ctx.onEvent(
|
|
146
|
-
{ type: 'agent:run:stopped', data: { ...target, reason: preStop.reason } },
|
|
147
|
-
state,
|
|
148
|
-
);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
let state: OpenBotState;
|
|
153
|
-
try {
|
|
154
|
-
state = await storageService.getOpenBotState({ ...target, event: step.event });
|
|
155
|
-
} catch (error) {
|
|
156
|
-
if ((error as Error & { code?: string }).code === 'AGENT_NOT_FOUND') {
|
|
157
|
-
const fallback = await storageService.getOpenBotState({
|
|
158
|
-
...target,
|
|
159
|
-
agentId: 'system',
|
|
160
|
-
event: step.event,
|
|
161
|
-
});
|
|
162
|
-
await ctx.onEvent(
|
|
163
|
-
{
|
|
164
|
-
type: 'agent:output',
|
|
165
|
-
data: { content: `⚠️ Agent **${step.agentId}** does not exist. Please check the agent ID and try again.` },
|
|
166
|
-
meta: { agentId: 'system', threadId: ctx.threadId },
|
|
167
|
-
},
|
|
168
|
-
fallback,
|
|
169
|
-
);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
throw error;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
await ctx.onEvent({ type: 'agent:run:start', data: { ...target } }, state);
|
|
176
|
-
|
|
177
|
-
const followUps: FollowUp[] = [];
|
|
178
|
-
const queuedAgentIds = new Set<string>();
|
|
179
|
-
let lastAgentOutput: string | undefined;
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
const runtime = await createAgentRuntime(state);
|
|
183
|
-
|
|
184
|
-
for await (const chunk of runtime.run(step.event, { state, runId: ctx.runId })) {
|
|
185
|
-
const stop = findStopRequest(target);
|
|
186
|
-
if (stop) {
|
|
187
|
-
await ctx.onEvent(
|
|
188
|
-
{ type: 'agent:run:stopped', data: { ...target, reason: stop.reason } },
|
|
189
|
-
state,
|
|
190
|
-
);
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (chunk.id === step.event.id && chunk.type === step.event.type) continue;
|
|
195
|
-
|
|
196
|
-
if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
|
|
197
|
-
ctx.threadId = chunk.data.threadId || ctx.threadId;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
chunk.type === 'agent:output' &&
|
|
202
|
-
(chunk.meta as { agentId?: string } | undefined)?.agentId === step.agentId
|
|
203
|
-
) {
|
|
204
|
-
const content = chunk.data?.content;
|
|
205
|
-
if (typeof content === 'string' && content.trim()) lastAgentOutput = content.trim();
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Handoff requests are internal: queue a follow-up step instead of forwarding.
|
|
209
|
-
if (chunk.type === 'handoff:request') {
|
|
210
|
-
const req = chunk as HandoffRequestEvent;
|
|
211
|
-
const targetAgent = req.data?.agentId;
|
|
212
|
-
if (targetAgent && targetAgent !== step.agentId && !queuedAgentIds.has(targetAgent)) {
|
|
213
|
-
queuedAgentIds.add(targetAgent);
|
|
214
|
-
followUps.push({
|
|
215
|
-
agentId: targetAgent,
|
|
216
|
-
event: makeInvoke(req.data.content, ctx.threadId, req.meta),
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
chunk.meta = { ...chunk.meta, agentId: step.agentId };
|
|
223
|
-
await ctx.onEvent(chunk, state);
|
|
224
|
-
}
|
|
225
|
-
} catch (error) {
|
|
226
|
-
console.error(`[dispatcher] Agent run failed: ${step.agentId}`, error);
|
|
227
|
-
} finally {
|
|
228
|
-
const endState = await storageService.getOpenBotState({ ...target, event: step.event });
|
|
229
|
-
await ctx.onEvent({ type: 'agent:run:end', data: { ...target } }, endState);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Autonomous todo advance: single trigger point, runs once per `agent:run:end`.
|
|
233
|
-
try {
|
|
234
|
-
const handoff = await advanceAfterRun({
|
|
235
|
-
storage: storageService,
|
|
236
|
-
channelId: ctx.channelId,
|
|
237
|
-
threadId: ctx.threadId,
|
|
238
|
-
endedAgentId: step.agentId,
|
|
239
|
-
lastAgentOutput,
|
|
240
|
-
});
|
|
241
|
-
if (handoff && !queuedAgentIds.has(handoff.agentId)) {
|
|
242
|
-
queuedAgentIds.add(handoff.agentId);
|
|
243
|
-
followUps.push({
|
|
244
|
-
agentId: handoff.agentId,
|
|
245
|
-
event: makeInvoke(handoff.content, ctx.threadId),
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
} catch (error) {
|
|
249
|
-
console.warn('[dispatcher] todo advance failed', error);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
for (const next of followUps) {
|
|
253
|
-
await runStep(next, ctx, depth + 1);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
// Bus pass-through: run an event through the targeted agent's runtime, forward
|
|
259
|
-
// chunks. No run:start/end, no advance, no follow-ups.
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
|
|
262
|
-
async function runBusEvent(
|
|
263
|
-
event: OpenBotEvent,
|
|
264
|
-
agentId: string,
|
|
265
|
-
ctx: StepContext,
|
|
266
|
-
): Promise<void> {
|
|
267
|
-
let state: OpenBotState;
|
|
268
|
-
try {
|
|
269
|
-
state = await storageService.getOpenBotState({
|
|
270
|
-
runId: ctx.runId,
|
|
271
|
-
agentId,
|
|
272
|
-
channelId: ctx.channelId,
|
|
273
|
-
threadId: ctx.threadId,
|
|
274
|
-
event,
|
|
275
|
-
});
|
|
276
|
-
} catch (error) {
|
|
277
|
-
if ((error as Error & { code?: string }).code === 'AGENT_NOT_FOUND') {
|
|
278
|
-
// Silently drop: bus pass-through has no UI surface to warn into.
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
throw error;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
const runtime = await createAgentRuntime(state);
|
|
286
|
-
for await (const chunk of runtime.run(event, { state, runId: ctx.runId })) {
|
|
287
|
-
if (chunk.id === event.id && chunk.type === event.type) continue;
|
|
288
|
-
chunk.meta = { ...chunk.meta, agentId };
|
|
289
|
-
await ctx.onEvent(chunk, state);
|
|
290
|
-
}
|
|
291
|
-
} catch (error) {
|
|
292
|
-
console.error(`[dispatcher] Bus event failed: ${event.type} (${agentId})`, error);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// ---------------------------------------------------------------------------
|
|
297
|
-
// Helpers
|
|
298
|
-
// ---------------------------------------------------------------------------
|
|
299
|
-
|
|
300
|
-
async function normalizeUserInput(
|
|
301
|
-
event: OpenBotEvent,
|
|
302
|
-
ctx: StepContext,
|
|
303
|
-
): Promise<AgentInvokeEvent> {
|
|
304
|
-
const rawContent = (event as { data?: { content?: string } }).data?.content || '';
|
|
305
|
-
|
|
306
|
-
// The user-facing copy stored/streamed for the UI.
|
|
307
|
-
const userFacing: AgentInvokeEvent = {
|
|
308
|
-
type: 'agent:invoke',
|
|
309
|
-
id: event.id,
|
|
310
|
-
data: { content: rawContent, role: 'user' },
|
|
311
|
-
meta: {
|
|
312
|
-
agentId: 'system',
|
|
313
|
-
userId: event.meta?.userId,
|
|
314
|
-
userName: event.meta?.userName,
|
|
315
|
-
userAvatarUrl: event.meta?.userAvatarUrl,
|
|
316
|
-
},
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
const initialState = await storageService.getOpenBotState({
|
|
320
|
-
runId: ctx.runId,
|
|
321
|
-
agentId: 'system',
|
|
322
|
-
channelId: ctx.channelId,
|
|
323
|
-
threadId: ctx.threadId,
|
|
324
|
-
event: userFacing,
|
|
325
|
-
});
|
|
326
|
-
await ctx.onEvent(userFacing, initialState);
|
|
327
|
-
|
|
328
|
-
// The event actually fed to the target agent. Carries the input threadId (or the
|
|
329
|
-
// message id, used as the anchor for Slack-style new threads).
|
|
330
|
-
return {
|
|
331
|
-
...(event as AgentInvokeEvent),
|
|
332
|
-
type: 'agent:invoke',
|
|
333
|
-
data: { ...((event as AgentInvokeEvent).data || {}), content: rawContent, role: 'user' },
|
|
334
|
-
meta: {
|
|
335
|
-
...(event.meta || {}),
|
|
336
|
-
threadId: ctx.threadId || event.id,
|
|
337
|
-
},
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function makeInvoke(
|
|
342
|
-
content: string,
|
|
343
|
-
threadId?: string,
|
|
344
|
-
baseMeta?: Record<string, unknown>,
|
|
345
|
-
): AgentInvokeEvent {
|
|
346
|
-
return ensureEventId({
|
|
347
|
-
type: 'agent:invoke',
|
|
348
|
-
data: { role: 'user', content },
|
|
349
|
-
meta: { ...(baseMeta || {}), threadId },
|
|
350
|
-
} satisfies AgentInvokeEvent) as AgentInvokeEvent;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async function handleStop(stopEvent: StopAgentRunEvent, options: DispatchOptions): Promise<void> {
|
|
354
|
-
const { runId, channelId, threadId, onEvent } = options;
|
|
355
|
-
stopRequests.push({
|
|
356
|
-
runId: stopEvent.data.runId,
|
|
357
|
-
agentId: stopEvent.data.agentId,
|
|
358
|
-
channelId: stopEvent.data.channelId || channelId,
|
|
359
|
-
threadId: stopEvent.data.threadId || threadId,
|
|
360
|
-
reason: stopEvent.data.reason,
|
|
361
|
-
requestedAt: Date.now(),
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
const state = await storageService.getOpenBotState({
|
|
365
|
-
runId,
|
|
366
|
-
agentId: options.agentId || 'system',
|
|
367
|
-
channelId,
|
|
368
|
-
threadId,
|
|
369
|
-
event: stopEvent,
|
|
370
|
-
});
|
|
371
|
-
await onEvent(
|
|
372
|
-
{
|
|
373
|
-
type: 'action:agent_run_stop:result',
|
|
374
|
-
data: { success: true, message: `Stop requested for run ${stopEvent.data.runId}.` },
|
|
375
|
-
meta: stopEvent.meta,
|
|
376
|
-
},
|
|
377
|
-
state,
|
|
378
|
-
);
|
|
379
|
-
}
|
package/src/harness/mcp.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
-
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
-
import { loadConfig, type MCPServerConfig } from '../app/config.js';
|
|
4
|
-
|
|
5
|
-
type MCPToolSummary = {
|
|
6
|
-
name: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
inputSchema?: unknown;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
class MCPService {
|
|
12
|
-
private clients = new Map<string, Client>();
|
|
13
|
-
|
|
14
|
-
private transports = new Map<string, StdioClientTransport>();
|
|
15
|
-
|
|
16
|
-
private getServerConfig(serverId: string): MCPServerConfig {
|
|
17
|
-
const config = loadConfig();
|
|
18
|
-
const server = (config.mcpServers || []).find((s) => s.id === serverId);
|
|
19
|
-
if (!server) {
|
|
20
|
-
throw new Error(`MCP server "${serverId}" is not configured`);
|
|
21
|
-
}
|
|
22
|
-
return server;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
private async getClient(serverId: string): Promise<Client> {
|
|
26
|
-
const existing = this.clients.get(serverId);
|
|
27
|
-
if (existing) {
|
|
28
|
-
return existing;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const server = this.getServerConfig(serverId);
|
|
32
|
-
const client = new Client(
|
|
33
|
-
{
|
|
34
|
-
name: 'openbot-v2',
|
|
35
|
-
version: '0.1.0',
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
capabilities: {},
|
|
39
|
-
},
|
|
40
|
-
);
|
|
41
|
-
const transport = new StdioClientTransport({
|
|
42
|
-
command: server.command,
|
|
43
|
-
args: server.args || [],
|
|
44
|
-
env: server.env,
|
|
45
|
-
cwd: server.cwd,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
await client.connect(transport);
|
|
49
|
-
this.clients.set(serverId, client);
|
|
50
|
-
this.transports.set(serverId, transport);
|
|
51
|
-
return client;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async listServers(): Promise<string[]> {
|
|
55
|
-
const config = loadConfig();
|
|
56
|
-
return (config.mcpServers || []).map((server) => server.id);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async listTools(serverId: string): Promise<MCPToolSummary[]> {
|
|
60
|
-
const client = await this.getClient(serverId);
|
|
61
|
-
const result = await client.listTools();
|
|
62
|
-
return (result.tools || []).map((tool) => ({
|
|
63
|
-
name: tool.name,
|
|
64
|
-
description: tool.description,
|
|
65
|
-
inputSchema: tool.inputSchema,
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async callTool(serverId: string, toolName: string, args: Record<string, unknown>) {
|
|
70
|
-
const client = await this.getClient(serverId);
|
|
71
|
-
return client.callTool({
|
|
72
|
-
name: toolName,
|
|
73
|
-
arguments: args,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export const mcpService = new MCPService();
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { melony, MelonyPlugin, Runtime } from 'melony';
|
|
2
|
-
import { OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
3
|
-
import type { Plugin, PluginContext, ToolDefinition } from '../bus/plugin.js';
|
|
4
|
-
import { resolvePlugin } from '../registry/plugins.js';
|
|
5
|
-
import { storageService } from '../services/storage.js';
|
|
6
|
-
import { busServicesPlugin } from '../bus/services.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Enhances the agent's instructions with a list of other available agents the
|
|
10
|
-
* orchestrator can hand off to. Agents that include the `delegation` plugin
|
|
11
|
-
* will surface peers; agents without it can ignore this.
|
|
12
|
-
*/
|
|
13
|
-
export async function enhanceInstructions(state: OpenBotState) {
|
|
14
|
-
const { agentId, agentDetails } = state;
|
|
15
|
-
if (!agentDetails) return;
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const agents = await storageService.getAgents();
|
|
19
|
-
const otherAgents = agents.filter((a) => a.id !== agentId);
|
|
20
|
-
if (otherAgents.length === 0) return;
|
|
21
|
-
|
|
22
|
-
const agentsList = otherAgents
|
|
23
|
-
.map((a) => `- **${a.id}**${a.description ? `: ${a.description}` : ''}`)
|
|
24
|
-
.join('\n');
|
|
25
|
-
|
|
26
|
-
const header = '### Available Agents:';
|
|
27
|
-
if (!agentDetails.instructions.includes(header)) {
|
|
28
|
-
const hasHandoff = (agentDetails.pluginRefs || []).some((r) => r.id === 'delegation');
|
|
29
|
-
const hasTodo = (agentDetails.pluginRefs || []).some((r) => r.id === 'todo');
|
|
30
|
-
const usage = hasTodo
|
|
31
|
-
? 'Use these ids as `assignee` when calling `todo_write` to plan multi-agent work.'
|
|
32
|
-
: hasHandoff
|
|
33
|
-
? 'Use `handoff` to transfer control to another agent in this thread.'
|
|
34
|
-
: '';
|
|
35
|
-
agentDetails.instructions += `\n\n${header}\n${agentsList}${usage ? `\n\n${usage}` : ''}`;
|
|
36
|
-
}
|
|
37
|
-
} catch (error) {
|
|
38
|
-
console.warn('[agent] Failed to enhance instructions', error);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const composeMelonyPlugin = (
|
|
43
|
-
...plugins: MelonyPlugin<OpenBotState, OpenBotEvent>[]
|
|
44
|
-
): MelonyPlugin<OpenBotState, OpenBotEvent> => {
|
|
45
|
-
return (builder) => {
|
|
46
|
-
for (const plugin of plugins) {
|
|
47
|
-
plugin(builder);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Build the Melony runtime that drives a single agent run on the OpenBot bus.
|
|
54
|
-
*
|
|
55
|
-
* The runtime always wires:
|
|
56
|
-
* 1. `busServicesPlugin` — bus-level services (storage, channels, threads,
|
|
57
|
-
* plugin install/marketplace) shared by every agent.
|
|
58
|
-
* 2. Every Plugin referenced by the agent's `plugins[]` frontmatter, in
|
|
59
|
-
* order. Tool definitions from each plugin are merged into a single map
|
|
60
|
-
* and passed to every plugin via `PluginContext.tools`. Runtime plugins
|
|
61
|
-
* (those that handle `agent:invoke`) consume the merged map; tool plugins
|
|
62
|
-
* ignore it.
|
|
63
|
-
*
|
|
64
|
-
* Tool name collisions across plugins log a warning; the first plugin wins.
|
|
65
|
-
*/
|
|
66
|
-
export async function createAgentRuntime(
|
|
67
|
-
state: OpenBotState,
|
|
68
|
-
): Promise<Runtime<OpenBotState, OpenBotEvent>> {
|
|
69
|
-
await enhanceInstructions(state);
|
|
70
|
-
|
|
71
|
-
const runtime = melony<OpenBotState, OpenBotEvent>({
|
|
72
|
-
initialState: state,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
runtime.use(busServicesPlugin({ storage: storageService }));
|
|
76
|
-
|
|
77
|
-
const refs = state.agentDetails?.pluginRefs || [];
|
|
78
|
-
if (refs.length === 0) {
|
|
79
|
-
console.warn(
|
|
80
|
-
`[agent] Agent "${state.agentId}" has no plugins; only bus services will be active.`,
|
|
81
|
-
);
|
|
82
|
-
return runtime.build();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Resolve all plugins first so we can merge tool definitions before factory calls.
|
|
86
|
-
const resolved: Array<{ ref: { id: string; config?: Record<string, unknown> }; plugin: Plugin }> = [];
|
|
87
|
-
for (const ref of refs) {
|
|
88
|
-
const plugin = await resolvePlugin(ref.id);
|
|
89
|
-
if (!plugin) {
|
|
90
|
-
console.warn(
|
|
91
|
-
`[agent] Plugin "${ref.id}" for agent "${state.agentId}" could not be resolved.`,
|
|
92
|
-
);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
resolved.push({ ref, plugin });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Merge tool definitions; first plugin wins on collision.
|
|
99
|
-
const tools: Record<string, ToolDefinition> = {};
|
|
100
|
-
for (const { plugin } of resolved) {
|
|
101
|
-
if (!plugin.toolDefinitions) continue;
|
|
102
|
-
for (const [name, def] of Object.entries(plugin.toolDefinitions)) {
|
|
103
|
-
if (tools[name]) {
|
|
104
|
-
console.warn(
|
|
105
|
-
`[agent] Tool name collision for "${name}" while loading plugin "${plugin.id}"; keeping first registration.`,
|
|
106
|
-
);
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
tools[name] = def;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Compose all plugin factories with the shared context.
|
|
114
|
-
const pluginPlugins: MelonyPlugin<OpenBotState, OpenBotEvent>[] = [];
|
|
115
|
-
for (const { ref, plugin } of resolved) {
|
|
116
|
-
const context: PluginContext = {
|
|
117
|
-
agentId: state.agentId,
|
|
118
|
-
agentDetails: state.agentDetails!,
|
|
119
|
-
config: ref.config || {},
|
|
120
|
-
storage: storageService,
|
|
121
|
-
tools,
|
|
122
|
-
};
|
|
123
|
-
pluginPlugins.push(plugin.factory(context));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
runtime.use(composeMelonyPlugin(...pluginPlugins));
|
|
127
|
-
|
|
128
|
-
return runtime.build();
|
|
129
|
-
}
|