openbot 0.2.12 → 0.2.13

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 (141) hide show
  1. package/.prettierrc +8 -0
  2. package/AGENTS.md +68 -0
  3. package/CONTRIBUTING.md +74 -0
  4. package/LICENSE +21 -0
  5. package/README.md +117 -14
  6. package/dist/agents/system.js +106 -0
  7. package/dist/app/cli.js +27 -0
  8. package/dist/app/config.js +64 -0
  9. package/dist/app/server.js +237 -0
  10. package/dist/app/utils.js +35 -0
  11. package/dist/harness/agent-harness.js +45 -0
  12. package/dist/harness/mcp.js +61 -0
  13. package/dist/harness/orchestrator.js +273 -0
  14. package/dist/harness/process.js +7 -0
  15. package/dist/plugins/ai-sdk.js +141 -0
  16. package/dist/plugins/delegation.js +52 -0
  17. package/dist/plugins/mcp.js +140 -0
  18. package/dist/plugins/storage.js +502 -0
  19. package/dist/plugins/ui.js +47 -0
  20. package/dist/registry/plugins.js +73 -0
  21. package/dist/services/storage.js +724 -0
  22. package/docs/README.md +7 -0
  23. package/docs/agents.md +83 -0
  24. package/docs/architecture.md +34 -0
  25. package/docs/plugins.md +77 -0
  26. package/logo-black.png +0 -0
  27. package/{dist/assets/logo.js → logo-black.svg} +24 -24
  28. package/{dist/ui/sidebar.js → logo-white.svg} +23 -88
  29. package/package.json +10 -9
  30. package/src/agents/system.ts +112 -0
  31. package/src/app/cli.ts +38 -0
  32. package/src/app/config.ts +104 -0
  33. package/src/app/server.ts +284 -0
  34. package/src/app/types.ts +476 -0
  35. package/src/app/utils.ts +43 -0
  36. package/src/assets/icon.svg +1 -0
  37. package/src/harness/agent-harness.ts +58 -0
  38. package/src/harness/mcp.ts +78 -0
  39. package/src/harness/orchestrator.ts +342 -0
  40. package/src/harness/process.ts +9 -0
  41. package/src/harness/types.ts +34 -0
  42. package/src/plugins/ai-sdk.ts +197 -0
  43. package/src/plugins/delegation.ts +60 -0
  44. package/src/plugins/mcp.ts +154 -0
  45. package/src/plugins/storage.ts +725 -0
  46. package/src/plugins/ui.ts +57 -0
  47. package/src/registry/plugins.ts +85 -0
  48. package/src/services/storage.ts +957 -0
  49. package/tsconfig.json +18 -0
  50. package/dist/agents/agent-creator.js +0 -74
  51. package/dist/agents/browser-agent.js +0 -31
  52. package/dist/agents/os-agent.js +0 -32
  53. package/dist/agents/planner-agent.js +0 -32
  54. package/dist/agents/topic-agent.js +0 -46
  55. package/dist/architecture/execution-engine.js +0 -151
  56. package/dist/architecture/intent-classifier.js +0 -26
  57. package/dist/architecture/planner.js +0 -106
  58. package/dist/automation-worker.js +0 -121
  59. package/dist/automations.js +0 -52
  60. package/dist/cli.js +0 -279
  61. package/dist/config.js +0 -53
  62. package/dist/core/agents.js +0 -41
  63. package/dist/core/delegation.js +0 -230
  64. package/dist/core/manager.js +0 -96
  65. package/dist/core/plugins.js +0 -74
  66. package/dist/core/router.js +0 -191
  67. package/dist/handlers/init.js +0 -29
  68. package/dist/handlers/session-change.js +0 -21
  69. package/dist/handlers/settings.js +0 -47
  70. package/dist/handlers/tab-change.js +0 -14
  71. package/dist/installers.js +0 -156
  72. package/dist/marketplace.js +0 -80
  73. package/dist/model-catalog.js +0 -132
  74. package/dist/model-defaults.js +0 -25
  75. package/dist/models.js +0 -47
  76. package/dist/open-bot.js +0 -51
  77. package/dist/orchestrator/direct-invocation.js +0 -13
  78. package/dist/orchestrator/events.js +0 -36
  79. package/dist/orchestrator/state.js +0 -54
  80. package/dist/orchestrator.js +0 -422
  81. package/dist/plugins/agent/index.js +0 -81
  82. package/dist/plugins/approval/index.js +0 -100
  83. package/dist/plugins/brain/identity.js +0 -77
  84. package/dist/plugins/brain/index.js +0 -204
  85. package/dist/plugins/brain/memory.js +0 -120
  86. package/dist/plugins/brain/prompt.js +0 -46
  87. package/dist/plugins/brain/types.js +0 -45
  88. package/dist/plugins/brain/ui.js +0 -7
  89. package/dist/plugins/browser/index.js +0 -629
  90. package/dist/plugins/browser/ui.js +0 -13
  91. package/dist/plugins/file-system/index.js +0 -171
  92. package/dist/plugins/file-system/ui.js +0 -6
  93. package/dist/plugins/llm/context-budget.js +0 -139
  94. package/dist/plugins/llm/context-shaping.js +0 -177
  95. package/dist/plugins/llm/index.js +0 -380
  96. package/dist/plugins/memory/index.js +0 -220
  97. package/dist/plugins/memory/memory.js +0 -122
  98. package/dist/plugins/memory/prompt.js +0 -55
  99. package/dist/plugins/memory/types.js +0 -45
  100. package/dist/plugins/meta-agent/index.js +0 -570
  101. package/dist/plugins/meta-agent/ui.js +0 -11
  102. package/dist/plugins/shell/index.js +0 -100
  103. package/dist/plugins/shell/ui.js +0 -6
  104. package/dist/plugins/skills/index.js +0 -286
  105. package/dist/plugins/skills/types.js +0 -50
  106. package/dist/plugins/skills/ui.js +0 -12
  107. package/dist/registry/agent-registry.js +0 -35
  108. package/dist/registry/index.js +0 -2
  109. package/dist/registry/plugin-loader.js +0 -499
  110. package/dist/registry/plugin-registry.js +0 -44
  111. package/dist/registry/ts-agent-loader.js +0 -82
  112. package/dist/registry/yaml-agent-loader.js +0 -246
  113. package/dist/runtime/execution-trace.js +0 -41
  114. package/dist/runtime/intent-routing.js +0 -26
  115. package/dist/runtime/openbot-runtime.js +0 -354
  116. package/dist/server.js +0 -890
  117. package/dist/session.js +0 -179
  118. package/dist/ui/block.js +0 -12
  119. package/dist/ui/header.js +0 -52
  120. package/dist/ui/layout.js +0 -26
  121. package/dist/ui/navigation.js +0 -15
  122. package/dist/ui/settings.js +0 -106
  123. package/dist/ui/skills.js +0 -7
  124. package/dist/ui/thread.js +0 -16
  125. package/dist/ui/widgets/action-list.js +0 -2
  126. package/dist/ui/widgets/approval-card.js +0 -9
  127. package/dist/ui/widgets/code-snippet.js +0 -2
  128. package/dist/ui/widgets/data-block.js +0 -2
  129. package/dist/ui/widgets/data-table.js +0 -2
  130. package/dist/ui/widgets/delegation.js +0 -29
  131. package/dist/ui/widgets/empty-state.js +0 -2
  132. package/dist/ui/widgets/index.js +0 -23
  133. package/dist/ui/widgets/inquiry.js +0 -7
  134. package/dist/ui/widgets/key-value.js +0 -2
  135. package/dist/ui/widgets/progress-step.js +0 -2
  136. package/dist/ui/widgets/resource-card.js +0 -2
  137. package/dist/ui/widgets/status.js +0 -2
  138. package/dist/ui/widgets/todo-list.js +0 -2
  139. package/dist/version.js +0 -62
  140. /package/dist/{types.js → app/types.js} +0 -0
  141. /package/dist/{architecture/contracts.js → harness/types.js} +0 -0
@@ -0,0 +1,237 @@
1
+ import 'dotenv/config';
2
+ import express from 'express';
3
+ import cors from 'cors';
4
+ import z from 'zod';
5
+ import path from 'path';
6
+ import fs from 'fs/promises';
7
+ import { generateId } from 'melony';
8
+ import { DEFAULT_BASE_DIR, loadConfig, loadVariables, resolvePath } from '../app/config.js';
9
+ import { processService } from '../harness/process.js';
10
+ import { storageService } from '../services/storage.js';
11
+ import { AgentHarness } from '../harness/agent-harness.js';
12
+ import { initPlugins } from '../registry/plugins.js';
13
+ import { ensureEventId, openBotEventFromQuery } from './utils.js';
14
+ export async function startServer(options = {}) {
15
+ const publishEventSchema = z
16
+ .object({
17
+ id: z.string().optional(),
18
+ type: z.string().min(1, 'Event type is required'),
19
+ data: z.unknown().optional(),
20
+ meta: z.unknown().optional(),
21
+ })
22
+ .passthrough();
23
+ const config = loadConfig();
24
+ const variables = loadVariables();
25
+ processService.applyVariablesToProcessEnv(variables.variables);
26
+ const baseDir = config.baseDir || DEFAULT_BASE_DIR;
27
+ const openBotDir = resolvePath(baseDir);
28
+ const PORT = Number(options.port ?? config.port ?? process.env.PORT ?? 4132);
29
+ const app = express();
30
+ const clients = new Map();
31
+ const GLOBAL_CHANNEL_ID = '__global__';
32
+ const activeRuns = new Map();
33
+ const agentsDir = path.join(openBotDir, 'agents');
34
+ const pluginsDir = path.join(openBotDir, 'plugins');
35
+ await fs.mkdir(agentsDir, { recursive: true });
36
+ await fs.mkdir(pluginsDir, { recursive: true });
37
+ initPlugins(pluginsDir);
38
+ const getContext = (req) => {
39
+ const channelId = req.get('x-openbot-channel-id') || req.query.channelId || (req.body && req.body.channelId);
40
+ const threadId = req.get('x-openbot-thread-id') || req.query.threadId || (req.body && req.body.threadId);
41
+ const agentId = req.get('x-openbot-agent-id') || req.query.agentId || (req.body && req.body.agentId);
42
+ const runId = req.get('x-openbot-run-id') ||
43
+ req.query.runId ||
44
+ (req.body && req.body.runId) ||
45
+ `run_${generateId()}`;
46
+ const responseType = req.get('x-openbot-response-type') ||
47
+ req.query.responseType ||
48
+ (req.body && req.body.responseType);
49
+ return {
50
+ channelId: (channelId || (threadId ? 'general' : 'general')), // Default to general if none
51
+ threadId: threadId,
52
+ agentId: agentId,
53
+ runId: runId,
54
+ responseType: responseType,
55
+ };
56
+ };
57
+ const getClientKey = (channelId, threadId) => threadId ? `${channelId}:${threadId}` : channelId;
58
+ const sendToClientKey = (clientKey, chunk) => {
59
+ const threadClients = clients.get(clientKey);
60
+ if (!threadClients)
61
+ return;
62
+ threadClients.forEach((client) => {
63
+ if (!client.writableEnded) {
64
+ client.write(`data: ${JSON.stringify(chunk)}\n\n`);
65
+ }
66
+ });
67
+ };
68
+ const buildActiveRunsSnapshot = () => {
69
+ const byChannel = new Map();
70
+ for (const run of activeRuns.values()) {
71
+ const existing = byChannel.get(run.channelId) ?? {
72
+ activeCount: 0,
73
+ agentIds: new Set(),
74
+ };
75
+ existing.activeCount += 1;
76
+ existing.agentIds.add(run.agentId);
77
+ byChannel.set(run.channelId, existing);
78
+ }
79
+ return {
80
+ type: 'agent:active-runs:snapshot',
81
+ data: {
82
+ channels: Array.from(byChannel.entries()).map(([channelId, value]) => ({
83
+ channelId,
84
+ activeCount: value.activeCount,
85
+ agentIds: Array.from(value.agentIds),
86
+ })),
87
+ },
88
+ };
89
+ };
90
+ app.use(cors());
91
+ app.use(express.json({ limit: '20mb' }));
92
+ app.get('/api/events', (req, res) => {
93
+ const { channelId, threadId } = getContext(req);
94
+ const clientKey = getClientKey(channelId, threadId);
95
+ // SSE response headers: keep the HTTP connection open and unbuffered.
96
+ res.setHeader('Content-Type', 'text/event-stream');
97
+ res.setHeader('Cache-Control', 'no-cache');
98
+ res.setHeader('Connection', 'keep-alive');
99
+ // Helpful behind proxies (for example nginx) to avoid response buffering.
100
+ res.setHeader('X-Accel-Buffering', 'no');
101
+ // Flush headers immediately so the browser moves from "connecting" to "open".
102
+ res.flushHeaders();
103
+ // Tell EventSource clients how long to wait before reconnecting.
104
+ res.write('retry: 3000\n');
105
+ // Initial comment frame so the stream has activity right after subscribe.
106
+ res.write(': connected\n\n');
107
+ // Track all active SSE subscribers for fan-out in /api/publish.
108
+ if (!clients.has(clientKey)) {
109
+ clients.set(clientKey, []);
110
+ }
111
+ clients.get(clientKey).push(res);
112
+ if (channelId === GLOBAL_CHANNEL_ID) {
113
+ const snapshot = buildActiveRunsSnapshot();
114
+ ensureEventId(snapshot);
115
+ res.write(`data: ${JSON.stringify(snapshot)}\n\n`);
116
+ }
117
+ // Keep connection alive through intermediaries that close idle streams.
118
+ const heartbeat = setInterval(() => {
119
+ if (!res.writableEnded) {
120
+ res.write(': keepalive\n\n');
121
+ }
122
+ }, 25000);
123
+ req.on('close', () => {
124
+ // Cleanup heartbeat + subscriber when the client disconnects.
125
+ clearInterval(heartbeat);
126
+ const threadClients = clients.get(clientKey);
127
+ if (threadClients) {
128
+ const index = threadClients.indexOf(res);
129
+ if (index !== -1) {
130
+ threadClients.splice(index, 1);
131
+ }
132
+ if (threadClients.length === 0) {
133
+ clients.delete(clientKey);
134
+ }
135
+ }
136
+ });
137
+ });
138
+ app.post('/api/publish', async (req, res) => {
139
+ const parseResult = publishEventSchema.safeParse(req.body);
140
+ if (!parseResult.success) {
141
+ res.status(400).json({
142
+ error: 'Invalid publish event payload',
143
+ details: parseResult.error.issues.map((issue) => issue.message),
144
+ });
145
+ return;
146
+ }
147
+ const event = parseResult.data;
148
+ const { channelId, threadId, agentId, runId } = getContext(req);
149
+ if (!channelId || !channelId.trim()) {
150
+ res.status(400).json({ error: 'channelId is required' });
151
+ return;
152
+ }
153
+ const onEvent = async (chunk, state) => {
154
+ ensureEventId(chunk);
155
+ const targetChannelId = state?.channelId || channelId;
156
+ const targetThreadId = state?.threadId || threadId;
157
+ const targetClientKey = getClientKey(targetChannelId, targetThreadId);
158
+ if (chunk.type === 'agent:run:start') {
159
+ activeRuns.set(chunk.data.runId, {
160
+ runId: chunk.data.runId,
161
+ channelId: chunk.data.channelId,
162
+ threadId: chunk.data.threadId,
163
+ agentId: chunk.data.agentId,
164
+ });
165
+ }
166
+ else if (chunk.type === 'agent:run:end') {
167
+ activeRuns.delete(chunk.data.runId);
168
+ }
169
+ await storageService.storeEvent({
170
+ channelId: targetChannelId,
171
+ threadId: targetThreadId,
172
+ event: chunk,
173
+ });
174
+ sendToClientKey(targetClientKey, chunk);
175
+ if (chunk.type === 'agent:run:start' || chunk.type === 'agent:run:end') {
176
+ sendToClientKey(GLOBAL_CHANNEL_ID, chunk);
177
+ }
178
+ };
179
+ try {
180
+ const harness = new AgentHarness({
181
+ runId,
182
+ agentId: agentId || 'system',
183
+ channelId,
184
+ threadId,
185
+ onEvent,
186
+ });
187
+ await harness.dispatch(event);
188
+ res.sendStatus(200);
189
+ }
190
+ catch (error) {
191
+ console.error('[publish] Failed to dispatch event', {
192
+ runId,
193
+ channelId,
194
+ threadId,
195
+ eventType: event.type,
196
+ error,
197
+ });
198
+ res.status(500).json({ error: 'Failed to process publish event' });
199
+ }
200
+ });
201
+ app.get('/api/state', async (req, res) => {
202
+ let event;
203
+ try {
204
+ event = openBotEventFromQuery(req.query);
205
+ }
206
+ catch (e) {
207
+ const message = e instanceof Error ? e.message : 'Invalid query';
208
+ res.status(400).json({ error: message });
209
+ return;
210
+ }
211
+ const { channelId, threadId, agentId, runId } = getContext(req);
212
+ const events = [];
213
+ const onEvent = async (chunk) => {
214
+ events.push(chunk);
215
+ };
216
+ try {
217
+ const harness = new AgentHarness({
218
+ runId,
219
+ agentId: agentId || 'system',
220
+ channelId,
221
+ threadId,
222
+ onEvent,
223
+ });
224
+ await harness.dispatch(event);
225
+ res.json({ events });
226
+ }
227
+ catch (error) {
228
+ res.status(500).json({ error: 'Failed to process state request' });
229
+ }
230
+ });
231
+ app.listen(PORT, () => {
232
+ console.log(`\x1b[32mOpenBot server listening at http://localhost:${PORT}\x1b[0m`);
233
+ console.log(` - Events endpoint: GET /events (SSE)`);
234
+ console.log(` - Publish endpoint: POST /publish`);
235
+ console.log(` - State endpoint: GET /state`);
236
+ });
237
+ }
@@ -0,0 +1,35 @@
1
+ import crypto from 'node:crypto';
2
+ /** Express query values are always strings; parse JSON `data` for typed events. */
3
+ export function openBotEventFromQuery(query) {
4
+ const typeRaw = query.type;
5
+ const type = typeof typeRaw === 'string'
6
+ ? typeRaw
7
+ : Array.isArray(typeRaw) && typeof typeRaw[0] === 'string'
8
+ ? typeRaw[0]
9
+ : '';
10
+ const dataRaw = query.data;
11
+ const dataStr = typeof dataRaw === 'string'
12
+ ? dataRaw
13
+ : Array.isArray(dataRaw) && typeof dataRaw[0] === 'string'
14
+ ? dataRaw[0]
15
+ : undefined;
16
+ if (dataStr === undefined || dataStr.trim() === '') {
17
+ return { type };
18
+ }
19
+ try {
20
+ const data = JSON.parse(dataStr);
21
+ return { type, data };
22
+ }
23
+ catch {
24
+ throw new Error('Query parameter "data" must be valid JSON when provided');
25
+ }
26
+ }
27
+ /**
28
+ * Ensures the event has a unique ID.
29
+ */
30
+ export function ensureEventId(event) {
31
+ if (!event.id) {
32
+ event.id = crypto.randomUUID();
33
+ }
34
+ return event;
35
+ }
@@ -0,0 +1,45 @@
1
+ import { orchestratorService } from './orchestrator.js';
2
+ import { ensureEventId } from '../app/utils.js';
3
+ /**
4
+ * Standard implementation of the Agent Harness.
5
+ * It wraps the orchestrator logic into a clean, stateful container.
6
+ */
7
+ export class AgentHarness {
8
+ constructor(options) {
9
+ this.eventCallbacks = [];
10
+ this.runId = options.runId;
11
+ this.agentId = options.agentId;
12
+ this.channelId = options.channelId;
13
+ this.threadId = options.threadId;
14
+ if (options.onEvent) {
15
+ this.eventCallbacks.push(options.onEvent);
16
+ }
17
+ }
18
+ /**
19
+ * Dispatches an event to the agent within this harness.
20
+ */
21
+ async dispatch(event) {
22
+ ensureEventId(event);
23
+ await orchestratorService.dispatch({
24
+ runId: this.runId,
25
+ agentId: this.agentId,
26
+ event,
27
+ channelId: this.channelId,
28
+ threadId: this.threadId,
29
+ onEvent: async (chunk, state) => {
30
+ // Update internal thread state if it changes (e.g. thread creation)
31
+ if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
32
+ this.threadId = chunk.data.threadId || this.threadId;
33
+ }
34
+ // Notify all observers
35
+ await Promise.all(this.eventCallbacks.map(cb => cb(chunk, state)));
36
+ }
37
+ });
38
+ }
39
+ /**
40
+ * Adds an event listener to the harness.
41
+ */
42
+ onEvent(callback) {
43
+ this.eventCallbacks.push(callback);
44
+ }
45
+ }
@@ -0,0 +1,61 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
+ import { loadConfig } from '../app/config.js';
4
+ class MCPService {
5
+ constructor() {
6
+ this.clients = new Map();
7
+ this.transports = new Map();
8
+ }
9
+ getServerConfig(serverId) {
10
+ const config = loadConfig();
11
+ const server = (config.mcpServers || []).find((s) => s.id === serverId);
12
+ if (!server) {
13
+ throw new Error(`MCP server "${serverId}" is not configured`);
14
+ }
15
+ return server;
16
+ }
17
+ async getClient(serverId) {
18
+ const existing = this.clients.get(serverId);
19
+ if (existing) {
20
+ return existing;
21
+ }
22
+ const server = this.getServerConfig(serverId);
23
+ const client = new Client({
24
+ name: 'openbot-v2',
25
+ version: '0.1.0',
26
+ }, {
27
+ capabilities: {},
28
+ });
29
+ const transport = new StdioClientTransport({
30
+ command: server.command,
31
+ args: server.args || [],
32
+ env: server.env,
33
+ cwd: server.cwd,
34
+ });
35
+ await client.connect(transport);
36
+ this.clients.set(serverId, client);
37
+ this.transports.set(serverId, transport);
38
+ return client;
39
+ }
40
+ async listServers() {
41
+ const config = loadConfig();
42
+ return (config.mcpServers || []).map((server) => server.id);
43
+ }
44
+ async listTools(serverId) {
45
+ const client = await this.getClient(serverId);
46
+ const result = await client.listTools();
47
+ return (result.tools || []).map((tool) => ({
48
+ name: tool.name,
49
+ description: tool.description,
50
+ inputSchema: tool.inputSchema,
51
+ }));
52
+ }
53
+ async callTool(serverId, toolName, args) {
54
+ const client = await this.getClient(serverId);
55
+ return client.callTool({
56
+ name: toolName,
57
+ arguments: args,
58
+ });
59
+ }
60
+ }
61
+ export const mcpService = new MCPService();
@@ -0,0 +1,273 @@
1
+ import { melony } from 'melony';
2
+ import { resolvePlugin } from '../registry/plugins.js';
3
+ 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
+ }
82
+ export const orchestratorService = {
83
+ /**
84
+ * The primary entry point for all events coming into the system (e.g. from the API).
85
+ * Handles routing and initial UI message creation.
86
+ */
87
+ 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
+ }
199
+ },
200
+ /**
201
+ * Executes a single agent runtime.
202
+ */
203
+ executeAgent: async (options) => {
204
+ const { runId, agentId, event, channelId, threadId, onEvent } = options;
205
+ let agentState;
206
+ try {
207
+ agentState = await storageService.getOpenBotState(options);
208
+ }
209
+ catch (error) {
210
+ if (error.code === 'AGENT_NOT_FOUND') {
211
+ const fallbackState = await storageService.getOpenBotState({
212
+ runId,
213
+ agentId: 'system',
214
+ channelId,
215
+ threadId,
216
+ event,
217
+ });
218
+ const warning = `⚠️ Agent **${agentId}** does not exist. Please check the agent ID and try again.`;
219
+ await onEvent({
220
+ type: 'agent:output',
221
+ data: { content: warning },
222
+ meta: { agentId: 'system', threadId },
223
+ }, fallbackState);
224
+ return;
225
+ }
226
+ throw error;
227
+ }
228
+ 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
+ try {
240
+ // RUN the agent runtime
241
+ 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
+ }
249
+ await onEvent(chunk, agentState);
250
+ }
251
+ }
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);
271
+ }
272
+ },
273
+ };
@@ -0,0 +1,7 @@
1
+ export const processService = {
2
+ applyVariablesToProcessEnv: (variables) => {
3
+ for (const variable of variables) {
4
+ process.env[variable.key] = variable.value;
5
+ }
6
+ },
7
+ };