openbot 0.3.3 → 0.3.5
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 +1 -1
- package/dist/app/cli.js +1 -1
- package/dist/app/server.js +39 -21
- package/dist/assets/icon.svg +4 -0
- package/dist/bus/services.js +6 -5
- package/dist/harness/dispatcher.js +267 -0
- package/dist/harness/orchestrator.js +76 -0
- package/dist/harness/queue-processor.js +26 -0
- package/dist/services/storage.js +39 -5
- package/docs/architecture.md +5 -8
- package/package.json +7 -7
- package/src/app/cli.ts +1 -1
- package/src/app/server.ts +49 -27
- package/src/app/types.ts +37 -0
- package/src/bus/services.ts +6 -5
- package/src/bus/types.ts +4 -0
- package/src/harness/dispatcher.ts +379 -0
- package/src/services/storage.ts +41 -4
- package/src/harness/agent-harness.ts +0 -58
- package/src/harness/event-normalizer.ts +0 -82
- package/src/harness/orchestrator.ts +0 -104
- package/src/harness/queue-processor.ts +0 -220
- package/src/harness/types.ts +0 -34
|
@@ -0,0 +1,379 @@
|
|
|
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/services/storage.ts
CHANGED
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
VARIABLES_FILE,
|
|
10
10
|
} from '../app/config.js';
|
|
11
11
|
import fs from 'node:fs/promises';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
12
13
|
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
15
|
import crypto from 'node:crypto';
|
|
14
16
|
import matter from 'gray-matter';
|
|
15
17
|
import {
|
|
@@ -28,7 +30,6 @@ import { listBuiltInPlugins, parsePluginModule } from '../registry/plugins.js';
|
|
|
28
30
|
import { OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
29
31
|
import { processService } from '../harness/process.js';
|
|
30
32
|
import { memoryService } from './memory.js';
|
|
31
|
-
import { pathToFileURL } from 'node:url';
|
|
32
33
|
|
|
33
34
|
const resolveBaseDir = () => {
|
|
34
35
|
const config = loadConfig();
|
|
@@ -40,6 +41,24 @@ const ENTITY_SVG_CANDIDATE_NAMES = ['avatar.svg', 'icon.svg', 'image.svg', 'logo
|
|
|
40
41
|
const toSvgDataUrl = (svg: string) =>
|
|
41
42
|
`data:image/svg+xml;base64,${Buffer.from(svg, 'utf-8').toString('base64')}`;
|
|
42
43
|
|
|
44
|
+
let bundledSystemAgentImage: string | undefined;
|
|
45
|
+
let bundledSystemAgentImageLoaded = false;
|
|
46
|
+
|
|
47
|
+
/** OpenBot mark from `src/assets/icon.svg` (also copied to `dist/assets` at build). */
|
|
48
|
+
function getBundledSystemAgentImage(): string | undefined {
|
|
49
|
+
if (bundledSystemAgentImageLoaded) return bundledSystemAgentImage;
|
|
50
|
+
bundledSystemAgentImageLoaded = true;
|
|
51
|
+
try {
|
|
52
|
+
const iconPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../assets/icon.svg');
|
|
53
|
+
const trimmed = readFileSync(iconPath, 'utf-8').trim();
|
|
54
|
+
if (!trimmed.startsWith('<svg')) return undefined;
|
|
55
|
+
bundledSystemAgentImage = toSvgDataUrl(trimmed);
|
|
56
|
+
} catch {
|
|
57
|
+
bundledSystemAgentImage = undefined;
|
|
58
|
+
}
|
|
59
|
+
return bundledSystemAgentImage;
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
const tryReadSvgDataUrl = async (filePath: string): Promise<string | null> => {
|
|
44
63
|
try {
|
|
45
64
|
const svg = await fs.readFile(filePath, 'utf-8');
|
|
@@ -101,7 +120,7 @@ function getSystemAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails
|
|
|
101
120
|
const defaults: AgentDetails = {
|
|
102
121
|
id: SYSTEM_AGENT_ID,
|
|
103
122
|
name: 'OpenBot',
|
|
104
|
-
image:
|
|
123
|
+
image: getBundledSystemAgentImage(),
|
|
105
124
|
description:
|
|
106
125
|
'First-party orchestration agent for OpenBot. Coordinates other agents via handoff.',
|
|
107
126
|
instructions: AI_SDK_SYSTEM_PROMPT,
|
|
@@ -440,7 +459,7 @@ export const storageService = {
|
|
|
440
459
|
await fs.writeFile(
|
|
441
460
|
specPath,
|
|
442
461
|
spec?.trim() ||
|
|
443
|
-
|
|
462
|
+
`# ${normalizedChannelId}\n\nDefine the goals and rules for this channel here.\n`,
|
|
444
463
|
);
|
|
445
464
|
await fs.writeFile(statePath, JSON.stringify(finalState, null, 2));
|
|
446
465
|
},
|
|
@@ -764,6 +783,10 @@ export const storageService = {
|
|
|
764
783
|
const stats = await fs.stat(agentMdPath);
|
|
765
784
|
|
|
766
785
|
const pluginRefs = parsePluginRefs(data.plugins);
|
|
786
|
+
const frontmatterImage =
|
|
787
|
+
typeof data.image === 'string' && data.image.trim() !== ''
|
|
788
|
+
? data.image.trim()
|
|
789
|
+
: undefined;
|
|
767
790
|
|
|
768
791
|
diskDetails = {
|
|
769
792
|
id: agentId,
|
|
@@ -772,7 +795,7 @@ export const storageService = {
|
|
|
772
795
|
plugins: pluginRefs.map((ref) => ref.id),
|
|
773
796
|
pluginRefs,
|
|
774
797
|
description: typeof data.description === 'string' ? data.description : '',
|
|
775
|
-
image: discoveredImage || undefined,
|
|
798
|
+
image: frontmatterImage || discoveredImage || undefined,
|
|
776
799
|
createdAt: stats.birthtime,
|
|
777
800
|
updatedAt: stats.mtime,
|
|
778
801
|
};
|
|
@@ -802,12 +825,14 @@ export const storageService = {
|
|
|
802
825
|
agentId,
|
|
803
826
|
name,
|
|
804
827
|
description = '',
|
|
828
|
+
image,
|
|
805
829
|
instructions,
|
|
806
830
|
plugins,
|
|
807
831
|
}: {
|
|
808
832
|
agentId: string;
|
|
809
833
|
name: string;
|
|
810
834
|
description?: string;
|
|
835
|
+
image?: string;
|
|
811
836
|
instructions: string;
|
|
812
837
|
plugins: PluginRef[];
|
|
813
838
|
}): Promise<void> => {
|
|
@@ -836,6 +861,9 @@ export const storageService = {
|
|
|
836
861
|
description,
|
|
837
862
|
plugins: serializePluginRefs(plugins),
|
|
838
863
|
};
|
|
864
|
+
if (typeof image === 'string' && image.trim() !== '') {
|
|
865
|
+
data.image = image.trim();
|
|
866
|
+
}
|
|
839
867
|
|
|
840
868
|
const body = matter.stringify(`${instructions.trim()}\n`, data);
|
|
841
869
|
await fs.writeFile(agentMdPath, body, 'utf-8');
|
|
@@ -844,12 +872,14 @@ export const storageService = {
|
|
|
844
872
|
agentId,
|
|
845
873
|
name,
|
|
846
874
|
description,
|
|
875
|
+
image,
|
|
847
876
|
instructions,
|
|
848
877
|
plugins,
|
|
849
878
|
}: {
|
|
850
879
|
agentId: string;
|
|
851
880
|
name?: string;
|
|
852
881
|
description?: string;
|
|
882
|
+
image?: string;
|
|
853
883
|
instructions?: string;
|
|
854
884
|
plugins?: PluginRef[];
|
|
855
885
|
}): Promise<void> => {
|
|
@@ -874,6 +904,13 @@ export const storageService = {
|
|
|
874
904
|
if (name !== undefined) nextData.name = name;
|
|
875
905
|
if (description !== undefined) nextData.description = description;
|
|
876
906
|
if (plugins !== undefined) nextData.plugins = serializePluginRefs(plugins);
|
|
907
|
+
if (image !== undefined) {
|
|
908
|
+
if (typeof image === 'string' && image.trim() !== '') {
|
|
909
|
+
nextData.image = image.trim();
|
|
910
|
+
} else {
|
|
911
|
+
delete nextData.image;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
877
914
|
|
|
878
915
|
const nextContent = instructions !== undefined ? instructions : parsed.content;
|
|
879
916
|
const body = matter.stringify(`${String(nextContent).trim()}\n`, nextData);
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { Runtime } from 'melony';
|
|
2
|
-
import { OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
3
|
-
import { Harness, HarnessOptions } from './types.js';
|
|
4
|
-
import { orchestratorService } from './orchestrator.js';
|
|
5
|
-
import { ensureEventId } from '../app/utils.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Standard implementation of the Agent Harness.
|
|
9
|
-
* It wraps the orchestrator logic into a clean, stateful container.
|
|
10
|
-
*/
|
|
11
|
-
export class AgentHarness implements Harness {
|
|
12
|
-
public readonly runId: string;
|
|
13
|
-
public readonly agentId: string;
|
|
14
|
-
public readonly channelId: string;
|
|
15
|
-
public threadId?: string;
|
|
16
|
-
private eventCallbacks: ((event: OpenBotEvent, state: OpenBotState) => Promise<void>)[] = [];
|
|
17
|
-
|
|
18
|
-
constructor(options: HarnessOptions) {
|
|
19
|
-
this.runId = options.runId;
|
|
20
|
-
this.agentId = options.agentId;
|
|
21
|
-
this.channelId = options.channelId;
|
|
22
|
-
this.threadId = options.threadId;
|
|
23
|
-
if (options.onEvent) {
|
|
24
|
-
this.eventCallbacks.push(options.onEvent);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Dispatches an event to the agent within this harness.
|
|
30
|
-
*/
|
|
31
|
-
async dispatch(event: OpenBotEvent): Promise<void> {
|
|
32
|
-
ensureEventId(event);
|
|
33
|
-
|
|
34
|
-
await orchestratorService.dispatch({
|
|
35
|
-
runId: this.runId,
|
|
36
|
-
agentId: this.agentId,
|
|
37
|
-
event,
|
|
38
|
-
channelId: this.channelId,
|
|
39
|
-
threadId: this.threadId,
|
|
40
|
-
onEvent: async (chunk, state) => {
|
|
41
|
-
// Update internal thread state if it changes (e.g. thread creation)
|
|
42
|
-
if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
|
|
43
|
-
this.threadId = chunk.data.threadId || this.threadId;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Notify all observers
|
|
47
|
-
await Promise.all(this.eventCallbacks.map(cb => cb(chunk, state)));
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Adds an event listener to the harness.
|
|
54
|
-
*/
|
|
55
|
-
onEvent(callback: (event: OpenBotEvent, state: OpenBotState) => Promise<void>): void {
|
|
56
|
-
this.eventCallbacks.push(callback);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { AgentInvokeEvent, OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
2
|
-
import { ensureEventId } from '../app/utils.js';
|
|
3
|
-
import { storageService } from '../services/storage.js';
|
|
4
|
-
|
|
5
|
-
export interface NormalizedEventResult {
|
|
6
|
-
finalEvent: OpenBotEvent;
|
|
7
|
-
finalAgentId: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const EventNormalizer = {
|
|
11
|
-
/**
|
|
12
|
-
* Normalizes incoming events, converting raw inputs like user:input to agent:invoke.
|
|
13
|
-
* Also handles initial state storage and event bus propagation for user inputs.
|
|
14
|
-
*/
|
|
15
|
-
normalize: async (
|
|
16
|
-
event: OpenBotEvent,
|
|
17
|
-
options: {
|
|
18
|
-
runId: string;
|
|
19
|
-
agentId?: string;
|
|
20
|
-
channelId: string;
|
|
21
|
-
threadId?: string;
|
|
22
|
-
onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<boolean | void>;
|
|
23
|
-
}
|
|
24
|
-
): Promise<NormalizedEventResult> => {
|
|
25
|
-
const { runId, agentId, channelId, threadId, onEvent } = options;
|
|
26
|
-
|
|
27
|
-
// 0. Ensure the incoming event has a unique ID immediately
|
|
28
|
-
ensureEventId(event);
|
|
29
|
-
|
|
30
|
-
let finalAgentId = agentId || 'system';
|
|
31
|
-
let finalEvent = event;
|
|
32
|
-
|
|
33
|
-
// 1. Convert user:input (or other raw inputs) to agent:invoke
|
|
34
|
-
const rawContent = (event as any).data?.content || '';
|
|
35
|
-
if (event.type === 'user:input' || event.type === 'agent:invoke') {
|
|
36
|
-
const normalizedInvokeEvent: AgentInvokeEvent = {
|
|
37
|
-
type: 'agent:invoke',
|
|
38
|
-
id: event.id,
|
|
39
|
-
data: {
|
|
40
|
-
content: rawContent,
|
|
41
|
-
role: 'user',
|
|
42
|
-
},
|
|
43
|
-
meta: {
|
|
44
|
-
agentId: 'system',
|
|
45
|
-
userId: event.meta?.userId,
|
|
46
|
-
userName: event.meta?.userName,
|
|
47
|
-
userAvatarUrl: event.meta?.userAvatarUrl,
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
finalEvent = normalizedInvokeEvent;
|
|
51
|
-
|
|
52
|
-
// 1. Store the user's input in the current context (main channel or existing thread)
|
|
53
|
-
const initialState = await storageService.getOpenBotState({
|
|
54
|
-
runId,
|
|
55
|
-
agentId: 'system',
|
|
56
|
-
channelId,
|
|
57
|
-
threadId: threadId,
|
|
58
|
-
event: finalEvent,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// 2. Propagate the user's input to the event bus
|
|
62
|
-
await onEvent(finalEvent, initialState);
|
|
63
|
-
|
|
64
|
-
// 3. Prepare the event for the target agent
|
|
65
|
-
finalEvent = {
|
|
66
|
-
...event,
|
|
67
|
-
type: 'agent:invoke',
|
|
68
|
-
data: {
|
|
69
|
-
...((event as any).data || {}),
|
|
70
|
-
content: rawContent,
|
|
71
|
-
},
|
|
72
|
-
meta: {
|
|
73
|
-
...(event.meta || {}),
|
|
74
|
-
// The threadId in meta is the anchor for new threads (Slack-style)
|
|
75
|
-
threadId: threadId || finalEvent.id,
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return { finalEvent, finalAgentId };
|
|
81
|
-
},
|
|
82
|
-
};
|