openbot 0.3.5 → 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/assets/icon.svg +9 -3
- package/dist/bus/services.js +78 -132
- 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 +98 -45
- 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 +41 -15
- 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 +70 -190
- package/src/assets/icon.svg +9 -3
- 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} +257 -72
- package/src/{bus/types.ts → services/plugins/domain.ts} +20 -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 -908
- package/src/harness/context.ts +0 -356
- 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
|
@@ -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,
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sliding window: max number of messages we replay to the model on each
|
|
3
|
+
* invocation. Older turns stay on disk but are not sent. Keeps both the
|
|
4
|
+
* recent prompts and the prompt token budget bounded.
|
|
5
|
+
*/
|
|
6
|
+
const MAX_WINDOW_MESSAGES = 80;
|
|
7
|
+
/**
|
|
8
|
+
* Reconstructs a valid `OpenBotMessage[]` chain from a raw event log.
|
|
9
|
+
* Handles grouping tool calls into assistant messages and matching results.
|
|
10
|
+
*
|
|
11
|
+
* This replaces the old `shortTermMessages` concept by treating the event log
|
|
12
|
+
* as the single source of truth for conversation history.
|
|
13
|
+
*/
|
|
14
|
+
export function reconstructHistory(events) {
|
|
15
|
+
const messages = [];
|
|
16
|
+
for (const event of events) {
|
|
17
|
+
switch (event.type) {
|
|
18
|
+
case 'user:input':
|
|
19
|
+
messages.push({ role: 'user', content: event.data.content });
|
|
20
|
+
break;
|
|
21
|
+
case 'agent:output': {
|
|
22
|
+
const last = messages[messages.length - 1];
|
|
23
|
+
if (last && last.role === 'assistant') {
|
|
24
|
+
if (typeof last.content === 'string') {
|
|
25
|
+
last.content += '\n' + event.data.content;
|
|
26
|
+
}
|
|
27
|
+
else if (Array.isArray(last.content)) {
|
|
28
|
+
const textPart = last.content.find((p) => p.type === 'text');
|
|
29
|
+
if (textPart && textPart.type === 'text') {
|
|
30
|
+
textPart.text += '\n' + event.data.content;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
last.content.unshift({ type: 'text', text: event.data.content });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
messages.push({ role: 'assistant', content: event.data.content });
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'agent:invoke': {
|
|
43
|
+
const invokeEvent = event;
|
|
44
|
+
// Only treat as a message if it has content and is explicitly from a role
|
|
45
|
+
if (invokeEvent.data?.content && invokeEvent.data?.role) {
|
|
46
|
+
const role = invokeEvent.data.role;
|
|
47
|
+
messages.push({ role, content: invokeEvent.data.content });
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
default:
|
|
52
|
+
// Handle tool calls (action:*)
|
|
53
|
+
if (event.type.startsWith('action:') && !event.type.endsWith(':result')) {
|
|
54
|
+
const toolName = event.type.slice(7);
|
|
55
|
+
const toolCallId = event.meta?.toolCallId;
|
|
56
|
+
if (!toolCallId)
|
|
57
|
+
break;
|
|
58
|
+
const toolCall = {
|
|
59
|
+
type: 'tool-call',
|
|
60
|
+
toolCallId,
|
|
61
|
+
toolName,
|
|
62
|
+
input: event.data,
|
|
63
|
+
};
|
|
64
|
+
const last = messages[messages.length - 1];
|
|
65
|
+
if (last && last.role === 'assistant') {
|
|
66
|
+
if (typeof last.content === 'string') {
|
|
67
|
+
last.content = [
|
|
68
|
+
{ type: 'text', text: last.content },
|
|
69
|
+
toolCall,
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
last.content.push(toolCall);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
messages.push({
|
|
78
|
+
role: 'assistant',
|
|
79
|
+
content: [toolCall],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Handle tool results (action:*:result)
|
|
84
|
+
else if (event.type.startsWith('action:') && event.type.endsWith(':result')) {
|
|
85
|
+
const toolName = event.type.slice(7, -7);
|
|
86
|
+
const toolCallId = event.meta?.toolCallId;
|
|
87
|
+
if (!toolCallId)
|
|
88
|
+
break;
|
|
89
|
+
const last = messages[messages.length - 1];
|
|
90
|
+
if (last && last.role === 'tool' && Array.isArray(last.content)) {
|
|
91
|
+
last.content.push({
|
|
92
|
+
type: 'tool-result',
|
|
93
|
+
toolCallId,
|
|
94
|
+
toolName,
|
|
95
|
+
output: event.data,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
messages.push({
|
|
100
|
+
role: 'tool',
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: 'tool-result',
|
|
104
|
+
toolCallId,
|
|
105
|
+
toolName,
|
|
106
|
+
output: event.data,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return repairAndWindow(messages);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Self-healing pass: every assistant tool_call must have a matching tool
|
|
119
|
+
* result before the next user/assistant turn. Also applies the sliding window.
|
|
120
|
+
*/
|
|
121
|
+
function repairAndWindow(messages) {
|
|
122
|
+
const fulfilled = new Set();
|
|
123
|
+
for (const m of messages) {
|
|
124
|
+
if (m.role === 'tool' && Array.isArray(m.content)) {
|
|
125
|
+
for (const part of m.content) {
|
|
126
|
+
if (part.type === 'tool-result') {
|
|
127
|
+
fulfilled.add(part.toolCallId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const repaired = [];
|
|
133
|
+
for (const m of messages) {
|
|
134
|
+
repaired.push(m);
|
|
135
|
+
if (m.role !== 'assistant' || typeof m.content === 'string')
|
|
136
|
+
continue;
|
|
137
|
+
const missingResults = [];
|
|
138
|
+
for (const part of m.content) {
|
|
139
|
+
if (part.type === 'tool-call' && !fulfilled.has(part.toolCallId)) {
|
|
140
|
+
missingResults.push({
|
|
141
|
+
type: 'tool-result',
|
|
142
|
+
toolCallId: part.toolCallId,
|
|
143
|
+
toolName: part.toolName,
|
|
144
|
+
output: {
|
|
145
|
+
success: false,
|
|
146
|
+
error: 'Tool result was lost (handler did not emit a matching :result event).',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
fulfilled.add(part.toolCallId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (missingResults.length > 0) {
|
|
153
|
+
repaired.push({
|
|
154
|
+
role: 'tool',
|
|
155
|
+
content: missingResults,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (repaired.length <= MAX_WINDOW_MESSAGES)
|
|
160
|
+
return repaired;
|
|
161
|
+
const tail = repaired.slice(-MAX_WINDOW_MESSAGES);
|
|
162
|
+
// Ensure the tail doesn't start with an orphan tool result
|
|
163
|
+
const knownAssistantCallIds = new Set();
|
|
164
|
+
for (const m of tail) {
|
|
165
|
+
if (m.role === 'assistant' && Array.isArray(m.content)) {
|
|
166
|
+
for (const part of m.content) {
|
|
167
|
+
if (part.type === 'tool-call')
|
|
168
|
+
knownAssistantCallIds.add(part.toolCallId);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return tail.filter((m) => {
|
|
173
|
+
if (m.role !== 'tool' || typeof m.content === 'string')
|
|
174
|
+
return true;
|
|
175
|
+
return m.content.some((part) => part.type === 'tool-result' && knownAssistantCallIds.has(part.toolCallId));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { melony } from 'melony';
|
|
2
|
+
import { ensureEventId } from '../app/utils.js';
|
|
3
|
+
import { storageService } from '../plugins/storage/service.js';
|
|
4
|
+
import { STATE_AGENT_ID, ORCHESTRATOR_AGENT_ID } from '../app/agent-ids.js';
|
|
5
|
+
import { resolvePlugin } from '../services/plugins/registry.js';
|
|
6
|
+
export { STATE_AGENT_ID, ORCHESTRATOR_AGENT_ID };
|
|
7
|
+
async function emitEvent(chunk, state, { persistEvents, channelId, threadId, onEvent, parentAgentId, parentToolCallId, }) {
|
|
8
|
+
ensureEventId(chunk);
|
|
9
|
+
// Enrich event with parent metadata if not already present
|
|
10
|
+
if (parentAgentId || parentToolCallId) {
|
|
11
|
+
chunk.meta = {
|
|
12
|
+
...chunk.meta,
|
|
13
|
+
parentAgentId: chunk.meta?.parentAgentId || parentAgentId,
|
|
14
|
+
parentToolCallId: chunk.meta?.parentToolCallId || parentToolCallId,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (persistEvents) {
|
|
18
|
+
await storageService.storeEvent({
|
|
19
|
+
channelId: state?.channelId || channelId,
|
|
20
|
+
threadId: state?.threadId || threadId,
|
|
21
|
+
event: chunk,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
await onEvent(chunk, state);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Runs a single agent turn.
|
|
28
|
+
* Fire and forget.
|
|
29
|
+
*/
|
|
30
|
+
export async function runAgent(options) {
|
|
31
|
+
const { runId, agentId, event, channelId, threadId, onEvent } = options;
|
|
32
|
+
const persistEvents = options.persistEvents !== false;
|
|
33
|
+
const parentAgentId = event.meta?.parentAgentId;
|
|
34
|
+
const parentToolCallId = event.meta?.parentToolCallId;
|
|
35
|
+
const agentDetails = await storageService.getAgentDetails({ agentId });
|
|
36
|
+
const state = await storageService.getOpenBotState({
|
|
37
|
+
runId,
|
|
38
|
+
agentId,
|
|
39
|
+
channelId,
|
|
40
|
+
threadId,
|
|
41
|
+
event,
|
|
42
|
+
});
|
|
43
|
+
await emitEvent({
|
|
44
|
+
type: 'agent:run:start',
|
|
45
|
+
data: { runId, agentId, channelId, threadId },
|
|
46
|
+
}, state, { persistEvents, channelId, threadId, onEvent, parentAgentId, parentToolCallId });
|
|
47
|
+
try {
|
|
48
|
+
const pluginRefs = agentDetails.pluginRefs ?? [];
|
|
49
|
+
const tools = {};
|
|
50
|
+
for (const ref of pluginRefs) {
|
|
51
|
+
const plugin = await resolvePlugin(ref.id);
|
|
52
|
+
if (plugin?.toolDefinitions) {
|
|
53
|
+
Object.assign(tools, plugin.toolDefinitions);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const builder = melony().initialState(state);
|
|
57
|
+
for (const ref of pluginRefs) {
|
|
58
|
+
const plugin = await resolvePlugin(ref.id);
|
|
59
|
+
if (!plugin)
|
|
60
|
+
continue;
|
|
61
|
+
builder.use(plugin.factory({
|
|
62
|
+
agentId,
|
|
63
|
+
agentDetails,
|
|
64
|
+
config: ref.config ?? {},
|
|
65
|
+
storage: storageService,
|
|
66
|
+
tools,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
const runtime = builder.build();
|
|
70
|
+
const generator = runtime.run(event, { runId, state });
|
|
71
|
+
for await (const outputEvent of generator) {
|
|
72
|
+
await emitEvent(outputEvent, state, {
|
|
73
|
+
persistEvents,
|
|
74
|
+
channelId,
|
|
75
|
+
threadId,
|
|
76
|
+
onEvent,
|
|
77
|
+
parentAgentId,
|
|
78
|
+
parentToolCallId,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error(`[harness] Error running agent ${agentId}:`, error);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await emitEvent({
|
|
87
|
+
type: 'agent:run:end',
|
|
88
|
+
data: { runId, agentId, channelId, threadId },
|
|
89
|
+
}, state, { persistEvents, channelId, threadId, onEvent, parentAgentId, parentToolCallId });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ORCHESTRATOR_AGENT_ID } from './context.js';
|
|
2
|
+
const readThreadState = (state) => state.threadDetails?.state ?? {};
|
|
3
|
+
export const readTodosFromState = (state) => {
|
|
4
|
+
const raw = readThreadState(state).todos;
|
|
5
|
+
return Array.isArray(raw) ? raw : [];
|
|
6
|
+
};
|
|
7
|
+
export const readOrchestration = (state) => {
|
|
8
|
+
const raw = readThreadState(state).orchestration;
|
|
9
|
+
if (!raw || typeof raw !== 'object')
|
|
10
|
+
return { active: false };
|
|
11
|
+
return raw;
|
|
12
|
+
};
|
|
13
|
+
export const isOrchestrationActive = (orch) => orch.active === true;
|
|
14
|
+
export const hasActiveTodos = (state) => readTodosFromState(state).some((t) => t.status === 'pending' || t.status === 'in_progress');
|
|
15
|
+
export const readPendingDelegation = (state) => readOrchestration(state).pendingDelegation;
|
|
16
|
+
async function patchOrchestration(storage, state, patch) {
|
|
17
|
+
if (!state.threadId)
|
|
18
|
+
throw new Error('No active thread');
|
|
19
|
+
const current = readOrchestration(state);
|
|
20
|
+
const next = { ...current, ...patch };
|
|
21
|
+
await storage.patchThreadState({
|
|
22
|
+
channelId: state.channelId,
|
|
23
|
+
threadId: state.threadId,
|
|
24
|
+
state: { orchestration: next },
|
|
25
|
+
});
|
|
26
|
+
state.threadDetails = await storage.getThreadDetails({
|
|
27
|
+
channelId: state.channelId,
|
|
28
|
+
threadId: state.threadId,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export async function activateOrchestration(storage, state) {
|
|
32
|
+
const current = readOrchestration(state);
|
|
33
|
+
if (current.active)
|
|
34
|
+
return;
|
|
35
|
+
await patchOrchestration(storage, state, { active: true, startedAt: Date.now() });
|
|
36
|
+
}
|
|
37
|
+
export async function deactivateOrchestration(storage, state) {
|
|
38
|
+
const current = readOrchestration(state);
|
|
39
|
+
if (!current.active)
|
|
40
|
+
return;
|
|
41
|
+
await patchOrchestration(storage, state, {
|
|
42
|
+
active: false,
|
|
43
|
+
pendingDelegation: undefined,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export async function queueDelegation(storage, state, delegation) {
|
|
47
|
+
await patchOrchestration(storage, state, {
|
|
48
|
+
active: true,
|
|
49
|
+
startedAt: readOrchestration(state).startedAt ?? Date.now(),
|
|
50
|
+
pendingDelegation: delegation,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export async function clearPendingDelegation(storage, state) {
|
|
54
|
+
const current = readOrchestration(state);
|
|
55
|
+
if (!current.pendingDelegation)
|
|
56
|
+
return;
|
|
57
|
+
await patchOrchestration(storage, state, { pendingDelegation: undefined });
|
|
58
|
+
}
|
|
59
|
+
export function buildWorkerReviewPrompt(workerAgentId, output) {
|
|
60
|
+
const body = output && output.trim()
|
|
61
|
+
? output.trim()
|
|
62
|
+
: '(The worker produced no text output.)';
|
|
63
|
+
return [
|
|
64
|
+
'[Orchestrator — worker step completed]',
|
|
65
|
+
'',
|
|
66
|
+
`Worker: @${workerAgentId}`,
|
|
67
|
+
'Output:',
|
|
68
|
+
'---',
|
|
69
|
+
body,
|
|
70
|
+
'---',
|
|
71
|
+
'',
|
|
72
|
+
'Review the shared todo plan, update statuses with `todo_write` if needed,',
|
|
73
|
+
'tell the user if appropriate, then either call `delegate_to_agent` for the next step',
|
|
74
|
+
'or confirm the goal is complete.',
|
|
75
|
+
].join('\n');
|
|
76
|
+
}
|
|
77
|
+
export function buildPlanContinuePrompt() {
|
|
78
|
+
return [
|
|
79
|
+
'[Orchestrator — continue plan execution]',
|
|
80
|
+
'',
|
|
81
|
+
'You have an active todo plan with remaining work.',
|
|
82
|
+
'Review the plan and either call `delegate_to_agent` for the next step',
|
|
83
|
+
'or handle the work yourself. Update todos to reflect progress.',
|
|
84
|
+
].join('\n');
|
|
85
|
+
}
|
|
86
|
+
export function isOrchestratorAgent(agentId) {
|
|
87
|
+
return agentId === ORCHESTRATOR_AGENT_ID;
|
|
88
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel `participants` (from `state.json`) scope which agents may collaborate
|
|
3
|
+
* in that channel. Used for system-prompt hints and dispatch guards.
|
|
4
|
+
*/
|
|
5
|
+
/** Solo DM: exactly one participant and it is the acting agent (no peer bots). */
|
|
6
|
+
export function isDmSoloChannel(participants, actingAgentId) {
|
|
7
|
+
return participants.length === 1 && participants[0] === actingAgentId;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* When `participants` is non-empty, dispatch targets must appear in that list.
|
|
11
|
+
* Solo DM forbids targeting any agent other than yourself.
|
|
12
|
+
*/
|
|
13
|
+
export function isParticipantDispatchAllowed(participants, actingAgentId, targetAgentId) {
|
|
14
|
+
if (participants.length === 0)
|
|
15
|
+
return true;
|
|
16
|
+
if (!participants.includes(targetAgentId))
|
|
17
|
+
return false;
|
|
18
|
+
if (isDmSoloChannel(participants, actingAgentId) && targetAgentId !== actingAgentId) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|