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
@@ -116,15 +116,8 @@ const persistShortTermMessages = async (state, storage) => {
116
116
  state: { shortTermMessages },
117
117
  });
118
118
  };
119
- async function buildSystemPrompt(state, system, context, storage, contextEngine) {
120
- const sections = [];
121
- if (system && typeof system === 'string')
122
- sections.push(system);
123
- if (system && typeof system === 'function' && context)
124
- sections.push(await system(context));
125
- if (contextEngine)
126
- sections.push(await contextEngine.buildContext(state, storage));
127
- return sections.join('\n\n');
119
+ async function buildSystemPrompt(state, storage, contextEngine) {
120
+ return contextEngine.buildContext(state, storage);
128
121
  }
129
122
  /**
130
123
  * Generic ai-sdk runtime plugin.
@@ -134,7 +127,7 @@ async function buildSystemPrompt(state, system, context, storage, contextEngine)
134
127
  * loader (merged from every tool plugin attached to the same agent).
135
128
  */
136
129
  export const aiSdkRuntime = (options) => (builder) => {
137
- const { model: modelString = 'openai/gpt-4o-mini', system, storage, contextEngine = createDefaultContextEngine(), toolDefinitions = {}, } = options;
130
+ const { model: modelString = 'openai/gpt-4o-mini', storage, contextEngine = createDefaultContextEngine(), toolDefinitions = {}, } = options;
138
131
  let currentModelString = modelString;
139
132
  let model = resolveModel(currentModelString);
140
133
  const ensureShortTermMessages = (state) => {
@@ -179,7 +172,7 @@ export const aiSdkRuntime = (options) => (builder) => {
179
172
  };
180
173
  const runLLM = async function* (context, threadId) {
181
174
  ensureShortTermMessages(context.state);
182
- const systemPrompt = await buildSystemPrompt(context.state, system, context, storage, contextEngine);
175
+ const systemPrompt = await buildSystemPrompt(context.state, storage, contextEngine);
183
176
  const coreMessages = mapToCoreMessages(buildMessageWindow(repairOpenToolCalls(context.state.shortTermMessages || [])));
184
177
  try {
185
178
  const result = await generateText({
@@ -1,3 +1,18 @@
1
- export const AI_SDK_SYSTEM_PROMPT = 'You are a helpful AI assistant on the OpenBot platform. ' +
2
- 'Use the tools available to you to help the user. ' +
3
- 'Be concise unless the user asks for depth.';
1
+ export const AI_SDK_SYSTEM_PROMPT = [
2
+ 'You are a helpful AI assistant on the OpenBot platform.',
3
+ 'Use the tools available to you to help the user.',
4
+ 'Be concise unless the user asks for depth.',
5
+ '',
6
+ '## Planning with todos',
7
+ 'The current thread has a shared todo list (visible under "Shared todo plan" in context).',
8
+ 'It is the single source of truth for multi-step work and is shared across every agent in the thread.',
9
+ '',
10
+ 'When planning:',
11
+ '- For any task that needs more than one step, call `todo_write` ONCE with the full ordered plan, then stop. Do not call any other tool in the same turn.',
12
+ '- The platform dispatches assignees automatically and completes their todo when their run ends. You do NOT need to call `handoff` to start the plan or `todo_update` to finish items.',
13
+ '- Each item must be concrete and atomic (one verb, one outcome). Skip the list entirely for trivial single-step requests.',
14
+ '',
15
+ 'When you are an assignee (you have an `in_progress` todo addressed to you):',
16
+ '- Just do the work and reply. The platform will mark your todo done and dispatch the next one.',
17
+ '- If you genuinely cannot complete it, call `todo_update(id, status: "cancelled")` with a brief reason in your reply.',
18
+ ].join('\n');
@@ -1,21 +1,14 @@
1
1
  import z from 'zod';
2
- const delegationToolDefinitions = {
2
+ const handoffToolDefinitions = {
3
3
  handoff: {
4
- description: 'Transfer control to another agent. The target agent continues the task and you do not wait for a tool result.',
4
+ description: 'Transfer control to another agent. The target agent continues the task in this thread.',
5
5
  inputSchema: z.object({
6
6
  agentId: z.string().describe('The ID of the target agent.'),
7
7
  content: z.string().describe('The message or task to hand off.'),
8
8
  }),
9
9
  },
10
- delegate: {
11
- description: 'Delegate a subtask to another agent and wait for its result so you can continue.',
12
- inputSchema: z.object({
13
- agentId: z.string().describe('The ID of the target agent.'),
14
- content: z.string().describe('The subtask you want the target agent to execute.'),
15
- }),
16
- },
17
10
  };
18
- const delegationPluginRuntime = () => (builder) => {
11
+ const handoffPluginRuntime = () => (builder) => {
19
12
  builder.on('action:handoff', async function* (event, context) {
20
13
  const { agentId, content } = event.data;
21
14
  yield {
@@ -36,44 +29,12 @@ const delegationPluginRuntime = () => (builder) => {
36
29
  };
37
30
  }
38
31
  });
39
- builder.on('action:delegate', async function* (event, context) {
40
- const { agentId, content } = event.data;
41
- const widgetId = event.meta?.toolCallId
42
- ? `delegate_${event.meta.toolCallId}`
43
- : `delegate_${Date.now()}`;
44
- yield {
45
- type: 'client:ui:widget',
46
- data: {
47
- kind: 'message',
48
- widgetId,
49
- title: `Delegation started: ${agentId}`,
50
- body: `Running delegated task in background.\n\n${content}`,
51
- state: 'open',
52
- metadata: {
53
- type: 'delegation:status',
54
- phase: 'started',
55
- delegatedAgentId: agentId,
56
- },
57
- },
58
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
59
- };
60
- yield {
61
- type: 'delegation:request',
62
- data: { agentId, content },
63
- meta: {
64
- ...(event.meta || {}),
65
- parentAgentId: context.state.agentId,
66
- delegationWidgetId: widgetId,
67
- agentId: context.state.agentId,
68
- },
69
- };
70
- });
71
32
  };
72
33
  export const delegationPlugin = {
73
34
  id: 'delegation',
74
- name: 'Delegation',
75
- description: 'Hand off or delegate sub-tasks to other agents on the bus.',
76
- toolDefinitions: delegationToolDefinitions,
77
- factory: () => delegationPluginRuntime(),
35
+ name: 'Handoff',
36
+ description: 'Hand off tasks to other agents on the bus.',
37
+ toolDefinitions: handoffToolDefinitions,
38
+ factory: () => handoffPluginRuntime(),
78
39
  };
79
40
  export default delegationPlugin;
@@ -42,20 +42,11 @@ const storageToolDefinitions = {
42
42
  .refine((value) => value.state !== undefined || value.spec !== undefined || value.cwd !== undefined, { message: 'Provide at least one of state, spec, or cwd.' }),
43
43
  },
44
44
  patch_thread_details: {
45
- description: 'Patch current thread details (state and/or spec).',
46
- inputSchema: z
47
- .object({
45
+ description: 'Patch current thread details (state).',
46
+ inputSchema: z.object({
48
47
  state: z
49
48
  .record(z.string(), z.unknown())
50
- .optional()
51
49
  .describe('JSON state object for the thread. Use for structured data like `todos` or progress.'),
52
- spec: z
53
- .string()
54
- .optional()
55
- .describe('Markdown content for the thread specification (SPEC.md). Use for plans and goals.'),
56
- })
57
- .refine((value) => value.state !== undefined || value.spec !== undefined, {
58
- message: 'Provide at least one of state or spec.',
59
50
  }),
60
51
  },
61
52
  create_variable: {
@@ -0,0 +1,54 @@
1
+ import z from 'zod';
2
+ /**
3
+ * `todo` — shared, per-thread task list for autonomous multi-agent flows.
4
+ *
5
+ * Todos live in `threadDetails.state.todos` and are owned by the system
6
+ * (handlers in `bus/services.ts`). Any agent in the thread can read the
7
+ * list via context, and propose mutations through these tools. Each item
8
+ * may carry an `assignee` agent id; combine with `handoff` to drive an
9
+ * autonomous, multi-step plan across agents.
10
+ *
11
+ * Keep the surface minimal: two tools (replace-all, patch-one) cover plan
12
+ * authoring, status transitions, and reassignment.
13
+ */
14
+ const todoStatus = z.enum(['pending', 'in_progress', 'done', 'cancelled']);
15
+ const todoToolDefinitions = {
16
+ todo_write: {
17
+ description: '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`.',
18
+ inputSchema: z.object({
19
+ todos: z
20
+ .array(z.object({
21
+ id: z
22
+ .string()
23
+ .optional()
24
+ .describe('Stable id. Reuse existing ids to preserve history; omit to create.'),
25
+ content: z.string().min(1).describe('What needs to be done. One concrete step.'),
26
+ status: todoStatus.optional().describe('Defaults to `pending`.'),
27
+ assignee: z
28
+ .string()
29
+ .optional()
30
+ .describe('Agent id responsible for this step. Pair with `handoff` to delegate.'),
31
+ }))
32
+ .describe('The complete, ordered plan.'),
33
+ }),
34
+ },
35
+ todo_update: {
36
+ description: 'Patch a single todo by id. Use to mark progress (`in_progress`, `done`, `cancelled`), rephrase, or reassign without rewriting the whole list.',
37
+ inputSchema: z.object({
38
+ id: z.string().describe('Todo id from `todo_write` or the rendered list.'),
39
+ status: todoStatus.optional(),
40
+ content: z.string().min(1).optional(),
41
+ assignee: z.string().optional().describe('Use empty string to clear.'),
42
+ }),
43
+ },
44
+ };
45
+ export const todoPlugin = {
46
+ id: 'todo',
47
+ name: 'Todo',
48
+ description: 'Shared per-thread task list for coordinating multi-step, multi-agent work.',
49
+ toolDefinitions: todoToolDefinitions,
50
+ factory: () => () => {
51
+ // Handlers live in bus/services.ts; this plugin only contributes tool definitions.
52
+ },
53
+ };
54
+ export default todoPlugin;
@@ -0,0 +1,65 @@
1
+ import z from 'zod';
2
+ import { setWorkflowPlan, updateWorkflowTodo } from '../../workflow/service.js';
3
+ const workflowToolDefinitions = {
4
+ workflow_set_plan: {
5
+ description: 'Define or replace the multi-agent plan for this thread.',
6
+ inputSchema: z.object({
7
+ title: z.string().optional().describe('Optional title for the plan.'),
8
+ todos: z.array(z.object({
9
+ id: z.string().optional().describe('Optional stable id.'),
10
+ text: z.string().describe('What needs to be done.'),
11
+ })).min(1).describe('Ordered steps.'),
12
+ }),
13
+ },
14
+ workflow_update_todo: {
15
+ description: 'Update a step in the plan with progress or results.',
16
+ inputSchema: z.object({
17
+ todoId: z.string().describe('The id of the todo to update.'),
18
+ status: z.enum(['pending', 'done', 'failed']).optional().describe('New status.'),
19
+ summary: z.string().optional().describe('Concise result or note about this step.'),
20
+ }),
21
+ },
22
+ };
23
+ const workflowPluginRuntime = () => (builder) => {
24
+ builder.on('action:workflow_set_plan', async function* (event, context) {
25
+ const { threadId, channelId } = context.state;
26
+ if (!threadId) {
27
+ yield { type: 'action:workflow_set_plan:result', data: { success: false, error: 'No active thread.' }, meta: event.meta };
28
+ return;
29
+ }
30
+ try {
31
+ const workflow = await setWorkflowPlan({ channelId, threadId, plan: event.data });
32
+ yield { type: 'action:workflow_set_plan:result', data: { success: true, workflowId: workflow.id }, meta: event.meta };
33
+ yield {
34
+ type: 'agent:output',
35
+ data: { content: `Plan updated: **${workflow.title || workflow.id}** (${workflow.todos.length} steps).` },
36
+ meta: { ...(event.meta || {}), agentId: context.state.agentId, threadId },
37
+ };
38
+ }
39
+ catch (error) {
40
+ yield { type: 'action:workflow_set_plan:result', data: { success: false, error: String(error) }, meta: event.meta };
41
+ }
42
+ });
43
+ builder.on('action:workflow_update_todo', async function* (event, context) {
44
+ const { threadId, channelId } = context.state;
45
+ if (!threadId) {
46
+ yield { type: 'action:workflow_update_todo:result', data: { success: false, error: 'No active thread.' }, meta: event.meta };
47
+ return;
48
+ }
49
+ try {
50
+ const workflow = await updateWorkflowTodo({ channelId, threadId, update: event.data });
51
+ yield { type: 'action:workflow_update_todo:result', data: { success: true, todoId: event.data.todoId }, meta: event.meta };
52
+ }
53
+ catch (error) {
54
+ yield { type: 'action:workflow_update_todo:result', data: { success: false, error: String(error) }, meta: event.meta };
55
+ }
56
+ });
57
+ };
58
+ export const workflowPlugin = {
59
+ id: 'workflow',
60
+ name: 'Workflow',
61
+ description: 'Simple thread-scoped planning for multi-agent coordination.',
62
+ toolDefinitions: workflowToolDefinitions,
63
+ factory: () => workflowPluginRuntime(),
64
+ };
65
+ export default workflowPlugin;
@@ -9,6 +9,7 @@ import { storageToolsPlugin } from '../plugins/storage-tools/index.js';
9
9
  import { uiPlugin } from '../plugins/ui/index.js';
10
10
  import { approvalPlugin } from '../plugins/approval/index.js';
11
11
  import { memoryPlugin } from '../plugins/memory/index.js';
12
+ import { todoPlugin } from '../plugins/todo/index.js';
12
13
  import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
13
14
  let pluginsDir = null;
14
15
  const loadedPlugins = new Set();
@@ -22,6 +23,7 @@ const BUILT_IN = {
22
23
  [uiPlugin.id]: uiPlugin,
23
24
  [approvalPlugin.id]: approvalPlugin,
24
25
  [memoryPlugin.id]: memoryPlugin,
26
+ [todoPlugin.id]: todoPlugin,
25
27
  };
26
28
  /** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
27
29
  export function parsePluginModule(module) {
@@ -35,14 +37,12 @@ export function parsePluginModule(module) {
35
37
  const name = typeof raw.name === 'string' ? raw.name : '';
36
38
  const description = typeof raw.description === 'string' ? raw.description : '';
37
39
  const image = typeof raw.image === 'string' ? raw.image : undefined;
38
- const defaultInstructions = typeof raw.defaultInstructions === 'string' ? raw.defaultInstructions : undefined;
39
40
  const configSchema = raw.configSchema;
40
41
  const toolDefinitions = raw.toolDefinitions;
41
42
  return {
42
43
  name,
43
44
  description,
44
45
  image,
45
- defaultInstructions,
46
46
  configSchema,
47
47
  toolDefinitions,
48
48
  factory: factory,
@@ -63,7 +63,7 @@ const SYSTEM_DEFAULT_PLUGINS = [
63
63
  { id: 'storage-tools' },
64
64
  // { id: 'mcp' },
65
65
  { id: 'shell' },
66
- { id: 'delegation' },
66
+ { id: 'todo' },
67
67
  // { id: 'ui' },
68
68
  { id: 'approval' },
69
69
  { id: 'memory' },
@@ -73,7 +73,7 @@ function getSystemAgentDetails(overrides) {
73
73
  id: SYSTEM_AGENT_ID,
74
74
  name: 'OpenBot',
75
75
  image: undefined,
76
- description: 'First-party orchestration agent for OpenBot. Coordinates other agents via handoff and delegation.',
76
+ description: 'First-party orchestration agent for OpenBot. Coordinates other agents via handoff.',
77
77
  instructions: AI_SDK_SYSTEM_PROMPT,
78
78
  plugins: SYSTEM_DEFAULT_PLUGINS.map((ref) => ref.id),
79
79
  pluginRefs: SYSTEM_DEFAULT_PLUGINS,
@@ -162,7 +162,6 @@ const listBuiltInPluginDescriptors = async () => {
162
162
  description: plugin.description,
163
163
  builtIn: true,
164
164
  image: plugin.image,
165
- defaultInstructions: plugin.defaultInstructions,
166
165
  configSchema: plugin.configSchema,
167
166
  createdAt: new Date(),
168
167
  updatedAt: new Date(),
@@ -230,7 +229,6 @@ const listPluginsFromDisk = async () => {
230
229
  description: parsed.description || '',
231
230
  builtIn: false,
232
231
  image: parsed.image || image,
233
- defaultInstructions: parsed.defaultInstructions,
234
232
  configSchema: parsed.configSchema,
235
233
  createdAt: new Date(),
236
234
  updatedAt: new Date(),
@@ -357,7 +355,7 @@ export const storageService = {
357
355
  `# ${normalizedChannelId}\n\nDefine the goals and rules for this channel here.\n`);
358
356
  await fs.writeFile(statePath, JSON.stringify(finalState, null, 2));
359
357
  },
360
- createThread: async ({ channelId, threadId, threadTitle, spec, initialState, }) => {
358
+ createThread: async ({ channelId, threadId, threadTitle, initialState, }) => {
361
359
  const normalizedChannelId = channelId.trim();
362
360
  const normalizedThreadId = threadId.trim();
363
361
  if (!normalizedChannelId)
@@ -365,7 +363,6 @@ export const storageService = {
365
363
  if (!normalizedThreadId)
366
364
  throw new Error('threadId is required');
367
365
  const threadDir = getConversationDir(normalizedChannelId, normalizedThreadId);
368
- const specPath = `${threadDir}/SPEC.md`;
369
366
  const statePath = `${threadDir}/state.json`;
370
367
  try {
371
368
  await fs.access(threadDir);
@@ -382,8 +379,6 @@ export const storageService = {
382
379
  baseState.generatedName = threadTitle.trim();
383
380
  }
384
381
  await fs.mkdir(threadDir, { recursive: true });
385
- await fs.writeFile(specPath, spec?.trim() ||
386
- `# ${normalizedThreadId}\n\nDefine the goals and plan for this thread here.\n`);
387
382
  await fs.writeFile(statePath, JSON.stringify(baseState, null, 2));
388
383
  },
389
384
  getThreads: async ({ channelId }) => {
@@ -425,17 +420,7 @@ export const storageService = {
425
420
  },
426
421
  getThreadDetails: async ({ channelId, threadId, }) => {
427
422
  const threadDir = getConversationDir(channelId, threadId);
428
- const specPath = `${threadDir}/SPEC.md`;
429
423
  const statePath = `${threadDir}/state.json`;
430
- let spec = '';
431
- try {
432
- spec = await fs.readFile(specPath, 'utf-8');
433
- }
434
- catch (error) {
435
- if (error?.code !== 'ENOENT') {
436
- console.error(`Failed to read thread spec for channel ${channelId} thread ${threadId}`, error);
437
- }
438
- }
439
424
  let state = {};
440
425
  try {
441
426
  const stateContent = await fs.readFile(statePath, 'utf-8');
@@ -453,7 +438,6 @@ export const storageService = {
453
438
  id: threadId,
454
439
  name: generatedName || threadId,
455
440
  channelId,
456
- spec,
457
441
  state,
458
442
  };
459
443
  },
@@ -539,18 +523,6 @@ export const storageService = {
539
523
  throw error;
540
524
  }
541
525
  },
542
- patchThreadSpec: async ({ channelId, threadId, spec, }) => {
543
- const threadDir = getConversationDir(channelId, threadId);
544
- const specPath = `${threadDir}/SPEC.md`;
545
- try {
546
- await fs.mkdir(threadDir, { recursive: true });
547
- await fs.writeFile(specPath, spec);
548
- }
549
- catch (error) {
550
- console.error(`Failed to patch thread spec for channel ${channelId} thread ${threadId}`, error);
551
- throw error;
552
- }
553
- },
554
526
  getAgents: async () => {
555
527
  const agentsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_AGENTS_DIR);
556
528
  try {
@@ -0,0 +1,106 @@
1
+ import { generateId } from 'melony';
2
+ import { storageService } from '../services/storage.js';
3
+ import { WORKFLOW_SCHEMA_VERSION, WORKFLOW_THREAD_STATE_KEY, } from './types.js';
4
+ const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value)
5
+ ? value
6
+ : {};
7
+ const isTodoStatus = (value) => value === 'pending' || value === 'done' || value === 'failed';
8
+ const parseTodo = (value) => {
9
+ const record = asRecord(value);
10
+ if (typeof record.id !== 'string' || !record.id)
11
+ return null;
12
+ if (typeof record.text !== 'string' || !record.text)
13
+ return null;
14
+ if (!isTodoStatus(record.status))
15
+ return null;
16
+ return {
17
+ id: record.id,
18
+ text: record.text,
19
+ status: record.status,
20
+ summary: typeof record.summary === 'string' ? record.summary : undefined,
21
+ };
22
+ };
23
+ export const parseThreadWorkflow = (state) => {
24
+ const record = asRecord(state);
25
+ const raw = record[WORKFLOW_THREAD_STATE_KEY];
26
+ const workflow = asRecord(raw);
27
+ if (typeof workflow.id !== 'string' || !workflow.id)
28
+ return null;
29
+ if (!Array.isArray(workflow.todos))
30
+ return null;
31
+ const todos = workflow.todos
32
+ .map((todo) => parseTodo(todo))
33
+ .filter((todo) => todo !== null);
34
+ if (todos.length === 0 && !workflow.title)
35
+ return null;
36
+ return {
37
+ version: typeof workflow.version === 'number' ? workflow.version : WORKFLOW_SCHEMA_VERSION,
38
+ id: workflow.id,
39
+ title: typeof workflow.title === 'string' ? workflow.title : undefined,
40
+ todos,
41
+ updatedAt: typeof workflow.updatedAt === 'string' ? workflow.updatedAt : new Date().toISOString(),
42
+ };
43
+ };
44
+ export const loadThreadWorkflow = async (channelId, threadId) => {
45
+ const details = await storageService.getThreadDetails({ channelId, threadId });
46
+ return parseThreadWorkflow(details.state);
47
+ };
48
+ export const persistWorkflow = async (channelId, threadId, workflow) => {
49
+ const next = {
50
+ ...workflow,
51
+ version: WORKFLOW_SCHEMA_VERSION,
52
+ updatedAt: new Date().toISOString(),
53
+ };
54
+ await storageService.patchThreadState({
55
+ channelId,
56
+ threadId,
57
+ state: { [WORKFLOW_THREAD_STATE_KEY]: next },
58
+ });
59
+ return next;
60
+ };
61
+ export const setWorkflowPlan = async (options) => {
62
+ const { channelId, threadId, plan } = options;
63
+ const workflow = {
64
+ version: WORKFLOW_SCHEMA_VERSION,
65
+ id: `wf_${generateId()}`,
66
+ title: plan.title,
67
+ todos: plan.todos.map((t) => ({
68
+ id: t.id || `todo_${generateId()}`,
69
+ text: t.text,
70
+ status: 'pending',
71
+ })),
72
+ updatedAt: new Date().toISOString(),
73
+ };
74
+ return persistWorkflow(channelId, threadId, workflow);
75
+ };
76
+ export const updateWorkflowTodo = async (options) => {
77
+ const { channelId, threadId, update } = options;
78
+ const workflow = await loadThreadWorkflow(channelId, threadId);
79
+ if (!workflow)
80
+ throw new Error('No active workflow found.');
81
+ const todos = workflow.todos.map((todo) => {
82
+ if (todo.id !== update.todoId)
83
+ return todo;
84
+ return {
85
+ ...todo,
86
+ ...(update.status ? { status: update.status } : {}),
87
+ ...(update.summary !== undefined ? { summary: update.summary } : {}),
88
+ };
89
+ });
90
+ return persistWorkflow(channelId, threadId, { ...workflow, todos });
91
+ };
92
+ export const formatWorkflowForPrompt = (workflow) => {
93
+ const lines = [
94
+ '## Current Workflow Plan',
95
+ workflow.title ? `Title: ${workflow.title}` : '',
96
+ 'This plan is for your internal coordination. Update it as you progress.',
97
+ '',
98
+ ].filter(Boolean);
99
+ for (const todo of workflow.todos) {
100
+ lines.push(`- [${todo.status}] ${todo.id}: ${todo.text}`);
101
+ if (todo.summary) {
102
+ lines.push(` Result: ${todo.summary}`);
103
+ }
104
+ }
105
+ return lines.join('\n');
106
+ };
@@ -0,0 +1,3 @@
1
+ export const WORKFLOW_SCHEMA_VERSION = 3;
2
+ export const WORKFLOW_THREAD_STATE_KEY = 'workflow';
3
+ export const OPENBOT_SYSTEM_AGENT_ID = 'system';
package/docs/plugins.md CHANGED
@@ -20,7 +20,6 @@ export interface Plugin {
20
20
  name: string;
21
21
  description: string;
22
22
  image?: string;
23
- defaultInstructions?: string;
24
23
  configSchema?: ConfigSchema;
25
24
  toolDefinitions?: Record<string, ToolDefinition>;
26
25
  factory: (context: PluginContext) => MelonyPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
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.1');
28
+ program.name('openbot').description('OpenBot CLI').version('0.3.2');
29
29
 
30
30
  program
31
31
  .command('start')
package/src/app/server.ts CHANGED
@@ -289,9 +289,8 @@ export async function startServer(options: ServerOptions = {}) {
289
289
 
290
290
  app.listen(PORT, () => {
291
291
  console.log(`\x1b[32mOpenBot server listening at http://localhost:${PORT}\x1b[0m`);
292
- console.log(` - Health endpoint: GET /health`);
293
- console.log(` - Events endpoint: GET /api/events (SSE)`);
294
- console.log(` - Publish endpoint: POST /api/publish`);
295
- console.log(` - State endpoint: GET /api/state`);
292
+ console.log(
293
+ `🌐 Visit \x1b[96m\x1b[1mhttps://openbot.one\x1b[0m to connect to this runtime and manage everything from there. ✨`,
294
+ );
296
295
  });
297
296
  }