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
|
@@ -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
|
+
}
|
|
@@ -1,159 +1,115 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* `approval` — gates protected tool calls behind a UI confirmation widget.
|
|
4
|
+
*
|
|
5
|
+
* This is a simplified version that intercepts specified actions (default: shell_exec)
|
|
6
|
+
* and requires user approval before they are allowed to proceed.
|
|
7
|
+
*/
|
|
8
|
+
// In-memory tracking for pending approval IDs with TTL (shared across plugin instances)
|
|
9
|
+
const pendingApprovals = new Map();
|
|
10
|
+
const TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
11
|
+
export const approvalPlugin = {
|
|
12
|
+
id: 'approval',
|
|
13
|
+
name: 'Approval',
|
|
14
|
+
description: 'Gate protected tool calls behind a UI confirmation widget.',
|
|
15
|
+
factory: ({ config }) => (builder) => {
|
|
16
|
+
// Actions that require approval. Defaults to shell_exec.
|
|
17
|
+
const actionsToApprove = config.actions || ['action:shell_exec'];
|
|
18
|
+
for (const action of actionsToApprove) {
|
|
19
|
+
builder.intercept(action, (event, context) => {
|
|
20
|
+
// If already approved in this flow, let it pass to the actual handler
|
|
21
|
+
if (event.meta?.approvalStatus === 'approved')
|
|
22
|
+
return event;
|
|
23
|
+
// Otherwise, intercept and ask for approval via a UI widget
|
|
24
|
+
const displayData = action === 'action:shell_exec'
|
|
25
|
+
? `\`\`\`bash\n${event.data.command}\n\`\`\``
|
|
26
|
+
: `\`\`\`json\n${JSON.stringify(event.data, null, 2)}\n\`\`\``;
|
|
27
|
+
const widgetId = randomUUID();
|
|
28
|
+
pendingApprovals.set(widgetId, Date.now());
|
|
29
|
+
context.suspend({
|
|
30
|
+
type: 'client:ui:widget',
|
|
31
|
+
data: {
|
|
32
|
+
widgetId,
|
|
33
|
+
kind: 'message',
|
|
34
|
+
title: `The agent wants to perform \`${action}\``,
|
|
35
|
+
body: displayData,
|
|
36
|
+
metadata: {
|
|
37
|
+
type: 'approval:request',
|
|
38
|
+
originalEvent: event,
|
|
39
|
+
},
|
|
40
|
+
actions: [
|
|
41
|
+
{ id: 'approve', label: 'Approve', variant: 'primary' },
|
|
42
|
+
{ id: 'deny', label: 'Deny', variant: 'danger' },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
meta: { agentId: context.state.agentId, threadId: context.state.threadId },
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Handle the user's response from the UI widget
|
|
50
|
+
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
51
|
+
const { widgetId, actionId } = event.data;
|
|
52
|
+
const metadata = event.data?.metadata;
|
|
53
|
+
if (metadata?.type !== 'approval:request')
|
|
44
54
|
return;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
action: rule.action,
|
|
62
|
-
executeEvent,
|
|
63
|
-
denyEvent,
|
|
64
|
-
denyData,
|
|
65
|
-
payload: eventData,
|
|
66
|
-
meta: eventMeta,
|
|
67
|
-
message: rule.message || `Approval required for ${rule.action}.`,
|
|
68
|
-
createdAt: new Date().toISOString(),
|
|
69
|
-
status: 'pending',
|
|
70
|
-
};
|
|
71
|
-
await persistApprovals(context.state, pendingApprovals);
|
|
55
|
+
// Verify the widget is still pending and hasn't expired
|
|
56
|
+
if (!widgetId || !pendingApprovals.has(widgetId)) {
|
|
57
|
+
console.warn(`[approval] Received response for unknown or already handled widget: ${widgetId}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const timestamp = pendingApprovals.get(widgetId);
|
|
61
|
+
if (Date.now() - timestamp > TTL_MS) {
|
|
62
|
+
pendingApprovals.delete(widgetId);
|
|
63
|
+
console.warn(`[approval] Received response for expired widget: ${widgetId}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Mark as handled
|
|
67
|
+
pendingApprovals.delete(widgetId);
|
|
68
|
+
const originalEvent = metadata.originalEvent;
|
|
69
|
+
const approved = actionId === 'approve';
|
|
70
|
+
// Yield a "responded" widget update to the UI
|
|
72
71
|
yield {
|
|
73
72
|
type: 'client:ui:widget',
|
|
74
73
|
data: {
|
|
75
|
-
kind: 'choice',
|
|
76
74
|
widgetId,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
],
|
|
75
|
+
kind: 'message',
|
|
76
|
+
title: `Action ${approved ? 'Approved' : 'Denied'}`,
|
|
77
|
+
body: `The request for \`${originalEvent.type}\` was ${approved ? 'approved' : 'denied'}.`,
|
|
78
|
+
state: approved ? 'submitted' : 'cancelled',
|
|
79
|
+
display: 'collapsed',
|
|
80
|
+
disabled: true,
|
|
81
|
+
actions: [], // Clear actions to disable buttons in UI
|
|
84
82
|
},
|
|
85
|
-
meta: {
|
|
86
|
-
};
|
|
87
|
-
yield {
|
|
88
|
-
type: 'agent:output',
|
|
89
|
-
data: { content: `Waiting for approval before running \`${rule.action}\`.` },
|
|
90
|
-
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
83
|
+
meta: { agentId: context.state.agentId, threadId: context.state.threadId },
|
|
91
84
|
};
|
|
92
|
-
|
|
85
|
+
if (approved) {
|
|
86
|
+
// Re-emit the original event with approved status so the actual handler can run
|
|
87
|
+
yield {
|
|
88
|
+
...originalEvent,
|
|
89
|
+
meta: {
|
|
90
|
+
...(originalEvent.meta || {}),
|
|
91
|
+
approvalStatus: 'approved',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Emit a failure result event for the denied action
|
|
97
|
+
// yield {
|
|
98
|
+
// type: `${originalEvent.type}:result` as OpenBotEvent['type'],
|
|
99
|
+
// data: {
|
|
100
|
+
// success: false,
|
|
101
|
+
// error: 'Action denied by user.',
|
|
102
|
+
// stderr: 'Action denied by user.',
|
|
103
|
+
// },
|
|
104
|
+
// meta: originalEvent.meta,
|
|
105
|
+
// } as OpenBotEvent;
|
|
106
|
+
yield {
|
|
107
|
+
type: 'agent:output',
|
|
108
|
+
data: { content: `Action \`${originalEvent.type}\` was denied.` },
|
|
109
|
+
meta: { agentId: context.state.agentId },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
93
112
|
});
|
|
94
|
-
}
|
|
95
|
-
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
96
|
-
const metadata = asRecord(event.data?.metadata);
|
|
97
|
-
if (metadata.type !== 'approval:request')
|
|
98
|
-
return;
|
|
99
|
-
const approvalId = String(metadata.approvalId || '');
|
|
100
|
-
if (!approvalId)
|
|
101
|
-
return;
|
|
102
|
-
const approvals = getApprovalsFromState(context.state);
|
|
103
|
-
const approval = approvals[approvalId];
|
|
104
|
-
if (!approval || approval.status !== 'pending') {
|
|
105
|
-
yield {
|
|
106
|
-
type: 'agent:output',
|
|
107
|
-
data: { content: 'Approval request not found or already resolved.' },
|
|
108
|
-
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
109
|
-
};
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const approved = event.data.actionId === 'approve';
|
|
113
|
-
approvals[approvalId] = {
|
|
114
|
-
...approval,
|
|
115
|
-
status: approved ? 'approved' : 'denied',
|
|
116
|
-
};
|
|
117
|
-
await persistApprovals(context.state, approvals);
|
|
118
|
-
if (approved) {
|
|
119
|
-
yield {
|
|
120
|
-
type: approval.executeEvent,
|
|
121
|
-
data: approval.payload,
|
|
122
|
-
meta: {
|
|
123
|
-
...(approval.meta || {}),
|
|
124
|
-
approvalId,
|
|
125
|
-
approvalStatus: 'approved',
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
yield {
|
|
131
|
-
type: approval.denyEvent,
|
|
132
|
-
data: {
|
|
133
|
-
success: false,
|
|
134
|
-
approved: false,
|
|
135
|
-
error: 'Action denied by user approval.',
|
|
136
|
-
...approval.denyData,
|
|
137
|
-
},
|
|
138
|
-
meta: { ...(approval.meta || {}), approvalId },
|
|
139
|
-
};
|
|
140
|
-
yield {
|
|
141
|
-
type: 'agent:output',
|
|
142
|
-
data: { content: 'Action denied by user approval.' },
|
|
143
|
-
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
144
|
-
};
|
|
145
|
-
});
|
|
146
|
-
};
|
|
147
|
-
const readRules = (config) => {
|
|
148
|
-
const raw = config.rules;
|
|
149
|
-
if (!Array.isArray(raw))
|
|
150
|
-
return DEFAULT_APPROVAL_RULES;
|
|
151
|
-
return raw.filter((entry) => !!entry && typeof entry === 'object' && typeof entry.action === 'string');
|
|
152
|
-
};
|
|
153
|
-
export const approvalPlugin = {
|
|
154
|
-
id: 'approval',
|
|
155
|
-
name: 'Approval',
|
|
156
|
-
description: 'Gate protected tool calls (e.g. shell_exec) behind a UI confirmation prompt.',
|
|
157
|
-
factory: ({ config }) => buildApprovalPlugin(readRules(config)),
|
|
113
|
+
},
|
|
158
114
|
};
|
|
159
115
|
export default approvalPlugin;
|
|
@@ -1,40 +1,127 @@
|
|
|
1
|
-
import z from 'zod';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { generateId } from 'melony';
|
|
3
|
+
import { runAgent } from '../../harness/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* `delegation` — allows agents to delegate tasks to other agents.
|
|
6
|
+
*
|
|
7
|
+
* Only the 'system' agent is allowed to delegate by default.
|
|
8
|
+
* It uses runAgent to execute the delegated agent in its own isolated runtime,
|
|
9
|
+
* bridging events back to the caller's stream.
|
|
10
|
+
*/
|
|
11
|
+
const delegationToolDefinitions = {
|
|
12
|
+
delegate_task: {
|
|
13
|
+
description: 'Delegate a specific task or question to another specialized agent.',
|
|
5
14
|
inputSchema: z.object({
|
|
6
|
-
agentId: z.string().describe('The ID of the
|
|
7
|
-
|
|
15
|
+
agentId: z.string().describe('The ID of the agent to delegate to (e.g., "researcher", "coder").'),
|
|
16
|
+
prompt: z.string().describe('The instructions or question for the delegated agent.'),
|
|
8
17
|
}),
|
|
9
18
|
},
|
|
10
19
|
};
|
|
11
|
-
const handoffPluginRuntime = () => (builder) => {
|
|
12
|
-
builder.on('action:handoff', async function* (event, context) {
|
|
13
|
-
const { agentId, content } = event.data;
|
|
14
|
-
yield {
|
|
15
|
-
type: 'agent:output',
|
|
16
|
-
data: { content: `Handing off to **${agentId}**: ${content}` },
|
|
17
|
-
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
18
|
-
};
|
|
19
|
-
yield {
|
|
20
|
-
type: 'handoff:request',
|
|
21
|
-
data: { agentId, content },
|
|
22
|
-
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
23
|
-
};
|
|
24
|
-
if (event.meta?.toolCallId) {
|
|
25
|
-
yield {
|
|
26
|
-
type: 'action:handoff:result',
|
|
27
|
-
data: { success: true, agentId, accepted: true },
|
|
28
|
-
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
};
|
|
33
20
|
export const delegationPlugin = {
|
|
34
21
|
id: 'delegation',
|
|
35
|
-
name: '
|
|
36
|
-
description: '
|
|
37
|
-
toolDefinitions:
|
|
38
|
-
factory: () =>
|
|
22
|
+
name: 'Delegation',
|
|
23
|
+
description: 'Allows agents to call upon other agents to solve sub-tasks.',
|
|
24
|
+
toolDefinitions: delegationToolDefinitions,
|
|
25
|
+
factory: () => (builder) => {
|
|
26
|
+
// Handle the tool execution
|
|
27
|
+
builder.on('action:delegate_task', async function* (event, context) {
|
|
28
|
+
const delegateEvent = event;
|
|
29
|
+
// POLICY: Only the 'system' agent can delegate
|
|
30
|
+
if (context.state.agentId !== 'system') {
|
|
31
|
+
yield {
|
|
32
|
+
type: 'action:delegate_task:result',
|
|
33
|
+
data: {
|
|
34
|
+
success: false,
|
|
35
|
+
error: 'Only the system agent can delegate.'
|
|
36
|
+
},
|
|
37
|
+
meta: delegateEvent.meta,
|
|
38
|
+
};
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const { agentId, prompt } = delegateEvent.data;
|
|
42
|
+
const toolCallId = delegateEvent.meta?.toolCallId;
|
|
43
|
+
if (!toolCallId)
|
|
44
|
+
return;
|
|
45
|
+
const runId = `dg_${generateId()}`;
|
|
46
|
+
let lastAgentOutput = '';
|
|
47
|
+
// Queue to bridge the async onEvent callback to this generator
|
|
48
|
+
const eventQueue = [];
|
|
49
|
+
let resolveNext = null;
|
|
50
|
+
let isFinished = false;
|
|
51
|
+
// Start the delegated agent in its own runtime.
|
|
52
|
+
// We don't await this immediately so we can yield events as they arrive.
|
|
53
|
+
const runPromise = runAgent({
|
|
54
|
+
runId,
|
|
55
|
+
agentId,
|
|
56
|
+
event: {
|
|
57
|
+
type: 'agent:invoke',
|
|
58
|
+
data: {
|
|
59
|
+
role: 'user',
|
|
60
|
+
content: prompt,
|
|
61
|
+
agentId: agentId,
|
|
62
|
+
},
|
|
63
|
+
meta: {
|
|
64
|
+
threadId: context.state.threadId,
|
|
65
|
+
parentAgentId: context.state.agentId,
|
|
66
|
+
parentToolCallId: toolCallId,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
channelId: context.state.channelId,
|
|
70
|
+
threadId: context.state.threadId,
|
|
71
|
+
onEvent: async (outEvent) => {
|
|
72
|
+
// Enrich events with parent metadata so the UI can track the hierarchy
|
|
73
|
+
const enrichedEvent = {
|
|
74
|
+
...outEvent,
|
|
75
|
+
meta: {
|
|
76
|
+
...outEvent.meta,
|
|
77
|
+
parentAgentId: context.state.agentId,
|
|
78
|
+
parentToolCallId: toolCallId,
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
eventQueue.push(enrichedEvent);
|
|
82
|
+
if (outEvent.type === 'agent:output') {
|
|
83
|
+
lastAgentOutput = outEvent.data.content;
|
|
84
|
+
}
|
|
85
|
+
// Wake up the generator loop if it's waiting
|
|
86
|
+
if (resolveNext) {
|
|
87
|
+
resolveNext();
|
|
88
|
+
resolveNext = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}).catch(error => {
|
|
92
|
+
console.error(`[delegation] Error in delegated run ${runId}:`, error);
|
|
93
|
+
}).finally(() => {
|
|
94
|
+
isFinished = true;
|
|
95
|
+
if (resolveNext) {
|
|
96
|
+
resolveNext();
|
|
97
|
+
resolveNext = null;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// Yield events from the delegated agent as they arrive
|
|
101
|
+
while (!isFinished || eventQueue.length > 0) {
|
|
102
|
+
if (eventQueue.length === 0) {
|
|
103
|
+
await new Promise(r => { resolveNext = r; });
|
|
104
|
+
}
|
|
105
|
+
while (eventQueue.length > 0) {
|
|
106
|
+
yield eventQueue.shift();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Ensure the run is fully complete (though isFinished already implies this)
|
|
110
|
+
await runPromise;
|
|
111
|
+
// Yield the result back to our own LLM runtime.
|
|
112
|
+
yield {
|
|
113
|
+
type: 'action:delegate_task:result',
|
|
114
|
+
data: {
|
|
115
|
+
success: true,
|
|
116
|
+
output: lastAgentOutput,
|
|
117
|
+
},
|
|
118
|
+
meta: {
|
|
119
|
+
...delegateEvent.meta,
|
|
120
|
+
agentId: context.state.agentId,
|
|
121
|
+
toolCallId: toolCallId,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
},
|
|
39
126
|
};
|
|
40
127
|
export default delegationPlugin;
|