openbot 0.3.0 → 0.3.2

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 (43) hide show
  1. package/dist/app/cli.js +1 -1
  2. package/dist/app/server.js +1 -4
  3. package/dist/bus/services.js +222 -15
  4. package/dist/harness/context.js +205 -26
  5. package/dist/harness/queue-processor.js +44 -110
  6. package/dist/harness/runtime-factory.js +11 -7
  7. package/dist/harness/todo-advance.js +93 -0
  8. package/dist/plugins/ai-sdk/index.js +0 -3
  9. package/dist/plugins/ai-sdk/runtime.js +78 -13
  10. package/dist/plugins/ai-sdk/system-prompt.js +18 -3
  11. package/dist/plugins/delegation/index.js +7 -46
  12. package/dist/plugins/memory/index.js +71 -0
  13. package/dist/plugins/storage-tools/index.js +2 -11
  14. package/dist/plugins/todo/index.js +54 -0
  15. package/dist/plugins/workflow/index.js +65 -0
  16. package/dist/registry/plugins.js +4 -2
  17. package/dist/services/memory.js +152 -0
  18. package/dist/services/storage.js +9 -31
  19. package/dist/workflow/service.js +106 -0
  20. package/dist/workflow/types.js +3 -0
  21. package/docs/agents.md +15 -1
  22. package/docs/plugins.md +0 -1
  23. package/package.json +1 -1
  24. package/src/app/cli.ts +1 -1
  25. package/src/app/server.ts +3 -4
  26. package/src/app/types.ts +140 -45
  27. package/src/bus/plugin.ts +0 -2
  28. package/src/bus/services.ts +258 -17
  29. package/src/bus/types.ts +13 -4
  30. package/src/harness/context.ts +233 -37
  31. package/src/harness/queue-processor.ts +54 -143
  32. package/src/harness/runtime-factory.ts +11 -7
  33. package/src/harness/todo-advance.ts +128 -0
  34. package/src/plugins/ai-sdk/index.ts +0 -3
  35. package/src/plugins/ai-sdk/runtime.ts +356 -298
  36. package/src/plugins/ai-sdk/system-prompt.ts +18 -4
  37. package/src/plugins/delegation/index.ts +7 -50
  38. package/src/plugins/memory/index.ts +85 -0
  39. package/src/plugins/storage-tools/index.ts +8 -19
  40. package/src/plugins/todo/index.ts +64 -0
  41. package/src/registry/plugins.ts +4 -3
  42. package/src/services/memory.ts +213 -0
  43. package/src/services/storage.ts +9 -49
@@ -1,11 +1,77 @@
1
1
  import { MelonyPlugin } from 'melony';
2
2
  import { DEFAULT_MARKETPLACE_REGISTRY_URL, loadConfig } from '../app/config.js';
3
- import { OpenBotEvent, OpenBotState } from '../app/types.js';
3
+ import {
4
+ OpenBotEvent,
5
+ OpenBotState,
6
+ MemoryScopeAlias,
7
+ TodoItem,
8
+ TodoStatus,
9
+ TodoWriteInput,
10
+ } from '../app/types.js';
4
11
  import type { PluginRef } from './plugin.js';
5
12
  import { Storage } from './types.js';
6
13
  import { storageService } from '../services/storage.js';
7
14
  import { pluginService } from '../services/plugins.js';
8
15
 
16
+ const readTodos = (state: OpenBotState): TodoItem[] => {
17
+ const raw = (state.threadDetails?.state as Record<string, unknown> | undefined)?.todos;
18
+ return Array.isArray(raw) ? (raw as TodoItem[]) : [];
19
+ };
20
+
21
+ let todoCounter = 0;
22
+ const newTodoId = (now: number, idx: number): string =>
23
+ `todo_${now.toString(36)}_${(todoCounter++).toString(36)}_${idx}`;
24
+
25
+ async function persistTodos(
26
+ storage: Storage,
27
+ state: OpenBotState,
28
+ todos: TodoItem[],
29
+ ): Promise<void> {
30
+ if (!state.threadId) throw new Error('No active thread');
31
+ await storage.patchThreadState({
32
+ channelId: state.channelId,
33
+ threadId: state.threadId,
34
+ state: { todos },
35
+ });
36
+ state.threadDetails = await storage.getThreadDetails({
37
+ channelId: state.channelId,
38
+ threadId: state.threadId,
39
+ });
40
+ }
41
+
42
+
43
+ /**
44
+ * Resolve a scope alias to a concrete scope string. Aliases let tools accept
45
+ * `agent`/`channel`/`global` without knowing the active ids; the bus rewrites
46
+ * them using `context.state`.
47
+ */
48
+ function resolveMemoryScope(
49
+ alias: MemoryScopeAlias | undefined,
50
+ state: OpenBotState,
51
+ ): string {
52
+ switch (alias) {
53
+ case 'agent':
54
+ return `agent:${state.agentId}`;
55
+ case 'channel':
56
+ return `channel:${state.channelId}`;
57
+ case 'global':
58
+ case undefined:
59
+ return 'global';
60
+ default:
61
+ return 'global';
62
+ }
63
+ }
64
+
65
+ function resolveMemoryScopeFilter(
66
+ alias: MemoryScopeAlias | 'all' | undefined,
67
+ state: OpenBotState,
68
+ ): string[] | undefined {
69
+ if (alias === 'all' || alias === undefined) {
70
+ return ['global', `agent:${state.agentId}`, `channel:${state.channelId}`];
71
+ }
72
+ return [resolveMemoryScope(alias, state)];
73
+ }
74
+
9
75
  /** One marketplace entry; matches `action:marketplace:list:result` agent shape. */
10
76
  export type MarketplaceAgentListing = {
11
77
  id: string;
@@ -122,7 +188,7 @@ export const busServicesPlugin =
122
188
  builder.on('action:create_thread', async function* (event, context) {
123
189
  const threadId = event.meta?.threadId;
124
190
  const channelId = context.state.channelId;
125
- const { threadTitle, spec, initialState } = (event as any).data;
191
+ const { threadTitle, initialState } = (event as any).data;
126
192
 
127
193
  if (!threadId) {
128
194
  console.warn('[bus] Cannot create thread: meta.threadId is missing');
@@ -137,7 +203,6 @@ export const busServicesPlugin =
137
203
  channelId,
138
204
  threadId,
139
205
  threadTitle,
140
- spec,
141
206
  initialState: (initialState as Record<string, unknown>) || {},
142
207
  });
143
208
 
@@ -156,7 +221,7 @@ export const busServicesPlugin =
156
221
  yield {
157
222
  type: 'action:create_thread:result',
158
223
  data: { success: true, threadId, threadTitle },
159
- meta: { threadId },
224
+ meta: { ...(event.meta || {}), threadId, agentId: context.state.agentId },
160
225
  } as OpenBotEvent;
161
226
  });
162
227
 
@@ -165,10 +230,13 @@ export const busServicesPlugin =
165
230
  const rawChannelId = (channelId || '').trim();
166
231
  const channelSpec = typeof spec === 'string' ? spec : '';
167
232
 
233
+ const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
234
+
168
235
  if (!rawChannelId) {
169
236
  yield {
170
237
  type: 'action:create_channel:result',
171
238
  data: { success: false, channelId: '', channelUrl: '' },
239
+ meta: resultMeta,
172
240
  } as OpenBotEvent;
173
241
  return;
174
242
  }
@@ -186,20 +254,19 @@ export const busServicesPlugin =
186
254
  yield {
187
255
  type: 'action:create_channel:result',
188
256
  data: { success: true, channelId: rawChannelId, channelUrl },
257
+ meta: resultMeta,
189
258
  } as OpenBotEvent;
190
259
 
191
260
  yield {
192
261
  type: 'agent:output',
193
262
  data: { content: `Created channel \`${rawChannelId}\`.` },
194
- meta: {
195
- ...(event.meta || {}),
196
- agentId: context.state.agentId,
197
- },
263
+ meta: resultMeta,
198
264
  } as OpenBotEvent;
199
265
  } catch {
200
266
  yield {
201
267
  type: 'action:create_channel:result',
202
268
  data: { success: false, channelId: rawChannelId, channelUrl },
269
+ meta: resultMeta,
203
270
  } as OpenBotEvent;
204
271
  }
205
272
  });
@@ -207,11 +274,13 @@ export const busServicesPlugin =
207
274
  builder.on('action:update_channel', async function* (event, context) {
208
275
  const data = (event.data || {}) as { channelId?: string; name?: string; cwd?: string };
209
276
  const targetChannelId = (data.channelId || context.state.channelId || '').trim();
277
+ const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
210
278
 
211
279
  if (!targetChannelId) {
212
280
  yield {
213
281
  type: 'action:update_channel:result',
214
282
  data: { success: false, channelId: '', updatedFields: [] as string[] },
283
+ meta: resultMeta,
215
284
  } as OpenBotEvent;
216
285
  return;
217
286
  }
@@ -242,17 +311,20 @@ export const busServicesPlugin =
242
311
  yield {
243
312
  type: 'action:update_channel:result',
244
313
  data: { success: true, channelId: targetChannelId, updatedFields },
314
+ meta: resultMeta,
245
315
  } as OpenBotEvent;
246
316
  } catch {
247
317
  yield {
248
318
  type: 'action:update_channel:result',
249
319
  data: { success: false, channelId: targetChannelId, updatedFields },
320
+ meta: resultMeta,
250
321
  } as OpenBotEvent;
251
322
  }
252
323
  });
253
324
 
254
325
  builder.on('action:patch_channel_details', async function* (event, context) {
255
326
  const updatedFields: ('state' | 'spec' | 'cwd')[] = [];
327
+ const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
256
328
  try {
257
329
  if ((event.data as any).state !== undefined) {
258
330
  await storage.patchChannelState({
@@ -283,17 +355,20 @@ export const busServicesPlugin =
283
355
  yield {
284
356
  type: 'action:patch_channel_details:result',
285
357
  data: { success: true, updatedFields },
358
+ meta: resultMeta,
286
359
  };
287
360
  } catch {
288
361
  yield {
289
362
  type: 'action:patch_channel_details:result',
290
363
  data: { success: false, updatedFields },
364
+ meta: resultMeta,
291
365
  };
292
366
  }
293
367
  });
294
368
 
295
369
  builder.on('action:patch_thread_details', async function* (event, context) {
296
- const updatedFields: ('state' | 'spec')[] = [];
370
+ const updatedFields: ('state')[] = [];
371
+ const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
297
372
  try {
298
373
  if (!context.state.threadId) {
299
374
  throw new Error('Missing threadId in state for patch_thread_details');
@@ -306,14 +381,6 @@ export const busServicesPlugin =
306
381
  });
307
382
  updatedFields.push('state');
308
383
  }
309
- if (typeof (event.data as any).spec === 'string') {
310
- await storage.patchThreadSpec({
311
- channelId: context.state.channelId,
312
- threadId: context.state.threadId,
313
- spec: (event.data as any).spec,
314
- });
315
- updatedFields.push('spec');
316
- }
317
384
 
318
385
  context.state.threadDetails = await storage.getThreadDetails({
319
386
  channelId: context.state.channelId,
@@ -323,15 +390,113 @@ export const busServicesPlugin =
323
390
  yield {
324
391
  type: 'action:patch_thread_details:result',
325
392
  data: { success: true, updatedFields },
393
+ meta: resultMeta,
326
394
  };
327
395
  } catch {
328
396
  yield {
329
397
  type: 'action:patch_thread_details:result',
330
398
  data: { success: false, updatedFields },
399
+ meta: resultMeta,
331
400
  };
332
401
  }
333
402
  });
334
403
 
404
+ builder.on('action:todo_write', async function* (event, context) {
405
+ const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
406
+ try {
407
+ if (!context.state.threadId) {
408
+ throw new Error('todo_write requires an active thread');
409
+ }
410
+ const existing = readTodos(context.state);
411
+ const byId = new Map(existing.map((t) => [t.id, t]));
412
+ const now = Date.now();
413
+ const author = context.state.agentId || 'system';
414
+
415
+ const inputs = (event.data as { todos: TodoWriteInput[] }).todos || [];
416
+ const next: TodoItem[] = inputs.map((raw, idx) => {
417
+ const prior = raw.id ? byId.get(raw.id) : undefined;
418
+ return {
419
+ id: prior?.id || raw.id || newTodoId(now, idx),
420
+ content: raw.content,
421
+ status: raw.status || prior?.status || 'pending',
422
+ assignee: raw.assignee ?? prior?.assignee,
423
+ createdBy: prior?.createdBy || author,
424
+ createdAt: prior?.createdAt || now,
425
+ updatedAt: now,
426
+ ...(prior?.result !== undefined ? { result: prior.result } : {}),
427
+ };
428
+ });
429
+
430
+ await persistTodos(storage, context.state, next);
431
+
432
+ yield {
433
+ type: 'action:todo_write:result',
434
+ data: { success: true, todos: next },
435
+ meta: resultMeta,
436
+ } as OpenBotEvent;
437
+ } catch (error) {
438
+ yield {
439
+ type: 'action:todo_write:result',
440
+ data: {
441
+ success: false,
442
+ todos: readTodos(context.state),
443
+ error: error instanceof Error ? error.message : 'Unknown error',
444
+ },
445
+ meta: resultMeta,
446
+ } as OpenBotEvent;
447
+ }
448
+ });
449
+
450
+ builder.on('action:todo_update', async function* (event, context) {
451
+ const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
452
+ const patch = event.data as {
453
+ id: string;
454
+ status?: TodoStatus;
455
+ content?: string;
456
+ assignee?: string;
457
+ };
458
+ try {
459
+ if (!context.state.threadId) {
460
+ throw new Error('todo_update requires an active thread');
461
+ }
462
+ const existing = readTodos(context.state);
463
+ const idx = existing.findIndex((t) => t.id === patch.id);
464
+ if (idx === -1) {
465
+ throw new Error(`Todo "${patch.id}" not found`);
466
+ }
467
+ const now = Date.now();
468
+ const updated: TodoItem = {
469
+ ...existing[idx],
470
+ ...(patch.content !== undefined ? { content: patch.content } : {}),
471
+ ...(patch.status !== undefined ? { status: patch.status } : {}),
472
+ ...(patch.assignee !== undefined
473
+ ? { assignee: patch.assignee === '' ? undefined : patch.assignee }
474
+ : {}),
475
+ updatedAt: now,
476
+ };
477
+ const next = [...existing];
478
+ next[idx] = updated;
479
+
480
+ await persistTodos(storage, context.state, next);
481
+
482
+ yield {
483
+ type: 'action:todo_update:result',
484
+ data: { success: true, todo: updated, todos: next },
485
+ meta: resultMeta,
486
+ } as OpenBotEvent;
487
+ } catch (error) {
488
+ yield {
489
+ type: 'action:todo_update:result',
490
+ data: {
491
+ success: false,
492
+ todos: readTodos(context.state),
493
+ error: error instanceof Error ? error.message : 'Unknown error',
494
+ },
495
+ meta: resultMeta,
496
+ } as OpenBotEvent;
497
+ }
498
+ });
499
+
335
500
  builder.on('action:storage:get-channels', async function* () {
336
501
  const channels = await storage.getChannels();
337
502
  yield { type: 'action:storage:get-channels-result', data: { channels } };
@@ -615,6 +780,82 @@ export const busServicesPlugin =
615
780
  } as OpenBotEvent;
616
781
  });
617
782
 
783
+ builder.on('action:remember', async function* (event, context) {
784
+ const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
785
+ try {
786
+ const { content, scope, tags } = event.data;
787
+ const record = await storage.appendMemory({
788
+ scope: resolveMemoryScope(scope, context.state),
789
+ content,
790
+ tags,
791
+ });
792
+ yield {
793
+ type: 'action:remember:result',
794
+ data: { success: true, record },
795
+ meta: resultMeta,
796
+ } as OpenBotEvent;
797
+ } catch (error) {
798
+ yield {
799
+ type: 'action:remember:result',
800
+ data: {
801
+ success: false,
802
+ error: error instanceof Error ? error.message : 'Unknown error',
803
+ },
804
+ meta: resultMeta,
805
+ } as OpenBotEvent;
806
+ }
807
+ });
808
+
809
+ builder.on('action:recall', async function* (event, context) {
810
+ const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
811
+ try {
812
+ const { query, tag, scope, limit } = event.data;
813
+ const records = await storage.listMemories({
814
+ scopes: resolveMemoryScopeFilter(scope, context.state),
815
+ query,
816
+ tag,
817
+ limit,
818
+ });
819
+ yield {
820
+ type: 'action:recall:result',
821
+ data: { success: true, records },
822
+ meta: resultMeta,
823
+ } as OpenBotEvent;
824
+ } catch (error) {
825
+ yield {
826
+ type: 'action:recall:result',
827
+ data: {
828
+ success: false,
829
+ records: [],
830
+ error: error instanceof Error ? error.message : 'Unknown error',
831
+ },
832
+ meta: resultMeta,
833
+ } as OpenBotEvent;
834
+ }
835
+ });
836
+
837
+ builder.on('action:forget', async function* (event, context) {
838
+ const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
839
+ try {
840
+ const deleted = await storage.deleteMemory({ id: event.data.id });
841
+ yield {
842
+ type: 'action:forget:result',
843
+ data: { success: true, deleted },
844
+ meta: resultMeta,
845
+ } as OpenBotEvent;
846
+ } catch (error) {
847
+ yield {
848
+ type: 'action:forget:result',
849
+ data: {
850
+ success: false,
851
+ deleted: false,
852
+ error: error instanceof Error ? error.message : 'Unknown error',
853
+ },
854
+ meta: resultMeta,
855
+ } as OpenBotEvent;
856
+ }
857
+ });
858
+
618
859
  builder.on('action:agent:install', async function* (event) {
619
860
  try {
620
861
  const { agentId, name, description, instructions, plugins } = event.data;
package/src/bus/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { OpenBotEvent } from '../app/types.js';
2
2
  import type { PluginRef } from './plugin.js';
3
+ import type { MemoryRecord, ListMemoriesArgs } from '../services/memory.js';
3
4
 
4
5
  /**
5
6
  * Public data types exposed by the OpenBot bus.
@@ -33,7 +34,6 @@ export type PluginDescriptor = {
33
34
  /** True when bundled with the core server (`src/registry/plugins`); false for ~/.openbot/plugins installs. */
34
35
  builtIn: boolean;
35
36
  image?: string;
36
- defaultInstructions?: string;
37
37
  configSchema?: ConfigSchema;
38
38
  createdAt: Date;
39
39
  updatedAt: Date;
@@ -78,7 +78,6 @@ export type ThreadDetails = {
78
78
  id: string;
79
79
  name: string;
80
80
  channelId: string;
81
- spec: string;
82
81
  state: unknown;
83
82
  };
84
83
 
@@ -103,7 +102,6 @@ export interface Storage {
103
102
  channelId: string;
104
103
  threadId: string;
105
104
  threadTitle?: string;
106
- spec?: string;
107
105
  initialState?: Record<string, unknown>;
108
106
  }) => Promise<void>;
109
107
  getThreads: (args: { channelId: string }) => Promise<Thread[]>;
@@ -135,7 +133,6 @@ export interface Storage {
135
133
  state: unknown;
136
134
  }) => Promise<void>;
137
135
  patchChannelSpec: (args: { channelId: string; spec: string }) => Promise<void>;
138
- patchThreadSpec: (args: { channelId: string; threadId: string; spec: string }) => Promise<void>;
139
136
  getVariables: () => Promise<Record<string, string | { value: string; secret: boolean }>>;
140
137
  createVariable: (args: { key: string; value: string; secret?: boolean }) => Promise<void>;
141
138
  deleteVariable: (args: { key: string }) => Promise<void>;
@@ -144,4 +141,16 @@ export interface Storage {
144
141
  path?: string;
145
142
  }) => Promise<Array<{ name: string; isDirectory: boolean }>>;
146
143
  readFile: (args: { channelId: string; path: string }) => Promise<string>;
144
+ /** Persist a memory record into the global memory log. */
145
+ appendMemory: (args: {
146
+ scope: string;
147
+ content: string;
148
+ tags?: string[];
149
+ }) => Promise<MemoryRecord>;
150
+ /** Read memories matching the given filter. */
151
+ listMemories: (args?: ListMemoriesArgs) => Promise<MemoryRecord[]>;
152
+ /** Soft-delete a memory by id. Returns true if a record was deleted. */
153
+ deleteMemory: (args: { id: string }) => Promise<boolean>;
154
+ /** Update a memory's content/tags by id. Returns true if a record was updated. */
155
+ updateMemory: (args: { id: string; content?: string; tags?: string[] }) => Promise<boolean>;
147
156
  }