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
@@ -3,26 +3,18 @@ import z from 'zod';
3
3
  import type { Plugin } from '../../bus/plugin.js';
4
4
  import { OpenBotEvent, OpenBotState } from '../../app/types.js';
5
5
 
6
- const delegationToolDefinitions = {
6
+ const handoffToolDefinitions = {
7
7
  handoff: {
8
8
  description:
9
- 'Transfer control to another agent. The target agent continues the task and you do not wait for a tool result.',
9
+ 'Transfer control to another agent. The target agent continues the task in this thread.',
10
10
  inputSchema: z.object({
11
11
  agentId: z.string().describe('The ID of the target agent.'),
12
12
  content: z.string().describe('The message or task to hand off.'),
13
13
  }),
14
14
  },
15
- delegate: {
16
- description:
17
- 'Delegate a subtask to another agent and wait for its result so you can continue.',
18
- inputSchema: z.object({
19
- agentId: z.string().describe('The ID of the target agent.'),
20
- content: z.string().describe('The subtask you want the target agent to execute.'),
21
- }),
22
- },
23
15
  };
24
16
 
25
- const delegationPluginRuntime = (): MelonyPlugin<OpenBotState, OpenBotEvent> => (builder) => {
17
+ const handoffPluginRuntime = (): MelonyPlugin<OpenBotState, OpenBotEvent> => (builder) => {
26
18
  builder.on('action:handoff', async function* (event, context) {
27
19
  const { agentId, content } = event.data;
28
20
 
@@ -46,49 +38,14 @@ const delegationPluginRuntime = (): MelonyPlugin<OpenBotState, OpenBotEvent> =>
46
38
  };
47
39
  }
48
40
  });
49
-
50
- builder.on('action:delegate', async function* (event, context) {
51
- const { agentId, content } = event.data;
52
- const widgetId = event.meta?.toolCallId
53
- ? `delegate_${event.meta.toolCallId}`
54
- : `delegate_${Date.now()}`;
55
-
56
- yield {
57
- type: 'client:ui:widget',
58
- data: {
59
- kind: 'message',
60
- widgetId,
61
- title: `Delegation started: ${agentId}`,
62
- body: `Running delegated task in background.\n\n${content}`,
63
- state: 'open',
64
- metadata: {
65
- type: 'delegation:status',
66
- phase: 'started',
67
- delegatedAgentId: agentId,
68
- },
69
- },
70
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
71
- };
72
-
73
- yield {
74
- type: 'delegation:request',
75
- data: { agentId, content },
76
- meta: {
77
- ...(event.meta || {}),
78
- parentAgentId: context.state.agentId,
79
- delegationWidgetId: widgetId,
80
- agentId: context.state.agentId,
81
- },
82
- };
83
- });
84
41
  };
85
42
 
86
43
  export const delegationPlugin: Plugin = {
87
44
  id: 'delegation',
88
- name: 'Delegation',
89
- description: 'Hand off or delegate sub-tasks to other agents on the bus.',
90
- toolDefinitions: delegationToolDefinitions,
91
- factory: () => delegationPluginRuntime(),
45
+ name: 'Handoff',
46
+ description: 'Hand off tasks to other agents on the bus.',
47
+ toolDefinitions: handoffToolDefinitions,
48
+ factory: () => handoffPluginRuntime(),
92
49
  };
93
50
 
94
51
  export default delegationPlugin;
@@ -52,25 +52,14 @@ const storageToolDefinitions = {
52
52
  ),
53
53
  },
54
54
  patch_thread_details: {
55
- description: 'Patch current thread details (state and/or spec).',
56
- inputSchema: z
57
- .object({
58
- state: z
59
- .record(z.string(), z.unknown())
60
- .optional()
61
- .describe(
62
- 'JSON state object for the thread. Use for structured data like `todos` or progress.',
63
- ),
64
- spec: z
65
- .string()
66
- .optional()
67
- .describe(
68
- 'Markdown content for the thread specification (SPEC.md). Use for plans and goals.',
69
- ),
70
- })
71
- .refine((value) => value.state !== undefined || value.spec !== undefined, {
72
- message: 'Provide at least one of state or spec.',
73
- }),
55
+ description: 'Patch current thread details (state).',
56
+ inputSchema: z.object({
57
+ state: z
58
+ .record(z.string(), z.unknown())
59
+ .describe(
60
+ 'JSON state object for the thread. Use for structured data like `todos` or progress.',
61
+ ),
62
+ }),
74
63
  },
75
64
  create_variable: {
76
65
  description: 'Create or update a variable in the workspace storage.',
@@ -0,0 +1,64 @@
1
+ import z from 'zod';
2
+ import type { Plugin } from '../../bus/plugin.js';
3
+
4
+ /**
5
+ * `todo` — shared, per-thread task list for autonomous multi-agent flows.
6
+ *
7
+ * Todos live in `threadDetails.state.todos` and are owned by the system
8
+ * (handlers in `bus/services.ts`). Any agent in the thread can read the
9
+ * list via context, and propose mutations through these tools. Each item
10
+ * may carry an `assignee` agent id; combine with `handoff` to drive an
11
+ * autonomous, multi-step plan across agents.
12
+ *
13
+ * Keep the surface minimal: two tools (replace-all, patch-one) cover plan
14
+ * authoring, status transitions, and reassignment.
15
+ */
16
+ const todoStatus = z.enum(['pending', 'in_progress', 'done', 'cancelled']);
17
+
18
+ const todoToolDefinitions = {
19
+ todo_write: {
20
+ description:
21
+ 'Author or rewrite the shared todo plan for the current thread. Pass the full ordered list — missing items are removed. Use at the start of multi-step work, or whenever the plan changes shape. For status flips, prefer `todo_update`.',
22
+ inputSchema: z.object({
23
+ todos: z
24
+ .array(
25
+ z.object({
26
+ id: z
27
+ .string()
28
+ .optional()
29
+ .describe('Stable id. Reuse existing ids to preserve history; omit to create.'),
30
+ content: z.string().min(1).describe('What needs to be done. One concrete step.'),
31
+ status: todoStatus.optional().describe('Defaults to `pending`.'),
32
+ assignee: z
33
+ .string()
34
+ .optional()
35
+ .describe('Agent id responsible for this step. Pair with `handoff` to delegate.'),
36
+ }),
37
+ )
38
+ .describe('The complete, ordered plan.'),
39
+ }),
40
+ },
41
+ todo_update: {
42
+ description:
43
+ 'Patch a single todo by id. Use to mark progress (`in_progress`, `done`, `cancelled`), rephrase, or reassign without rewriting the whole list.',
44
+ inputSchema: z.object({
45
+ id: z.string().describe('Todo id from `todo_write` or the rendered list.'),
46
+ status: todoStatus.optional(),
47
+ content: z.string().min(1).optional(),
48
+ assignee: z.string().optional().describe('Use empty string to clear.'),
49
+ }),
50
+ },
51
+ };
52
+
53
+ export const todoPlugin: Plugin = {
54
+ id: 'todo',
55
+ name: 'Todo',
56
+ description:
57
+ 'Shared per-thread task list for coordinating multi-step, multi-agent work.',
58
+ toolDefinitions: todoToolDefinitions,
59
+ factory: () => () => {
60
+ // Handlers live in bus/services.ts; this plugin only contributes tool definitions.
61
+ },
62
+ };
63
+
64
+ export default todoPlugin;
@@ -10,6 +10,7 @@ import { storageToolsPlugin } from '../plugins/storage-tools/index.js';
10
10
  import { uiPlugin } from '../plugins/ui/index.js';
11
11
  import { approvalPlugin } from '../plugins/approval/index.js';
12
12
  import { memoryPlugin } from '../plugins/memory/index.js';
13
+ import { todoPlugin } from '../plugins/todo/index.js';
13
14
  import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
14
15
 
15
16
  let pluginsDir: string | null = null;
@@ -25,6 +26,7 @@ const BUILT_IN: Record<string, Plugin> = {
25
26
  [uiPlugin.id]: uiPlugin,
26
27
  [approvalPlugin.id]: approvalPlugin,
27
28
  [memoryPlugin.id]: memoryPlugin,
29
+ [todoPlugin.id]: todoPlugin,
28
30
  };
29
31
 
30
32
  /**
@@ -50,8 +52,6 @@ export function parsePluginModule(
50
52
  const name = typeof raw.name === 'string' ? raw.name : '';
51
53
  const description = typeof raw.description === 'string' ? raw.description : '';
52
54
  const image = typeof raw.image === 'string' ? raw.image : undefined;
53
- const defaultInstructions =
54
- typeof raw.defaultInstructions === 'string' ? raw.defaultInstructions : undefined;
55
55
  const configSchema = raw.configSchema as Plugin['configSchema'];
56
56
  const toolDefinitions = raw.toolDefinitions as Plugin['toolDefinitions'];
57
57
 
@@ -59,7 +59,6 @@ export function parsePluginModule(
59
59
  name,
60
60
  description,
61
61
  image,
62
- defaultInstructions,
63
62
  configSchema,
64
63
  toolDefinitions,
65
64
  factory: factory as Plugin['factory'],
@@ -91,7 +91,7 @@ const SYSTEM_DEFAULT_PLUGINS: PluginRef[] = [
91
91
  { id: 'storage-tools' },
92
92
  // { id: 'mcp' },
93
93
  { id: 'shell' },
94
- { id: 'delegation' },
94
+ { id: 'todo' },
95
95
  // { id: 'ui' },
96
96
  { id: 'approval' },
97
97
  { id: 'memory' },
@@ -103,7 +103,7 @@ function getSystemAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails
103
103
  name: 'OpenBot',
104
104
  image: undefined,
105
105
  description:
106
- 'First-party orchestration agent for OpenBot. Coordinates other agents via handoff and delegation.',
106
+ 'First-party orchestration agent for OpenBot. Coordinates other agents via handoff.',
107
107
  instructions: AI_SDK_SYSTEM_PROMPT,
108
108
  plugins: SYSTEM_DEFAULT_PLUGINS.map((ref) => ref.id),
109
109
  pluginRefs: SYSTEM_DEFAULT_PLUGINS,
@@ -211,7 +211,6 @@ const listBuiltInPluginDescriptors = async (): Promise<PluginDescriptor[]> => {
211
211
  description: plugin.description,
212
212
  builtIn: true,
213
213
  image: plugin.image,
214
- defaultInstructions: plugin.defaultInstructions,
215
214
  configSchema: plugin.configSchema,
216
215
  createdAt: new Date(),
217
216
  updatedAt: new Date(),
@@ -281,7 +280,6 @@ const listPluginsFromDisk = async (): Promise<PluginDescriptor[]> => {
281
280
  description: parsed.description || '',
282
281
  builtIn: false,
283
282
  image: parsed.image || image,
284
- defaultInstructions: parsed.defaultInstructions,
285
283
  configSchema: parsed.configSchema,
286
284
  createdAt: new Date(),
287
285
  updatedAt: new Date(),
@@ -450,13 +448,11 @@ export const storageService = {
450
448
  channelId,
451
449
  threadId,
452
450
  threadTitle,
453
- spec,
454
451
  initialState,
455
452
  }: {
456
453
  channelId: string;
457
454
  threadId: string;
458
455
  threadTitle?: string;
459
- spec?: string;
460
456
  initialState?: Record<string, unknown>;
461
457
  }): Promise<void> => {
462
458
  const normalizedChannelId = channelId.trim();
@@ -466,7 +462,6 @@ export const storageService = {
466
462
  if (!normalizedThreadId) throw new Error('threadId is required');
467
463
 
468
464
  const threadDir = getConversationDir(normalizedChannelId, normalizedThreadId);
469
- const specPath = `${threadDir}/SPEC.md`;
470
465
  const statePath = `${threadDir}/state.json`;
471
466
 
472
467
  try {
@@ -487,11 +482,6 @@ export const storageService = {
487
482
  }
488
483
 
489
484
  await fs.mkdir(threadDir, { recursive: true });
490
- await fs.writeFile(
491
- specPath,
492
- spec?.trim() ||
493
- `# ${normalizedThreadId}\n\nDefine the goals and plan for this thread here.\n`,
494
- );
495
485
  await fs.writeFile(statePath, JSON.stringify(baseState, null, 2));
496
486
  },
497
487
  getThreads: async ({ channelId }: { channelId: string }): Promise<Thread[]> => {
@@ -550,21 +540,8 @@ export const storageService = {
550
540
  threadId: string;
551
541
  }): Promise<ThreadDetails> => {
552
542
  const threadDir = getConversationDir(channelId, threadId);
553
- const specPath = `${threadDir}/SPEC.md`;
554
543
  const statePath = `${threadDir}/state.json`;
555
544
 
556
- let spec = '';
557
- try {
558
- spec = await fs.readFile(specPath, 'utf-8');
559
- } catch (error: unknown) {
560
- if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
561
- console.error(
562
- `Failed to read thread spec for channel ${channelId} thread ${threadId}`,
563
- error,
564
- );
565
- }
566
- }
567
-
568
545
  let state: unknown = {};
569
546
  try {
570
547
  const stateContent = await fs.readFile(statePath, 'utf-8');
@@ -587,7 +564,6 @@ export const storageService = {
587
564
  id: threadId,
588
565
  name: generatedName || threadId,
589
566
  channelId,
590
- spec,
591
567
  state,
592
568
  };
593
569
  },
@@ -704,29 +680,6 @@ export const storageService = {
704
680
  throw error;
705
681
  }
706
682
  },
707
- patchThreadSpec: async ({
708
- channelId,
709
- threadId,
710
- spec,
711
- }: {
712
- channelId: string;
713
- threadId: string;
714
- spec: string;
715
- }): Promise<void> => {
716
- const threadDir = getConversationDir(channelId, threadId);
717
- const specPath = `${threadDir}/SPEC.md`;
718
-
719
- try {
720
- await fs.mkdir(threadDir, { recursive: true });
721
- await fs.writeFile(specPath, spec);
722
- } catch (error) {
723
- console.error(
724
- `Failed to patch thread spec for channel ${channelId} thread ${threadId}`,
725
- error,
726
- );
727
- throw error;
728
- }
729
- },
730
683
  getAgents: async (): Promise<Agent[]> => {
731
684
  const agentsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_AGENTS_DIR);
732
685
  try {