openbot 0.2.14 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/agents/openbot/index.js +76 -0
  2. package/dist/agents/openbot/middleware/approval.js +132 -0
  3. package/dist/agents/openbot/runtime.js +289 -0
  4. package/dist/agents/openbot/system-prompt.js +32 -0
  5. package/dist/agents/openbot/tools/delegation.js +78 -0
  6. package/dist/agents/openbot/tools/mcp.js +99 -0
  7. package/dist/agents/openbot/tools/shell.js +91 -0
  8. package/dist/agents/openbot/tools/storage.js +75 -0
  9. package/dist/agents/openbot/tools/ui.js +176 -0
  10. package/dist/agents/system.js +20 -93
  11. package/dist/app/cli.js +1 -1
  12. package/dist/app/config.js +4 -1
  13. package/dist/app/server.js +15 -8
  14. package/dist/bus/agent-package.js +1 -0
  15. package/dist/bus/plugin.js +1 -0
  16. package/dist/bus/services.js +711 -0
  17. package/dist/bus/types.js +1 -0
  18. package/dist/harness/context.js +250 -0
  19. package/dist/harness/event-normalizer.js +59 -0
  20. package/dist/harness/orchestrator.js +27 -227
  21. package/dist/harness/process.js +25 -3
  22. package/dist/harness/queue-processor.js +227 -0
  23. package/dist/harness/runtime-factory.js +103 -0
  24. package/dist/plugins/ai-sdk/index.js +37 -0
  25. package/dist/plugins/ai-sdk/runtime.js +402 -0
  26. package/dist/plugins/ai-sdk/system-prompt.js +3 -0
  27. package/dist/plugins/ai-sdk.js +277 -87
  28. package/dist/plugins/approval/index.js +159 -0
  29. package/dist/plugins/approval.js +163 -0
  30. package/dist/plugins/delegation/index.js +79 -0
  31. package/dist/plugins/delegation.js +67 -11
  32. package/dist/plugins/mcp/index.js +108 -0
  33. package/dist/plugins/memory/index.js +71 -0
  34. package/dist/plugins/shell/index.js +99 -0
  35. package/dist/plugins/shell.js +123 -0
  36. package/dist/plugins/storage-tools/index.js +85 -0
  37. package/dist/plugins/storage.js +240 -5
  38. package/dist/plugins/ui/index.js +184 -0
  39. package/dist/plugins/ui.js +185 -21
  40. package/dist/registry/agents.js +138 -0
  41. package/dist/registry/plugins.js +93 -50
  42. package/dist/services/agent-packages.js +103 -0
  43. package/dist/services/memory.js +152 -0
  44. package/dist/services/plugins.js +98 -0
  45. package/dist/services/storage.js +366 -94
  46. package/docs/agents.md +52 -65
  47. package/docs/architecture.md +1 -1
  48. package/docs/plugins.md +70 -58
  49. package/docs/templates/AGENT.example.md +57 -0
  50. package/package.json +8 -7
  51. package/src/app/cli.ts +1 -1
  52. package/src/app/config.ts +14 -4
  53. package/src/app/server.ts +23 -10
  54. package/src/app/types.ts +445 -16
  55. package/src/assets/icon.svg +4 -1
  56. package/src/bus/plugin.ts +67 -0
  57. package/src/bus/services.ts +786 -0
  58. package/src/bus/types.ts +160 -0
  59. package/src/harness/context.ts +293 -0
  60. package/src/harness/event-normalizer.ts +82 -0
  61. package/src/harness/orchestrator.ts +35 -273
  62. package/src/harness/process.ts +28 -4
  63. package/src/harness/queue-processor.ts +309 -0
  64. package/src/harness/runtime-factory.ts +125 -0
  65. package/src/plugins/ai-sdk/index.ts +44 -0
  66. package/src/plugins/ai-sdk/runtime.ts +484 -0
  67. package/src/plugins/ai-sdk/system-prompt.ts +4 -0
  68. package/src/plugins/approval/index.ts +228 -0
  69. package/src/plugins/delegation/index.ts +94 -0
  70. package/src/plugins/mcp/index.ts +128 -0
  71. package/src/plugins/memory/index.ts +85 -0
  72. package/src/plugins/shell/index.ts +123 -0
  73. package/src/plugins/storage-tools/index.ts +101 -0
  74. package/src/plugins/ui/index.ts +227 -0
  75. package/src/registry/plugins.ts +108 -55
  76. package/src/services/memory.ts +213 -0
  77. package/src/services/plugins.ts +133 -0
  78. package/src/services/storage.ts +472 -137
  79. package/src/agents/system.ts +0 -112
  80. package/src/plugins/ai-sdk.ts +0 -197
  81. package/src/plugins/delegation.ts +0 -60
  82. package/src/plugins/mcp.ts +0 -154
  83. package/src/plugins/storage.ts +0 -725
  84. package/src/plugins/ui.ts +0 -57
@@ -1,9 +1,8 @@
1
- import { melony, Runtime } from 'melony';
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 { ensureEventId } from '../app/utils.js';
6
- import { loadConfig, PluginSpec } from '../app/config.js';
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, 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
- }
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 onEvent(
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
- 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
- }
97
+ chunk.meta = { ...chunk.meta, agentId };
311
98
  await onEvent(chunk, agentState);
312
99
  }
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
- );
100
+ } catch (error) {
101
+ console.error(`[orchestrator] Agent run failed: ${agentId}`, error);
340
102
  }
341
103
  },
342
104
  };
@@ -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
- for (const variable of variables) {
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
+ }