openbot 0.3.1 → 0.3.3
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.
- package/dist/app/cli.js +1 -1
- package/dist/app/server.js +1 -4
- package/dist/bus/services.js +106 -10
- package/dist/harness/context.js +66 -6
- package/dist/harness/queue-processor.js +44 -110
- package/dist/harness/runtime-factory.js +11 -7
- package/dist/harness/todo-advance.js +93 -0
- package/dist/plugins/ai-sdk/index.js +0 -3
- package/dist/plugins/ai-sdk/runtime.js +4 -11
- package/dist/plugins/ai-sdk/system-prompt.js +18 -3
- package/dist/plugins/delegation/index.js +7 -46
- package/dist/plugins/storage-tools/index.js +2 -11
- package/dist/plugins/todo/index.js +54 -0
- package/dist/plugins/workflow/index.js +65 -0
- package/dist/registry/plugins.js +2 -2
- package/dist/services/storage.js +3 -31
- package/dist/workflow/service.js +106 -0
- package/dist/workflow/types.js +3 -0
- package/docs/plugins.md +0 -1
- package/package.json +1 -1
- package/src/app/cli.ts +1 -1
- package/src/app/server.ts +3 -4
- package/src/app/types.ts +80 -45
- package/src/bus/plugin.ts +0 -2
- package/src/bus/services.ts +133 -12
- package/src/bus/types.ts +0 -4
- package/src/harness/context.ts +73 -10
- package/src/harness/queue-processor.ts +54 -143
- package/src/harness/runtime-factory.ts +11 -7
- package/src/harness/todo-advance.ts +128 -0
- package/src/plugins/ai-sdk/index.ts +0 -3
- package/src/plugins/ai-sdk/runtime.ts +284 -300
- package/src/plugins/ai-sdk/system-prompt.ts +18 -4
- package/src/plugins/delegation/index.ts +7 -50
- package/src/plugins/storage-tools/index.ts +8 -19
- package/src/plugins/todo/index.ts +64 -0
- package/src/registry/plugins.ts +2 -3
- 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,
|
|
120
|
-
|
|
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',
|
|
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,
|
|
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 =
|
|
2
|
-
'
|
|
3
|
-
'
|
|
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
|
|
2
|
+
const handoffToolDefinitions = {
|
|
3
3
|
handoff: {
|
|
4
|
-
description: 'Transfer control to another agent. The target agent continues the task
|
|
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
|
|
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: '
|
|
75
|
-
description: 'Hand off
|
|
76
|
-
toolDefinitions:
|
|
77
|
-
factory: () =>
|
|
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
|
|
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;
|
package/dist/registry/plugins.js
CHANGED
|
@@ -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,
|
package/dist/services/storage.js
CHANGED
|
@@ -63,7 +63,7 @@ const SYSTEM_DEFAULT_PLUGINS = [
|
|
|
63
63
|
{ id: 'storage-tools' },
|
|
64
64
|
// { id: 'mcp' },
|
|
65
65
|
{ id: 'shell' },
|
|
66
|
-
{ id: '
|
|
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
|
|
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,
|
|
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
|
+
};
|
package/docs/plugins.md
CHANGED
package/package.json
CHANGED
package/src/app/cli.ts
CHANGED
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(
|
|
293
|
-
|
|
294
|
-
|
|
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
|
}
|