openbot 0.3.6 → 0.4.0
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/README.md +15 -16
- package/dist/app/agent-ids.js +4 -0
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +0 -19
- package/dist/app/server.js +8 -14
- package/dist/bus/services.js +34 -124
- package/dist/harness/agent-invoke-run.js +44 -0
- package/dist/harness/agent-turn.js +99 -0
- package/dist/harness/channel-participants.js +40 -0
- package/dist/harness/constants.js +2 -0
- package/dist/harness/context-meter.js +97 -0
- package/dist/harness/context.js +95 -47
- package/dist/harness/dispatch.js +144 -0
- package/dist/harness/dispatcher.js +45 -156
- package/dist/harness/history.js +177 -0
- package/dist/harness/index.js +91 -0
- package/dist/harness/orchestration.js +88 -0
- package/dist/harness/participants.js +22 -0
- package/dist/harness/run-harness.js +154 -0
- package/dist/harness/run.js +98 -0
- package/dist/harness/runtime-factory.js +0 -34
- package/dist/harness/runtime.js +57 -0
- package/dist/harness/todo-dispatch.js +51 -0
- package/dist/harness/todos.js +5 -0
- package/dist/harness/turn.js +79 -0
- package/dist/plugins/approval/index.js +105 -149
- package/dist/plugins/delegation/index.js +119 -32
- package/dist/plugins/memory/index.js +103 -14
- package/dist/plugins/memory/service.js +152 -0
- package/dist/plugins/openbot/context.js +80 -0
- package/dist/plugins/openbot/history.js +98 -0
- package/dist/plugins/openbot/index.js +31 -0
- package/dist/plugins/openbot/runtime.js +317 -0
- package/dist/plugins/openbot/system-prompt.js +5 -0
- package/dist/plugins/plugin-manager/index.js +105 -0
- package/dist/plugins/storage/index.js +573 -0
- package/dist/plugins/storage/service.js +1159 -0
- package/dist/plugins/storage-tools/index.js +2 -2
- package/dist/plugins/thread-namer/index.js +72 -0
- package/dist/plugins/thread-naming/generate-title.js +44 -0
- package/dist/plugins/thread-naming/index.js +103 -0
- package/dist/plugins/threads/index.js +114 -0
- package/dist/plugins/todo/index.js +24 -25
- package/dist/plugins/ui/index.js +2 -32
- package/dist/registry/plugins.js +3 -9
- package/dist/services/plugins/domain.js +1 -0
- package/dist/services/plugins/plugin-cache.js +9 -0
- package/dist/services/plugins/registry.js +110 -0
- package/dist/services/plugins/service.js +177 -0
- package/dist/services/plugins/types.js +1 -0
- package/dist/services/process.js +29 -0
- package/dist/services/storage.js +11 -10
- package/dist/services/thread-naming.js +81 -0
- package/docs/agents.md +16 -10
- package/docs/architecture.md +2 -2
- package/docs/plugins.md +6 -15
- package/docs/templates/AGENT.example.md +7 -13
- package/package.json +1 -2
- package/src/app/agent-ids.ts +5 -0
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +1 -31
- package/src/app/server.ts +8 -16
- package/src/app/types.ts +63 -189
- package/src/harness/index.ts +145 -0
- package/src/plugins/approval/index.ts +91 -189
- package/src/plugins/delegation/index.ts +136 -39
- package/src/plugins/memory/index.ts +112 -15
- package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
- package/src/plugins/openbot/context.ts +91 -0
- package/src/plugins/openbot/history.ts +107 -0
- package/src/plugins/openbot/index.ts +37 -0
- package/src/plugins/openbot/runtime.ts +384 -0
- package/src/plugins/openbot/system-prompt.ts +7 -0
- package/src/plugins/plugin-manager/index.ts +122 -0
- package/src/plugins/shell/index.ts +1 -1
- package/src/plugins/storage/index.ts +633 -0
- package/src/{services/storage.ts → plugins/storage/service.ts} +224 -67
- package/src/{bus/types.ts → services/plugins/domain.ts} +16 -7
- package/src/services/plugins/plugin-cache.ts +13 -0
- package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
- package/src/services/{plugins.ts → plugins/service.ts} +96 -2
- package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
- package/src/bus/services.ts +0 -954
- package/src/harness/context.ts +0 -365
- package/src/harness/dispatcher.ts +0 -379
- package/src/harness/mcp.ts +0 -78
- package/src/harness/runtime-factory.ts +0 -129
- package/src/harness/todo-advance.ts +0 -128
- package/src/plugins/ai-sdk/index.ts +0 -41
- package/src/plugins/ai-sdk/runtime.ts +0 -468
- package/src/plugins/ai-sdk/system-prompt.ts +0 -18
- package/src/plugins/mcp/index.ts +0 -128
- package/src/plugins/storage-tools/index.ts +0 -90
- package/src/plugins/todo/index.ts +0 -64
- package/src/plugins/ui/index.ts +0 -227
- /package/src/{harness → services}/process.ts +0 -0
package/dist/harness/context.js
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
import { isDmSoloChannel } from './channel-participants.js';
|
|
2
|
+
export const DEFAULT_CONTEXT_BUDGET = 8000;
|
|
3
|
+
/**
|
|
4
|
+
* Returns the known context window budget (in tokens) for a given model string.
|
|
5
|
+
* This is used to drive the context usage ring in the UI and to configure
|
|
6
|
+
* the prompt pruning budget.
|
|
7
|
+
*/
|
|
8
|
+
export const getContextBudgetForModel = (modelString) => {
|
|
9
|
+
const budgets = {
|
|
10
|
+
'openai/gpt-4o': 128000,
|
|
11
|
+
'openai/gpt-4o-mini': 128000,
|
|
12
|
+
'openai/o1-preview': 128000,
|
|
13
|
+
'openai/o1-mini': 128000,
|
|
14
|
+
'anthropic/claude-3-5-sonnet-20240620': 200000,
|
|
15
|
+
'anthropic/claude-3-5-sonnet-latest': 200000,
|
|
16
|
+
'anthropic/claude-3-opus-20240229': 200000,
|
|
17
|
+
'anthropic/claude-3-sonnet-20240229': 200000,
|
|
18
|
+
'anthropic/claude-3-haiku-20240307': 200000,
|
|
19
|
+
};
|
|
20
|
+
return budgets[modelString] || DEFAULT_CONTEXT_BUDGET;
|
|
21
|
+
};
|
|
22
|
+
/** Built-in orchestrator agent id (`~/.openbot/agents/system/AGENT.md` overrides instructions). */
|
|
23
|
+
export const ORCHESTRATOR_AGENT_ID = 'system';
|
|
1
24
|
/**
|
|
2
25
|
* Cheap, dependency-free token estimator. Roughly char/4 — fine for budget
|
|
3
26
|
* enforcement; can be swapped for a tokenizer-backed implementation later
|
|
@@ -55,71 +78,95 @@ export class ContextEngine {
|
|
|
55
78
|
*/
|
|
56
79
|
export function createDefaultContextEngine() {
|
|
57
80
|
const engine = new ContextEngine();
|
|
81
|
+
engine.registerProvider(new EnvironmentProvider());
|
|
82
|
+
engine.registerProvider(new ChannelSpecProvider());
|
|
58
83
|
engine.registerProvider(new AgentDetailsProvider());
|
|
59
|
-
engine.registerProvider(new ChannelDetailsProvider());
|
|
60
|
-
engine.registerProvider(new ThreadDetailsProvider());
|
|
61
84
|
engine.registerProvider(new TodoProvider());
|
|
62
85
|
engine.registerProvider(new MemoryProvider());
|
|
63
|
-
engine.registerProvider(new RecentEventsProvider());
|
|
86
|
+
// engine.registerProvider(new RecentEventsProvider());
|
|
64
87
|
engine.registerProcessor(new TokenBudgetProcessor());
|
|
65
88
|
return engine;
|
|
66
89
|
}
|
|
67
|
-
class
|
|
90
|
+
class EnvironmentProvider {
|
|
68
91
|
constructor() {
|
|
69
|
-
this.name = '
|
|
92
|
+
this.name = 'environment';
|
|
70
93
|
}
|
|
71
94
|
async provide(state) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
95
|
+
const { channelId, threadId, channelDetails, agentId, threadDetails } = state;
|
|
96
|
+
const participants = channelDetails?.participants || [];
|
|
97
|
+
const isDm = isDmSoloChannel(participants, agentId);
|
|
98
|
+
let content = '## ENVIRONMENT\n';
|
|
99
|
+
if (isDm) {
|
|
100
|
+
content += '- Mode: Direct Message (Solo)\n';
|
|
101
|
+
content += '- Context: You are in a private conversation. No other agents are present.\n';
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const channelName = channelDetails?.name || channelId;
|
|
105
|
+
content += `- Mode: Channel (#${channelName})\n`;
|
|
106
|
+
if (threadId) {
|
|
107
|
+
content += `- Thread: ${threadDetails?.name || threadId}\n`;
|
|
108
|
+
}
|
|
109
|
+
const peerIds = participants.filter((id) => id !== agentId);
|
|
110
|
+
if (peerIds.length > 0) {
|
|
111
|
+
content += `- Participants: ${peerIds.join(', ')}\n`;
|
|
112
|
+
content += ` (Use these plain ids for todo assignees and delegate_to_agent — no @ prefix.)\n`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
id: 'environment',
|
|
118
|
+
type: 'environment',
|
|
119
|
+
priority: 110,
|
|
120
|
+
content,
|
|
121
|
+
},
|
|
122
|
+
];
|
|
83
123
|
}
|
|
84
124
|
}
|
|
85
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Injects SPEC.md (`channelDetails.spec`). Kept distinct from EnvironmentProvider
|
|
127
|
+
* so each block gets its own truncate budget and channel rules survive long
|
|
128
|
+
* participant lists under {@link ITEM_HARD_CHAR_CAP}.
|
|
129
|
+
*/
|
|
130
|
+
class ChannelSpecProvider {
|
|
86
131
|
constructor() {
|
|
87
|
-
this.name = 'channel-
|
|
132
|
+
this.name = 'channel-spec';
|
|
88
133
|
}
|
|
89
134
|
async provide(state) {
|
|
90
|
-
|
|
135
|
+
const raw = state.channelDetails?.spec;
|
|
136
|
+
const spec = typeof raw === 'string' ? raw.trim() : '';
|
|
137
|
+
if (!spec)
|
|
91
138
|
return [];
|
|
92
|
-
const participants = state.channelDetails.participants;
|
|
93
|
-
if (!participants?.length)
|
|
94
|
-
return [];
|
|
95
|
-
const channelLabel = state.channelDetails.name?.trim() || state.channelDetails.id;
|
|
96
|
-
const lines = participants.map((id) => `- \`${id}\``).join('\n');
|
|
97
139
|
return [
|
|
98
140
|
{
|
|
99
|
-
id: 'channel-
|
|
100
|
-
type: 'channel',
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
141
|
+
id: 'channel-spec',
|
|
142
|
+
type: 'channel-spec',
|
|
143
|
+
/** Below environment (110), above agent / {@link TokenBudgetProcessor.KEEP_FLOOR}. */
|
|
144
|
+
priority: 108,
|
|
145
|
+
content: `## CHANNEL SPECIFICATION (SPEC.md)\n` +
|
|
146
|
+
`Channel-level goals and constraints. Prefer these unless the user contradicts them.\n\n` +
|
|
147
|
+
`${spec}`,
|
|
104
148
|
},
|
|
105
149
|
];
|
|
106
150
|
}
|
|
107
151
|
}
|
|
108
|
-
class
|
|
152
|
+
class AgentDetailsProvider {
|
|
109
153
|
constructor() {
|
|
110
|
-
this.name = '
|
|
154
|
+
this.name = 'agent-details';
|
|
111
155
|
}
|
|
112
156
|
async provide(state) {
|
|
113
|
-
if (!state.
|
|
157
|
+
if (!state.agentDetails)
|
|
158
|
+
return [];
|
|
159
|
+
if (state.agentId === ORCHESTRATOR_AGENT_ID)
|
|
160
|
+
return [];
|
|
161
|
+
const instructions = state.agentDetails.instructions?.trim();
|
|
162
|
+
if (!instructions)
|
|
114
163
|
return [];
|
|
115
|
-
// For now, this provider is a placeholder for future state-based assembly.
|
|
116
|
-
// It currently only surfaces the thread name to provide basic context.
|
|
117
164
|
return [
|
|
118
165
|
{
|
|
119
|
-
id: '
|
|
120
|
-
type: '
|
|
121
|
-
priority:
|
|
122
|
-
content:
|
|
166
|
+
id: 'agent-details',
|
|
167
|
+
type: 'agent',
|
|
168
|
+
priority: 100,
|
|
169
|
+
content: `## AGENT: ${state.agentDetails.name}\n\n${instructions}`,
|
|
123
170
|
},
|
|
124
171
|
];
|
|
125
172
|
}
|
|
@@ -135,6 +182,8 @@ class TodoProvider {
|
|
|
135
182
|
this.name = 'todos';
|
|
136
183
|
}
|
|
137
184
|
async provide(state) {
|
|
185
|
+
if (state.agentId !== ORCHESTRATOR_AGENT_ID)
|
|
186
|
+
return [];
|
|
138
187
|
const raw = state.threadDetails?.state?.todos;
|
|
139
188
|
const todos = Array.isArray(raw) ? raw : [];
|
|
140
189
|
if (todos.length === 0)
|
|
@@ -165,9 +214,7 @@ class TodoProvider {
|
|
|
165
214
|
id: 'todos',
|
|
166
215
|
type: 'todos',
|
|
167
216
|
priority: 92,
|
|
168
|
-
content: `##
|
|
169
|
-
`Orchestrator authors with \`todo_write\`; assignees run one step at a time. ` +
|
|
170
|
-
`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` +
|
|
217
|
+
content: `## SHARED TODO PLAN (thread state)\n` +
|
|
171
218
|
`${formatted}`,
|
|
172
219
|
},
|
|
173
220
|
];
|
|
@@ -204,7 +251,7 @@ class MemoryProvider {
|
|
|
204
251
|
id: 'memory',
|
|
205
252
|
type: 'memory',
|
|
206
253
|
priority: 95,
|
|
207
|
-
content: `## Remembered facts\nTrust these unless the user contradicts them. Use \`forget\` to remove stale ones.\n\n${formatted}`,
|
|
254
|
+
content: `## Remembered global facts\nTrust these unless the user contradicts them. Use \`forget\` to remove stale ones.\n\n${formatted}`,
|
|
208
255
|
},
|
|
209
256
|
];
|
|
210
257
|
}
|
|
@@ -280,20 +327,21 @@ class RecentEventsProvider {
|
|
|
280
327
|
}
|
|
281
328
|
/**
|
|
282
329
|
* Drops the lowest-priority items until the assembled prompt fits within the
|
|
283
|
-
* token budget. The first item with priority >=
|
|
330
|
+
* token budget. The first item with priority >= \`keepFloor\` is always kept,
|
|
284
331
|
* so the agent's own instructions can never be evicted. Stable on ties:
|
|
285
332
|
* later-emitted items go first.
|
|
286
333
|
*/
|
|
287
334
|
export class TokenBudgetProcessor {
|
|
288
|
-
constructor(budget =
|
|
335
|
+
constructor(budget = undefined, keepFloor = TokenBudgetProcessor.KEEP_FLOOR) {
|
|
289
336
|
this.budget = budget;
|
|
290
337
|
this.keepFloor = keepFloor;
|
|
291
338
|
this.name = 'token-budget';
|
|
292
339
|
}
|
|
293
|
-
async process(items) {
|
|
340
|
+
async process(items, state) {
|
|
294
341
|
const sorted = [...items].sort((a, b) => b.priority - a.priority);
|
|
295
342
|
const out = [];
|
|
296
343
|
let used = 0;
|
|
344
|
+
const activeBudget = this.budget ?? (state.model ? getContextBudgetForModel(state.model) : TokenBudgetProcessor.DEFAULT_BUDGET);
|
|
297
345
|
for (const item of sorted) {
|
|
298
346
|
const cost = estimateTokens(item.content);
|
|
299
347
|
if (item.priority >= this.keepFloor) {
|
|
@@ -301,7 +349,7 @@ export class TokenBudgetProcessor {
|
|
|
301
349
|
used += cost;
|
|
302
350
|
continue;
|
|
303
351
|
}
|
|
304
|
-
if (used + cost <=
|
|
352
|
+
if (used + cost <= activeBudget) {
|
|
305
353
|
out.push(item);
|
|
306
354
|
used += cost;
|
|
307
355
|
}
|
|
@@ -310,6 +358,6 @@ export class TokenBudgetProcessor {
|
|
|
310
358
|
}
|
|
311
359
|
}
|
|
312
360
|
/** Soft prompt budget in tokens (matches gpt-4o-mini's reasonable system slice). */
|
|
313
|
-
TokenBudgetProcessor.DEFAULT_BUDGET =
|
|
361
|
+
TokenBudgetProcessor.DEFAULT_BUDGET = DEFAULT_CONTEXT_BUDGET;
|
|
314
362
|
/** Items at or above this priority are never dropped. */
|
|
315
363
|
TokenBudgetProcessor.KEEP_FLOOR = 100;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { ensureEventId } from '../app/utils.js';
|
|
2
|
+
import { storageService } from '../services/storage.js';
|
|
3
|
+
import { ORCHESTRATOR_AGENT_ID } from './constants.js';
|
|
4
|
+
import { runTurn } from './run.js';
|
|
5
|
+
const stopRequests = [];
|
|
6
|
+
const STOP_REQUEST_TTL_MS = 30 * 60 * 1000;
|
|
7
|
+
const pruneStopRequests = () => {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
for (let i = stopRequests.length - 1; i >= 0; i -= 1) {
|
|
10
|
+
if (now - stopRequests[i].requestedAt > STOP_REQUEST_TTL_MS) {
|
|
11
|
+
stopRequests.splice(i, 1);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const findStopRequest = (target) => {
|
|
16
|
+
pruneStopRequests();
|
|
17
|
+
return stopRequests.find((r) => {
|
|
18
|
+
if (r.runId !== target.runId)
|
|
19
|
+
return false;
|
|
20
|
+
if (r.agentId && r.agentId !== target.agentId)
|
|
21
|
+
return false;
|
|
22
|
+
if (r.channelId && r.channelId !== target.channelId)
|
|
23
|
+
return false;
|
|
24
|
+
if (r.threadId && r.threadId !== target.threadId)
|
|
25
|
+
return false;
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
export async function dispatch(options) {
|
|
30
|
+
const { runId, channelId, onEvent } = options;
|
|
31
|
+
let { threadId } = options;
|
|
32
|
+
const { event } = options;
|
|
33
|
+
ensureEventId(event);
|
|
34
|
+
if (event.type === 'action:agent_run_stop') {
|
|
35
|
+
await handleStop(event, options);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const agentId = options.agentId || ORCHESTRATOR_AGENT_ID;
|
|
39
|
+
let turnEvent = event;
|
|
40
|
+
if (event.type === 'user:input' || event.type === 'agent:invoke') {
|
|
41
|
+
turnEvent = await normalizeUserInput(event, { runId, channelId, threadId, onEvent });
|
|
42
|
+
if (event.type === 'user:input') {
|
|
43
|
+
threadId = turnEvent.meta?.threadId || threadId || event.id;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const target = { runId, agentId, channelId, threadId };
|
|
47
|
+
const preStop = findStopRequest(target);
|
|
48
|
+
if (preStop) {
|
|
49
|
+
const state = await storageService.getOpenBotState({ ...target, event: turnEvent });
|
|
50
|
+
await onEvent({ type: 'agent:run:stopped', data: { ...target, reason: preStop.reason } }, state);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let state;
|
|
54
|
+
try {
|
|
55
|
+
state = await storageService.getOpenBotState({ ...target, event: turnEvent });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error.code === 'AGENT_NOT_FOUND') {
|
|
59
|
+
const fallback = await storageService.getOpenBotState({
|
|
60
|
+
...target,
|
|
61
|
+
agentId: ORCHESTRATOR_AGENT_ID,
|
|
62
|
+
event: turnEvent,
|
|
63
|
+
});
|
|
64
|
+
await onEvent({
|
|
65
|
+
type: 'agent:output',
|
|
66
|
+
data: {
|
|
67
|
+
content: `⚠️ Agent **${agentId}** does not exist. Use participant ids without an @ prefix.`,
|
|
68
|
+
},
|
|
69
|
+
meta: { agentId: ORCHESTRATOR_AGENT_ID, threadId },
|
|
70
|
+
}, fallback);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
const turn = runTurn({ runId, channelId, threadId, agentId, event: turnEvent });
|
|
76
|
+
let next = await turn.next();
|
|
77
|
+
while (!next.done) {
|
|
78
|
+
const chunk = next.value;
|
|
79
|
+
const stop = findStopRequest(target);
|
|
80
|
+
if (stop) {
|
|
81
|
+
await onEvent({ type: 'agent:run:stopped', data: { ...target, reason: stop.reason } }, state);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
|
|
85
|
+
threadId = chunk.data.threadId || threadId;
|
|
86
|
+
}
|
|
87
|
+
await onEvent(chunk, state);
|
|
88
|
+
next = await turn.next();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function normalizeUserInput(event, ctx) {
|
|
92
|
+
const rawContent = event.data?.content || '';
|
|
93
|
+
const userFacing = {
|
|
94
|
+
type: 'agent:invoke',
|
|
95
|
+
id: event.id,
|
|
96
|
+
data: { content: rawContent, role: 'user' },
|
|
97
|
+
meta: {
|
|
98
|
+
agentId: ORCHESTRATOR_AGENT_ID,
|
|
99
|
+
userId: event.meta?.userId,
|
|
100
|
+
userName: event.meta?.userName,
|
|
101
|
+
userAvatarUrl: event.meta?.userAvatarUrl,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const initialState = await storageService.getOpenBotState({
|
|
105
|
+
runId: ctx.runId,
|
|
106
|
+
agentId: ORCHESTRATOR_AGENT_ID,
|
|
107
|
+
channelId: ctx.channelId,
|
|
108
|
+
threadId: ctx.threadId,
|
|
109
|
+
event: userFacing,
|
|
110
|
+
});
|
|
111
|
+
await ctx.onEvent(userFacing, initialState);
|
|
112
|
+
return {
|
|
113
|
+
...event,
|
|
114
|
+
type: 'agent:invoke',
|
|
115
|
+
data: { ...(event.data || {}), content: rawContent, role: 'user' },
|
|
116
|
+
meta: {
|
|
117
|
+
...(event.meta || {}),
|
|
118
|
+
threadId: ctx.threadId || event.id,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function handleStop(stopEvent, options) {
|
|
123
|
+
const { runId, channelId, threadId, onEvent } = options;
|
|
124
|
+
stopRequests.push({
|
|
125
|
+
runId: stopEvent.data.runId,
|
|
126
|
+
agentId: stopEvent.data.agentId,
|
|
127
|
+
channelId: stopEvent.data.channelId || channelId,
|
|
128
|
+
threadId: stopEvent.data.threadId || threadId,
|
|
129
|
+
reason: stopEvent.data.reason,
|
|
130
|
+
requestedAt: Date.now(),
|
|
131
|
+
});
|
|
132
|
+
const state = await storageService.getOpenBotState({
|
|
133
|
+
runId,
|
|
134
|
+
agentId: options.agentId || ORCHESTRATOR_AGENT_ID,
|
|
135
|
+
channelId,
|
|
136
|
+
threadId,
|
|
137
|
+
event: stopEvent,
|
|
138
|
+
});
|
|
139
|
+
await onEvent({
|
|
140
|
+
type: 'action:agent_run_stop:result',
|
|
141
|
+
data: { success: true, message: `Stop requested for run ${stopEvent.data.runId}.` },
|
|
142
|
+
meta: stopEvent.meta,
|
|
143
|
+
}, state);
|
|
144
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ensureEventId } from '../app/utils.js';
|
|
2
2
|
import { storageService } from '../services/storage.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { ORCHESTRATOR_AGENT_ID } from './context.js';
|
|
4
|
+
import { resolveMessageTargetAgent } from './channel-participants.js';
|
|
5
|
+
import { runTurn } from './turn.js';
|
|
6
6
|
const stopRequests = [];
|
|
7
7
|
const STOP_REQUEST_TTL_MS = 30 * 60 * 1000;
|
|
8
8
|
const pruneStopRequests = () => {
|
|
@@ -27,9 +27,6 @@ const findStopRequest = (target) => {
|
|
|
27
27
|
return true;
|
|
28
28
|
});
|
|
29
29
|
};
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Public API
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
30
|
export async function dispatch(options) {
|
|
34
31
|
const { event } = options;
|
|
35
32
|
ensureEventId(event);
|
|
@@ -44,188 +41,86 @@ export async function dispatch(options) {
|
|
|
44
41
|
onEvent: options.onEvent,
|
|
45
42
|
};
|
|
46
43
|
if (event.type === 'user:input' || event.type === 'agent:invoke') {
|
|
47
|
-
const invoke = await normalizeUserInput(event, ctx);
|
|
48
|
-
await
|
|
44
|
+
const { invoke, targetAgentId } = await normalizeUserInput(event, ctx, options.agentId);
|
|
45
|
+
await executeTurn(targetAgentId, invoke, ctx);
|
|
49
46
|
return;
|
|
50
47
|
}
|
|
51
|
-
|
|
52
|
-
// no advance, no follow-ups. Keeps queries (`/api/state`) cheap and idempotent.
|
|
53
|
-
await runBusEvent(event, options.agentId || 'system', ctx);
|
|
48
|
+
await executeTurn(options.agentId || ORCHESTRATOR_AGENT_ID, event, ctx, { lifecycle: false });
|
|
54
49
|
}
|
|
55
|
-
|
|
56
|
-
// Agent step: run:start -> runtime -> run:end -> advance -> chain
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
async function runStep(step, ctx, depth) {
|
|
59
|
-
if (depth >= MAX_CHAIN_DEPTH) {
|
|
60
|
-
console.warn(`[dispatcher] Reached MAX_CHAIN_DEPTH (${MAX_CHAIN_DEPTH}); stopping chain.`);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
50
|
+
async function executeTurn(agentId, event, ctx, opts = {}) {
|
|
63
51
|
const target = {
|
|
64
52
|
runId: ctx.runId,
|
|
65
|
-
agentId
|
|
53
|
+
agentId,
|
|
66
54
|
channelId: ctx.channelId,
|
|
67
55
|
threadId: ctx.threadId,
|
|
68
56
|
};
|
|
69
|
-
const preStop = findStopRequest(target);
|
|
57
|
+
const preStop = opts.lifecycle !== false ? findStopRequest(target) : undefined;
|
|
70
58
|
if (preStop) {
|
|
71
|
-
const state = await storageService.getOpenBotState({ ...target, event
|
|
59
|
+
const state = await storageService.getOpenBotState({ ...target, event });
|
|
72
60
|
await ctx.onEvent({ type: 'agent:run:stopped', data: { ...target, reason: preStop.reason } }, state);
|
|
73
61
|
return;
|
|
74
62
|
}
|
|
75
63
|
let state;
|
|
76
64
|
try {
|
|
77
|
-
state = await storageService.getOpenBotState({ ...target, event
|
|
65
|
+
state = await storageService.getOpenBotState({ ...target, event });
|
|
78
66
|
}
|
|
79
67
|
catch (error) {
|
|
80
68
|
if (error.code === 'AGENT_NOT_FOUND') {
|
|
81
69
|
const fallback = await storageService.getOpenBotState({
|
|
82
70
|
...target,
|
|
83
|
-
agentId:
|
|
84
|
-
event
|
|
71
|
+
agentId: ORCHESTRATOR_AGENT_ID,
|
|
72
|
+
event,
|
|
85
73
|
});
|
|
86
74
|
await ctx.onEvent({
|
|
87
75
|
type: 'agent:output',
|
|
88
|
-
data: {
|
|
89
|
-
|
|
76
|
+
data: {
|
|
77
|
+
content: `⚠️ Agent **${agentId}** does not exist. Use participant ids without an @ prefix.`,
|
|
78
|
+
},
|
|
79
|
+
meta: { agentId: ORCHESTRATOR_AGENT_ID, threadId: ctx.threadId },
|
|
90
80
|
}, fallback);
|
|
91
81
|
return;
|
|
92
82
|
}
|
|
93
83
|
throw error;
|
|
94
84
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
continue;
|
|
109
|
-
if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
|
|
110
|
-
ctx.threadId = chunk.data.threadId || ctx.threadId;
|
|
111
|
-
}
|
|
112
|
-
if (chunk.type === 'agent:output' &&
|
|
113
|
-
chunk.meta?.agentId === step.agentId) {
|
|
114
|
-
const content = chunk.data?.content;
|
|
115
|
-
if (typeof content === 'string' && content.trim())
|
|
116
|
-
lastAgentOutput = content.trim();
|
|
117
|
-
}
|
|
118
|
-
// Handoff requests are internal: queue a follow-up step instead of forwarding.
|
|
119
|
-
if (chunk.type === 'handoff:request') {
|
|
120
|
-
const req = chunk;
|
|
121
|
-
const targetAgent = req.data?.agentId;
|
|
122
|
-
if (targetAgent && targetAgent !== step.agentId && !queuedAgentIds.has(targetAgent)) {
|
|
123
|
-
queuedAgentIds.add(targetAgent);
|
|
124
|
-
followUps.push({
|
|
125
|
-
agentId: targetAgent,
|
|
126
|
-
event: makeInvoke(req.data.content, ctx.threadId, req.meta),
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
chunk.meta = { ...chunk.meta, agentId: step.agentId };
|
|
132
|
-
await ctx.onEvent(chunk, state);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
console.error(`[dispatcher] Agent run failed: ${step.agentId}`, error);
|
|
137
|
-
}
|
|
138
|
-
finally {
|
|
139
|
-
const endState = await storageService.getOpenBotState({ ...target, event: step.event });
|
|
140
|
-
await ctx.onEvent({ type: 'agent:run:end', data: { ...target } }, endState);
|
|
141
|
-
}
|
|
142
|
-
// Autonomous todo advance: single trigger point, runs once per `agent:run:end`.
|
|
143
|
-
try {
|
|
144
|
-
const handoff = await advanceAfterRun({
|
|
145
|
-
storage: storageService,
|
|
146
|
-
channelId: ctx.channelId,
|
|
147
|
-
threadId: ctx.threadId,
|
|
148
|
-
endedAgentId: step.agentId,
|
|
149
|
-
lastAgentOutput,
|
|
150
|
-
});
|
|
151
|
-
if (handoff && !queuedAgentIds.has(handoff.agentId)) {
|
|
152
|
-
queuedAgentIds.add(handoff.agentId);
|
|
153
|
-
followUps.push({
|
|
154
|
-
agentId: handoff.agentId,
|
|
155
|
-
event: makeInvoke(handoff.content, ctx.threadId),
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
console.warn('[dispatcher] todo advance failed', error);
|
|
161
|
-
}
|
|
162
|
-
for (const next of followUps) {
|
|
163
|
-
await runStep(next, ctx, depth + 1);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
// Bus pass-through: run an event through the targeted agent's runtime, forward
|
|
168
|
-
// chunks. No run:start/end, no advance, no follow-ups.
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
async function runBusEvent(event, agentId, ctx) {
|
|
171
|
-
let state;
|
|
172
|
-
try {
|
|
173
|
-
state = await storageService.getOpenBotState({
|
|
174
|
-
runId: ctx.runId,
|
|
175
|
-
agentId,
|
|
176
|
-
channelId: ctx.channelId,
|
|
177
|
-
threadId: ctx.threadId,
|
|
178
|
-
event,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
if (error.code === 'AGENT_NOT_FOUND') {
|
|
183
|
-
// Silently drop: bus pass-through has no UI surface to warn into.
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
throw error;
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
189
|
-
const runtime = await createAgentRuntime(state);
|
|
190
|
-
for await (const chunk of runtime.run(event, { state, runId: ctx.runId })) {
|
|
191
|
-
if (chunk.id === event.id && chunk.type === event.type)
|
|
192
|
-
continue;
|
|
193
|
-
chunk.meta = { ...chunk.meta, agentId };
|
|
194
|
-
await ctx.onEvent(chunk, state);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
catch (error) {
|
|
198
|
-
console.error(`[dispatcher] Bus event failed: ${event.type} (${agentId})`, error);
|
|
85
|
+
const turn = runTurn({
|
|
86
|
+
...target,
|
|
87
|
+
event,
|
|
88
|
+
lifecycle: opts.lifecycle !== false,
|
|
89
|
+
shouldStop: () => findStopRequest(target),
|
|
90
|
+
onThreadId: (threadId) => {
|
|
91
|
+
ctx.threadId = threadId;
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
let next = await turn.next();
|
|
95
|
+
while (!next.done) {
|
|
96
|
+
await ctx.onEvent(next.value, state);
|
|
97
|
+
next = await turn.next();
|
|
199
98
|
}
|
|
200
99
|
}
|
|
201
|
-
|
|
202
|
-
// Helpers
|
|
203
|
-
// ---------------------------------------------------------------------------
|
|
204
|
-
async function normalizeUserInput(event, ctx) {
|
|
100
|
+
async function normalizeUserInput(event, ctx, requestedAgentId) {
|
|
205
101
|
const rawContent = event.data?.content || '';
|
|
206
|
-
|
|
102
|
+
const previewState = await storageService.getOpenBotState({
|
|
103
|
+
runId: ctx.runId,
|
|
104
|
+
agentId: ORCHESTRATOR_AGENT_ID,
|
|
105
|
+
channelId: ctx.channelId,
|
|
106
|
+
threadId: ctx.threadId,
|
|
107
|
+
event,
|
|
108
|
+
});
|
|
109
|
+
const participants = previewState.channelDetails?.participants ?? [];
|
|
110
|
+
const targetAgentId = resolveMessageTargetAgent(participants, ORCHESTRATOR_AGENT_ID, requestedAgentId);
|
|
207
111
|
const userFacing = {
|
|
208
112
|
type: 'agent:invoke',
|
|
209
113
|
id: event.id,
|
|
210
114
|
data: { content: rawContent, role: 'user' },
|
|
211
115
|
meta: {
|
|
212
|
-
agentId:
|
|
116
|
+
agentId: targetAgentId,
|
|
213
117
|
userId: event.meta?.userId,
|
|
214
118
|
userName: event.meta?.userName,
|
|
215
119
|
userAvatarUrl: event.meta?.userAvatarUrl,
|
|
216
120
|
},
|
|
217
121
|
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
agentId: 'system',
|
|
221
|
-
channelId: ctx.channelId,
|
|
222
|
-
threadId: ctx.threadId,
|
|
223
|
-
event: userFacing,
|
|
224
|
-
});
|
|
225
|
-
await ctx.onEvent(userFacing, initialState);
|
|
226
|
-
// The event actually fed to the target agent. Carries the input threadId (or the
|
|
227
|
-
// message id, used as the anchor for Slack-style new threads).
|
|
228
|
-
return {
|
|
122
|
+
await ctx.onEvent(userFacing, previewState);
|
|
123
|
+
const invoke = {
|
|
229
124
|
...event,
|
|
230
125
|
type: 'agent:invoke',
|
|
231
126
|
data: { ...(event.data || {}), content: rawContent, role: 'user' },
|
|
@@ -234,13 +129,7 @@ async function normalizeUserInput(event, ctx) {
|
|
|
234
129
|
threadId: ctx.threadId || event.id,
|
|
235
130
|
},
|
|
236
131
|
};
|
|
237
|
-
}
|
|
238
|
-
function makeInvoke(content, threadId, baseMeta) {
|
|
239
|
-
return ensureEventId({
|
|
240
|
-
type: 'agent:invoke',
|
|
241
|
-
data: { role: 'user', content },
|
|
242
|
-
meta: { ...(baseMeta || {}), threadId },
|
|
243
|
-
});
|
|
132
|
+
return { invoke, targetAgentId };
|
|
244
133
|
}
|
|
245
134
|
async function handleStop(stopEvent, options) {
|
|
246
135
|
const { runId, channelId, threadId, onEvent } = options;
|
|
@@ -254,7 +143,7 @@ async function handleStop(stopEvent, options) {
|
|
|
254
143
|
});
|
|
255
144
|
const state = await storageService.getOpenBotState({
|
|
256
145
|
runId,
|
|
257
|
-
agentId: options.agentId ||
|
|
146
|
+
agentId: options.agentId || ORCHESTRATOR_AGENT_ID,
|
|
258
147
|
channelId,
|
|
259
148
|
threadId,
|
|
260
149
|
event: stopEvent,
|