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
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Cheap, dependency-free token estimator. Roughly char/4 — fine for budget
3
+ * enforcement; can be swapped for a tokenizer-backed implementation later
4
+ * without touching providers.
5
+ */
6
+ export const estimateTokens = (text) => Math.ceil((text?.length ?? 0) / 4);
7
+ /**
8
+ * Hard cap (in characters) on a single context item. Keeps any one provider
9
+ * — typically the recent-events feed — from monopolising the prompt budget.
10
+ */
11
+ const ITEM_HARD_CHAR_CAP = 6000;
12
+ const truncate = (text, maxChars) => text.length <= maxChars ? text : `${text.slice(0, maxChars)}\n…[truncated]`;
13
+ export class ContextEngine {
14
+ constructor() {
15
+ this.providers = [];
16
+ this.processors = [];
17
+ }
18
+ registerProvider(provider) {
19
+ this.providers.push(provider);
20
+ }
21
+ registerProcessor(processor) {
22
+ this.processors.push(processor);
23
+ }
24
+ async buildContext(state, storage) {
25
+ let items = [];
26
+ for (const provider of this.providers) {
27
+ try {
28
+ const providedItems = await provider.provide(state, storage);
29
+ for (const item of providedItems) {
30
+ items.push({ ...item, content: truncate(item.content, ITEM_HARD_CHAR_CAP) });
31
+ }
32
+ }
33
+ catch (error) {
34
+ console.warn(`[ContextEngine] Provider ${provider.name} failed:`, error);
35
+ }
36
+ }
37
+ for (const processor of this.processors) {
38
+ try {
39
+ items = await processor.process(items, state);
40
+ }
41
+ catch (error) {
42
+ console.warn(`[ContextEngine] Processor ${processor.name} failed:`, error);
43
+ }
44
+ }
45
+ return items
46
+ .sort((a, b) => b.priority - a.priority)
47
+ .map((item) => item.content)
48
+ .join('\n\n');
49
+ }
50
+ }
51
+ /**
52
+ * Default context engine. Order of providers is by emit order; final ordering
53
+ * in the prompt is determined by `priority`. The token-budget processor runs
54
+ * last so dropping happens after every provider has contributed.
55
+ */
56
+ export function createDefaultContextEngine() {
57
+ const engine = new ContextEngine();
58
+ engine.registerProvider(new AgentDetailsProvider());
59
+ engine.registerProvider(new ChannelDetailsProvider());
60
+ engine.registerProvider(new ThreadDetailsProvider());
61
+ engine.registerProvider(new MemoryProvider());
62
+ engine.registerProvider(new RecentEventsProvider());
63
+ engine.registerProcessor(new TokenBudgetProcessor());
64
+ return engine;
65
+ }
66
+ class AgentDetailsProvider {
67
+ constructor() {
68
+ this.name = 'agent-details';
69
+ }
70
+ async provide(state) {
71
+ if (!state.agentDetails)
72
+ return [];
73
+ return [{
74
+ id: 'agent-details',
75
+ type: 'agent',
76
+ priority: 100,
77
+ content: `## AGENT NAME\n${state.agentDetails.name}\n\n## AGENT SPECIFICATION\n${state.agentDetails.instructions}`
78
+ }];
79
+ }
80
+ }
81
+ class ChannelDetailsProvider {
82
+ constructor() {
83
+ this.name = 'channel-details';
84
+ }
85
+ async provide(state) {
86
+ if (!state.channelDetails)
87
+ return [];
88
+ return [{
89
+ id: 'channel-details',
90
+ type: 'channel',
91
+ priority: 80,
92
+ content: `## CHANNEL NAME\n${state.channelDetails.name}\n\n## CHANNEL SPECIFICATION\n${state.channelDetails.spec}`
93
+ }];
94
+ }
95
+ }
96
+ class ThreadDetailsProvider {
97
+ constructor() {
98
+ this.name = 'thread-details';
99
+ }
100
+ async provide(state) {
101
+ if (!state.threadDetails)
102
+ return [];
103
+ return [{
104
+ id: 'thread-details',
105
+ type: 'thread',
106
+ priority: 90,
107
+ content: `## THREAD NAME\n${state.threadDetails.name}\n\n## THREAD SPECIFICATION\n${state.threadDetails.spec}`
108
+ }];
109
+ }
110
+ }
111
+ /**
112
+ * Fetches relevant memories (global + active agent + active channel) and
113
+ * surfaces them at high priority so the LLM treats them as ground truth
114
+ * rather than chat history.
115
+ */
116
+ class MemoryProvider {
117
+ constructor() {
118
+ this.name = 'memory';
119
+ }
120
+ async provide(state, storage) {
121
+ if (!storage?.listMemories)
122
+ return [];
123
+ try {
124
+ const scopes = ['global', `agent:${state.agentId}`];
125
+ if (state.channelId)
126
+ scopes.push(`channel:${state.channelId}`);
127
+ const records = await storage.listMemories({ scopes, limit: 50 });
128
+ if (records.length === 0)
129
+ return [];
130
+ const formatted = records
131
+ .map((r) => {
132
+ const tags = r.tags?.length ? ` [${r.tags.join(', ')}]` : '';
133
+ const scopeLabel = r.scope === 'global' ? 'global' : r.scope;
134
+ return `- (${scopeLabel}${tags}) ${r.content}`;
135
+ })
136
+ .join('\n');
137
+ return [
138
+ {
139
+ id: 'memory',
140
+ type: 'memory',
141
+ priority: 95,
142
+ content: `## REMEMBERED FACTS\nThese are durable facts you previously stored with the \`remember\` tool. Trust them unless contradicted by the user. Use \`forget\` to remove ones that are stale.\n\n${formatted}`,
143
+ },
144
+ ];
145
+ }
146
+ catch (error) {
147
+ console.warn('[ContextEngine] MemoryProvider failed:', error);
148
+ return [];
149
+ }
150
+ }
151
+ }
152
+ /**
153
+ * Event types we omit from the recent-events context block. They duplicate
154
+ * information already in the conversation history, are infrastructural
155
+ * noise, or are too large to be useful as a tail summary.
156
+ */
157
+ const NOISY_EVENT_PREFIXES = [
158
+ 'agent:invoke',
159
+ 'agent:output',
160
+ 'agent:run',
161
+ 'agent:active-runs',
162
+ 'client:ui',
163
+ 'stream:',
164
+ 'action:storage:get-',
165
+ 'action:storage:patch-',
166
+ ];
167
+ const MAX_RECENT_EVENTS = 20;
168
+ const MAX_EVENT_DATA_CHARS = 300;
169
+ const isNoisyEvent = (event) => NOISY_EVENT_PREFIXES.some((prefix) => event.type.startsWith(prefix));
170
+ const summarizeEvent = (event) => {
171
+ const data = event.data;
172
+ if (data === undefined)
173
+ return `- ${event.type}`;
174
+ let payload;
175
+ try {
176
+ payload = typeof data === 'string' ? data : JSON.stringify(data);
177
+ }
178
+ catch {
179
+ payload = '[unserialisable]';
180
+ }
181
+ if (payload.length > MAX_EVENT_DATA_CHARS) {
182
+ payload = `${payload.slice(0, MAX_EVENT_DATA_CHARS)}…`;
183
+ }
184
+ return `- ${event.type}: ${payload}`;
185
+ };
186
+ class RecentEventsProvider {
187
+ constructor() {
188
+ this.name = 'recent-events';
189
+ }
190
+ async provide(state, storage) {
191
+ if (!storage)
192
+ return [];
193
+ const channelId = state.channelId;
194
+ const threadId = state.threadId;
195
+ try {
196
+ const events = await storage.getEvents({ channelId, threadId });
197
+ const filtered = events.filter((e) => !isNoisyEvent(e));
198
+ if (filtered.length === 0)
199
+ return [];
200
+ const formatted = filtered.slice(-MAX_RECENT_EVENTS).map(summarizeEvent).join('\n');
201
+ return [
202
+ {
203
+ id: threadId ? 'thread-events' : 'channel-events',
204
+ type: 'events',
205
+ priority: 70,
206
+ content: `## ${threadId ? 'THREAD' : 'CHANNEL'} RECENT ACTIVITIES (events)\n${formatted}`,
207
+ },
208
+ ];
209
+ }
210
+ catch (error) {
211
+ console.warn('[ContextEngine] Failed to fetch events:', error);
212
+ return [];
213
+ }
214
+ }
215
+ }
216
+ /**
217
+ * Drops the lowest-priority items until the assembled prompt fits within the
218
+ * token budget. The first item with priority >= `keepFloor` is always kept,
219
+ * so the agent's own instructions can never be evicted. Stable on ties:
220
+ * later-emitted items go first.
221
+ */
222
+ export class TokenBudgetProcessor {
223
+ constructor(budget = TokenBudgetProcessor.DEFAULT_BUDGET, keepFloor = TokenBudgetProcessor.KEEP_FLOOR) {
224
+ this.budget = budget;
225
+ this.keepFloor = keepFloor;
226
+ this.name = 'token-budget';
227
+ }
228
+ async process(items) {
229
+ const sorted = [...items].sort((a, b) => b.priority - a.priority);
230
+ const out = [];
231
+ let used = 0;
232
+ for (const item of sorted) {
233
+ const cost = estimateTokens(item.content);
234
+ if (item.priority >= this.keepFloor) {
235
+ out.push(item);
236
+ used += cost;
237
+ continue;
238
+ }
239
+ if (used + cost <= this.budget) {
240
+ out.push(item);
241
+ used += cost;
242
+ }
243
+ }
244
+ return out;
245
+ }
246
+ }
247
+ /** Soft prompt budget in tokens (matches gpt-4o-mini's reasonable system slice). */
248
+ TokenBudgetProcessor.DEFAULT_BUDGET = 8000;
249
+ /** Items at or above this priority are never dropped. */
250
+ TokenBudgetProcessor.KEEP_FLOOR = 100;
@@ -0,0 +1,59 @@
1
+ import { ensureEventId } from '../app/utils.js';
2
+ import { storageService } from '../services/storage.js';
3
+ export const EventNormalizer = {
4
+ /**
5
+ * Normalizes incoming events, converting raw inputs like user:input to agent:invoke.
6
+ * Also handles initial state storage and event bus propagation for user inputs.
7
+ */
8
+ normalize: async (event, options) => {
9
+ const { runId, agentId, channelId, threadId, onEvent } = options;
10
+ // 0. Ensure the incoming event has a unique ID immediately
11
+ ensureEventId(event);
12
+ let finalAgentId = agentId || 'system';
13
+ let finalEvent = event;
14
+ // 1. Convert user:input (or other raw inputs) to agent:invoke
15
+ const rawContent = event.data?.content || '';
16
+ if (event.type === 'user:input' || event.type === 'agent:invoke') {
17
+ const normalizedInvokeEvent = {
18
+ type: 'agent:invoke',
19
+ id: event.id,
20
+ data: {
21
+ content: rawContent,
22
+ role: 'user',
23
+ },
24
+ meta: {
25
+ agentId: 'system',
26
+ userId: event.meta?.userId,
27
+ userName: event.meta?.userName,
28
+ userAvatarUrl: event.meta?.userAvatarUrl,
29
+ },
30
+ };
31
+ finalEvent = normalizedInvokeEvent;
32
+ // 1. Store the user's input in the current context (main channel or existing thread)
33
+ const initialState = await storageService.getOpenBotState({
34
+ runId,
35
+ agentId: 'system',
36
+ channelId,
37
+ threadId: threadId,
38
+ event: finalEvent,
39
+ });
40
+ // 2. Propagate the user's input to the event bus
41
+ await onEvent(finalEvent, initialState);
42
+ // 3. Prepare the event for the target agent
43
+ finalEvent = {
44
+ ...event,
45
+ type: 'agent:invoke',
46
+ data: {
47
+ ...(event.data || {}),
48
+ content: rawContent,
49
+ },
50
+ meta: {
51
+ ...(event.meta || {}),
52
+ // The threadId in meta is the anchor for new threads (Slack-style)
53
+ threadId: threadId || finalEvent.id,
54
+ },
55
+ };
56
+ }
57
+ return { finalEvent, finalAgentId };
58
+ },
59
+ };
@@ -1,201 +1,34 @@
1
- import { melony } from 'melony';
2
- import { resolvePlugin } from '../registry/plugins.js';
3
1
  import { storageService } from '../services/storage.js';
4
- import { ensureEventId } from '../app/utils.js';
5
- import { loadConfig } from '../app/config.js';
6
- /**
7
- * Enhances agent instructions with a list of other available agents.
8
- */
9
- export async function enhanceInstructions(state) {
10
- const { agentId, agentDetails } = state;
11
- if (!agentDetails)
12
- return;
13
- try {
14
- const agents = await storageService.getAgents();
15
- const otherAgents = agents.filter((a) => a.id !== agentId);
16
- if (otherAgents.length === 0)
17
- return;
18
- const agentsList = otherAgents
19
- .map((a) => `- **${a.id}**${a.description ? `: ${a.description}` : ''}`)
20
- .join('\n');
21
- const header = '### Available Agents for Delegation:';
22
- if (!agentDetails.instructions.includes(header)) {
23
- 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.`;
24
- }
25
- }
26
- catch (error) {
27
- console.warn('[agent] Failed to enhance instructions', error);
28
- }
29
- }
30
- /**
31
- * Factory for creating an OpenBot Melony Runtime.
32
- */
33
- async function createAgentRuntime(state) {
34
- // 1. Prepare instructions
35
- await enhanceInstructions(state);
36
- // 2. Initialize runtime with the agent plugin
37
- const runtime = melony({
38
- initialState: state,
39
- });
40
- // 3. Normalize plugin specs:
41
- // - runtime can be a single spec or an array (for backward/forward compatibility)
42
- // - plugins remains supported as additional specs
43
- const runtimeSpecs = Array.isArray(state.agentDetails?.runtime)
44
- ? state.agentDetails.runtime
45
- : state.agentDetails?.runtime
46
- ? [state.agentDetails.runtime]
47
- : [];
48
- const { globalPlugins = [] } = loadConfig();
49
- const agentSpecs = [...runtimeSpecs, ...(state.agentDetails?.plugins || [])];
50
- const pluginSpecs = mergePluginSpecs(globalPlugins, agentSpecs);
51
- // 4. Load normalized plugins
52
- for (const p of pluginSpecs) {
53
- const name = typeof p === 'string' ? p : p?.name;
54
- if (!name || typeof name !== 'string') {
55
- continue;
56
- }
57
- const config = typeof p === 'string' ? {} : { ...(p.config || {}) };
58
- const plugin = await resolvePlugin(name, config);
59
- if (plugin) {
60
- runtime.use(plugin);
61
- }
62
- }
63
- return runtime.build();
64
- }
65
- function mergePluginSpecs(globalSpecs, agentSpecs) {
66
- const specsByName = new Map();
67
- for (const spec of globalSpecs) {
68
- const name = typeof spec === 'string' ? spec : spec?.name;
69
- if (!name || typeof name !== 'string')
70
- continue;
71
- specsByName.set(name, spec);
72
- }
73
- // Agent-defined plugins override global ones with the same name.
74
- for (const spec of agentSpecs) {
75
- const name = typeof spec === 'string' ? spec : spec?.name;
76
- if (!name || typeof name !== 'string')
77
- continue;
78
- specsByName.set(name, spec);
79
- }
80
- return [...specsByName.values()];
81
- }
2
+ import { createAgentRuntime } from './runtime-factory.js';
3
+ import { EventNormalizer } from './event-normalizer.js';
4
+ import { QueueProcessor } from './queue-processor.js';
82
5
  export const orchestratorService = {
83
6
  /**
84
7
  * The primary entry point for all events coming into the system (e.g. from the API).
85
8
  * Handles routing and initial UI message creation.
86
9
  */
87
10
  dispatch: async (options) => {
88
- const { runId, agentId, event, channelId, threadId, onEvent } = options;
89
- // 0. Ensure the incoming event has a unique ID immediately
90
- ensureEventId(event);
91
- let finalAgentId = agentId || 'system';
92
- let finalEvent = event;
93
- let currentThreadId = threadId;
94
- // 1. Convert user:input (or other raw inputs) to agent:invoke
95
- const rawContent = event.data?.content || '';
96
- if (event.type === 'user:input' || event.type === 'agent:invoke') {
97
- const normalizedInvokeEvent = {
98
- type: 'agent:invoke',
99
- id: event.id,
100
- data: {
101
- content: rawContent,
102
- role: 'user',
103
- },
104
- meta: {
105
- agentId: 'system',
106
- userId: event.meta?.userId,
107
- userName: event.meta?.userName,
108
- userAvatarUrl: event.meta?.userAvatarUrl,
109
- },
110
- };
111
- finalEvent = normalizedInvokeEvent;
112
- // 1. Store the user's input in the current context (main channel or existing thread)
113
- const initialState = await storageService.getOpenBotState({
114
- runId,
115
- agentId: 'system',
116
- channelId,
117
- threadId: currentThreadId,
118
- event: finalEvent,
119
- });
120
- // 2. Propagate the user's input to the event bus
121
- await onEvent(finalEvent, initialState);
122
- // 3. Prepare the event for the target agent
123
- finalEvent = {
124
- ...event,
125
- type: 'agent:invoke',
126
- data: {
127
- ...(event.data || {}),
128
- content: rawContent,
129
- },
130
- meta: {
131
- ...(event.meta || {}),
132
- // The threadId in meta is the anchor for new threads (Slack-style)
133
- threadId: currentThreadId || finalEvent.id,
134
- },
135
- };
136
- }
137
- // 4. Linear Execution Loop
138
- // Instead of recursion, we use a queue to process agents one after another.
139
- const queue = [
140
- { agentId: finalAgentId, event: finalEvent },
141
- ];
142
- // Safety check to prevent infinite loops
143
- let iterations = 0;
144
- const MAX_ITERATIONS = 20;
145
- while (queue.length > 0 && iterations < MAX_ITERATIONS) {
146
- iterations++;
147
- const { agentId, event: currentEvent } = queue.shift();
148
- // Track agents queued in this step to avoid double-runs (e.g. from tool delegation)
149
- const queuedAgents = new Set();
150
- const delegations = [];
151
- await orchestratorService.executeAgent({
152
- runId,
153
- agentId,
154
- event: currentEvent,
155
- channelId,
156
- threadId: currentThreadId,
157
- onEvent: async (chunk, state) => {
158
- // 0. Filter out echoed input events to prevent duplication in the UI/storage
159
- if (chunk.type === currentEvent.type && chunk.id === currentEvent.id) {
160
- return;
161
- }
162
- // 1. Detect if a new thread was created and update the context for the rest of the loop
163
- if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
164
- currentThreadId = chunk.data.threadId || currentThreadId;
165
- }
166
- // 2. Detect delegations to queue them for the next iteration
167
- let targetAgentId = null;
168
- let targetEvent = null;
169
- if (chunk.type === 'agent:invoke' &&
170
- chunk.data.agentId &&
171
- chunk.data.agentId !== agentId) {
172
- targetAgentId = chunk.data.agentId;
173
- targetEvent = {
174
- ...chunk,
175
- meta: {
176
- ...(chunk.meta || {}),
177
- threadId: currentThreadId,
178
- },
179
- };
180
- }
181
- // 3. Queue only if not already queued in this step
182
- if (targetAgentId && targetEvent && !queuedAgents.has(targetAgentId)) {
183
- queuedAgents.add(targetAgentId);
184
- delegations.push({
185
- agentId: targetAgentId,
186
- event: targetEvent,
187
- });
188
- }
189
- // Propagate all events
190
- await onEvent(chunk, state);
191
- },
192
- });
193
- // Add found delegations to the queue
194
- queue.push(...delegations);
195
- }
196
- if (iterations >= MAX_ITERATIONS) {
197
- console.warn(`[orchestrator] Reached MAX_ITERATIONS (${MAX_ITERATIONS}). Stopping execution.`);
198
- }
11
+ const { runId, channelId, threadId, onEvent } = options;
12
+ // 1. Normalize incoming event
13
+ const { finalEvent, finalAgentId } = await EventNormalizer.normalize(options.event, {
14
+ runId,
15
+ agentId: options.agentId,
16
+ channelId,
17
+ threadId,
18
+ onEvent,
19
+ });
20
+ // 2. Initialize Queue Processor
21
+ const processor = new QueueProcessor({
22
+ runId,
23
+ channelId,
24
+ threadId,
25
+ onEvent,
26
+ executeAgent: orchestratorService.executeAgent,
27
+ });
28
+ // 3. Enqueue initial event
29
+ processor.enqueue({ agentId: finalAgentId, event: finalEvent });
30
+ // 4. Run execution loop
31
+ await processor.run();
199
32
  },
200
33
  /**
201
34
  * Executes a single agent runtime.
@@ -226,48 +59,15 @@ export const orchestratorService = {
226
59
  throw error;
227
60
  }
228
61
  const agentRuntime = await createAgentRuntime(agentState);
229
- let hasProducedOutput = false;
230
- await onEvent({
231
- type: 'agent:run:start',
232
- data: {
233
- runId,
234
- agentId,
235
- channelId,
236
- threadId,
237
- },
238
- }, agentState);
239
62
  try {
240
63
  // RUN the agent runtime
241
64
  for await (const chunk of agentRuntime.run(event, { state: agentState, runId })) {
242
- if (chunk.type === 'agent:output') {
243
- hasProducedOutput = true;
244
- chunk.meta = { ...chunk.meta, agentId };
245
- }
246
- else if (chunk.type.startsWith('action:')) {
247
- hasProducedOutput = true;
248
- }
65
+ chunk.meta = { ...chunk.meta, agentId };
249
66
  await onEvent(chunk, agentState);
250
67
  }
251
68
  }
252
- finally {
253
- await onEvent({
254
- type: 'agent:run:end',
255
- data: {
256
- runId,
257
- agentId,
258
- channelId,
259
- threadId,
260
- },
261
- }, agentState);
262
- }
263
- // Fallback for agents that don't produce output (e.g. misconfigured or silent)
264
- if (event.type === 'agent:invoke' && !hasProducedOutput) {
265
- const warning = `⚠️ **${agentId}** is not configured to handle inputs. Please check its plugin configuration.`;
266
- await onEvent({
267
- type: 'agent:output',
268
- data: { content: warning },
269
- meta: { agentId },
270
- }, agentState);
69
+ catch (error) {
70
+ console.error(`[orchestrator] Agent run failed: ${agentId}`, error);
271
71
  }
272
72
  },
273
73
  };
@@ -1,7 +1,29 @@
1
+ import { loadVariables } from '../app/config.js';
2
+ /** Keys last applied from workspace `variables.json` (used to unset removed entries). */
3
+ let lastWorkspaceVariableKeys = new Set();
4
+ function applyVariablesList(variables) {
5
+ const nextKeys = new Set(variables.map((v) => v.key));
6
+ for (const key of lastWorkspaceVariableKeys) {
7
+ if (!nextKeys.has(key)) {
8
+ delete process.env[key];
9
+ }
10
+ }
11
+ for (const variable of variables) {
12
+ process.env[variable.key] = variable.value;
13
+ }
14
+ lastWorkspaceVariableKeys = nextKeys;
15
+ }
1
16
  export const processService = {
17
+ /**
18
+ * Reload workspace variables from disk into `process.env`.
19
+ * Call after server start and whenever `variables.json` changes.
20
+ */
21
+ syncWorkspaceVariablesToProcessEnv: () => {
22
+ const { variables } = loadVariables();
23
+ applyVariablesList(variables);
24
+ },
25
+ /** Apply a variable list directly (same unset semantics as sync). Prefer `syncWorkspaceVariablesToProcessEnv` when reading from disk. */
2
26
  applyVariablesToProcessEnv: (variables) => {
3
- for (const variable of variables) {
4
- process.env[variable.key] = variable.value;
5
- }
27
+ applyVariablesList(variables);
6
28
  },
7
29
  };