openbot 0.3.0 → 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.
- package/dist/app/cli.js +1 -1
- package/dist/app/server.js +1 -4
- package/dist/bus/services.js +222 -15
- package/dist/harness/context.js +205 -26
- 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 +78 -13
- package/dist/plugins/ai-sdk/system-prompt.js +18 -3
- package/dist/plugins/delegation/index.js +7 -46
- package/dist/plugins/memory/index.js +71 -0
- 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 +4 -2
- package/dist/services/memory.js +152 -0
- package/dist/services/storage.js +9 -31
- package/dist/workflow/service.js +106 -0
- package/dist/workflow/types.js +3 -0
- package/docs/agents.md +15 -1
- 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 +140 -45
- package/src/bus/plugin.ts +0 -2
- package/src/bus/services.ts +258 -17
- package/src/bus/types.ts +13 -4
- package/src/harness/context.ts +233 -37
- 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 +356 -298
- package/src/plugins/ai-sdk/system-prompt.ts +18 -4
- package/src/plugins/delegation/index.ts +7 -50
- package/src/plugins/memory/index.ts +85 -0
- package/src/plugins/storage-tools/index.ts +8 -19
- package/src/plugins/todo/index.ts +64 -0
- package/src/registry/plugins.ts +4 -3
- package/src/services/memory.ts +213 -0
- package/src/services/storage.ts +9 -49
package/src/harness/context.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { OpenBotState } from '../app/types.js';
|
|
1
|
+
import { OpenBotEvent, OpenBotState, TodoItem } from '../app/types.js';
|
|
2
2
|
import { Storage } from '../bus/types.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Represents a piece of context that can be used in a prompt.
|
|
6
|
+
*
|
|
7
|
+
* Items flow through the engine in two phases:
|
|
8
|
+
* 1. Each registered `ContextProvider` emits zero or more items.
|
|
9
|
+
* 2. Each registered `ContextProcessor` may transform / drop / re-rank
|
|
10
|
+
* items (e.g. token-budget enforcement).
|
|
11
|
+
*
|
|
12
|
+
* Higher `priority` items appear first in the assembled prompt and are the
|
|
13
|
+
* last to be dropped under budget pressure.
|
|
6
14
|
*/
|
|
7
15
|
export interface ContextItem {
|
|
8
16
|
id: string;
|
|
@@ -12,25 +20,33 @@ export interface ContextItem {
|
|
|
12
20
|
metadata?: Record<string, any>;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
|
-
/**
|
|
16
|
-
* A provider that can fetch or generate context items.
|
|
17
|
-
*/
|
|
18
23
|
export interface ContextProvider {
|
|
19
24
|
name: string;
|
|
20
25
|
provide(state: OpenBotState, storage?: Storage): Promise<ContextItem[]>;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
/**
|
|
24
|
-
* A processor that can transform or filter context items (e.g., ranking, truncation).
|
|
25
|
-
*/
|
|
26
28
|
export interface ContextProcessor {
|
|
27
29
|
name: string;
|
|
28
30
|
process(items: ContextItem[], state: OpenBotState): Promise<ContextItem[]>;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
|
-
*
|
|
34
|
+
* Cheap, dependency-free token estimator. Roughly char/4 — fine for budget
|
|
35
|
+
* enforcement; can be swapped for a tokenizer-backed implementation later
|
|
36
|
+
* without touching providers.
|
|
33
37
|
*/
|
|
38
|
+
export const estimateTokens = (text: string): number =>
|
|
39
|
+
Math.ceil((text?.length ?? 0) / 4);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Hard cap (in characters) on a single context item. Keeps any one provider
|
|
43
|
+
* — typically the recent-events feed — from monopolising the prompt budget.
|
|
44
|
+
*/
|
|
45
|
+
const ITEM_HARD_CHAR_CAP = 6000;
|
|
46
|
+
|
|
47
|
+
const truncate = (text: string, maxChars: number): string =>
|
|
48
|
+
text.length <= maxChars ? text : `${text.slice(0, maxChars)}\n…[truncated]`;
|
|
49
|
+
|
|
34
50
|
export class ContextEngine {
|
|
35
51
|
private providers: ContextProvider[] = [];
|
|
36
52
|
private processors: ContextProcessor[] = [];
|
|
@@ -44,18 +60,18 @@ export class ContextEngine {
|
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
async buildContext(state: OpenBotState, storage?: Storage): Promise<string> {
|
|
47
|
-
// 1. Collect context from all providers
|
|
48
63
|
let items: ContextItem[] = [];
|
|
49
64
|
for (const provider of this.providers) {
|
|
50
65
|
try {
|
|
51
66
|
const providedItems = await provider.provide(state, storage);
|
|
52
|
-
|
|
67
|
+
for (const item of providedItems) {
|
|
68
|
+
items.push({ ...item, content: truncate(item.content, ITEM_HARD_CHAR_CAP) });
|
|
69
|
+
}
|
|
53
70
|
} catch (error) {
|
|
54
71
|
console.warn(`[ContextEngine] Provider ${provider.name} failed:`, error);
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
74
|
|
|
58
|
-
// 2. Run through processors
|
|
59
75
|
for (const processor of this.processors) {
|
|
60
76
|
try {
|
|
61
77
|
items = await processor.process(items, state);
|
|
@@ -64,26 +80,30 @@ export class ContextEngine {
|
|
|
64
80
|
}
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
// 3. Format items into a single string
|
|
68
83
|
return items
|
|
69
84
|
.sort((a, b) => b.priority - a.priority)
|
|
70
|
-
.map(item => item.content)
|
|
85
|
+
.map((item) => item.content)
|
|
71
86
|
.join('\n\n');
|
|
72
87
|
}
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
/**
|
|
76
|
-
* Default
|
|
91
|
+
* Default context engine. Order of providers is by emit order; final ordering
|
|
92
|
+
* in the prompt is determined by `priority`. The token-budget processor runs
|
|
93
|
+
* last so dropping happens after every provider has contributed.
|
|
77
94
|
*/
|
|
78
95
|
export function createDefaultContextEngine(): ContextEngine {
|
|
79
96
|
const engine = new ContextEngine();
|
|
80
97
|
|
|
81
|
-
// Basic Providers
|
|
82
98
|
engine.registerProvider(new AgentDetailsProvider());
|
|
83
99
|
engine.registerProvider(new ChannelDetailsProvider());
|
|
84
100
|
engine.registerProvider(new ThreadDetailsProvider());
|
|
101
|
+
engine.registerProvider(new TodoProvider());
|
|
102
|
+
engine.registerProvider(new MemoryProvider());
|
|
85
103
|
engine.registerProvider(new RecentEventsProvider());
|
|
86
104
|
|
|
105
|
+
engine.registerProcessor(new TokenBudgetProcessor());
|
|
106
|
+
|
|
87
107
|
return engine;
|
|
88
108
|
}
|
|
89
109
|
|
|
@@ -91,11 +111,14 @@ class AgentDetailsProvider implements ContextProvider {
|
|
|
91
111
|
name = 'agent-details';
|
|
92
112
|
async provide(state: OpenBotState): Promise<ContextItem[]> {
|
|
93
113
|
if (!state.agentDetails) return [];
|
|
114
|
+
const instructions = state.agentDetails.instructions?.trim();
|
|
115
|
+
if (!instructions) return [];
|
|
116
|
+
|
|
94
117
|
return [{
|
|
95
118
|
id: 'agent-details',
|
|
96
119
|
type: 'agent',
|
|
97
120
|
priority: 100,
|
|
98
|
-
content:
|
|
121
|
+
content: `# ${state.agentDetails.name}\n\n${instructions}`,
|
|
99
122
|
}];
|
|
100
123
|
}
|
|
101
124
|
}
|
|
@@ -104,11 +127,14 @@ class ChannelDetailsProvider implements ContextProvider {
|
|
|
104
127
|
name = 'channel-details';
|
|
105
128
|
async provide(state: OpenBotState): Promise<ContextItem[]> {
|
|
106
129
|
if (!state.channelDetails) return [];
|
|
130
|
+
const spec = state.channelDetails.spec?.trim();
|
|
131
|
+
if (!spec) return [];
|
|
132
|
+
|
|
107
133
|
return [{
|
|
108
134
|
id: 'channel-details',
|
|
109
135
|
type: 'channel',
|
|
110
136
|
priority: 80,
|
|
111
|
-
content:
|
|
137
|
+
content: `# Channel you are in: ${state.channelDetails.name}\n\n Channel Specification: ${spec}`,
|
|
112
138
|
}];
|
|
113
139
|
}
|
|
114
140
|
}
|
|
@@ -117,44 +143,214 @@ class ThreadDetailsProvider implements ContextProvider {
|
|
|
117
143
|
name = 'thread-details';
|
|
118
144
|
async provide(state: OpenBotState): Promise<ContextItem[]> {
|
|
119
145
|
if (!state.threadDetails) return [];
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
146
|
+
|
|
147
|
+
// For now, this provider is a placeholder for future state-based assembly.
|
|
148
|
+
// It currently only surfaces the thread name to provide basic context.
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
id: 'thread-details',
|
|
152
|
+
type: 'thread',
|
|
153
|
+
priority: 90,
|
|
154
|
+
content: `# Thread you are in: ${state.threadDetails.name}`,
|
|
155
|
+
},
|
|
156
|
+
];
|
|
126
157
|
}
|
|
127
158
|
}
|
|
128
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Surfaces the shared per-thread todo list. The list lives in
|
|
162
|
+
* `threadDetails.state.todos` and is owned by bus services — every agent in
|
|
163
|
+
* the thread reads from the same canonical source, which is how multi-agent
|
|
164
|
+
* autonomous flows stay coordinated.
|
|
165
|
+
*/
|
|
166
|
+
class TodoProvider implements ContextProvider {
|
|
167
|
+
name = 'todos';
|
|
168
|
+
async provide(state: OpenBotState): Promise<ContextItem[]> {
|
|
169
|
+
const raw = (state.threadDetails?.state as Record<string, unknown> | undefined)?.todos;
|
|
170
|
+
const todos: TodoItem[] = Array.isArray(raw) ? (raw as TodoItem[]) : [];
|
|
171
|
+
if (todos.length === 0) return [];
|
|
172
|
+
|
|
173
|
+
const DISPLAY_RESULT_CAP = 2500;
|
|
174
|
+
|
|
175
|
+
const marker: Record<TodoItem['status'], string> = {
|
|
176
|
+
pending: '[ ]',
|
|
177
|
+
in_progress: '[~]',
|
|
178
|
+
done: '[x]',
|
|
179
|
+
cancelled: '[-]',
|
|
180
|
+
};
|
|
181
|
+
const formatted = todos
|
|
182
|
+
.map((t) => {
|
|
183
|
+
const assignee = t.assignee ? ` @${t.assignee}` : '';
|
|
184
|
+
let line = `- ${marker[t.status]} (${t.id})${assignee} ${t.content}`;
|
|
185
|
+
if (t.status === 'done' && t.result?.trim()) {
|
|
186
|
+
let snippet = t.result.trim();
|
|
187
|
+
if (snippet.length > DISPLAY_RESULT_CAP) {
|
|
188
|
+
snippet = `${snippet.slice(0, DISPLAY_RESULT_CAP)}…[truncated]`;
|
|
189
|
+
}
|
|
190
|
+
line += `\n Result: ${snippet}`;
|
|
191
|
+
}
|
|
192
|
+
return line;
|
|
193
|
+
})
|
|
194
|
+
.join('\n');
|
|
195
|
+
|
|
196
|
+
return [
|
|
197
|
+
{
|
|
198
|
+
id: 'todos',
|
|
199
|
+
type: 'todos',
|
|
200
|
+
priority: 92,
|
|
201
|
+
content:
|
|
202
|
+
`## Shared todo plan (thread state)\n` +
|
|
203
|
+
`Orchestrator authors with \`todo_write\`; assignees run one step at a time. ` +
|
|
204
|
+
`When an item is \`done\`, its captured output appears below so every agent can see prior steps without relying on merged chat history.\n\n` +
|
|
205
|
+
`${formatted}`,
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Fetches relevant memories (global + active agent + active channel) and
|
|
213
|
+
* surfaces them at high priority so the LLM treats them as ground truth
|
|
214
|
+
* rather than chat history.
|
|
215
|
+
*/
|
|
216
|
+
class MemoryProvider implements ContextProvider {
|
|
217
|
+
name = 'memory';
|
|
218
|
+
async provide(state: OpenBotState, storage?: Storage): Promise<ContextItem[]> {
|
|
219
|
+
if (!storage?.listMemories) return [];
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const scopes = ['global', `agent:${state.agentId}`];
|
|
223
|
+
if (state.channelId) scopes.push(`channel:${state.channelId}`);
|
|
224
|
+
|
|
225
|
+
const records = await storage.listMemories({ scopes, limit: 50 });
|
|
226
|
+
if (records.length === 0) return [];
|
|
227
|
+
|
|
228
|
+
const formatted = records
|
|
229
|
+
.map((r) => {
|
|
230
|
+
const tags = r.tags?.length ? ` [${r.tags.join(', ')}]` : '';
|
|
231
|
+
const scopeLabel = r.scope === 'global' ? 'global' : r.scope;
|
|
232
|
+
return `- (${scopeLabel}${tags}) ${r.content}`;
|
|
233
|
+
})
|
|
234
|
+
.join('\n');
|
|
235
|
+
|
|
236
|
+
return [
|
|
237
|
+
{
|
|
238
|
+
id: 'memory',
|
|
239
|
+
type: 'memory',
|
|
240
|
+
priority: 95,
|
|
241
|
+
content: `## Remembered facts\nTrust these unless the user contradicts them. Use \`forget\` to remove stale ones.\n\n${formatted}`,
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.warn('[ContextEngine] MemoryProvider failed:', error);
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Event types we omit from the recent-events context block. They duplicate
|
|
253
|
+
* information already in the conversation history, are infrastructural
|
|
254
|
+
* noise, or are too large to be useful as a tail summary.
|
|
255
|
+
*/
|
|
256
|
+
const NOISY_EVENT_PREFIXES = [
|
|
257
|
+
'agent:invoke',
|
|
258
|
+
'agent:output',
|
|
259
|
+
'agent:run',
|
|
260
|
+
'agent:active-runs',
|
|
261
|
+
'client:ui',
|
|
262
|
+
'stream:',
|
|
263
|
+
'action:storage:get-',
|
|
264
|
+
'action:storage:patch-',
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
const MAX_RECENT_EVENTS = 20;
|
|
268
|
+
const MAX_EVENT_DATA_CHARS = 300;
|
|
269
|
+
|
|
270
|
+
const isNoisyEvent = (event: OpenBotEvent): boolean =>
|
|
271
|
+
NOISY_EVENT_PREFIXES.some((prefix) => event.type.startsWith(prefix));
|
|
272
|
+
|
|
273
|
+
const summarizeEvent = (event: OpenBotEvent): string => {
|
|
274
|
+
const data = (event as { data?: unknown }).data;
|
|
275
|
+
if (data === undefined) return `- ${event.type}`;
|
|
276
|
+
let payload: string;
|
|
277
|
+
try {
|
|
278
|
+
payload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
279
|
+
} catch {
|
|
280
|
+
payload = '[unserialisable]';
|
|
281
|
+
}
|
|
282
|
+
if (payload.length > MAX_EVENT_DATA_CHARS) {
|
|
283
|
+
payload = `${payload.slice(0, MAX_EVENT_DATA_CHARS)}…`;
|
|
284
|
+
}
|
|
285
|
+
return `- ${event.type}: ${payload}`;
|
|
286
|
+
};
|
|
287
|
+
|
|
129
288
|
class RecentEventsProvider implements ContextProvider {
|
|
130
289
|
name = 'recent-events';
|
|
131
290
|
async provide(state: OpenBotState, storage?: Storage): Promise<ContextItem[]> {
|
|
132
291
|
if (!storage) return [];
|
|
133
|
-
const items: ContextItem[] = [];
|
|
134
292
|
|
|
135
|
-
// Fetch channel events if no thread, otherwise fetch thread events
|
|
136
293
|
const channelId = state.channelId;
|
|
137
294
|
const threadId = state.threadId;
|
|
138
295
|
|
|
139
296
|
try {
|
|
140
297
|
const events = await storage.getEvents({ channelId, threadId });
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
298
|
+
const filtered = events.filter((e) => !isNoisyEvent(e));
|
|
299
|
+
if (filtered.length === 0) return [];
|
|
300
|
+
|
|
301
|
+
const formatted = filtered.slice(-MAX_RECENT_EVENTS).map(summarizeEvent).join('\n');
|
|
302
|
+
|
|
303
|
+
return [
|
|
304
|
+
{
|
|
148
305
|
id: threadId ? 'thread-events' : 'channel-events',
|
|
149
306
|
type: 'events',
|
|
150
307
|
priority: 70,
|
|
151
|
-
content: `## ${threadId ? 'THREAD' : 'CHANNEL'} RECENT ACTIVITIES (events)\n${
|
|
152
|
-
}
|
|
153
|
-
|
|
308
|
+
content: `## ${threadId ? 'THREAD' : 'CHANNEL'} RECENT ACTIVITIES (events)\n${formatted}`,
|
|
309
|
+
},
|
|
310
|
+
];
|
|
154
311
|
} catch (error) {
|
|
155
|
-
console.warn(
|
|
312
|
+
console.warn('[ContextEngine] Failed to fetch events:', error);
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Drops the lowest-priority items until the assembled prompt fits within the
|
|
320
|
+
* token budget. The first item with priority >= `keepFloor` is always kept,
|
|
321
|
+
* so the agent's own instructions can never be evicted. Stable on ties:
|
|
322
|
+
* later-emitted items go first.
|
|
323
|
+
*/
|
|
324
|
+
export class TokenBudgetProcessor implements ContextProcessor {
|
|
325
|
+
name = 'token-budget';
|
|
326
|
+
/** Soft prompt budget in tokens (matches gpt-4o-mini's reasonable system slice). */
|
|
327
|
+
static DEFAULT_BUDGET = 8000;
|
|
328
|
+
/** Items at or above this priority are never dropped. */
|
|
329
|
+
static KEEP_FLOOR = 100;
|
|
330
|
+
|
|
331
|
+
constructor(
|
|
332
|
+
private budget: number = TokenBudgetProcessor.DEFAULT_BUDGET,
|
|
333
|
+
private keepFloor: number = TokenBudgetProcessor.KEEP_FLOOR,
|
|
334
|
+
) {}
|
|
335
|
+
|
|
336
|
+
async process(items: ContextItem[]): Promise<ContextItem[]> {
|
|
337
|
+
const sorted = [...items].sort((a, b) => b.priority - a.priority);
|
|
338
|
+
const out: ContextItem[] = [];
|
|
339
|
+
let used = 0;
|
|
340
|
+
|
|
341
|
+
for (const item of sorted) {
|
|
342
|
+
const cost = estimateTokens(item.content);
|
|
343
|
+
if (item.priority >= this.keepFloor) {
|
|
344
|
+
out.push(item);
|
|
345
|
+
used += cost;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (used + cost <= this.budget) {
|
|
349
|
+
out.push(item);
|
|
350
|
+
used += cost;
|
|
351
|
+
}
|
|
156
352
|
}
|
|
157
353
|
|
|
158
|
-
return
|
|
354
|
+
return out;
|
|
159
355
|
}
|
|
160
356
|
}
|
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AgentInvokeEvent,
|
|
3
|
-
DelegateResultEvent,
|
|
4
|
-
DelegationRequestEvent,
|
|
5
3
|
HandoffRequestEvent,
|
|
6
4
|
OpenBotEvent,
|
|
7
5
|
OpenBotState,
|
|
8
6
|
} from '../app/types.js';
|
|
9
7
|
import { ensureEventId } from '../app/utils.js';
|
|
10
8
|
import { storageService } from '../services/storage.js';
|
|
9
|
+
import { advanceAfterRun } from './todo-advance.js';
|
|
11
10
|
|
|
12
11
|
export interface QueueItem {
|
|
13
12
|
agentId: string;
|
|
14
13
|
event: OpenBotEvent;
|
|
15
|
-
delegationContext?: {
|
|
16
|
-
parentAgentId: string;
|
|
17
|
-
toolCallId: string;
|
|
18
|
-
delegationWidgetId?: string;
|
|
19
|
-
};
|
|
20
14
|
}
|
|
21
15
|
|
|
22
16
|
export interface QueueProcessorOptions {
|
|
@@ -68,12 +62,12 @@ export class QueueProcessor {
|
|
|
68
62
|
Array.from(groups.entries()).map(async ([agentId, items]) => {
|
|
69
63
|
// Run items for the SAME agent sequentially to preserve event order and state consistency.
|
|
70
64
|
for (const item of items) {
|
|
71
|
-
const { event: currentEvent
|
|
65
|
+
const { event: currentEvent } = item;
|
|
72
66
|
|
|
73
|
-
// Track
|
|
67
|
+
// Track handoff requests queued in this step to avoid accidental duplicates.
|
|
74
68
|
const queuedRequestKeys = new Set<string>();
|
|
75
69
|
const queuedItems: QueueItem[] = [];
|
|
76
|
-
|
|
70
|
+
let lastAgentOutput: string | undefined;
|
|
77
71
|
|
|
78
72
|
const runOnEvent = async (chunk: OpenBotEvent, state: OpenBotState) => {
|
|
79
73
|
// 0. Filter out echoed input events to prevent duplication in the UI/storage
|
|
@@ -81,26 +75,27 @@ export class QueueProcessor {
|
|
|
81
75
|
return false;
|
|
82
76
|
}
|
|
83
77
|
|
|
78
|
+
if (chunk.type === 'agent:output') {
|
|
79
|
+
const outMeta = chunk.meta as { agentId?: string } | undefined;
|
|
80
|
+
if (outMeta?.agentId === agentId) {
|
|
81
|
+
const content = chunk.data?.content;
|
|
82
|
+
if (typeof content === 'string' && content.trim()) {
|
|
83
|
+
lastAgentOutput = content.trim();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
84
88
|
// 1. Detect if a new thread was created and update the context for the rest of the loop
|
|
85
89
|
if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
|
|
86
90
|
this.currentThreadId = chunk.data.threadId || this.currentThreadId;
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
// 2. Internal routing (handoff
|
|
90
|
-
if (chunk.type === 'handoff:request'
|
|
91
|
-
const
|
|
92
|
-
const request = isHandoff
|
|
93
|
-
? (chunk as HandoffRequestEvent)
|
|
94
|
-
: (chunk as DelegationRequestEvent);
|
|
93
|
+
// 2. Internal routing (handoff requests are internal — not forwarded)
|
|
94
|
+
if (chunk.type === 'handoff:request') {
|
|
95
|
+
const request = chunk as HandoffRequestEvent;
|
|
95
96
|
const targetAgentId = request.data?.agentId;
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
? request.meta.toolCallId
|
|
99
|
-
: undefined;
|
|
100
|
-
const requestKey = isHandoff
|
|
101
|
-
? `handoff:${targetAgentId}:${request.data?.content ?? ''}`
|
|
102
|
-
: `delegate:${toolCallId ?? 'missing'}:${targetAgentId}:${request.data?.content ?? ''}`;
|
|
103
|
-
|
|
97
|
+
const requestKey = `handoff:${targetAgentId}`;
|
|
98
|
+
|
|
104
99
|
if (
|
|
105
100
|
targetAgentId &&
|
|
106
101
|
targetAgentId !== agentId &&
|
|
@@ -119,62 +114,11 @@ export class QueueProcessor {
|
|
|
119
114
|
},
|
|
120
115
|
} satisfies AgentInvokeEvent) as AgentInvokeEvent;
|
|
121
116
|
|
|
122
|
-
|
|
123
|
-
queuedItems.push({ agentId: targetAgentId, event: targetEvent });
|
|
124
|
-
} else {
|
|
125
|
-
if (!toolCallId) {
|
|
126
|
-
// Emit error output (this triggers run start if not already started)
|
|
127
|
-
await runOnEvent(
|
|
128
|
-
ensureEventId({
|
|
129
|
-
type: 'agent:output',
|
|
130
|
-
data: {
|
|
131
|
-
content:
|
|
132
|
-
'Delegation request ignored: missing toolCallId. Please retry delegation.',
|
|
133
|
-
},
|
|
134
|
-
meta: {
|
|
135
|
-
agentId,
|
|
136
|
-
threadId: this.currentThreadId,
|
|
137
|
-
},
|
|
138
|
-
} as OpenBotEvent),
|
|
139
|
-
state,
|
|
140
|
-
);
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
const parentAgentId =
|
|
144
|
-
typeof request.meta?.parentAgentId === 'string'
|
|
145
|
-
? request.meta.parentAgentId
|
|
146
|
-
: agentId;
|
|
147
|
-
const delegationWidgetId =
|
|
148
|
-
typeof request.meta?.delegationWidgetId === 'string'
|
|
149
|
-
? request.meta.delegationWidgetId
|
|
150
|
-
: undefined;
|
|
151
|
-
queuedItems.push({
|
|
152
|
-
agentId: targetAgentId,
|
|
153
|
-
event: targetEvent,
|
|
154
|
-
delegationContext: {
|
|
155
|
-
parentAgentId,
|
|
156
|
-
toolCallId,
|
|
157
|
-
delegationWidgetId,
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
}
|
|
117
|
+
queuedItems.push({ agentId: targetAgentId, event: targetEvent });
|
|
161
118
|
}
|
|
162
119
|
return false;
|
|
163
120
|
}
|
|
164
121
|
|
|
165
|
-
if (chunk.type === 'agent:output') {
|
|
166
|
-
const content = chunk.data?.content;
|
|
167
|
-
if (typeof content === 'string' && content.trim().length > 0) {
|
|
168
|
-
runOutputs.push(content.trim());
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// For delegate mode, child agent execution is internal:
|
|
173
|
-
// capture outputs for parent tool result, but don't stream child events to clients/storage.
|
|
174
|
-
if (delegationContext) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
122
|
// If we get here, the event is accepted and should be emitted.
|
|
179
123
|
await this.options.onEvent(chunk, state);
|
|
180
124
|
return true;
|
|
@@ -191,11 +135,11 @@ export class QueueProcessor {
|
|
|
191
135
|
await this.options.onEvent(
|
|
192
136
|
{
|
|
193
137
|
type: 'agent:run:start',
|
|
194
|
-
data: {
|
|
195
|
-
runId: this.options.runId,
|
|
196
|
-
agentId,
|
|
197
|
-
channelId: this.options.channelId,
|
|
198
|
-
threadId: this.currentThreadId
|
|
138
|
+
data: {
|
|
139
|
+
runId: this.options.runId,
|
|
140
|
+
agentId,
|
|
141
|
+
channelId: this.options.channelId,
|
|
142
|
+
threadId: this.currentThreadId,
|
|
199
143
|
},
|
|
200
144
|
},
|
|
201
145
|
startState,
|
|
@@ -221,75 +165,42 @@ export class QueueProcessor {
|
|
|
221
165
|
await this.options.onEvent(
|
|
222
166
|
{
|
|
223
167
|
type: 'agent:run:end',
|
|
224
|
-
data: {
|
|
225
|
-
runId: this.options.runId,
|
|
226
|
-
agentId,
|
|
227
|
-
channelId: this.options.channelId,
|
|
228
|
-
threadId: this.currentThreadId
|
|
168
|
+
data: {
|
|
169
|
+
runId: this.options.runId,
|
|
170
|
+
agentId,
|
|
171
|
+
channelId: this.options.channelId,
|
|
172
|
+
threadId: this.currentThreadId,
|
|
229
173
|
},
|
|
230
174
|
},
|
|
231
175
|
endState,
|
|
232
176
|
);
|
|
233
|
-
}
|
|
234
177
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
type: 'action:delegate:result',
|
|
243
|
-
data: {
|
|
244
|
-
success: true,
|
|
245
|
-
agentId,
|
|
246
|
-
summary,
|
|
247
|
-
},
|
|
248
|
-
meta: {
|
|
249
|
-
toolCallId: delegationContext.toolCallId,
|
|
250
|
-
agentId: delegationContext.parentAgentId,
|
|
178
|
+
// Autonomous todo advance: mark this agent's in_progress todo done
|
|
179
|
+
// and dispatch the next assignee, if any. Single trigger point,
|
|
180
|
+
// no reliance on the LLM remembering to call `todo_update`.
|
|
181
|
+
try {
|
|
182
|
+
const handoff = await advanceAfterRun({
|
|
183
|
+
storage: storageService,
|
|
184
|
+
channelId: this.options.channelId,
|
|
251
185
|
threadId: this.currentThreadId,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
type: 'delegation:status',
|
|
270
|
-
phase: 'completed',
|
|
271
|
-
delegatedAgentId: agentId,
|
|
272
|
-
},
|
|
273
|
-
},
|
|
274
|
-
meta: {
|
|
275
|
-
agentId: delegationContext.parentAgentId,
|
|
276
|
-
threadId: this.currentThreadId,
|
|
277
|
-
},
|
|
278
|
-
} as OpenBotEvent),
|
|
279
|
-
await storageService.getOpenBotState({
|
|
280
|
-
runId: this.options.runId,
|
|
281
|
-
agentId: delegationContext.parentAgentId,
|
|
282
|
-
channelId: this.options.channelId,
|
|
283
|
-
threadId: this.currentThreadId,
|
|
284
|
-
event: delegateResultEvent,
|
|
285
|
-
}),
|
|
286
|
-
);
|
|
186
|
+
endedAgentId: agentId,
|
|
187
|
+
lastAgentOutput,
|
|
188
|
+
});
|
|
189
|
+
if (handoff) {
|
|
190
|
+
const requestKey = `handoff:${handoff.agentId}`;
|
|
191
|
+
if (!queuedRequestKeys.has(requestKey)) {
|
|
192
|
+
queuedRequestKeys.add(requestKey);
|
|
193
|
+
const targetEvent = ensureEventId({
|
|
194
|
+
type: 'agent:invoke',
|
|
195
|
+
data: { role: 'user', content: handoff.content },
|
|
196
|
+
meta: { threadId: this.currentThreadId },
|
|
197
|
+
} satisfies AgentInvokeEvent) as AgentInvokeEvent;
|
|
198
|
+
queuedItems.push({ agentId: handoff.agentId, event: targetEvent });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.warn('[queue] todo advance failed', error);
|
|
287
203
|
}
|
|
288
|
-
|
|
289
|
-
nextQueue.push({
|
|
290
|
-
agentId: delegationContext.parentAgentId,
|
|
291
|
-
event: delegateResultEvent,
|
|
292
|
-
});
|
|
293
204
|
}
|
|
294
205
|
|
|
295
206
|
nextQueue.push(...queuedItems);
|
|
@@ -7,8 +7,8 @@ import { busServicesPlugin } from '../bus/services.js';
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Enhances the agent's instructions with a list of other available agents the
|
|
10
|
-
* orchestrator can hand off
|
|
11
|
-
*
|
|
10
|
+
* orchestrator can hand off to. Agents that include the `delegation` plugin
|
|
11
|
+
* will surface peers; agents without it can ignore this.
|
|
12
12
|
*/
|
|
13
13
|
export async function enhanceInstructions(state: OpenBotState) {
|
|
14
14
|
const { agentId, agentDetails } = state;
|
|
@@ -23,12 +23,16 @@ export async function enhanceInstructions(state: OpenBotState) {
|
|
|
23
23
|
.map((a) => `- **${a.id}**${a.description ? `: ${a.description}` : ''}`)
|
|
24
24
|
.join('\n');
|
|
25
25
|
|
|
26
|
-
const header = '### Available Agents
|
|
26
|
+
const header = '### Available Agents:';
|
|
27
27
|
if (!agentDetails.instructions.includes(header)) {
|
|
28
|
-
agentDetails.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
'Use `
|
|
28
|
+
const hasHandoff = (agentDetails.pluginRefs || []).some((r) => r.id === 'delegation');
|
|
29
|
+
const hasTodo = (agentDetails.pluginRefs || []).some((r) => r.id === 'todo');
|
|
30
|
+
const usage = hasTodo
|
|
31
|
+
? 'Use these ids as `assignee` when calling `todo_write` to plan multi-agent work.'
|
|
32
|
+
: hasHandoff
|
|
33
|
+
? 'Use `handoff` to transfer control to another agent in this thread.'
|
|
34
|
+
: '';
|
|
35
|
+
agentDetails.instructions += `\n\n${header}\n${agentsList}${usage ? `\n\n${usage}` : ''}`;
|
|
32
36
|
}
|
|
33
37
|
} catch (error) {
|
|
34
38
|
console.warn('[agent] Failed to enhance instructions', error);
|