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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { ensureEventId } from '../app/utils.js';
|
|
2
|
+
import { storageService } from '../services/storage.js';
|
|
3
|
+
import { createAgentRuntime } from './runtime-factory.js';
|
|
4
|
+
const stopRequests = [];
|
|
5
|
+
const STOP_TTL_MS = 30 * 60 * 1000;
|
|
6
|
+
const pruneStops = () => {
|
|
7
|
+
const cutoff = Date.now() - STOP_TTL_MS;
|
|
8
|
+
for (let i = stopRequests.length - 1; i >= 0; i -= 1) {
|
|
9
|
+
if (stopRequests[i].requestedAt < cutoff)
|
|
10
|
+
stopRequests.splice(i, 1);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const findStop = (target) => {
|
|
14
|
+
pruneStops();
|
|
15
|
+
return stopRequests.find((r) => r.runId === target.runId &&
|
|
16
|
+
(!r.agentId || r.agentId === target.agentId) &&
|
|
17
|
+
(!r.channelId || r.channelId === target.channelId) &&
|
|
18
|
+
(!r.threadId || r.threadId === target.threadId));
|
|
19
|
+
};
|
|
20
|
+
const target = (ctx, agentId) => ({ ...ctx, agentId });
|
|
21
|
+
const loadState = (t, event) => storageService.getOpenBotState({ ...t, event });
|
|
22
|
+
export async function runHarness(options) {
|
|
23
|
+
const { event } = options;
|
|
24
|
+
ensureEventId(event);
|
|
25
|
+
if (event.type === 'action:agent_run_stop') {
|
|
26
|
+
await handleStop(event, options);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const ctx = {
|
|
30
|
+
runId: options.runId,
|
|
31
|
+
channelId: options.channelId,
|
|
32
|
+
threadId: options.threadId,
|
|
33
|
+
onEvent: options.onEvent,
|
|
34
|
+
};
|
|
35
|
+
const agentId = options.agentId || 'system';
|
|
36
|
+
if (event.type === 'user:input' || event.type === 'agent:invoke') {
|
|
37
|
+
await runStep(ctx, agentId, await prepareInvoke(event, ctx));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await runPassThrough(ctx, agentId, event);
|
|
41
|
+
}
|
|
42
|
+
async function runStep(ctx, agentId, event) {
|
|
43
|
+
const t = target(ctx, agentId);
|
|
44
|
+
const early = findStop(t);
|
|
45
|
+
if (early) {
|
|
46
|
+
const state = await loadState(t, event);
|
|
47
|
+
await ctx.onEvent({ type: 'agent:run:stopped', data: { ...t, reason: early.reason } }, state);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let state;
|
|
51
|
+
try {
|
|
52
|
+
state = await loadState(t, event);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code !== 'AGENT_NOT_FOUND')
|
|
56
|
+
throw error;
|
|
57
|
+
const fallback = await loadState(target(ctx, 'system'), event);
|
|
58
|
+
await ctx.onEvent({
|
|
59
|
+
type: 'agent:output',
|
|
60
|
+
data: {
|
|
61
|
+
content: `⚠️ Agent **${agentId}** does not exist. Please check the agent ID and try again.`,
|
|
62
|
+
},
|
|
63
|
+
meta: { agentId: 'system', threadId: ctx.threadId },
|
|
64
|
+
}, fallback);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await ctx.onEvent({ type: 'agent:run:start', data: { ...t } }, state);
|
|
68
|
+
try {
|
|
69
|
+
await streamEvents(ctx, t, event, state, true);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error(`[run-harness] Agent run failed: ${agentId}`, error);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
const after = await loadState(t, event);
|
|
76
|
+
await ctx.onEvent({ type: 'agent:run:end', data: { ...t } }, after);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function runPassThrough(ctx, agentId, event) {
|
|
80
|
+
const t = target(ctx, agentId);
|
|
81
|
+
let state;
|
|
82
|
+
try {
|
|
83
|
+
state = await loadState(t, event);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code === 'AGENT_NOT_FOUND')
|
|
87
|
+
return;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
await streamEvents(ctx, t, event, state, false);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error(`[run-harness] Event failed: ${event.type} (${agentId})`, error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function streamEvents(ctx, t, event, state, step) {
|
|
98
|
+
const runtime = await createAgentRuntime(state);
|
|
99
|
+
for await (const chunk of runtime.run(event, { state, runId: ctx.runId })) {
|
|
100
|
+
if (step) {
|
|
101
|
+
const stop = findStop(t);
|
|
102
|
+
if (stop) {
|
|
103
|
+
await ctx.onEvent({ type: 'agent:run:stopped', data: { ...t, reason: stop.reason } }, state);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (chunk.id === event.id && chunk.type === event.type)
|
|
108
|
+
continue;
|
|
109
|
+
if (step && chunk.type === 'action:create_thread:result' && chunk.data.success) {
|
|
110
|
+
ctx.threadId = chunk.data.threadId || ctx.threadId;
|
|
111
|
+
}
|
|
112
|
+
chunk.meta = { ...chunk.meta, agentId: t.agentId };
|
|
113
|
+
await ctx.onEvent(chunk, state);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function prepareInvoke(event, ctx) {
|
|
117
|
+
const rawContent = event.data?.content || '';
|
|
118
|
+
const userFacing = {
|
|
119
|
+
type: 'agent:invoke',
|
|
120
|
+
id: event.id,
|
|
121
|
+
data: { content: rawContent, role: 'user' },
|
|
122
|
+
meta: {
|
|
123
|
+
agentId: 'system',
|
|
124
|
+
userId: event.meta?.userId,
|
|
125
|
+
userName: event.meta?.userName,
|
|
126
|
+
userAvatarUrl: event.meta?.userAvatarUrl,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
const initialState = await loadState(target(ctx, 'system'), userFacing);
|
|
130
|
+
await ctx.onEvent(userFacing, initialState);
|
|
131
|
+
return {
|
|
132
|
+
...event,
|
|
133
|
+
type: 'agent:invoke',
|
|
134
|
+
data: { ...(event.data || {}), content: rawContent, role: 'user' },
|
|
135
|
+
meta: { ...(event.meta || {}), threadId: ctx.threadId || event.id },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function handleStop(stopEvent, options) {
|
|
139
|
+
const { runId, channelId, threadId, onEvent } = options;
|
|
140
|
+
stopRequests.push({
|
|
141
|
+
runId: stopEvent.data.runId,
|
|
142
|
+
agentId: stopEvent.data.agentId,
|
|
143
|
+
channelId: stopEvent.data.channelId || channelId,
|
|
144
|
+
threadId: stopEvent.data.threadId || threadId,
|
|
145
|
+
reason: stopEvent.data.reason,
|
|
146
|
+
requestedAt: Date.now(),
|
|
147
|
+
});
|
|
148
|
+
const state = await loadState({ runId, channelId, threadId, onEvent, agentId: options.agentId || 'system' }, stopEvent);
|
|
149
|
+
await onEvent({
|
|
150
|
+
type: 'action:agent_run_stop:result',
|
|
151
|
+
data: { success: true, message: `Stop requested for run ${stopEvent.data.runId}.` },
|
|
152
|
+
meta: stopEvent.meta,
|
|
153
|
+
}, state);
|
|
154
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ensureEventId } from '../app/utils.js';
|
|
2
|
+
import { storageService } from '../services/storage.js';
|
|
3
|
+
import { createAgentRuntime } from './runtime.js';
|
|
4
|
+
const TODO_RESULT_MAX_CHARS = 12000;
|
|
5
|
+
const readTodos = (state) => {
|
|
6
|
+
const raw = state.threadDetails?.state?.todos;
|
|
7
|
+
return Array.isArray(raw) ? raw : [];
|
|
8
|
+
};
|
|
9
|
+
function truncateTodoResult(text, maxChars = TODO_RESULT_MAX_CHARS) {
|
|
10
|
+
const trimmed = text.trim();
|
|
11
|
+
if (!trimmed)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (trimmed.length <= maxChars)
|
|
14
|
+
return trimmed;
|
|
15
|
+
return `${trimmed.slice(0, maxChars)}\n…[truncated]`;
|
|
16
|
+
}
|
|
17
|
+
function resolveTodoIdForWorker(todos, workerId, delegationTodoId) {
|
|
18
|
+
if (delegationTodoId && todos.some((t) => t.id === delegationTodoId)) {
|
|
19
|
+
return delegationTodoId;
|
|
20
|
+
}
|
|
21
|
+
const inProgress = todos.find((t) => t.status === 'in_progress' && t.assignee === workerId);
|
|
22
|
+
if (inProgress)
|
|
23
|
+
return inProgress.id;
|
|
24
|
+
const assigned = todos.find((t) => (t.status === 'pending' || t.status === 'in_progress') && t.assignee === workerId);
|
|
25
|
+
return assigned?.id;
|
|
26
|
+
}
|
|
27
|
+
async function recordWorkerTodoResult(state, workerId, output, delegationTodoId) {
|
|
28
|
+
if (!state.threadId)
|
|
29
|
+
return;
|
|
30
|
+
const result = truncateTodoResult(output ?? '');
|
|
31
|
+
if (!result)
|
|
32
|
+
return;
|
|
33
|
+
const todos = readTodos(state);
|
|
34
|
+
if (todos.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const todoId = resolveTodoIdForWorker(todos, workerId, delegationTodoId);
|
|
37
|
+
if (!todoId)
|
|
38
|
+
return;
|
|
39
|
+
const prior = todos.find((t) => t.id === todoId);
|
|
40
|
+
if (prior?.result === result)
|
|
41
|
+
return;
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const next = todos.map((t) => (t.id === todoId ? { ...t, result, updatedAt: now } : t));
|
|
44
|
+
await storageService.patchThreadState({
|
|
45
|
+
channelId: state.channelId,
|
|
46
|
+
threadId: state.threadId,
|
|
47
|
+
state: { todos: next },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function makeInternalInvoke(content, threadId) {
|
|
51
|
+
return ensureEventId({
|
|
52
|
+
type: 'agent:invoke',
|
|
53
|
+
data: { role: 'user', content },
|
|
54
|
+
meta: { threadId, internal: true },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run one agent turn. Yields runtime events for persistence/streaming;
|
|
59
|
+
* returns the last non-empty `agent:output` text when used as a worker.
|
|
60
|
+
*/
|
|
61
|
+
export async function* runTurn(options) {
|
|
62
|
+
const { runId, channelId, threadId, agentId, event, delegationTodoId } = options;
|
|
63
|
+
const target = { runId, agentId, channelId, threadId };
|
|
64
|
+
let state;
|
|
65
|
+
try {
|
|
66
|
+
state = await storageService.getOpenBotState({ ...target, event });
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code === 'AGENT_NOT_FOUND') {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
yield { type: 'agent:run:start', data: { ...target } };
|
|
75
|
+
let lastAgentOutput;
|
|
76
|
+
try {
|
|
77
|
+
const runtime = await createAgentRuntime(state);
|
|
78
|
+
for await (const chunk of runtime.run(event, { state, runId })) {
|
|
79
|
+
if (chunk.id === event.id && chunk.type === event.type)
|
|
80
|
+
continue;
|
|
81
|
+
if (chunk.type === 'agent:output' &&
|
|
82
|
+
chunk.meta?.agentId === agentId) {
|
|
83
|
+
const content = chunk.data?.content;
|
|
84
|
+
if (typeof content === 'string' && content.trim()) {
|
|
85
|
+
lastAgentOutput = content.trim();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
chunk.meta = { ...chunk.meta, agentId };
|
|
89
|
+
yield chunk;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
const stateAfterRun = await storageService.getOpenBotState({ ...target, event });
|
|
94
|
+
yield { type: 'agent:run:end', data: { ...target } };
|
|
95
|
+
await recordWorkerTodoResult(stateAfterRun, agentId, lastAgentOutput, delegationTodoId);
|
|
96
|
+
}
|
|
97
|
+
return lastAgentOutput;
|
|
98
|
+
}
|
|
@@ -2,39 +2,6 @@ import { melony } from 'melony';
|
|
|
2
2
|
import { resolvePlugin } from '../registry/plugins.js';
|
|
3
3
|
import { storageService } from '../services/storage.js';
|
|
4
4
|
import { busServicesPlugin } from '../bus/services.js';
|
|
5
|
-
/**
|
|
6
|
-
* Enhances the agent's instructions with a list of other available agents the
|
|
7
|
-
* orchestrator can hand off to. Agents that include the `delegation` plugin
|
|
8
|
-
* will surface peers; agents without it can ignore this.
|
|
9
|
-
*/
|
|
10
|
-
export async function enhanceInstructions(state) {
|
|
11
|
-
const { agentId, agentDetails } = state;
|
|
12
|
-
if (!agentDetails)
|
|
13
|
-
return;
|
|
14
|
-
try {
|
|
15
|
-
const agents = await storageService.getAgents();
|
|
16
|
-
const otherAgents = agents.filter((a) => a.id !== agentId);
|
|
17
|
-
if (otherAgents.length === 0)
|
|
18
|
-
return;
|
|
19
|
-
const agentsList = otherAgents
|
|
20
|
-
.map((a) => `- **${a.id}**${a.description ? `: ${a.description}` : ''}`)
|
|
21
|
-
.join('\n');
|
|
22
|
-
const header = '### Available Agents:';
|
|
23
|
-
if (!agentDetails.instructions.includes(header)) {
|
|
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}` : ''}`;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
console.warn('[agent] Failed to enhance instructions', error);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
5
|
const composeMelonyPlugin = (...plugins) => {
|
|
39
6
|
return (builder) => {
|
|
40
7
|
for (const plugin of plugins) {
|
|
@@ -57,7 +24,6 @@ const composeMelonyPlugin = (...plugins) => {
|
|
|
57
24
|
* Tool name collisions across plugins log a warning; the first plugin wins.
|
|
58
25
|
*/
|
|
59
26
|
export async function createAgentRuntime(state) {
|
|
60
|
-
await enhanceInstructions(state);
|
|
61
27
|
const runtime = melony({
|
|
62
28
|
initialState: state,
|
|
63
29
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { melony } from 'melony';
|
|
2
|
+
import { resolvePlugin } from '../registry/plugins.js';
|
|
3
|
+
import { storageService } from '../services/storage.js';
|
|
4
|
+
import { busServicesPlugin } from '../bus/services.js';
|
|
5
|
+
const composeMelonyPlugin = (...plugins) => {
|
|
6
|
+
return (builder) => {
|
|
7
|
+
for (const plugin of plugins) {
|
|
8
|
+
plugin(builder);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
/** Build the Melony runtime for a single agent turn. */
|
|
13
|
+
export async function createAgentRuntime(state) {
|
|
14
|
+
const runtime = melony({
|
|
15
|
+
initialState: state,
|
|
16
|
+
});
|
|
17
|
+
runtime.use(busServicesPlugin({ storage: storageService }));
|
|
18
|
+
const refs = state.agentDetails?.pluginRefs || [];
|
|
19
|
+
if (refs.length === 0) {
|
|
20
|
+
console.warn(`[harness] Agent "${state.agentId}" has no plugins; only bus services will be active.`);
|
|
21
|
+
return runtime.build();
|
|
22
|
+
}
|
|
23
|
+
const resolved = [];
|
|
24
|
+
for (const ref of refs) {
|
|
25
|
+
const plugin = await resolvePlugin(ref.id);
|
|
26
|
+
if (!plugin) {
|
|
27
|
+
console.warn(`[harness] Plugin "${ref.id}" for agent "${state.agentId}" could not be resolved.`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
resolved.push({ ref, plugin });
|
|
31
|
+
}
|
|
32
|
+
const tools = {};
|
|
33
|
+
for (const { plugin } of resolved) {
|
|
34
|
+
if (!plugin.toolDefinitions)
|
|
35
|
+
continue;
|
|
36
|
+
for (const [name, def] of Object.entries(plugin.toolDefinitions)) {
|
|
37
|
+
if (tools[name]) {
|
|
38
|
+
console.warn(`[harness] Tool name collision for "${name}" while loading plugin "${plugin.id}"; keeping first registration.`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
tools[name] = def;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const pluginPlugins = [];
|
|
45
|
+
for (const { ref, plugin } of resolved) {
|
|
46
|
+
const context = {
|
|
47
|
+
agentId: state.agentId,
|
|
48
|
+
agentDetails: state.agentDetails,
|
|
49
|
+
config: ref.config || {},
|
|
50
|
+
storage: storageService,
|
|
51
|
+
tools,
|
|
52
|
+
};
|
|
53
|
+
pluginPlugins.push(plugin.factory(context));
|
|
54
|
+
}
|
|
55
|
+
runtime.use(composeMelonyPlugin(...pluginPlugins));
|
|
56
|
+
return runtime.build();
|
|
57
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ORCHESTRATOR_AGENT_ID } from './context.js';
|
|
2
|
+
import { hasOpenTodos, readTodosFromState } from './todos.js';
|
|
3
|
+
export const ORCHESTRATOR_REVIEW_PREFIX = '[Orchestrator review]';
|
|
4
|
+
export const ORCHESTRATOR_KICKOFF_PREFIX = '[Orchestrator kickoff]';
|
|
5
|
+
/** After a specialist finishes — decide what's next. */
|
|
6
|
+
export const ORCHESTRATOR_REVIEW_PROMPT = [
|
|
7
|
+
ORCHESTRATOR_REVIEW_PREFIX,
|
|
8
|
+
'A specialist agent just finished a step in this thread.',
|
|
9
|
+
'Review the shared todo list in your context.',
|
|
10
|
+
'Use `todo_write` to update statuses or capture results.',
|
|
11
|
+
'If more work remains, call `delegate` with a clear briefing for the right participant.',
|
|
12
|
+
'If the plan is complete, summarize outcomes for the user.',
|
|
13
|
+
].join(' ');
|
|
14
|
+
/** After the orchestrator wrote a plan but did not delegate — start execution. */
|
|
15
|
+
export const ORCHESTRATOR_KICKOFF_PROMPT = [
|
|
16
|
+
ORCHESTRATOR_KICKOFF_PREFIX,
|
|
17
|
+
'The thread has open todos but no specialist is running yet.',
|
|
18
|
+
'Pick the next actionable item, mark it `in_progress` with `todo_write`,',
|
|
19
|
+
'and call `delegate` to the assignee (or best participant) with a concrete briefing.',
|
|
20
|
+
'Use reasonable defaults when the user did not specify a preference — do not stop at planning.',
|
|
21
|
+
].join(' ');
|
|
22
|
+
export function isInternalOrchestratorInvoke(content) {
|
|
23
|
+
const text = content?.trim() ?? '';
|
|
24
|
+
return (text.startsWith(ORCHESTRATOR_REVIEW_PREFIX) ||
|
|
25
|
+
text.startsWith(ORCHESTRATOR_KICKOFF_PREFIX));
|
|
26
|
+
}
|
|
27
|
+
export async function planOrchestratorReview(options) {
|
|
28
|
+
const { storage, channelId, threadId, endedAgentId, invokeContent, hasQueuedFollowUps, orchestratorKickoffUsed, } = options;
|
|
29
|
+
if (!threadId || hasQueuedFollowUps)
|
|
30
|
+
return { review: false };
|
|
31
|
+
if (isInternalOrchestratorInvoke(invokeContent))
|
|
32
|
+
return { review: false };
|
|
33
|
+
const details = await storage.getThreadDetails({ channelId, threadId });
|
|
34
|
+
const todos = readTodosFromState(details?.state);
|
|
35
|
+
if (!hasOpenTodos(todos))
|
|
36
|
+
return { review: false };
|
|
37
|
+
if (endedAgentId === ORCHESTRATOR_AGENT_ID) {
|
|
38
|
+
if (orchestratorKickoffUsed)
|
|
39
|
+
return { review: false };
|
|
40
|
+
return {
|
|
41
|
+
review: true,
|
|
42
|
+
reason: 'plan_not_started',
|
|
43
|
+
prompt: ORCHESTRATOR_KICKOFF_PROMPT,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
review: true,
|
|
48
|
+
reason: 'specialist_finished',
|
|
49
|
+
prompt: ORCHESTRATOR_REVIEW_PROMPT,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ensureEventId } from '../app/utils.js';
|
|
2
|
+
import { storageService } from '../services/storage.js';
|
|
3
|
+
import { createAgentRuntime } from './runtime-factory.js';
|
|
4
|
+
export function makeInternalInvoke(content, threadId) {
|
|
5
|
+
return ensureEventId({
|
|
6
|
+
type: 'agent:invoke',
|
|
7
|
+
data: { role: 'user', content },
|
|
8
|
+
meta: { threadId, internal: true },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Run one agent turn. Yields all runtime events for persistence/streaming;
|
|
13
|
+
* returns the last non-empty `agent:output` text from the target agent.
|
|
14
|
+
*/
|
|
15
|
+
export async function* runTurn(options) {
|
|
16
|
+
const { runId, channelId, threadId, agentId, event, lifecycle = true, stream = true, shouldStop, onThreadId, } = options;
|
|
17
|
+
const target = { runId, agentId, channelId, threadId };
|
|
18
|
+
let state;
|
|
19
|
+
try {
|
|
20
|
+
state = await storageService.getOpenBotState({ ...target, event });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === 'AGENT_NOT_FOUND') {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
if (lifecycle && stream) {
|
|
29
|
+
yield {
|
|
30
|
+
type: 'agent:run:start',
|
|
31
|
+
data: { ...target },
|
|
32
|
+
meta: { agentId, threadId },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
let lastAgentOutput;
|
|
36
|
+
try {
|
|
37
|
+
const runtime = await createAgentRuntime(state);
|
|
38
|
+
for await (const chunk of runtime.run(event, { state, runId })) {
|
|
39
|
+
const stop = shouldStop?.();
|
|
40
|
+
if (stop) {
|
|
41
|
+
yield {
|
|
42
|
+
type: 'agent:run:stopped',
|
|
43
|
+
data: { ...target, reason: stop.reason },
|
|
44
|
+
};
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
if (chunk.id === event.id && chunk.type === event.type)
|
|
48
|
+
continue;
|
|
49
|
+
if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
|
|
50
|
+
const newThreadId = chunk.data.threadId;
|
|
51
|
+
if (newThreadId)
|
|
52
|
+
onThreadId?.(newThreadId);
|
|
53
|
+
}
|
|
54
|
+
if (chunk.type === 'agent:output') {
|
|
55
|
+
const content = chunk.data?.content;
|
|
56
|
+
if (typeof content === 'string' && content.trim()) {
|
|
57
|
+
lastAgentOutput = content.trim();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (stream) {
|
|
61
|
+
chunk.meta = { ...chunk.meta, agentId: chunk.meta?.agentId ?? agentId };
|
|
62
|
+
yield chunk;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error(`[turn] Agent run failed: ${agentId}`, error);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
if (lifecycle && stream) {
|
|
71
|
+
yield {
|
|
72
|
+
type: 'agent:run:end',
|
|
73
|
+
data: { ...target },
|
|
74
|
+
meta: { agentId, threadId },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return lastAgentOutput;
|
|
79
|
+
}
|