openbot 0.3.1 → 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 (37) hide show
  1. package/dist/app/server.js +1 -4
  2. package/dist/bus/services.js +106 -10
  3. package/dist/harness/context.js +66 -6
  4. package/dist/harness/queue-processor.js +44 -110
  5. package/dist/harness/runtime-factory.js +11 -7
  6. package/dist/harness/todo-advance.js +93 -0
  7. package/dist/plugins/ai-sdk/index.js +0 -3
  8. package/dist/plugins/ai-sdk/runtime.js +4 -11
  9. package/dist/plugins/ai-sdk/system-prompt.js +18 -3
  10. package/dist/plugins/delegation/index.js +7 -46
  11. package/dist/plugins/storage-tools/index.js +2 -11
  12. package/dist/plugins/todo/index.js +54 -0
  13. package/dist/plugins/workflow/index.js +65 -0
  14. package/dist/registry/plugins.js +2 -2
  15. package/dist/services/storage.js +3 -31
  16. package/dist/workflow/service.js +106 -0
  17. package/dist/workflow/types.js +3 -0
  18. package/docs/plugins.md +0 -1
  19. package/package.json +1 -1
  20. package/src/app/cli.ts +1 -1
  21. package/src/app/server.ts +3 -4
  22. package/src/app/types.ts +80 -45
  23. package/src/bus/plugin.ts +0 -2
  24. package/src/bus/services.ts +133 -12
  25. package/src/bus/types.ts +0 -4
  26. package/src/harness/context.ts +73 -10
  27. package/src/harness/queue-processor.ts +54 -143
  28. package/src/harness/runtime-factory.ts +11 -7
  29. package/src/harness/todo-advance.ts +128 -0
  30. package/src/plugins/ai-sdk/index.ts +0 -3
  31. package/src/plugins/ai-sdk/runtime.ts +284 -300
  32. package/src/plugins/ai-sdk/system-prompt.ts +18 -4
  33. package/src/plugins/delegation/index.ts +7 -50
  34. package/src/plugins/storage-tools/index.ts +8 -19
  35. package/src/plugins/todo/index.ts +64 -0
  36. package/src/registry/plugins.ts +2 -3
  37. package/src/services/storage.ts +2 -49
package/src/app/types.ts CHANGED
@@ -290,7 +290,6 @@ export type PatchThreadDetailsEvent = BaseEvent & {
290
290
  type: 'action:patch_thread_details';
291
291
  data: {
292
292
  state?: Record<string, unknown>;
293
- spec?: string;
294
293
  };
295
294
  };
296
295
 
@@ -298,7 +297,7 @@ export type PatchThreadDetailsResultEvent = BaseEvent & {
298
297
  type: 'action:patch_thread_details:result';
299
298
  data: {
300
299
  success: boolean;
301
- updatedFields: ('state' | 'spec')[];
300
+ updatedFields: ('state')[];
302
301
  };
303
302
  };
304
303
 
@@ -381,7 +380,6 @@ export type CreateThreadEvent = BaseEvent & {
381
380
  type: 'action:create_thread';
382
381
  data: {
383
382
  threadTitle: string;
384
- spec?: string;
385
383
  initialState?: Record<string, unknown>;
386
384
  };
387
385
  meta: {
@@ -570,34 +568,7 @@ export type HandoffResultEvent = BaseEvent & {
570
568
  };
571
569
  };
572
570
 
573
- export type DelegateEvent = BaseEvent & {
574
- type: 'action:delegate';
575
- data: {
576
- agentId: string;
577
- content: string;
578
- };
579
- meta?: {
580
- toolCallId?: string;
581
- [key: string]: any;
582
- };
583
- };
584
-
585
- export type DelegateResultEvent = BaseEvent & {
586
- type: 'action:delegate:result';
587
- data: {
588
- success: boolean;
589
- agentId: string;
590
- summary: string;
591
- };
592
- meta: {
593
- toolCallId: string;
594
- agentId: string;
595
- threadId?: string;
596
- [key: string]: any;
597
- };
598
- };
599
-
600
- /** Internal routing: delegation plugin → orchestrator only (not stored or broadcast). */
571
+ /** Internal routing: handoff plugin orchestrator only (not stored or broadcast). */
601
572
  export type HandoffRequestEvent = BaseEvent & {
602
573
  type: 'handoff:request';
603
574
  data: {
@@ -607,16 +578,6 @@ export type HandoffRequestEvent = BaseEvent & {
607
578
  meta?: Record<string, unknown>;
608
579
  };
609
580
 
610
- /** Internal routing: delegation plugin → orchestrator only (not stored or broadcast). */
611
- export type DelegationRequestEvent = BaseEvent & {
612
- type: 'delegation:request';
613
- data: {
614
- agentId: string;
615
- content: string;
616
- };
617
- meta?: Record<string, unknown>;
618
- };
619
-
620
581
  export type MCPListToolsEvent = BaseEvent & {
621
582
  type: 'action:mcp_list_tools';
622
583
  data: {
@@ -820,6 +781,79 @@ export type ForgetResultEvent = BaseEvent & {
820
781
  };
821
782
  };
822
783
 
784
+ export type TodoStatus = 'pending' | 'in_progress' | 'done' | 'cancelled';
785
+
786
+ /**
787
+ * A single unit of work tracked in thread state. Todos are owned by the
788
+ * system (bus services); agents can only mutate them by calling the
789
+ * `todo_write` / `todo_update` tools so every change is observable on the
790
+ * event stream and audit-friendly.
791
+ */
792
+ export interface TodoItem {
793
+ id: string;
794
+ content: string;
795
+ status: TodoStatus;
796
+ /** Optional agent id responsible for this item — drives autonomous handoffs. */
797
+ assignee?: string;
798
+ /** Agent id that created the todo (or "system"). */
799
+ createdBy: string;
800
+ createdAt: number;
801
+ updatedAt: number;
802
+ /**
803
+ * Captured final reply when this item reaches `done` (last `agent:output`
804
+ * from the assignee for that run). Lets downstream agents rely on thread
805
+ * state instead of merged short-term messages.
806
+ */
807
+ result?: string;
808
+ }
809
+
810
+ export type TodoWriteInput = {
811
+ id?: string;
812
+ content: string;
813
+ status?: TodoStatus;
814
+ assignee?: string;
815
+ };
816
+
817
+ export type TodoWriteEvent = BaseEvent & {
818
+ type: 'action:todo_write';
819
+ data: {
820
+ todos: TodoWriteInput[];
821
+ };
822
+ meta?: { toolCallId?: string; agentId?: string; threadId?: string };
823
+ };
824
+
825
+ export type TodoWriteResultEvent = BaseEvent & {
826
+ type: 'action:todo_write:result';
827
+ data: {
828
+ success: boolean;
829
+ todos: TodoItem[];
830
+ error?: string;
831
+ };
832
+ meta?: { toolCallId?: string; agentId?: string; threadId?: string };
833
+ };
834
+
835
+ export type TodoUpdateEvent = BaseEvent & {
836
+ type: 'action:todo_update';
837
+ data: {
838
+ id: string;
839
+ status?: TodoStatus;
840
+ content?: string;
841
+ assignee?: string;
842
+ };
843
+ meta?: { toolCallId?: string; agentId?: string; threadId?: string };
844
+ };
845
+
846
+ export type TodoUpdateResultEvent = BaseEvent & {
847
+ type: 'action:todo_update:result';
848
+ data: {
849
+ success: boolean;
850
+ todo?: TodoItem;
851
+ todos: TodoItem[];
852
+ error?: string;
853
+ };
854
+ meta?: { toolCallId?: string; agentId?: string; threadId?: string };
855
+ };
856
+
823
857
  export type OpenBotEvent =
824
858
  | UserInputEvent
825
859
  | AgentInvokeEvent
@@ -879,10 +913,7 @@ export type OpenBotEvent =
879
913
  | UIWidgetResponseEvent
880
914
  | HandoffEvent
881
915
  | HandoffResultEvent
882
- | DelegateEvent
883
- | DelegateResultEvent
884
916
  | HandoffRequestEvent
885
- | DelegationRequestEvent
886
917
  | MCPListToolsEvent
887
918
  | MCPListToolsResultEvent
888
919
  | MCPCallEvent
@@ -902,4 +933,8 @@ export type OpenBotEvent =
902
933
  | RecallEvent
903
934
  | RecallResultEvent
904
935
  | ForgetEvent
905
- | ForgetResultEvent;
936
+ | ForgetResultEvent
937
+ | TodoWriteEvent
938
+ | TodoWriteResultEvent
939
+ | TodoUpdateEvent
940
+ | TodoUpdateResultEvent;
package/src/bus/plugin.ts CHANGED
@@ -56,8 +56,6 @@ export interface Plugin {
56
56
  name: string;
57
57
  description: string;
58
58
  image?: string;
59
- /** Optional system-prompt body suggested when this plugin is used as the runtime. */
60
- defaultInstructions?: string;
61
59
  /** JSON-schema-like description of `config` accepted in AGENT.md `plugins[].config`. */
62
60
  configSchema?: ConfigSchema;
63
61
  /** Tool definitions contributed to any runtime plugin attached to the same agent. */
@@ -1,11 +1,45 @@
1
1
  import { MelonyPlugin } from 'melony';
2
2
  import { DEFAULT_MARKETPLACE_REGISTRY_URL, loadConfig } from '../app/config.js';
3
- import { OpenBotEvent, OpenBotState, MemoryScopeAlias } 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
+
9
43
  /**
10
44
  * Resolve a scope alias to a concrete scope string. Aliases let tools accept
11
45
  * `agent`/`channel`/`global` without knowing the active ids; the bus rewrites
@@ -154,7 +188,7 @@ export const busServicesPlugin =
154
188
  builder.on('action:create_thread', async function* (event, context) {
155
189
  const threadId = event.meta?.threadId;
156
190
  const channelId = context.state.channelId;
157
- const { threadTitle, spec, initialState } = (event as any).data;
191
+ const { threadTitle, initialState } = (event as any).data;
158
192
 
159
193
  if (!threadId) {
160
194
  console.warn('[bus] Cannot create thread: meta.threadId is missing');
@@ -169,7 +203,6 @@ export const busServicesPlugin =
169
203
  channelId,
170
204
  threadId,
171
205
  threadTitle,
172
- spec,
173
206
  initialState: (initialState as Record<string, unknown>) || {},
174
207
  });
175
208
 
@@ -334,7 +367,7 @@ export const busServicesPlugin =
334
367
  });
335
368
 
336
369
  builder.on('action:patch_thread_details', async function* (event, context) {
337
- const updatedFields: ('state' | 'spec')[] = [];
370
+ const updatedFields: ('state')[] = [];
338
371
  const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
339
372
  try {
340
373
  if (!context.state.threadId) {
@@ -348,14 +381,6 @@ export const busServicesPlugin =
348
381
  });
349
382
  updatedFields.push('state');
350
383
  }
351
- if (typeof (event.data as any).spec === 'string') {
352
- await storage.patchThreadSpec({
353
- channelId: context.state.channelId,
354
- threadId: context.state.threadId,
355
- spec: (event.data as any).spec,
356
- });
357
- updatedFields.push('spec');
358
- }
359
384
 
360
385
  context.state.threadDetails = await storage.getThreadDetails({
361
386
  channelId: context.state.channelId,
@@ -376,6 +401,102 @@ export const busServicesPlugin =
376
401
  }
377
402
  });
378
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
+
379
500
  builder.on('action:storage:get-channels', async function* () {
380
501
  const channels = await storage.getChannels();
381
502
  yield { type: 'action:storage:get-channels-result', data: { channels } };
package/src/bus/types.ts CHANGED
@@ -34,7 +34,6 @@ export type PluginDescriptor = {
34
34
  /** True when bundled with the core server (`src/registry/plugins`); false for ~/.openbot/plugins installs. */
35
35
  builtIn: boolean;
36
36
  image?: string;
37
- defaultInstructions?: string;
38
37
  configSchema?: ConfigSchema;
39
38
  createdAt: Date;
40
39
  updatedAt: Date;
@@ -79,7 +78,6 @@ export type ThreadDetails = {
79
78
  id: string;
80
79
  name: string;
81
80
  channelId: string;
82
- spec: string;
83
81
  state: unknown;
84
82
  };
85
83
 
@@ -104,7 +102,6 @@ export interface Storage {
104
102
  channelId: string;
105
103
  threadId: string;
106
104
  threadTitle?: string;
107
- spec?: string;
108
105
  initialState?: Record<string, unknown>;
109
106
  }) => Promise<void>;
110
107
  getThreads: (args: { channelId: string }) => Promise<Thread[]>;
@@ -136,7 +133,6 @@ export interface Storage {
136
133
  state: unknown;
137
134
  }) => Promise<void>;
138
135
  patchChannelSpec: (args: { channelId: string; spec: string }) => Promise<void>;
139
- patchThreadSpec: (args: { channelId: string; threadId: string; spec: string }) => Promise<void>;
140
136
  getVariables: () => Promise<Record<string, string | { value: string; secret: boolean }>>;
141
137
  createVariable: (args: { key: string; value: string; secret?: boolean }) => Promise<void>;
142
138
  deleteVariable: (args: { key: string }) => Promise<void>;
@@ -1,4 +1,4 @@
1
- import { OpenBotEvent, OpenBotState } from '../app/types.js';
1
+ import { OpenBotEvent, OpenBotState, TodoItem } from '../app/types.js';
2
2
  import { Storage } from '../bus/types.js';
3
3
 
4
4
  /**
@@ -98,6 +98,7 @@ export function createDefaultContextEngine(): ContextEngine {
98
98
  engine.registerProvider(new AgentDetailsProvider());
99
99
  engine.registerProvider(new ChannelDetailsProvider());
100
100
  engine.registerProvider(new ThreadDetailsProvider());
101
+ engine.registerProvider(new TodoProvider());
101
102
  engine.registerProvider(new MemoryProvider());
102
103
  engine.registerProvider(new RecentEventsProvider());
103
104
 
@@ -110,11 +111,14 @@ class AgentDetailsProvider implements ContextProvider {
110
111
  name = 'agent-details';
111
112
  async provide(state: OpenBotState): Promise<ContextItem[]> {
112
113
  if (!state.agentDetails) return [];
114
+ const instructions = state.agentDetails.instructions?.trim();
115
+ if (!instructions) return [];
116
+
113
117
  return [{
114
118
  id: 'agent-details',
115
119
  type: 'agent',
116
120
  priority: 100,
117
- content: `## AGENT NAME\n${state.agentDetails.name}\n\n## AGENT SPECIFICATION\n${state.agentDetails.instructions}`
121
+ content: `# ${state.agentDetails.name}\n\n${instructions}`,
118
122
  }];
119
123
  }
120
124
  }
@@ -123,11 +127,14 @@ class ChannelDetailsProvider implements ContextProvider {
123
127
  name = 'channel-details';
124
128
  async provide(state: OpenBotState): Promise<ContextItem[]> {
125
129
  if (!state.channelDetails) return [];
130
+ const spec = state.channelDetails.spec?.trim();
131
+ if (!spec) return [];
132
+
126
133
  return [{
127
134
  id: 'channel-details',
128
135
  type: 'channel',
129
136
  priority: 80,
130
- content: `## CHANNEL NAME\n${state.channelDetails.name}\n\n## CHANNEL SPECIFICATION\n${state.channelDetails.spec}`
137
+ content: `# Channel you are in: ${state.channelDetails.name}\n\n Channel Specification: ${spec}`,
131
138
  }];
132
139
  }
133
140
  }
@@ -136,12 +143,68 @@ class ThreadDetailsProvider implements ContextProvider {
136
143
  name = 'thread-details';
137
144
  async provide(state: OpenBotState): Promise<ContextItem[]> {
138
145
  if (!state.threadDetails) return [];
139
- return [{
140
- id: 'thread-details',
141
- type: 'thread',
142
- priority: 90,
143
- content: `## THREAD NAME\n${state.threadDetails.name}\n\n## THREAD SPECIFICATION\n${state.threadDetails.spec}`
144
- }];
146
+
147
+ // For now, this provider is a placeholder for future state-based assembly.
148
+ // It currently only surfaces the thread name to provide basic context.
149
+ return [
150
+ {
151
+ id: 'thread-details',
152
+ type: 'thread',
153
+ priority: 90,
154
+ content: `# Thread you are in: ${state.threadDetails.name}`,
155
+ },
156
+ ];
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Surfaces the shared per-thread todo list. The list lives in
162
+ * `threadDetails.state.todos` and is owned by bus services — every agent in
163
+ * the thread reads from the same canonical source, which is how multi-agent
164
+ * autonomous flows stay coordinated.
165
+ */
166
+ class TodoProvider implements ContextProvider {
167
+ name = 'todos';
168
+ async provide(state: OpenBotState): Promise<ContextItem[]> {
169
+ const raw = (state.threadDetails?.state as Record<string, unknown> | undefined)?.todos;
170
+ const todos: TodoItem[] = Array.isArray(raw) ? (raw as TodoItem[]) : [];
171
+ if (todos.length === 0) return [];
172
+
173
+ const DISPLAY_RESULT_CAP = 2500;
174
+
175
+ const marker: Record<TodoItem['status'], string> = {
176
+ pending: '[ ]',
177
+ in_progress: '[~]',
178
+ done: '[x]',
179
+ cancelled: '[-]',
180
+ };
181
+ const formatted = todos
182
+ .map((t) => {
183
+ const assignee = t.assignee ? ` @${t.assignee}` : '';
184
+ let line = `- ${marker[t.status]} (${t.id})${assignee} ${t.content}`;
185
+ if (t.status === 'done' && t.result?.trim()) {
186
+ let snippet = t.result.trim();
187
+ if (snippet.length > DISPLAY_RESULT_CAP) {
188
+ snippet = `${snippet.slice(0, DISPLAY_RESULT_CAP)}…[truncated]`;
189
+ }
190
+ line += `\n Result: ${snippet}`;
191
+ }
192
+ return line;
193
+ })
194
+ .join('\n');
195
+
196
+ return [
197
+ {
198
+ id: 'todos',
199
+ type: 'todos',
200
+ priority: 92,
201
+ content:
202
+ `## Shared todo plan (thread state)\n` +
203
+ `Orchestrator authors with \`todo_write\`; assignees run one step at a time. ` +
204
+ `When an item is \`done\`, its captured output appears below so every agent can see prior steps without relying on merged chat history.\n\n` +
205
+ `${formatted}`,
206
+ },
207
+ ];
145
208
  }
146
209
  }
147
210
 
@@ -175,7 +238,7 @@ class MemoryProvider implements ContextProvider {
175
238
  id: 'memory',
176
239
  type: 'memory',
177
240
  priority: 95,
178
- 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}`,
241
+ content: `## Remembered facts\nTrust these unless the user contradicts them. Use \`forget\` to remove stale ones.\n\n${formatted}`,
179
242
  },
180
243
  ];
181
244
  } catch (error) {