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.
Files changed (80) 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 +600 -0
  17. package/dist/bus/types.js +1 -0
  18. package/dist/harness/context.js +131 -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 +330 -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/shell/index.js +99 -0
  34. package/dist/plugins/shell.js +123 -0
  35. package/dist/plugins/storage-tools/index.js +85 -0
  36. package/dist/plugins/storage.js +240 -5
  37. package/dist/plugins/ui/index.js +184 -0
  38. package/dist/plugins/ui.js +185 -21
  39. package/dist/registry/agents.js +138 -0
  40. package/dist/registry/plugins.js +91 -50
  41. package/dist/services/agent-packages.js +103 -0
  42. package/dist/services/plugins.js +98 -0
  43. package/dist/services/storage.js +360 -94
  44. package/docs/agents.md +39 -66
  45. package/docs/architecture.md +1 -1
  46. package/docs/plugins.md +70 -58
  47. package/docs/templates/AGENT.example.md +57 -0
  48. package/package.json +3 -2
  49. package/src/app/cli.ts +1 -1
  50. package/src/app/config.ts +14 -4
  51. package/src/app/server.ts +23 -10
  52. package/src/app/types.ts +385 -16
  53. package/src/assets/icon.svg +4 -1
  54. package/src/bus/plugin.ts +67 -0
  55. package/src/bus/services.ts +666 -0
  56. package/src/bus/types.ts +147 -0
  57. package/src/harness/context.ts +160 -0
  58. package/src/harness/event-normalizer.ts +82 -0
  59. package/src/harness/orchestrator.ts +35 -273
  60. package/src/harness/process.ts +28 -4
  61. package/src/harness/queue-processor.ts +309 -0
  62. package/src/harness/runtime-factory.ts +125 -0
  63. package/src/plugins/ai-sdk/index.ts +44 -0
  64. package/src/plugins/ai-sdk/runtime.ts +410 -0
  65. package/src/plugins/ai-sdk/system-prompt.ts +4 -0
  66. package/src/plugins/approval/index.ts +228 -0
  67. package/src/plugins/delegation/index.ts +94 -0
  68. package/src/plugins/mcp/index.ts +128 -0
  69. package/src/plugins/shell/index.ts +123 -0
  70. package/src/plugins/storage-tools/index.ts +101 -0
  71. package/src/plugins/ui/index.ts +227 -0
  72. package/src/registry/plugins.ts +106 -55
  73. package/src/services/plugins.ts +133 -0
  74. package/src/services/storage.ts +465 -137
  75. package/src/agents/system.ts +0 -112
  76. package/src/plugins/ai-sdk.ts +0 -197
  77. package/src/plugins/delegation.ts +0 -60
  78. package/src/plugins/mcp.ts +0 -154
  79. package/src/plugins/storage.ts +0 -725
  80. package/src/plugins/ui.ts +0 -57
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The core engine that orchestrates context building.
3
+ */
4
+ export class ContextEngine {
5
+ constructor() {
6
+ this.providers = [];
7
+ this.processors = [];
8
+ }
9
+ registerProvider(provider) {
10
+ this.providers.push(provider);
11
+ }
12
+ registerProcessor(processor) {
13
+ this.processors.push(processor);
14
+ }
15
+ async buildContext(state, storage) {
16
+ // 1. Collect context from all providers
17
+ let items = [];
18
+ for (const provider of this.providers) {
19
+ try {
20
+ const providedItems = await provider.provide(state, storage);
21
+ items.push(...providedItems);
22
+ }
23
+ catch (error) {
24
+ console.warn(`[ContextEngine] Provider ${provider.name} failed:`, error);
25
+ }
26
+ }
27
+ // 2. Run through processors
28
+ for (const processor of this.processors) {
29
+ try {
30
+ items = await processor.process(items, state);
31
+ }
32
+ catch (error) {
33
+ console.warn(`[ContextEngine] Processor ${processor.name} failed:`, error);
34
+ }
35
+ }
36
+ // 3. Format items into a single string
37
+ return items
38
+ .sort((a, b) => b.priority - a.priority)
39
+ .map(item => item.content)
40
+ .join('\n\n');
41
+ }
42
+ }
43
+ /**
44
+ * Default implementation of a Context Engine with basic providers.
45
+ */
46
+ export function createDefaultContextEngine() {
47
+ const engine = new ContextEngine();
48
+ // Basic Providers
49
+ engine.registerProvider(new AgentDetailsProvider());
50
+ engine.registerProvider(new ChannelDetailsProvider());
51
+ engine.registerProvider(new ThreadDetailsProvider());
52
+ engine.registerProvider(new RecentEventsProvider());
53
+ return engine;
54
+ }
55
+ class AgentDetailsProvider {
56
+ constructor() {
57
+ this.name = 'agent-details';
58
+ }
59
+ async provide(state) {
60
+ if (!state.agentDetails)
61
+ return [];
62
+ return [{
63
+ id: 'agent-details',
64
+ type: 'agent',
65
+ priority: 100,
66
+ content: `## AGENT NAME\n${state.agentDetails.name}\n\n## AGENT SPECIFICATION\n${state.agentDetails.instructions}`
67
+ }];
68
+ }
69
+ }
70
+ class ChannelDetailsProvider {
71
+ constructor() {
72
+ this.name = 'channel-details';
73
+ }
74
+ async provide(state) {
75
+ if (!state.channelDetails)
76
+ return [];
77
+ return [{
78
+ id: 'channel-details',
79
+ type: 'channel',
80
+ priority: 80,
81
+ content: `## CHANNEL NAME\n${state.channelDetails.name}\n\n## CHANNEL SPECIFICATION\n${state.channelDetails.spec}`
82
+ }];
83
+ }
84
+ }
85
+ class ThreadDetailsProvider {
86
+ constructor() {
87
+ this.name = 'thread-details';
88
+ }
89
+ async provide(state) {
90
+ if (!state.threadDetails)
91
+ return [];
92
+ return [{
93
+ id: 'thread-details',
94
+ type: 'thread',
95
+ priority: 90,
96
+ content: `## THREAD NAME\n${state.threadDetails.name}\n\n## THREAD SPECIFICATION\n${state.threadDetails.spec}`
97
+ }];
98
+ }
99
+ }
100
+ class RecentEventsProvider {
101
+ constructor() {
102
+ this.name = 'recent-events';
103
+ }
104
+ async provide(state, storage) {
105
+ if (!storage)
106
+ return [];
107
+ const items = [];
108
+ // Fetch channel events if no thread, otherwise fetch thread events
109
+ const channelId = state.channelId;
110
+ const threadId = state.threadId;
111
+ try {
112
+ const events = await storage.getEvents({ channelId, threadId });
113
+ if (events.length > 0) {
114
+ const formattedEvents = events
115
+ .slice(-20)
116
+ .map((e) => `- ${e.type}: ${JSON.stringify(e.data || {})}`)
117
+ .join('\n');
118
+ items.push({
119
+ id: threadId ? 'thread-events' : 'channel-events',
120
+ type: 'events',
121
+ priority: 70,
122
+ content: `## ${threadId ? 'THREAD' : 'CHANNEL'} RECENT ACTIVITIES (events)\n${formattedEvents}`
123
+ });
124
+ }
125
+ }
126
+ catch (error) {
127
+ console.warn(`[ContextEngine] Failed to fetch events:`, error);
128
+ }
129
+ return items;
130
+ }
131
+ }
@@ -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
  };