openbot 0.3.3 → 0.3.5

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.
@@ -1,6 +1,8 @@
1
1
  import { DEFAULT_PLUGINS_DIR, DEFAULT_AGENTS_DIR, DEFAULT_BASE_DIR, DEFAULT_CHANNELS_DIR, loadConfig, resolvePath, VARIABLES_FILE, } from '../app/config.js';
2
2
  import fs from 'node:fs/promises';
3
+ import { readFileSync } from 'node:fs';
3
4
  import path from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
6
  import crypto from 'node:crypto';
5
7
  import matter from 'gray-matter';
6
8
  import { aiSdkPlugin } from '../plugins/ai-sdk/index.js';
@@ -8,13 +10,31 @@ import { AI_SDK_SYSTEM_PROMPT } from '../plugins/ai-sdk/system-prompt.js';
8
10
  import { listBuiltInPlugins, parsePluginModule } from '../registry/plugins.js';
9
11
  import { processService } from '../harness/process.js';
10
12
  import { memoryService } from './memory.js';
11
- import { pathToFileURL } from 'node:url';
12
13
  const resolveBaseDir = () => {
13
14
  const config = loadConfig();
14
15
  return resolvePath(config.baseDir || DEFAULT_BASE_DIR);
15
16
  };
16
17
  const ENTITY_SVG_CANDIDATE_NAMES = ['avatar.svg', 'icon.svg', 'image.svg', 'logo.svg'];
17
18
  const toSvgDataUrl = (svg) => `data:image/svg+xml;base64,${Buffer.from(svg, 'utf-8').toString('base64')}`;
19
+ let bundledSystemAgentImage;
20
+ let bundledSystemAgentImageLoaded = false;
21
+ /** OpenBot mark from `src/assets/icon.svg` (also copied to `dist/assets` at build). */
22
+ function getBundledSystemAgentImage() {
23
+ if (bundledSystemAgentImageLoaded)
24
+ return bundledSystemAgentImage;
25
+ bundledSystemAgentImageLoaded = true;
26
+ try {
27
+ const iconPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../assets/icon.svg');
28
+ const trimmed = readFileSync(iconPath, 'utf-8').trim();
29
+ if (!trimmed.startsWith('<svg'))
30
+ return undefined;
31
+ bundledSystemAgentImage = toSvgDataUrl(trimmed);
32
+ }
33
+ catch {
34
+ bundledSystemAgentImage = undefined;
35
+ }
36
+ return bundledSystemAgentImage;
37
+ }
18
38
  const tryReadSvgDataUrl = async (filePath) => {
19
39
  try {
20
40
  const svg = await fs.readFile(filePath, 'utf-8');
@@ -72,7 +92,7 @@ function getSystemAgentDetails(overrides) {
72
92
  const defaults = {
73
93
  id: SYSTEM_AGENT_ID,
74
94
  name: 'OpenBot',
75
- image: undefined,
95
+ image: getBundledSystemAgentImage(),
76
96
  description: 'First-party orchestration agent for OpenBot. Coordinates other agents via handoff.',
77
97
  instructions: AI_SDK_SYSTEM_PROMPT,
78
98
  plugins: SYSTEM_DEFAULT_PLUGINS.map((ref) => ref.id),
@@ -599,6 +619,9 @@ export const storageService = {
599
619
  const discoveredImage = await resolveEntityImageDataUrl(agentDir);
600
620
  const stats = await fs.stat(agentMdPath);
601
621
  const pluginRefs = parsePluginRefs(data.plugins);
622
+ const frontmatterImage = typeof data.image === 'string' && data.image.trim() !== ''
623
+ ? data.image.trim()
624
+ : undefined;
602
625
  diskDetails = {
603
626
  id: agentId,
604
627
  name: typeof data.name === 'string' ? data.name : agentId,
@@ -606,7 +629,7 @@ export const storageService = {
606
629
  plugins: pluginRefs.map((ref) => ref.id),
607
630
  pluginRefs,
608
631
  description: typeof data.description === 'string' ? data.description : '',
609
- image: discoveredImage || undefined,
632
+ image: frontmatterImage || discoveredImage || undefined,
610
633
  createdAt: stats.birthtime,
611
634
  updatedAt: stats.mtime,
612
635
  };
@@ -630,7 +653,7 @@ export const storageService = {
630
653
  }
631
654
  return diskDetails;
632
655
  },
633
- createAgent: async ({ agentId, name, description = '', instructions, plugins, }) => {
656
+ createAgent: async ({ agentId, name, description = '', image, instructions, plugins, }) => {
634
657
  assertValidDiskAgentId(agentId);
635
658
  const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
636
659
  const agentMdPath = path.join(agentDir, 'AGENT.md');
@@ -656,10 +679,13 @@ export const storageService = {
656
679
  description,
657
680
  plugins: serializePluginRefs(plugins),
658
681
  };
682
+ if (typeof image === 'string' && image.trim() !== '') {
683
+ data.image = image.trim();
684
+ }
659
685
  const body = matter.stringify(`${instructions.trim()}\n`, data);
660
686
  await fs.writeFile(agentMdPath, body, 'utf-8');
661
687
  },
662
- updateAgent: async ({ agentId, name, description, instructions, plugins, }) => {
688
+ updateAgent: async ({ agentId, name, description, image, instructions, plugins, }) => {
663
689
  assertValidDiskAgentId(agentId);
664
690
  const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
665
691
  const agentMdPath = path.join(agentDir, 'AGENT.md');
@@ -683,6 +709,14 @@ export const storageService = {
683
709
  nextData.description = description;
684
710
  if (plugins !== undefined)
685
711
  nextData.plugins = serializePluginRefs(plugins);
712
+ if (image !== undefined) {
713
+ if (typeof image === 'string' && image.trim() !== '') {
714
+ nextData.image = image.trim();
715
+ }
716
+ else {
717
+ delete nextData.image;
718
+ }
719
+ }
686
720
  const nextContent = instructions !== undefined ? instructions : parsed.content;
687
721
  const body = matter.stringify(`${String(nextContent).trim()}\n`, nextData);
688
722
  await fs.writeFile(agentMdPath, body, 'utf-8');
@@ -1,14 +1,11 @@
1
1
  # Architecture
2
2
 
3
- OpenBot is an orchestration platform built on a modular, event-driven architecture. It leverages the `melony` framework to coordinate interactions between multiple specialized agents via a standardized **Agent Harness**.
3
+ OpenBot is an orchestration platform built on a modular, event-driven architecture. It leverages the `melony` framework to coordinate interactions between multiple specialized agents through a central **orchestrator** (HTTP handlers call it directly).
4
4
 
5
5
  ## Core Components
6
6
 
7
- ### 1. Agent Harness
8
- The Harness is the execution environment for agents. It provides the necessary "plumbing" (storage, communication, tools) so the agent can focus on reasoning. It handles event routing and state management.
9
-
10
- ### 2. Agent Orchestration & Routing
11
- The orchestrator is the central dispatcher within the harness. It receives user input and determines how to delegate tasks across the agent network using the following logic:
7
+ ### 1. Orchestrator & routing
8
+ The orchestrator is the execution entry point for agent work: it normalizes incoming events, runs the queue processor (handoffs and todo-driven assignees), builds per-agent Melony runtimes, and streams emitted events back to callers (for example storage and SSE). Routing across the agent network uses:
12
9
 
13
10
  1. **Command Prefix** — Explicit delegation to a specific agent (e.g., `/os list files`).
14
11
  2. **DM context** — Direct communication with a specific agent.
@@ -20,10 +17,10 @@ A dynamic registry that manages all available agents. Agents can be:
20
17
  - **YAML-based**: Rapidly defined agents in `~/.openbot/agents/*/AGENT.md`.
21
18
  - **TS Packages**: Advanced agents with custom logic in `~/.openbot/agents/*/index.ts`.
22
19
 
23
- ### 3. Plugin Registry
20
+ ### 3. Plugin registry
24
21
  The "capability layer" that provides tools and logic shared across the platform. Plugins (like `shell`, `file-system`, or `mcp`) define the actions agents can perform.
25
22
 
26
- ### 4. Orchestration Layer (Melony)
23
+ ### 4. Orchestration layer (Melony)
27
24
  The underlying event bus that handles all communication. It ensures that agents can collaborate asynchronously, share context, and emit real-time updates to the UI.
28
25
 
29
26
  ## Multi-Agent Workflow
package/package.json CHANGED
@@ -1,16 +1,11 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=20.12.0"
8
8
  },
9
- "scripts": {
10
- "dev": "tsx watch src/app/cli.ts start",
11
- "build": "tsc",
12
- "start": "node dist/app/cli.js start"
13
- },
14
9
  "bin": {
15
10
  "openbot": "./dist/app/cli.js"
16
11
  },
@@ -36,5 +31,10 @@
36
31
  "@types/node": "^20.10.1",
37
32
  "tsx": "^4.21.0",
38
33
  "typescript": "^5.9.3"
34
+ },
35
+ "scripts": {
36
+ "dev": "tsx watch src/app/cli.ts start",
37
+ "build": "tsc && mkdir -p dist/assets && cp src/assets/icon.svg dist/assets/icon.svg",
38
+ "start": "node dist/app/cli.js start"
39
39
  }
40
- }
40
+ }
package/src/app/cli.ts CHANGED
@@ -25,7 +25,7 @@ function checkNodeVersion() {
25
25
 
26
26
  checkNodeVersion();
27
27
 
28
- program.name('openbot').description('OpenBot CLI').version('0.3.3');
28
+ program.name('openbot').description('OpenBot CLI').version('0.3.4');
29
29
 
30
30
  program
31
31
  .command('start')
package/src/app/server.ts CHANGED
@@ -12,10 +12,12 @@ import { DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
12
12
  import { ActiveRunsSnapshotEvent, OpenBotEvent, OpenBotState } from './types.js';
13
13
  import { processService } from '../harness/process.js';
14
14
  import { storageService } from '../services/storage.js';
15
- import { AgentHarness } from '../harness/agent-harness.js';
15
+ import { dispatch } from '../harness/dispatcher.js';
16
16
  import { initPlugins } from '../registry/plugins.js';
17
17
  import { ensureEventId, openBotEventFromQuery } from './utils.js';
18
18
 
19
+ type Bucket = { channelId: string; threadId?: string; activeCount: number; agentIds: Set<string> };
20
+
19
21
  export interface ServerOptions {
20
22
  port?: number;
21
23
  }
@@ -94,25 +96,38 @@ export async function startServer(options: ServerOptions = {}) {
94
96
  };
95
97
 
96
98
  const buildActiveRunsSnapshot = (): ActiveRunsSnapshotEvent => {
97
- const byChannel = new Map<string, { activeCount: number; agentIds: Set<string> }>();
99
+ const byBucket = new Map<string, Bucket>();
98
100
  for (const run of activeRuns.values()) {
99
- const existing = byChannel.get(run.channelId) ?? {
100
- activeCount: 0,
101
- agentIds: new Set<string>(),
102
- };
103
- existing.activeCount += 1;
104
- existing.agentIds.add(run.agentId);
105
- byChannel.set(run.channelId, existing);
101
+ const threadId = run.threadId || undefined;
102
+ const key = JSON.stringify([run.channelId, threadId ?? null]);
103
+ let bucket = byBucket.get(key);
104
+ if (!bucket) {
105
+ bucket = { channelId: run.channelId, threadId, activeCount: 0, agentIds: new Set<string>() };
106
+ byBucket.set(key, bucket);
107
+ }
108
+ bucket.activeCount += 1;
109
+ bucket.agentIds.add(run.agentId);
106
110
  }
111
+ const channels = Array.from(byBucket.values())
112
+ .sort((a, b) => {
113
+ const c = a.channelId.localeCompare(b.channelId);
114
+ if (c !== 0) return c;
115
+ return (a.threadId ?? '').localeCompare(b.threadId ?? '');
116
+ })
117
+ .map(({ channelId, threadId, activeCount, agentIds }) => {
118
+ const row: ActiveRunsSnapshotEvent['data']['channels'][number] = {
119
+ channelId,
120
+ activeCount,
121
+ agentIds: Array.from(agentIds),
122
+ };
123
+ if (threadId !== undefined) {
124
+ row.threadId = threadId;
125
+ }
126
+ return row;
127
+ });
107
128
  return {
108
129
  type: 'agent:active-runs:snapshot',
109
- data: {
110
- channels: Array.from(byChannel.entries()).map(([channelId, value]) => ({
111
- channelId,
112
- activeCount: value.activeCount,
113
- agentIds: Array.from(value.agentIds),
114
- })),
115
- },
130
+ data: { channels },
116
131
  };
117
132
  };
118
133
 
@@ -149,6 +164,7 @@ export async function startServer(options: ServerOptions = {}) {
149
164
 
150
165
  if (channelId === GLOBAL_CHANNEL_ID) {
151
166
  const snapshot = buildActiveRunsSnapshot();
167
+
152
168
  ensureEventId(snapshot);
153
169
  res.write(`data: ${JSON.stringify(snapshot)}\n\n`);
154
170
  }
@@ -206,10 +222,10 @@ export async function startServer(options: ServerOptions = {}) {
206
222
  activeRuns.set(
207
223
  getRunKey(chunk.data.runId, chunk.data.agentId, chunk.data.channelId, chunk.data.threadId),
208
224
  {
209
- runId: chunk.data.runId,
210
- channelId: chunk.data.channelId,
211
- threadId: chunk.data.threadId,
212
- agentId: chunk.data.agentId,
225
+ runId: chunk.data.runId,
226
+ channelId: chunk.data.channelId,
227
+ threadId: chunk.data.threadId,
228
+ agentId: chunk.data.agentId,
213
229
  },
214
230
  );
215
231
  } else if (chunk.type === 'agent:run:end') {
@@ -226,21 +242,26 @@ export async function startServer(options: ServerOptions = {}) {
226
242
 
227
243
  sendToClientKey(targetClientKey, chunk);
228
244
 
229
- if (chunk.type === 'agent:run:start' || chunk.type === 'agent:run:end') {
245
+ if (
246
+ chunk.type === 'agent:run:start' ||
247
+ chunk.type === 'agent:run:end' ||
248
+ chunk.type === 'agent:run:stopped'
249
+ ) {
230
250
  sendToClientKey(GLOBAL_CHANNEL_ID, chunk);
231
251
  }
232
252
  };
233
253
 
234
254
  try {
235
- const harness = new AgentHarness({
255
+ ensureEventId(event);
256
+
257
+ await dispatch({
236
258
  runId,
237
259
  agentId: agentId || 'system',
260
+ event,
238
261
  channelId,
239
262
  threadId,
240
263
  onEvent,
241
264
  });
242
-
243
- await harness.dispatch(event);
244
265
  res.sendStatus(200);
245
266
  } catch (error) {
246
267
  console.error('[publish] Failed to dispatch event', {
@@ -272,15 +293,16 @@ export async function startServer(options: ServerOptions = {}) {
272
293
  };
273
294
 
274
295
  try {
275
- const harness = new AgentHarness({
296
+ ensureEventId(event);
297
+
298
+ await dispatch({
276
299
  runId,
277
300
  agentId: agentId || 'system',
301
+ event,
278
302
  channelId,
279
303
  threadId,
280
304
  onEvent,
281
305
  });
282
-
283
- await harness.dispatch(event);
284
306
  res.json({ events });
285
307
  } catch (error) {
286
308
  res.status(500).json({ error: 'Failed to process state request' });
package/src/app/types.ts CHANGED
@@ -143,6 +143,7 @@ export type CreateAgentEvent = BaseEvent & {
143
143
  agentId: string;
144
144
  name: string;
145
145
  description?: string;
146
+ image?: string;
146
147
  instructions: string;
147
148
  plugins: PluginRef[];
148
149
  };
@@ -162,6 +163,7 @@ export type UpdateAgentEvent = BaseEvent & {
162
163
  agentId: string;
163
164
  name?: string;
164
165
  description?: string;
166
+ image?: string;
165
167
  instructions?: string;
166
168
  plugins?: PluginRef[];
167
169
  };
@@ -365,17 +367,48 @@ export type AgentRunEndEvent = BaseEvent & {
365
367
  };
366
368
  };
367
369
 
370
+ export type AgentRunStoppedEvent = BaseEvent & {
371
+ type: 'agent:run:stopped';
372
+ data: {
373
+ runId: string;
374
+ agentId: string;
375
+ channelId: string;
376
+ threadId?: string;
377
+ reason?: string;
378
+ };
379
+ };
380
+
368
381
  export type ActiveRunsSnapshotEvent = BaseEvent & {
369
382
  type: 'agent:active-runs:snapshot';
370
383
  data: {
371
384
  channels: Array<{
372
385
  channelId: string;
386
+ threadId?: string;
373
387
  activeCount: number;
374
388
  agentIds: string[];
375
389
  }>;
376
390
  };
377
391
  };
378
392
 
393
+ export type StopAgentRunEvent = BaseEvent & {
394
+ type: 'action:agent_run_stop';
395
+ data: {
396
+ runId: string;
397
+ agentId?: string;
398
+ channelId?: string;
399
+ threadId?: string;
400
+ reason?: string;
401
+ };
402
+ };
403
+
404
+ export type StopAgentRunResultEvent = BaseEvent & {
405
+ type: 'action:agent_run_stop:result';
406
+ data: {
407
+ success: boolean;
408
+ message?: string;
409
+ };
410
+ };
411
+
379
412
  export type CreateThreadEvent = BaseEvent & {
380
413
  type: 'action:create_thread';
381
414
  data: {
@@ -714,6 +747,7 @@ export type InstallAgentEvent = BaseEvent & {
714
747
  agentId: string;
715
748
  name: string;
716
749
  description?: string;
750
+ image?: string;
717
751
  instructions: string;
718
752
  plugins: PluginRef[];
719
753
  };
@@ -860,7 +894,10 @@ export type OpenBotEvent =
860
894
  | AgentOutputEvent
861
895
  | AgentRunStartEvent
862
896
  | AgentRunEndEvent
897
+ | AgentRunStoppedEvent
863
898
  | ActiveRunsSnapshotEvent
899
+ | StopAgentRunEvent
900
+ | StopAgentRunResultEvent
864
901
  | GetChannelsEvent
865
902
  | GetChannelsResultEvent
866
903
  | GetThreadsEvent
@@ -550,8 +550,8 @@ export const busServicesPlugin =
550
550
 
551
551
  builder.on('action:storage:create-agent', async function* (event) {
552
552
  try {
553
- const { agentId, name, description, instructions, plugins } = event.data;
554
- await storage.createAgent({ agentId, name, description, instructions, plugins });
553
+ const { agentId, name, description, image, instructions, plugins } = event.data;
554
+ await storage.createAgent({ agentId, name, description, image, instructions, plugins });
555
555
  yield { type: 'action:storage:create-agent-result', data: { success: true } };
556
556
  } catch (error) {
557
557
  yield {
@@ -566,8 +566,8 @@ export const busServicesPlugin =
566
566
 
567
567
  builder.on('action:storage:update-agent', async function* (event) {
568
568
  try {
569
- const { agentId, name, description, instructions, plugins } = event.data;
570
- await storage.updateAgent({ agentId, name, description, instructions, plugins });
569
+ const { agentId, name, description, image, instructions, plugins } = event.data;
570
+ await storage.updateAgent({ agentId, name, description, image, instructions, plugins });
571
571
  yield { type: 'action:storage:update-agent-result', data: { success: true } };
572
572
  } catch (error) {
573
573
  yield {
@@ -858,7 +858,7 @@ export const busServicesPlugin =
858
858
 
859
859
  builder.on('action:agent:install', async function* (event) {
860
860
  try {
861
- const { agentId, name, description, instructions, plugins } = event.data;
861
+ const { agentId, name, description, image, instructions, plugins } = event.data;
862
862
 
863
863
  // Ensure each plugin is available locally. Built-in ids resolve
864
864
  // immediately; npm-name ids are fetched on demand.
@@ -881,6 +881,7 @@ export const busServicesPlugin =
881
881
  agentId,
882
882
  name,
883
883
  description,
884
+ image,
884
885
  instructions,
885
886
  plugins,
886
887
  });
package/src/bus/types.ts CHANGED
@@ -113,6 +113,8 @@ export interface Storage {
113
113
  agentId: string;
114
114
  name: string;
115
115
  description?: string;
116
+ /** Avatar/logo URL or data URI; persisted in AGENT.md frontmatter. */
117
+ image?: string;
116
118
  instructions: string;
117
119
  plugins: PluginRef[];
118
120
  }) => Promise<void>;
@@ -120,6 +122,8 @@ export interface Storage {
120
122
  agentId: string;
121
123
  name?: string;
122
124
  description?: string;
125
+ /** Omit to leave unchanged; empty string removes stored image. */
126
+ image?: string;
123
127
  instructions?: string;
124
128
  plugins?: PluginRef[];
125
129
  }) => Promise<void>;