openbot 0.2.13 → 0.3.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/dist/agents/openbot/index.js +76 -0
- package/dist/agents/openbot/middleware/approval.js +132 -0
- package/dist/agents/openbot/runtime.js +289 -0
- package/dist/agents/openbot/system-prompt.js +32 -0
- package/dist/agents/openbot/tools/delegation.js +78 -0
- package/dist/agents/openbot/tools/mcp.js +99 -0
- package/dist/agents/openbot/tools/shell.js +91 -0
- package/dist/agents/openbot/tools/storage.js +75 -0
- package/dist/agents/openbot/tools/ui.js +176 -0
- package/dist/agents/system.js +20 -93
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +4 -1
- package/dist/app/server.js +15 -8
- package/dist/bus/agent-package.js +1 -0
- package/dist/bus/plugin.js +1 -0
- package/dist/bus/services.js +600 -0
- package/dist/bus/types.js +1 -0
- package/dist/harness/context.js +131 -0
- package/dist/harness/event-normalizer.js +59 -0
- package/dist/harness/orchestrator.js +27 -227
- package/dist/harness/process.js +25 -3
- package/dist/harness/queue-processor.js +227 -0
- package/dist/harness/runtime-factory.js +103 -0
- package/dist/plugins/ai-sdk/index.js +37 -0
- package/dist/plugins/ai-sdk/runtime.js +330 -0
- package/dist/plugins/ai-sdk/system-prompt.js +3 -0
- package/dist/plugins/ai-sdk.js +277 -87
- package/dist/plugins/approval/index.js +159 -0
- package/dist/plugins/approval.js +163 -0
- package/dist/plugins/delegation/index.js +79 -0
- package/dist/plugins/delegation.js +67 -11
- package/dist/plugins/mcp/index.js +108 -0
- package/dist/plugins/shell/index.js +99 -0
- package/dist/plugins/shell.js +123 -0
- package/dist/plugins/storage-tools/index.js +85 -0
- package/dist/plugins/storage.js +240 -5
- package/dist/plugins/ui/index.js +184 -0
- package/dist/plugins/ui.js +185 -21
- package/dist/registry/agents.js +138 -0
- package/dist/registry/plugins.js +91 -50
- package/dist/services/agent-packages.js +103 -0
- package/dist/services/plugins.js +98 -0
- package/dist/services/storage.js +360 -94
- package/docs/agents.md +39 -66
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +70 -58
- package/docs/templates/AGENT.example.md +57 -0
- package/package.json +3 -2
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +14 -4
- package/src/app/server.ts +23 -10
- package/src/app/types.ts +385 -16
- package/src/assets/icon.svg +4 -1
- package/src/bus/plugin.ts +67 -0
- package/src/bus/services.ts +666 -0
- package/src/bus/types.ts +147 -0
- package/src/harness/context.ts +160 -0
- package/src/harness/event-normalizer.ts +82 -0
- package/src/harness/orchestrator.ts +35 -273
- package/src/harness/process.ts +28 -4
- package/src/harness/queue-processor.ts +309 -0
- package/src/harness/runtime-factory.ts +125 -0
- package/src/plugins/ai-sdk/index.ts +44 -0
- package/src/plugins/ai-sdk/runtime.ts +410 -0
- package/src/plugins/ai-sdk/system-prompt.ts +4 -0
- package/src/plugins/approval/index.ts +228 -0
- package/src/plugins/delegation/index.ts +94 -0
- package/src/plugins/mcp/index.ts +128 -0
- package/src/plugins/shell/index.ts +123 -0
- package/src/plugins/storage-tools/index.ts +101 -0
- package/src/plugins/ui/index.ts +227 -0
- package/src/registry/plugins.ts +106 -55
- package/src/services/plugins.ts +133 -0
- package/src/services/storage.ts +465 -137
- package/src/agents/system.ts +0 -112
- package/src/plugins/ai-sdk.ts +0 -197
- package/src/plugins/delegation.ts +0 -60
- package/src/plugins/mcp.ts +0 -154
- package/src/plugins/storage.ts +0 -725
- package/src/plugins/ui.ts +0 -57
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AgentInvokeEvent, OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
3
|
-
import { resolvePlugin } from '../registry/plugins.js';
|
|
1
|
+
import { OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
4
2
|
import { storageService } from '../services/storage.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { createAgentRuntime } from './runtime-factory.js';
|
|
4
|
+
import { EventNormalizer } from './event-normalizer.js';
|
|
5
|
+
import { QueueProcessor } from './queue-processor.js';
|
|
7
6
|
|
|
8
7
|
export interface ExecuteAgentOptions {
|
|
9
8
|
runId: string;
|
|
@@ -11,7 +10,7 @@ export interface ExecuteAgentOptions {
|
|
|
11
10
|
event: OpenBotEvent;
|
|
12
11
|
channelId: string;
|
|
13
12
|
threadId?: string;
|
|
14
|
-
onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<void>;
|
|
13
|
+
onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<boolean | void>;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
export interface DispatchOptions {
|
|
@@ -20,94 +19,7 @@ export interface DispatchOptions {
|
|
|
20
19
|
event: OpenBotEvent;
|
|
21
20
|
channelId: string;
|
|
22
21
|
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()];
|
|
22
|
+
onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<boolean | void>;
|
|
111
23
|
}
|
|
112
24
|
|
|
113
25
|
export const orchestratorService = {
|
|
@@ -116,137 +28,31 @@ export const orchestratorService = {
|
|
|
116
28
|
* Handles routing and initial UI message creation.
|
|
117
29
|
*/
|
|
118
30
|
dispatch: async (options: DispatchOptions): Promise<void> => {
|
|
119
|
-
const { runId,
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
}
|
|
31
|
+
const { runId, channelId, threadId, onEvent } = options;
|
|
32
|
+
|
|
33
|
+
// 1. Normalize incoming event
|
|
34
|
+
const { finalEvent, finalAgentId } = await EventNormalizer.normalize(options.event, {
|
|
35
|
+
runId,
|
|
36
|
+
agentId: options.agentId,
|
|
37
|
+
channelId,
|
|
38
|
+
threadId,
|
|
39
|
+
onEvent,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 2. Initialize Queue Processor
|
|
43
|
+
const processor = new QueueProcessor({
|
|
44
|
+
runId,
|
|
45
|
+
channelId,
|
|
46
|
+
threadId,
|
|
47
|
+
onEvent,
|
|
48
|
+
executeAgent: orchestratorService.executeAgent,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 3. Enqueue initial event
|
|
52
|
+
processor.enqueue({ agentId: finalAgentId, event: finalEvent });
|
|
53
|
+
|
|
54
|
+
// 4. Run execution loop
|
|
55
|
+
await processor.run();
|
|
250
56
|
},
|
|
251
57
|
|
|
252
58
|
/**
|
|
@@ -282,61 +88,17 @@ export const orchestratorService = {
|
|
|
282
88
|
}
|
|
283
89
|
throw error;
|
|
284
90
|
}
|
|
285
|
-
const agentRuntime = await createAgentRuntime(agentState);
|
|
286
|
-
|
|
287
|
-
let hasProducedOutput = false;
|
|
288
91
|
|
|
289
|
-
await
|
|
290
|
-
{
|
|
291
|
-
type: 'agent:run:start',
|
|
292
|
-
data: {
|
|
293
|
-
runId,
|
|
294
|
-
agentId,
|
|
295
|
-
channelId,
|
|
296
|
-
threadId,
|
|
297
|
-
},
|
|
298
|
-
},
|
|
299
|
-
agentState,
|
|
300
|
-
);
|
|
92
|
+
const agentRuntime = await createAgentRuntime(agentState);
|
|
301
93
|
|
|
302
94
|
try {
|
|
303
95
|
// RUN the agent runtime
|
|
304
96
|
for await (const chunk of agentRuntime.run(event, { state: agentState, runId })) {
|
|
305
|
-
|
|
306
|
-
hasProducedOutput = true;
|
|
307
|
-
chunk.meta = { ...chunk.meta, agentId };
|
|
308
|
-
} else if (chunk.type.startsWith('action:')) {
|
|
309
|
-
hasProducedOutput = true;
|
|
310
|
-
}
|
|
97
|
+
chunk.meta = { ...chunk.meta, agentId };
|
|
311
98
|
await onEvent(chunk, agentState);
|
|
312
99
|
}
|
|
313
|
-
}
|
|
314
|
-
|
|
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
|
-
);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error(`[orchestrator] Agent run failed: ${agentId}`, error);
|
|
340
102
|
}
|
|
341
103
|
},
|
|
342
104
|
};
|
package/src/harness/process.ts
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
|
-
import { StoredVariable } from '../app/config.js';
|
|
1
|
+
import { loadVariables, StoredVariable } from '../app/config.js';
|
|
2
|
+
|
|
3
|
+
/** Keys last applied from workspace `variables.json` (used to unset removed entries). */
|
|
4
|
+
let lastWorkspaceVariableKeys = new Set<string>();
|
|
5
|
+
|
|
6
|
+
function applyVariablesList(variables: StoredVariable[]) {
|
|
7
|
+
const nextKeys = new Set(variables.map((v) => v.key));
|
|
8
|
+
for (const key of lastWorkspaceVariableKeys) {
|
|
9
|
+
if (!nextKeys.has(key)) {
|
|
10
|
+
delete process.env[key];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
for (const variable of variables) {
|
|
14
|
+
process.env[variable.key] = variable.value;
|
|
15
|
+
}
|
|
16
|
+
lastWorkspaceVariableKeys = nextKeys;
|
|
17
|
+
}
|
|
2
18
|
|
|
3
19
|
export const processService = {
|
|
20
|
+
/**
|
|
21
|
+
* Reload workspace variables from disk into `process.env`.
|
|
22
|
+
* Call after server start and whenever `variables.json` changes.
|
|
23
|
+
*/
|
|
24
|
+
syncWorkspaceVariablesToProcessEnv: () => {
|
|
25
|
+
const { variables } = loadVariables();
|
|
26
|
+
applyVariablesList(variables);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/** Apply a variable list directly (same unset semantics as sync). Prefer `syncWorkspaceVariablesToProcessEnv` when reading from disk. */
|
|
4
30
|
applyVariablesToProcessEnv: (variables: StoredVariable[]) => {
|
|
5
|
-
|
|
6
|
-
process.env[variable.key] = variable.value;
|
|
7
|
-
}
|
|
31
|
+
applyVariablesList(variables);
|
|
8
32
|
},
|
|
9
33
|
};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AgentInvokeEvent,
|
|
3
|
+
DelegateResultEvent,
|
|
4
|
+
DelegationRequestEvent,
|
|
5
|
+
HandoffRequestEvent,
|
|
6
|
+
OpenBotEvent,
|
|
7
|
+
OpenBotState,
|
|
8
|
+
} from '../app/types.js';
|
|
9
|
+
import { ensureEventId } from '../app/utils.js';
|
|
10
|
+
import { storageService } from '../services/storage.js';
|
|
11
|
+
|
|
12
|
+
export interface QueueItem {
|
|
13
|
+
agentId: string;
|
|
14
|
+
event: OpenBotEvent;
|
|
15
|
+
delegationContext?: {
|
|
16
|
+
parentAgentId: string;
|
|
17
|
+
toolCallId: string;
|
|
18
|
+
delegationWidgetId?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface QueueProcessorOptions {
|
|
23
|
+
runId: string;
|
|
24
|
+
channelId: string;
|
|
25
|
+
threadId?: string;
|
|
26
|
+
onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<boolean | void>;
|
|
27
|
+
executeAgent: (options: {
|
|
28
|
+
runId: string;
|
|
29
|
+
agentId: string;
|
|
30
|
+
event: OpenBotEvent;
|
|
31
|
+
channelId: string;
|
|
32
|
+
threadId?: string;
|
|
33
|
+
onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<boolean | void>;
|
|
34
|
+
}) => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class QueueProcessor {
|
|
38
|
+
private currentQueue: QueueItem[] = [];
|
|
39
|
+
private currentThreadId?: string;
|
|
40
|
+
private readonly MAX_ITERATIONS = 20;
|
|
41
|
+
|
|
42
|
+
constructor(private options: QueueProcessorOptions) {
|
|
43
|
+
this.currentThreadId = options.threadId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
enqueue(item: QueueItem) {
|
|
47
|
+
this.currentQueue.push(item);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async run() {
|
|
51
|
+
let iterations = 0;
|
|
52
|
+
|
|
53
|
+
while (this.currentQueue.length > 0 && iterations < this.MAX_ITERATIONS) {
|
|
54
|
+
iterations++;
|
|
55
|
+
|
|
56
|
+
// Group by agentId to avoid parallel state corruption for the same agent.
|
|
57
|
+
const groups = new Map<string, QueueItem[]>();
|
|
58
|
+
for (const item of this.currentQueue) {
|
|
59
|
+
const list = groups.get(item.agentId) || [];
|
|
60
|
+
list.push(item);
|
|
61
|
+
groups.set(item.agentId, list);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const nextQueue: QueueItem[] = [];
|
|
65
|
+
|
|
66
|
+
// Run each agent group in parallel
|
|
67
|
+
await Promise.all(
|
|
68
|
+
Array.from(groups.entries()).map(async ([agentId, items]) => {
|
|
69
|
+
// Run items for the SAME agent sequentially to preserve event order and state consistency.
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
const { event: currentEvent, delegationContext } = item;
|
|
72
|
+
|
|
73
|
+
// Track delegation/handoff requests queued in this step to avoid accidental duplicates.
|
|
74
|
+
const queuedRequestKeys = new Set<string>();
|
|
75
|
+
const queuedItems: QueueItem[] = [];
|
|
76
|
+
const runOutputs: string[] = [];
|
|
77
|
+
|
|
78
|
+
const runOnEvent = async (chunk: OpenBotEvent, state: OpenBotState) => {
|
|
79
|
+
// 0. Filter out echoed input events to prevent duplication in the UI/storage
|
|
80
|
+
if (chunk.type === currentEvent.type && chunk.id === currentEvent.id) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 1. Detect if a new thread was created and update the context for the rest of the loop
|
|
85
|
+
if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
|
|
86
|
+
this.currentThreadId = chunk.data.threadId || this.currentThreadId;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 2. Internal routing (handoff/delegation requests are internal — not forwarded)
|
|
90
|
+
if (chunk.type === 'handoff:request' || chunk.type === 'delegation:request') {
|
|
91
|
+
const isHandoff = chunk.type === 'handoff:request';
|
|
92
|
+
const request = isHandoff
|
|
93
|
+
? (chunk as HandoffRequestEvent)
|
|
94
|
+
: (chunk as DelegationRequestEvent);
|
|
95
|
+
const targetAgentId = request.data?.agentId;
|
|
96
|
+
const toolCallId =
|
|
97
|
+
typeof request.meta?.toolCallId === 'string'
|
|
98
|
+
? request.meta.toolCallId
|
|
99
|
+
: undefined;
|
|
100
|
+
const requestKey = isHandoff
|
|
101
|
+
? `handoff:${targetAgentId}:${request.data?.content ?? ''}`
|
|
102
|
+
: `delegate:${toolCallId ?? 'missing'}:${targetAgentId}:${request.data?.content ?? ''}`;
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
targetAgentId &&
|
|
106
|
+
targetAgentId !== agentId &&
|
|
107
|
+
!queuedRequestKeys.has(requestKey)
|
|
108
|
+
) {
|
|
109
|
+
queuedRequestKeys.add(requestKey);
|
|
110
|
+
const targetEvent = ensureEventId({
|
|
111
|
+
type: 'agent:invoke',
|
|
112
|
+
data: {
|
|
113
|
+
role: 'user',
|
|
114
|
+
content: request.data.content,
|
|
115
|
+
},
|
|
116
|
+
meta: {
|
|
117
|
+
...(request.meta || {}),
|
|
118
|
+
threadId: this.currentThreadId,
|
|
119
|
+
},
|
|
120
|
+
} satisfies AgentInvokeEvent) as AgentInvokeEvent;
|
|
121
|
+
|
|
122
|
+
if (isHandoff) {
|
|
123
|
+
queuedItems.push({ agentId: targetAgentId, event: targetEvent });
|
|
124
|
+
} else {
|
|
125
|
+
if (!toolCallId) {
|
|
126
|
+
// Emit error output (this triggers run start if not already started)
|
|
127
|
+
await runOnEvent(
|
|
128
|
+
ensureEventId({
|
|
129
|
+
type: 'agent:output',
|
|
130
|
+
data: {
|
|
131
|
+
content:
|
|
132
|
+
'Delegation request ignored: missing toolCallId. Please retry delegation.',
|
|
133
|
+
},
|
|
134
|
+
meta: {
|
|
135
|
+
agentId,
|
|
136
|
+
threadId: this.currentThreadId,
|
|
137
|
+
},
|
|
138
|
+
} as OpenBotEvent),
|
|
139
|
+
state,
|
|
140
|
+
);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
const parentAgentId =
|
|
144
|
+
typeof request.meta?.parentAgentId === 'string'
|
|
145
|
+
? request.meta.parentAgentId
|
|
146
|
+
: agentId;
|
|
147
|
+
const delegationWidgetId =
|
|
148
|
+
typeof request.meta?.delegationWidgetId === 'string'
|
|
149
|
+
? request.meta.delegationWidgetId
|
|
150
|
+
: undefined;
|
|
151
|
+
queuedItems.push({
|
|
152
|
+
agentId: targetAgentId,
|
|
153
|
+
event: targetEvent,
|
|
154
|
+
delegationContext: {
|
|
155
|
+
parentAgentId,
|
|
156
|
+
toolCallId,
|
|
157
|
+
delegationWidgetId,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (chunk.type === 'agent:output') {
|
|
166
|
+
const content = chunk.data?.content;
|
|
167
|
+
if (typeof content === 'string' && content.trim().length > 0) {
|
|
168
|
+
runOutputs.push(content.trim());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// For delegate mode, child agent execution is internal:
|
|
173
|
+
// capture outputs for parent tool result, but don't stream child events to clients/storage.
|
|
174
|
+
if (delegationContext) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// If we get here, the event is accepted and should be emitted.
|
|
179
|
+
await this.options.onEvent(chunk, state);
|
|
180
|
+
return true;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const startState = await storageService.getOpenBotState({
|
|
184
|
+
runId: this.options.runId,
|
|
185
|
+
agentId,
|
|
186
|
+
channelId: this.options.channelId,
|
|
187
|
+
threadId: this.currentThreadId,
|
|
188
|
+
event: currentEvent,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await this.options.onEvent(
|
|
192
|
+
{
|
|
193
|
+
type: 'agent:run:start',
|
|
194
|
+
data: {
|
|
195
|
+
runId: this.options.runId,
|
|
196
|
+
agentId,
|
|
197
|
+
channelId: this.options.channelId,
|
|
198
|
+
threadId: this.currentThreadId
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
startState,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
await this.options.executeAgent({
|
|
206
|
+
runId: this.options.runId,
|
|
207
|
+
agentId,
|
|
208
|
+
event: currentEvent,
|
|
209
|
+
channelId: this.options.channelId,
|
|
210
|
+
threadId: this.currentThreadId,
|
|
211
|
+
onEvent: runOnEvent,
|
|
212
|
+
});
|
|
213
|
+
} finally {
|
|
214
|
+
const endState = await storageService.getOpenBotState({
|
|
215
|
+
runId: this.options.runId,
|
|
216
|
+
agentId,
|
|
217
|
+
channelId: this.options.channelId,
|
|
218
|
+
threadId: this.currentThreadId,
|
|
219
|
+
event: currentEvent,
|
|
220
|
+
});
|
|
221
|
+
await this.options.onEvent(
|
|
222
|
+
{
|
|
223
|
+
type: 'agent:run:end',
|
|
224
|
+
data: {
|
|
225
|
+
runId: this.options.runId,
|
|
226
|
+
agentId,
|
|
227
|
+
channelId: this.options.channelId,
|
|
228
|
+
threadId: this.currentThreadId
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
endState,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (delegationContext) {
|
|
236
|
+
const summary =
|
|
237
|
+
runOutputs.length > 0
|
|
238
|
+
? runOutputs.join('\n\n').slice(0, 4000)
|
|
239
|
+
: `Delegated agent "${agentId}" completed with no textual output.`;
|
|
240
|
+
|
|
241
|
+
const delegateResultEvent: DelegateResultEvent = ensureEventId({
|
|
242
|
+
type: 'action:delegate:result',
|
|
243
|
+
data: {
|
|
244
|
+
success: true,
|
|
245
|
+
agentId,
|
|
246
|
+
summary,
|
|
247
|
+
},
|
|
248
|
+
meta: {
|
|
249
|
+
toolCallId: delegationContext.toolCallId,
|
|
250
|
+
agentId: delegationContext.parentAgentId,
|
|
251
|
+
threadId: this.currentThreadId,
|
|
252
|
+
},
|
|
253
|
+
} satisfies DelegateResultEvent) as DelegateResultEvent;
|
|
254
|
+
|
|
255
|
+
if (delegationContext.delegationWidgetId) {
|
|
256
|
+
await this.options.onEvent(
|
|
257
|
+
ensureEventId({
|
|
258
|
+
type: 'client:ui:widget',
|
|
259
|
+
data: {
|
|
260
|
+
kind: 'message',
|
|
261
|
+
widgetId: delegationContext.delegationWidgetId,
|
|
262
|
+
title: `Delegation complete: ${agentId}`,
|
|
263
|
+
body:
|
|
264
|
+
runOutputs.length > 0
|
|
265
|
+
? 'Delegated task finished. Parent agent is preparing final response.'
|
|
266
|
+
: 'Delegated task finished with no textual output. Parent agent will continue.',
|
|
267
|
+
state: 'submitted',
|
|
268
|
+
metadata: {
|
|
269
|
+
type: 'delegation:status',
|
|
270
|
+
phase: 'completed',
|
|
271
|
+
delegatedAgentId: agentId,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
meta: {
|
|
275
|
+
agentId: delegationContext.parentAgentId,
|
|
276
|
+
threadId: this.currentThreadId,
|
|
277
|
+
},
|
|
278
|
+
} as OpenBotEvent),
|
|
279
|
+
await storageService.getOpenBotState({
|
|
280
|
+
runId: this.options.runId,
|
|
281
|
+
agentId: delegationContext.parentAgentId,
|
|
282
|
+
channelId: this.options.channelId,
|
|
283
|
+
threadId: this.currentThreadId,
|
|
284
|
+
event: delegateResultEvent,
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
nextQueue.push({
|
|
290
|
+
agentId: delegationContext.parentAgentId,
|
|
291
|
+
event: delegateResultEvent,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
nextQueue.push(...queuedItems);
|
|
296
|
+
}
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
this.currentQueue = nextQueue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (iterations >= this.MAX_ITERATIONS) {
|
|
304
|
+
console.warn(
|
|
305
|
+
`[orchestrator] Reached MAX_ITERATIONS (${this.MAX_ITERATIONS}). Stopping execution.`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|