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
package/dist/app/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ function checkNodeVersion() {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
checkNodeVersion();
|
|
19
|
-
program.name('openbot').description('OpenBot CLI').version('0.3.
|
|
19
|
+
program.name('openbot').description('OpenBot CLI').version('0.3.3');
|
|
20
20
|
program
|
|
21
21
|
.command('start')
|
|
22
22
|
.description('Start the OpenBot harness')
|
package/dist/app/server.js
CHANGED
|
@@ -236,9 +236,6 @@ export async function startServer(options = {}) {
|
|
|
236
236
|
});
|
|
237
237
|
app.listen(PORT, () => {
|
|
238
238
|
console.log(`\x1b[32mOpenBot server listening at http://localhost:${PORT}\x1b[0m`);
|
|
239
|
-
console.log(
|
|
240
|
-
console.log(` - Events endpoint: GET /api/events (SSE)`);
|
|
241
|
-
console.log(` - Publish endpoint: POST /api/publish`);
|
|
242
|
-
console.log(` - State endpoint: GET /api/state`);
|
|
239
|
+
console.log(`š Visit \x1b[96m\x1b[1mhttps://openbot.one\x1b[0m to connect to this runtime and manage everything from there. āØ`);
|
|
243
240
|
});
|
|
244
241
|
}
|
package/dist/bus/services.js
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { DEFAULT_MARKETPLACE_REGISTRY_URL, loadConfig } from '../app/config.js';
|
|
2
2
|
import { storageService } from '../services/storage.js';
|
|
3
3
|
import { pluginService } from '../services/plugins.js';
|
|
4
|
+
const readTodos = (state) => {
|
|
5
|
+
const raw = state.threadDetails?.state?.todos;
|
|
6
|
+
return Array.isArray(raw) ? raw : [];
|
|
7
|
+
};
|
|
8
|
+
let todoCounter = 0;
|
|
9
|
+
const newTodoId = (now, idx) => `todo_${now.toString(36)}_${(todoCounter++).toString(36)}_${idx}`;
|
|
10
|
+
async function persistTodos(storage, state, todos) {
|
|
11
|
+
if (!state.threadId)
|
|
12
|
+
throw new Error('No active thread');
|
|
13
|
+
await storage.patchThreadState({
|
|
14
|
+
channelId: state.channelId,
|
|
15
|
+
threadId: state.threadId,
|
|
16
|
+
state: { todos },
|
|
17
|
+
});
|
|
18
|
+
state.threadDetails = await storage.getThreadDetails({
|
|
19
|
+
channelId: state.channelId,
|
|
20
|
+
threadId: state.threadId,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
4
23
|
/**
|
|
5
24
|
* Resolve a scope alias to a concrete scope string. Aliases let tools accept
|
|
6
25
|
* `agent`/`channel`/`global` without knowing the active ids; the bus rewrites
|
|
@@ -114,7 +133,7 @@ export const busServicesPlugin = (options) => (builder) => {
|
|
|
114
133
|
builder.on('action:create_thread', async function* (event, context) {
|
|
115
134
|
const threadId = event.meta?.threadId;
|
|
116
135
|
const channelId = context.state.channelId;
|
|
117
|
-
const { threadTitle,
|
|
136
|
+
const { threadTitle, initialState } = event.data;
|
|
118
137
|
if (!threadId) {
|
|
119
138
|
console.warn('[bus] Cannot create thread: meta.threadId is missing');
|
|
120
139
|
return;
|
|
@@ -126,7 +145,6 @@ export const busServicesPlugin = (options) => (builder) => {
|
|
|
126
145
|
channelId,
|
|
127
146
|
threadId,
|
|
128
147
|
threadTitle,
|
|
129
|
-
spec,
|
|
130
148
|
initialState: initialState || {},
|
|
131
149
|
});
|
|
132
150
|
context.state.threadDetails = await storage.getThreadDetails({
|
|
@@ -286,14 +304,6 @@ export const busServicesPlugin = (options) => (builder) => {
|
|
|
286
304
|
});
|
|
287
305
|
updatedFields.push('state');
|
|
288
306
|
}
|
|
289
|
-
if (typeof event.data.spec === 'string') {
|
|
290
|
-
await storage.patchThreadSpec({
|
|
291
|
-
channelId: context.state.channelId,
|
|
292
|
-
threadId: context.state.threadId,
|
|
293
|
-
spec: event.data.spec,
|
|
294
|
-
});
|
|
295
|
-
updatedFields.push('spec');
|
|
296
|
-
}
|
|
297
307
|
context.state.threadDetails = await storage.getThreadDetails({
|
|
298
308
|
channelId: context.state.channelId,
|
|
299
309
|
threadId: context.state.threadId,
|
|
@@ -312,6 +322,92 @@ export const busServicesPlugin = (options) => (builder) => {
|
|
|
312
322
|
};
|
|
313
323
|
}
|
|
314
324
|
});
|
|
325
|
+
builder.on('action:todo_write', async function* (event, context) {
|
|
326
|
+
const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
|
|
327
|
+
try {
|
|
328
|
+
if (!context.state.threadId) {
|
|
329
|
+
throw new Error('todo_write requires an active thread');
|
|
330
|
+
}
|
|
331
|
+
const existing = readTodos(context.state);
|
|
332
|
+
const byId = new Map(existing.map((t) => [t.id, t]));
|
|
333
|
+
const now = Date.now();
|
|
334
|
+
const author = context.state.agentId || 'system';
|
|
335
|
+
const inputs = event.data.todos || [];
|
|
336
|
+
const next = inputs.map((raw, idx) => {
|
|
337
|
+
const prior = raw.id ? byId.get(raw.id) : undefined;
|
|
338
|
+
return {
|
|
339
|
+
id: prior?.id || raw.id || newTodoId(now, idx),
|
|
340
|
+
content: raw.content,
|
|
341
|
+
status: raw.status || prior?.status || 'pending',
|
|
342
|
+
assignee: raw.assignee ?? prior?.assignee,
|
|
343
|
+
createdBy: prior?.createdBy || author,
|
|
344
|
+
createdAt: prior?.createdAt || now,
|
|
345
|
+
updatedAt: now,
|
|
346
|
+
...(prior?.result !== undefined ? { result: prior.result } : {}),
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
await persistTodos(storage, context.state, next);
|
|
350
|
+
yield {
|
|
351
|
+
type: 'action:todo_write:result',
|
|
352
|
+
data: { success: true, todos: next },
|
|
353
|
+
meta: resultMeta,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
yield {
|
|
358
|
+
type: 'action:todo_write:result',
|
|
359
|
+
data: {
|
|
360
|
+
success: false,
|
|
361
|
+
todos: readTodos(context.state),
|
|
362
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
363
|
+
},
|
|
364
|
+
meta: resultMeta,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
builder.on('action:todo_update', async function* (event, context) {
|
|
369
|
+
const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
|
|
370
|
+
const patch = event.data;
|
|
371
|
+
try {
|
|
372
|
+
if (!context.state.threadId) {
|
|
373
|
+
throw new Error('todo_update requires an active thread');
|
|
374
|
+
}
|
|
375
|
+
const existing = readTodos(context.state);
|
|
376
|
+
const idx = existing.findIndex((t) => t.id === patch.id);
|
|
377
|
+
if (idx === -1) {
|
|
378
|
+
throw new Error(`Todo "${patch.id}" not found`);
|
|
379
|
+
}
|
|
380
|
+
const now = Date.now();
|
|
381
|
+
const updated = {
|
|
382
|
+
...existing[idx],
|
|
383
|
+
...(patch.content !== undefined ? { content: patch.content } : {}),
|
|
384
|
+
...(patch.status !== undefined ? { status: patch.status } : {}),
|
|
385
|
+
...(patch.assignee !== undefined
|
|
386
|
+
? { assignee: patch.assignee === '' ? undefined : patch.assignee }
|
|
387
|
+
: {}),
|
|
388
|
+
updatedAt: now,
|
|
389
|
+
};
|
|
390
|
+
const next = [...existing];
|
|
391
|
+
next[idx] = updated;
|
|
392
|
+
await persistTodos(storage, context.state, next);
|
|
393
|
+
yield {
|
|
394
|
+
type: 'action:todo_update:result',
|
|
395
|
+
data: { success: true, todo: updated, todos: next },
|
|
396
|
+
meta: resultMeta,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
yield {
|
|
401
|
+
type: 'action:todo_update:result',
|
|
402
|
+
data: {
|
|
403
|
+
success: false,
|
|
404
|
+
todos: readTodos(context.state),
|
|
405
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
406
|
+
},
|
|
407
|
+
meta: resultMeta,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
});
|
|
315
411
|
builder.on('action:storage:get-channels', async function* () {
|
|
316
412
|
const channels = await storage.getChannels();
|
|
317
413
|
yield { type: 'action:storage:get-channels-result', data: { channels } };
|
package/dist/harness/context.js
CHANGED
|
@@ -58,6 +58,7 @@ export function createDefaultContextEngine() {
|
|
|
58
58
|
engine.registerProvider(new AgentDetailsProvider());
|
|
59
59
|
engine.registerProvider(new ChannelDetailsProvider());
|
|
60
60
|
engine.registerProvider(new ThreadDetailsProvider());
|
|
61
|
+
engine.registerProvider(new TodoProvider());
|
|
61
62
|
engine.registerProvider(new MemoryProvider());
|
|
62
63
|
engine.registerProvider(new RecentEventsProvider());
|
|
63
64
|
engine.registerProcessor(new TokenBudgetProcessor());
|
|
@@ -70,11 +71,14 @@ class AgentDetailsProvider {
|
|
|
70
71
|
async provide(state) {
|
|
71
72
|
if (!state.agentDetails)
|
|
72
73
|
return [];
|
|
74
|
+
const instructions = state.agentDetails.instructions?.trim();
|
|
75
|
+
if (!instructions)
|
|
76
|
+
return [];
|
|
73
77
|
return [{
|
|
74
78
|
id: 'agent-details',
|
|
75
79
|
type: 'agent',
|
|
76
80
|
priority: 100,
|
|
77
|
-
content:
|
|
81
|
+
content: `# ${state.agentDetails.name}\n\n${instructions}`,
|
|
78
82
|
}];
|
|
79
83
|
}
|
|
80
84
|
}
|
|
@@ -85,11 +89,14 @@ class ChannelDetailsProvider {
|
|
|
85
89
|
async provide(state) {
|
|
86
90
|
if (!state.channelDetails)
|
|
87
91
|
return [];
|
|
92
|
+
const spec = state.channelDetails.spec?.trim();
|
|
93
|
+
if (!spec)
|
|
94
|
+
return [];
|
|
88
95
|
return [{
|
|
89
96
|
id: 'channel-details',
|
|
90
97
|
type: 'channel',
|
|
91
98
|
priority: 80,
|
|
92
|
-
content:
|
|
99
|
+
content: `# Channel you are in: ${state.channelDetails.name}\n\n Channel Specification: ${spec}`,
|
|
93
100
|
}];
|
|
94
101
|
}
|
|
95
102
|
}
|
|
@@ -100,12 +107,65 @@ class ThreadDetailsProvider {
|
|
|
100
107
|
async provide(state) {
|
|
101
108
|
if (!state.threadDetails)
|
|
102
109
|
return [];
|
|
103
|
-
|
|
110
|
+
// For now, this provider is a placeholder for future state-based assembly.
|
|
111
|
+
// It currently only surfaces the thread name to provide basic context.
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
104
114
|
id: 'thread-details',
|
|
105
115
|
type: 'thread',
|
|
106
116
|
priority: 90,
|
|
107
|
-
content:
|
|
108
|
-
}
|
|
117
|
+
content: `# Thread you are in: ${state.threadDetails.name}`,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Surfaces the shared per-thread todo list. The list lives in
|
|
124
|
+
* `threadDetails.state.todos` and is owned by bus services ā every agent in
|
|
125
|
+
* the thread reads from the same canonical source, which is how multi-agent
|
|
126
|
+
* autonomous flows stay coordinated.
|
|
127
|
+
*/
|
|
128
|
+
class TodoProvider {
|
|
129
|
+
constructor() {
|
|
130
|
+
this.name = 'todos';
|
|
131
|
+
}
|
|
132
|
+
async provide(state) {
|
|
133
|
+
const raw = state.threadDetails?.state?.todos;
|
|
134
|
+
const todos = Array.isArray(raw) ? raw : [];
|
|
135
|
+
if (todos.length === 0)
|
|
136
|
+
return [];
|
|
137
|
+
const DISPLAY_RESULT_CAP = 2500;
|
|
138
|
+
const marker = {
|
|
139
|
+
pending: '[ ]',
|
|
140
|
+
in_progress: '[~]',
|
|
141
|
+
done: '[x]',
|
|
142
|
+
cancelled: '[-]',
|
|
143
|
+
};
|
|
144
|
+
const formatted = todos
|
|
145
|
+
.map((t) => {
|
|
146
|
+
const assignee = t.assignee ? ` @${t.assignee}` : '';
|
|
147
|
+
let line = `- ${marker[t.status]} (${t.id})${assignee} ${t.content}`;
|
|
148
|
+
if (t.status === 'done' && t.result?.trim()) {
|
|
149
|
+
let snippet = t.result.trim();
|
|
150
|
+
if (snippet.length > DISPLAY_RESULT_CAP) {
|
|
151
|
+
snippet = `${snippet.slice(0, DISPLAY_RESULT_CAP)}ā¦[truncated]`;
|
|
152
|
+
}
|
|
153
|
+
line += `\n Result: ${snippet}`;
|
|
154
|
+
}
|
|
155
|
+
return line;
|
|
156
|
+
})
|
|
157
|
+
.join('\n');
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
id: 'todos',
|
|
161
|
+
type: 'todos',
|
|
162
|
+
priority: 92,
|
|
163
|
+
content: `## Shared todo plan (thread state)\n` +
|
|
164
|
+
`Orchestrator authors with \`todo_write\`; assignees run one step at a time. ` +
|
|
165
|
+
`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` +
|
|
166
|
+
`${formatted}`,
|
|
167
|
+
},
|
|
168
|
+
];
|
|
109
169
|
}
|
|
110
170
|
}
|
|
111
171
|
/**
|
|
@@ -139,7 +199,7 @@ class MemoryProvider {
|
|
|
139
199
|
id: 'memory',
|
|
140
200
|
type: 'memory',
|
|
141
201
|
priority: 95,
|
|
142
|
-
content: `##
|
|
202
|
+
content: `## Remembered facts\nTrust these unless the user contradicts them. Use \`forget\` to remove stale ones.\n\n${formatted}`,
|
|
143
203
|
},
|
|
144
204
|
];
|
|
145
205
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ensureEventId } from '../app/utils.js';
|
|
2
2
|
import { storageService } from '../services/storage.js';
|
|
3
|
+
import { advanceAfterRun } from './todo-advance.js';
|
|
3
4
|
export class QueueProcessor {
|
|
4
5
|
constructor(options) {
|
|
5
6
|
this.options = options;
|
|
@@ -26,33 +27,34 @@ export class QueueProcessor {
|
|
|
26
27
|
await Promise.all(Array.from(groups.entries()).map(async ([agentId, items]) => {
|
|
27
28
|
// Run items for the SAME agent sequentially to preserve event order and state consistency.
|
|
28
29
|
for (const item of items) {
|
|
29
|
-
const { event: currentEvent
|
|
30
|
-
// Track
|
|
30
|
+
const { event: currentEvent } = item;
|
|
31
|
+
// Track handoff requests queued in this step to avoid accidental duplicates.
|
|
31
32
|
const queuedRequestKeys = new Set();
|
|
32
33
|
const queuedItems = [];
|
|
33
|
-
|
|
34
|
+
let lastAgentOutput;
|
|
34
35
|
const runOnEvent = async (chunk, state) => {
|
|
35
36
|
// 0. Filter out echoed input events to prevent duplication in the UI/storage
|
|
36
37
|
if (chunk.type === currentEvent.type && chunk.id === currentEvent.id) {
|
|
37
38
|
return false;
|
|
38
39
|
}
|
|
40
|
+
if (chunk.type === 'agent:output') {
|
|
41
|
+
const outMeta = chunk.meta;
|
|
42
|
+
if (outMeta?.agentId === agentId) {
|
|
43
|
+
const content = chunk.data?.content;
|
|
44
|
+
if (typeof content === 'string' && content.trim()) {
|
|
45
|
+
lastAgentOutput = content.trim();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
39
49
|
// 1. Detect if a new thread was created and update the context for the rest of the loop
|
|
40
50
|
if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
|
|
41
51
|
this.currentThreadId = chunk.data.threadId || this.currentThreadId;
|
|
42
52
|
}
|
|
43
|
-
// 2. Internal routing (handoff
|
|
44
|
-
if (chunk.type === 'handoff:request'
|
|
45
|
-
const
|
|
46
|
-
const request = isHandoff
|
|
47
|
-
? chunk
|
|
48
|
-
: chunk;
|
|
53
|
+
// 2. Internal routing (handoff requests are internal ā not forwarded)
|
|
54
|
+
if (chunk.type === 'handoff:request') {
|
|
55
|
+
const request = chunk;
|
|
49
56
|
const targetAgentId = request.data?.agentId;
|
|
50
|
-
const
|
|
51
|
-
? request.meta.toolCallId
|
|
52
|
-
: undefined;
|
|
53
|
-
const requestKey = isHandoff
|
|
54
|
-
? `handoff:${targetAgentId}:${request.data?.content ?? ''}`
|
|
55
|
-
: `delegate:${toolCallId ?? 'missing'}:${targetAgentId}:${request.data?.content ?? ''}`;
|
|
57
|
+
const requestKey = `handoff:${targetAgentId}`;
|
|
56
58
|
if (targetAgentId &&
|
|
57
59
|
targetAgentId !== agentId &&
|
|
58
60
|
!queuedRequestKeys.has(requestKey)) {
|
|
@@ -68,54 +70,10 @@ export class QueueProcessor {
|
|
|
68
70
|
threadId: this.currentThreadId,
|
|
69
71
|
},
|
|
70
72
|
});
|
|
71
|
-
|
|
72
|
-
queuedItems.push({ agentId: targetAgentId, event: targetEvent });
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
if (!toolCallId) {
|
|
76
|
-
// Emit error output (this triggers run start if not already started)
|
|
77
|
-
await runOnEvent(ensureEventId({
|
|
78
|
-
type: 'agent:output',
|
|
79
|
-
data: {
|
|
80
|
-
content: 'Delegation request ignored: missing toolCallId. Please retry delegation.',
|
|
81
|
-
},
|
|
82
|
-
meta: {
|
|
83
|
-
agentId,
|
|
84
|
-
threadId: this.currentThreadId,
|
|
85
|
-
},
|
|
86
|
-
}), state);
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
const parentAgentId = typeof request.meta?.parentAgentId === 'string'
|
|
90
|
-
? request.meta.parentAgentId
|
|
91
|
-
: agentId;
|
|
92
|
-
const delegationWidgetId = typeof request.meta?.delegationWidgetId === 'string'
|
|
93
|
-
? request.meta.delegationWidgetId
|
|
94
|
-
: undefined;
|
|
95
|
-
queuedItems.push({
|
|
96
|
-
agentId: targetAgentId,
|
|
97
|
-
event: targetEvent,
|
|
98
|
-
delegationContext: {
|
|
99
|
-
parentAgentId,
|
|
100
|
-
toolCallId,
|
|
101
|
-
delegationWidgetId,
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
}
|
|
73
|
+
queuedItems.push({ agentId: targetAgentId, event: targetEvent });
|
|
105
74
|
}
|
|
106
75
|
return false;
|
|
107
76
|
}
|
|
108
|
-
if (chunk.type === 'agent:output') {
|
|
109
|
-
const content = chunk.data?.content;
|
|
110
|
-
if (typeof content === 'string' && content.trim().length > 0) {
|
|
111
|
-
runOutputs.push(content.trim());
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
// For delegate mode, child agent execution is internal:
|
|
115
|
-
// capture outputs for parent tool result, but don't stream child events to clients/storage.
|
|
116
|
-
if (delegationContext) {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
77
|
// If we get here, the event is accepted and should be emitted.
|
|
120
78
|
await this.options.onEvent(chunk, state);
|
|
121
79
|
return true;
|
|
@@ -133,7 +91,7 @@ export class QueueProcessor {
|
|
|
133
91
|
runId: this.options.runId,
|
|
134
92
|
agentId,
|
|
135
93
|
channelId: this.options.channelId,
|
|
136
|
-
threadId: this.currentThreadId
|
|
94
|
+
threadId: this.currentThreadId,
|
|
137
95
|
},
|
|
138
96
|
}, startState);
|
|
139
97
|
try {
|
|
@@ -160,60 +118,36 @@ export class QueueProcessor {
|
|
|
160
118
|
runId: this.options.runId,
|
|
161
119
|
agentId,
|
|
162
120
|
channelId: this.options.channelId,
|
|
163
|
-
threadId: this.currentThreadId
|
|
164
|
-
},
|
|
165
|
-
}, endState);
|
|
166
|
-
}
|
|
167
|
-
if (delegationContext) {
|
|
168
|
-
const summary = runOutputs.length > 0
|
|
169
|
-
? runOutputs.join('\n\n').slice(0, 4000)
|
|
170
|
-
: `Delegated agent "${agentId}" completed with no textual output.`;
|
|
171
|
-
const delegateResultEvent = ensureEventId({
|
|
172
|
-
type: 'action:delegate:result',
|
|
173
|
-
data: {
|
|
174
|
-
success: true,
|
|
175
|
-
agentId,
|
|
176
|
-
summary,
|
|
177
|
-
},
|
|
178
|
-
meta: {
|
|
179
|
-
toolCallId: delegationContext.toolCallId,
|
|
180
|
-
agentId: delegationContext.parentAgentId,
|
|
181
121
|
threadId: this.currentThreadId,
|
|
182
122
|
},
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
title: `Delegation complete: ${agentId}`,
|
|
191
|
-
body: runOutputs.length > 0
|
|
192
|
-
? 'Delegated task finished. Parent agent is preparing final response.'
|
|
193
|
-
: 'Delegated task finished with no textual output. Parent agent will continue.',
|
|
194
|
-
state: 'submitted',
|
|
195
|
-
metadata: {
|
|
196
|
-
type: 'delegation:status',
|
|
197
|
-
phase: 'completed',
|
|
198
|
-
delegatedAgentId: agentId,
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
meta: {
|
|
202
|
-
agentId: delegationContext.parentAgentId,
|
|
203
|
-
threadId: this.currentThreadId,
|
|
204
|
-
},
|
|
205
|
-
}), await storageService.getOpenBotState({
|
|
206
|
-
runId: this.options.runId,
|
|
207
|
-
agentId: delegationContext.parentAgentId,
|
|
123
|
+
}, endState);
|
|
124
|
+
// Autonomous todo advance: mark this agent's in_progress todo done
|
|
125
|
+
// and dispatch the next assignee, if any. Single trigger point,
|
|
126
|
+
// no reliance on the LLM remembering to call `todo_update`.
|
|
127
|
+
try {
|
|
128
|
+
const handoff = await advanceAfterRun({
|
|
129
|
+
storage: storageService,
|
|
208
130
|
channelId: this.options.channelId,
|
|
209
131
|
threadId: this.currentThreadId,
|
|
210
|
-
|
|
211
|
-
|
|
132
|
+
endedAgentId: agentId,
|
|
133
|
+
lastAgentOutput,
|
|
134
|
+
});
|
|
135
|
+
if (handoff) {
|
|
136
|
+
const requestKey = `handoff:${handoff.agentId}`;
|
|
137
|
+
if (!queuedRequestKeys.has(requestKey)) {
|
|
138
|
+
queuedRequestKeys.add(requestKey);
|
|
139
|
+
const targetEvent = ensureEventId({
|
|
140
|
+
type: 'agent:invoke',
|
|
141
|
+
data: { role: 'user', content: handoff.content },
|
|
142
|
+
meta: { threadId: this.currentThreadId },
|
|
143
|
+
});
|
|
144
|
+
queuedItems.push({ agentId: handoff.agentId, event: targetEvent });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
console.warn('[queue] todo advance failed', error);
|
|
212
150
|
}
|
|
213
|
-
nextQueue.push({
|
|
214
|
-
agentId: delegationContext.parentAgentId,
|
|
215
|
-
event: delegateResultEvent,
|
|
216
|
-
});
|
|
217
151
|
}
|
|
218
152
|
nextQueue.push(...queuedItems);
|
|
219
153
|
}
|
|
@@ -4,8 +4,8 @@ import { storageService } from '../services/storage.js';
|
|
|
4
4
|
import { busServicesPlugin } from '../bus/services.js';
|
|
5
5
|
/**
|
|
6
6
|
* Enhances the agent's instructions with a list of other available agents the
|
|
7
|
-
* orchestrator can hand off
|
|
8
|
-
*
|
|
7
|
+
* orchestrator can hand off to. Agents that include the `delegation` plugin
|
|
8
|
+
* will surface peers; agents without it can ignore this.
|
|
9
9
|
*/
|
|
10
10
|
export async function enhanceInstructions(state) {
|
|
11
11
|
const { agentId, agentDetails } = state;
|
|
@@ -19,12 +19,16 @@ export async function enhanceInstructions(state) {
|
|
|
19
19
|
const agentsList = otherAgents
|
|
20
20
|
.map((a) => `- **${a.id}**${a.description ? `: ${a.description}` : ''}`)
|
|
21
21
|
.join('\n');
|
|
22
|
-
const header = '### Available Agents
|
|
22
|
+
const header = '### Available Agents:';
|
|
23
23
|
if (!agentDetails.instructions.includes(header)) {
|
|
24
|
-
agentDetails.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const hasHandoff = (agentDetails.pluginRefs || []).some((r) => r.id === 'delegation');
|
|
25
|
+
const hasTodo = (agentDetails.pluginRefs || []).some((r) => r.id === 'todo');
|
|
26
|
+
const usage = hasTodo
|
|
27
|
+
? 'Use these ids as `assignee` when calling `todo_write` to plan multi-agent work.'
|
|
28
|
+
: hasHandoff
|
|
29
|
+
? 'Use `handoff` to transfer control to another agent in this thread.'
|
|
30
|
+
: '';
|
|
31
|
+
agentDetails.instructions += `\n\n${header}\n${agentsList}${usage ? `\n\n${usage}` : ''}`;
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
catch (error) {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Stored on each todo and inlined into the next assignee's invoke payload. */
|
|
2
|
+
const RESULT_MAX_CHARS = 12000;
|
|
3
|
+
/**
|
|
4
|
+
* Shared helpers that drive the autonomous todo loop. The queue processor
|
|
5
|
+
* calls `advanceAfterRun` once per `agent:run:end`; that is the only place
|
|
6
|
+
* todos are completed and dispatched, which keeps the autonomous flow
|
|
7
|
+
* single-threaded and easy to reason about.
|
|
8
|
+
*/
|
|
9
|
+
export const readTodosFromState = (state) => {
|
|
10
|
+
const raw = state?.todos;
|
|
11
|
+
return Array.isArray(raw) ? raw : [];
|
|
12
|
+
};
|
|
13
|
+
export function truncateTodoResult(text, maxChars = RESULT_MAX_CHARS) {
|
|
14
|
+
const trimmed = text.trim();
|
|
15
|
+
if (!trimmed)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (trimmed.length <= maxChars)
|
|
18
|
+
return trimmed;
|
|
19
|
+
return `${trimmed.slice(0, maxChars)}\nā¦[truncated]`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Apply a single advance step:
|
|
23
|
+
* 1. If a todo is `in_progress` and `assignee` matches the agent whose run
|
|
24
|
+
* just ended, mark it `done` and attach `result` from `lastOutput` when present.
|
|
25
|
+
* 2. Pick the next `pending` todo with an `assignee` and flip it to
|
|
26
|
+
* `in_progress`. That assignee gets handed off to; `invoke content` includes
|
|
27
|
+
* the previous step output when available so agents without short-term
|
|
28
|
+
* history still see prior work.
|
|
29
|
+
*
|
|
30
|
+
* If a todo is already `in_progress` and the just-ended agent wasn't its
|
|
31
|
+
* assignee, leave it alone ā someone else is working.
|
|
32
|
+
*/
|
|
33
|
+
export function advanceTodos(todos, endedAgentId, lastOutput) {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const truncated = truncateTodoResult(lastOutput ?? '');
|
|
36
|
+
let completedOutput;
|
|
37
|
+
let working = todos.map((t) => {
|
|
38
|
+
if (t.status === 'in_progress' && t.assignee === endedAgentId) {
|
|
39
|
+
completedOutput = truncated;
|
|
40
|
+
return {
|
|
41
|
+
...t,
|
|
42
|
+
status: 'done',
|
|
43
|
+
updatedAt: now,
|
|
44
|
+
...(truncated !== undefined ? { result: truncated } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return t;
|
|
48
|
+
});
|
|
49
|
+
if (working.some((t) => t.status === 'in_progress')) {
|
|
50
|
+
return { todos: working, handoff: null };
|
|
51
|
+
}
|
|
52
|
+
const idx = working.findIndex((t) => t.status === 'pending' && t.assignee);
|
|
53
|
+
if (idx === -1)
|
|
54
|
+
return { todos: working, handoff: null };
|
|
55
|
+
const picked = working[idx];
|
|
56
|
+
working = working.map((t, i) => i === idx ? { ...t, status: 'in_progress', updatedAt: now } : t);
|
|
57
|
+
const invokeContent = completedOutput !== undefined && completedOutput !== ''
|
|
58
|
+
? `${picked.content}\n\n--- Output from previous step ---\n${completedOutput}`
|
|
59
|
+
: picked.content;
|
|
60
|
+
return {
|
|
61
|
+
todos: working,
|
|
62
|
+
handoff: {
|
|
63
|
+
agentId: picked.assignee,
|
|
64
|
+
content: invokeContent,
|
|
65
|
+
todoId: picked.id,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export async function advanceAfterRun(options) {
|
|
70
|
+
const { storage, channelId, threadId, endedAgentId, lastAgentOutput } = options;
|
|
71
|
+
if (!threadId)
|
|
72
|
+
return null;
|
|
73
|
+
const details = await storage.getThreadDetails({ channelId, threadId });
|
|
74
|
+
const todos = readTodosFromState(details?.state);
|
|
75
|
+
if (todos.length === 0)
|
|
76
|
+
return null;
|
|
77
|
+
const { todos: nextList, handoff } = advanceTodos(todos, endedAgentId, lastAgentOutput);
|
|
78
|
+
const changed = nextList.length !== todos.length ||
|
|
79
|
+
nextList.some((t, i) => {
|
|
80
|
+
const u = todos[i];
|
|
81
|
+
if (!u)
|
|
82
|
+
return true;
|
|
83
|
+
return (t.status !== u.status ||
|
|
84
|
+
t.updatedAt !== u.updatedAt ||
|
|
85
|
+
t.result !== u.result ||
|
|
86
|
+
t.assignee !== u.assignee ||
|
|
87
|
+
t.content !== u.content);
|
|
88
|
+
});
|
|
89
|
+
if (changed) {
|
|
90
|
+
await storage.patchThreadState({ channelId, threadId, state: { todos: nextList } });
|
|
91
|
+
}
|
|
92
|
+
return handoff;
|
|
93
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { aiSdkRuntime } from './runtime.js';
|
|
2
|
-
import { AI_SDK_SYSTEM_PROMPT } from './system-prompt.js';
|
|
3
2
|
/**
|
|
4
3
|
* `ai-sdk` ā generic LLM runtime plugin built on the Vercel AI SDK.
|
|
5
4
|
*
|
|
@@ -11,7 +10,6 @@ export const aiSdkPlugin = {
|
|
|
11
10
|
id: 'ai-sdk',
|
|
12
11
|
name: 'AI SDK Runtime',
|
|
13
12
|
description: 'Generic LLM runtime built on the Vercel AI SDK. Consumes tools contributed by other plugins.',
|
|
14
|
-
defaultInstructions: AI_SDK_SYSTEM_PROMPT,
|
|
15
13
|
configSchema: {
|
|
16
14
|
type: 'object',
|
|
17
15
|
properties: {
|
|
@@ -28,7 +26,6 @@ export const aiSdkPlugin = {
|
|
|
28
26
|
: 'openai/gpt-4o-mini';
|
|
29
27
|
return aiSdkRuntime({
|
|
30
28
|
model,
|
|
31
|
-
system: agentDetails.instructions || AI_SDK_SYSTEM_PROMPT,
|
|
32
29
|
storage,
|
|
33
30
|
toolDefinitions: tools,
|
|
34
31
|
});
|