openbot 0.2.9 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/topic-agent.js +17 -3
- package/dist/cli.js +1 -1
- package/dist/core/delegation.js +128 -22
- package/dist/core/manager.js +31 -8
- package/dist/core/plugins.js +5 -8
- package/dist/core/router.js +164 -13
- package/dist/open-bot.js +2 -2
- package/dist/plugins/approval/index.js +75 -162
- package/dist/plugins/file-system/index.js +2 -2
- package/dist/plugins/llm/context-budget.js +139 -0
- package/dist/plugins/llm/index.js +85 -31
- package/dist/plugins/memory/index.js +2 -2
- package/dist/plugins/shell/index.js +2 -2
- package/dist/plugins/skills/index.js +4 -4
- package/dist/registry/plugin-loader.js +81 -9
- package/dist/registry/plugin-registry.js +5 -5
- package/dist/server.js +5 -32
- package/dist/session.js +23 -5
- package/dist/ui/block.js +12 -0
- package/dist/ui/widgets/action-list.js +2 -9
- package/dist/ui/widgets/approval-card.js +9 -35
- package/dist/ui/widgets/code-snippet.js +2 -2
- package/dist/ui/widgets/data-block.js +2 -5
- package/dist/ui/widgets/data-table.js +2 -8
- package/dist/ui/widgets/empty-state.js +2 -7
- package/dist/ui/widgets/index.js +4 -0
- package/dist/ui/widgets/inquiry.js +7 -0
- package/dist/ui/widgets/key-value.js +2 -12
- package/dist/ui/widgets/progress-step.js +2 -5
- package/dist/ui/widgets/resource-card.js +2 -10
- package/dist/ui/widgets/status.js +2 -5
- package/dist/ui/widgets/todo-list.js +2 -0
- package/package.json +1 -3
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import { generateText } from "ai";
|
|
2
|
+
function getTitleSourceMessages(state, event) {
|
|
3
|
+
if (Array.isArray(state.messages) && state.messages.length >= 2) {
|
|
4
|
+
return state.messages;
|
|
5
|
+
}
|
|
6
|
+
const agentName = event.meta?.agentName;
|
|
7
|
+
if (!agentName)
|
|
8
|
+
return [];
|
|
9
|
+
const agentMessages = state.agentStates?.[agentName]?.messages;
|
|
10
|
+
if (Array.isArray(agentMessages) && agentMessages.length >= 2) {
|
|
11
|
+
return agentMessages;
|
|
12
|
+
}
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
2
15
|
export const topicAgent = (options) => (builder) => {
|
|
3
16
|
builder.on("agent:output", async function* (event, { state }) {
|
|
4
17
|
// Only title if it doesn't have one and there's history
|
|
5
|
-
if (state.title
|
|
18
|
+
if (state.title) {
|
|
6
19
|
return;
|
|
7
20
|
}
|
|
21
|
+
const messagesForTitle = getTitleSourceMessages(state, event);
|
|
8
22
|
// Don't title if there are too few messages
|
|
9
|
-
if (
|
|
23
|
+
if (messagesForTitle.length < 2) {
|
|
10
24
|
return;
|
|
11
25
|
}
|
|
12
26
|
try {
|
|
13
27
|
const { text } = await generateText({
|
|
14
28
|
model: options.model,
|
|
15
29
|
system: "You are a Topic Agent. Create a very concise (3-5 words) title for the conversation based on the user's intent. Do not use quotes or special characters.",
|
|
16
|
-
prompt: `Analyze these messages and provide a title: ${JSON.stringify(
|
|
30
|
+
prompt: `Analyze these messages and provide a title: ${JSON.stringify(messagesForTitle.slice(0, 6))}`,
|
|
17
31
|
});
|
|
18
32
|
const newTitle = text.replace(/["']/g, "").trim();
|
|
19
33
|
if (newTitle) {
|
package/dist/cli.js
CHANGED
|
@@ -26,7 +26,7 @@ checkNodeVersion();
|
|
|
26
26
|
program
|
|
27
27
|
.name("openbot")
|
|
28
28
|
.description("OpenBot CLI - Secure and easy configuration")
|
|
29
|
-
.version("0.2.
|
|
29
|
+
.version("0.2.11");
|
|
30
30
|
async function installPlugin(source, id, quiet = false) {
|
|
31
31
|
try {
|
|
32
32
|
const parsed = parsePluginInstallSource(source);
|
package/dist/core/delegation.js
CHANGED
|
@@ -1,15 +1,87 @@
|
|
|
1
1
|
import { generateId } from "melony";
|
|
2
|
+
import { uiEvent } from "../ui/block.js";
|
|
3
|
+
import { widgets } from "../ui/widgets/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Simple helper to set a value in an object by a dot-separated path.
|
|
6
|
+
*/
|
|
7
|
+
function setByPath(obj, path, value) {
|
|
8
|
+
const parts = path.split(".");
|
|
9
|
+
let current = obj;
|
|
10
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
11
|
+
const part = parts[i];
|
|
12
|
+
if (!(part in current)) {
|
|
13
|
+
current[part] = {};
|
|
14
|
+
}
|
|
15
|
+
current = current[part];
|
|
16
|
+
}
|
|
17
|
+
current[parts[parts.length - 1]] = value;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Helper to emit a UI snapshot for a widget if applicable.
|
|
21
|
+
*/
|
|
22
|
+
function* maybeEmitWidget(key, value) {
|
|
23
|
+
if (!value || typeof value !== "object")
|
|
24
|
+
return;
|
|
25
|
+
let widgetName = value.widget;
|
|
26
|
+
let data = value;
|
|
27
|
+
// 1. Check for nested .todos array (common pattern for planner/task agents)
|
|
28
|
+
if (!widgetName && Array.isArray(value.todos)) {
|
|
29
|
+
widgetName = "todoList";
|
|
30
|
+
data = value.todos;
|
|
31
|
+
}
|
|
32
|
+
// 2. Fallback for direct arrays if key matches known patterns
|
|
33
|
+
if (!widgetName && Array.isArray(value) && ["todos", "todoList", "project_plan"].includes(key)) {
|
|
34
|
+
widgetName = "todoList";
|
|
35
|
+
data = value;
|
|
36
|
+
}
|
|
37
|
+
// If we found a valid widget and data is an array, emit the UI event
|
|
38
|
+
if (widgetName && widgets[widgetName] && Array.isArray(data)) {
|
|
39
|
+
const isTodo = widgetName === "todoList";
|
|
40
|
+
yield uiEvent(widgets[widgetName](data, {
|
|
41
|
+
placement: isTodo ? "attention" : "sidebar",
|
|
42
|
+
id: isTodo ? `attention-${key}` : `sidebar-${key}`,
|
|
43
|
+
meta: { title: key === "project_plan" ? "Project Plan" : (key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, ' ')) }
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
2
47
|
export function setupDelegation(builder, agentRuntimes) {
|
|
48
|
+
builder.on("action:updateSessionState", async function* (event, context) {
|
|
49
|
+
const { path, value, toolCallId } = event.data;
|
|
50
|
+
const state = context.state;
|
|
51
|
+
try {
|
|
52
|
+
setByPath(state, path, value);
|
|
53
|
+
const topLevelKey = path.split(".")[0];
|
|
54
|
+
yield* maybeEmitWidget(topLevelKey, state[topLevelKey]);
|
|
55
|
+
yield {
|
|
56
|
+
type: "action:result",
|
|
57
|
+
data: {
|
|
58
|
+
action: "updateSessionState",
|
|
59
|
+
result: `Successfully updated state at path "${path}".`,
|
|
60
|
+
toolCallId,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
yield {
|
|
66
|
+
type: "action:result",
|
|
67
|
+
data: {
|
|
68
|
+
action: "updateSessionState",
|
|
69
|
+
result: `Error updating state: ${error.message}`,
|
|
70
|
+
toolCallId,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
});
|
|
3
75
|
builder.on("action:delegateTask", async function* (event, context) {
|
|
4
|
-
const {
|
|
5
|
-
const agentRuntime = agentRuntimes.get(
|
|
76
|
+
const { agentId, toolCallId, task, stateKey, attachments } = event.data;
|
|
77
|
+
const agentRuntime = agentRuntimes.get(agentId);
|
|
6
78
|
// If the agent is not found, return an error
|
|
7
79
|
if (!agentRuntime) {
|
|
8
80
|
yield {
|
|
9
81
|
type: "action:result",
|
|
10
82
|
data: {
|
|
11
83
|
action: "delegateTask",
|
|
12
|
-
result: `Error: Agent "${
|
|
84
|
+
result: `Error: Agent "${agentId}" not found.`,
|
|
13
85
|
toolCallId,
|
|
14
86
|
},
|
|
15
87
|
};
|
|
@@ -19,16 +91,16 @@ export function setupDelegation(builder, agentRuntimes) {
|
|
|
19
91
|
// Signal delegation start for UI
|
|
20
92
|
yield {
|
|
21
93
|
type: "delegation:start",
|
|
22
|
-
meta: { delegationId, agentName },
|
|
23
|
-
data: { agent:
|
|
94
|
+
meta: { delegationId, agentName: agentId },
|
|
95
|
+
data: { agent: agentId, task },
|
|
24
96
|
};
|
|
25
97
|
// Initialize agent isolated state if not present
|
|
26
98
|
const state = context.state;
|
|
27
99
|
if (!state.agentStates)
|
|
28
100
|
state.agentStates = {};
|
|
29
|
-
if (!state.agentStates[
|
|
30
|
-
state.agentStates[
|
|
31
|
-
const agentState = state.agentStates[
|
|
101
|
+
if (!state.agentStates[agentId])
|
|
102
|
+
state.agentStates[agentId] = {};
|
|
103
|
+
const agentState = state.agentStates[agentId];
|
|
32
104
|
const agentIterator = agentRuntime.run({
|
|
33
105
|
type: "agent:input",
|
|
34
106
|
data: { content: task, attachments },
|
|
@@ -37,8 +109,26 @@ export function setupDelegation(builder, agentRuntimes) {
|
|
|
37
109
|
state: agentState,
|
|
38
110
|
});
|
|
39
111
|
let lastAgentOutput = "";
|
|
112
|
+
let pendingApprovalId;
|
|
40
113
|
try {
|
|
41
114
|
for await (const agentEvent of agentIterator) {
|
|
115
|
+
// Dedicated suspend event from approval plugin.
|
|
116
|
+
// Emit included UI event (if any), then park this delegation until approve/deny.
|
|
117
|
+
if (agentEvent.type === "suspend") {
|
|
118
|
+
const suspendData = agentEvent.data ?? {};
|
|
119
|
+
const suspendId = typeof suspendData.id === "string" ? suspendData.id : undefined;
|
|
120
|
+
const suspendUiEvent = suspendData.event;
|
|
121
|
+
if (suspendUiEvent && typeof suspendUiEvent === "object" && typeof suspendUiEvent.type === "string") {
|
|
122
|
+
yield {
|
|
123
|
+
...suspendUiEvent,
|
|
124
|
+
meta: { ...suspendUiEvent.meta, delegationId, agentName: agentId },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (suspendId) {
|
|
128
|
+
pendingApprovalId = suspendId;
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
42
132
|
// Forward agent events to the main runtime so the user sees progress.
|
|
43
133
|
// We SKIP forwarding 'agent:input' because it triggers the manager's LLM again.
|
|
44
134
|
// Instead, we yield it as 'agent:sub-input' for logging/monitoring.
|
|
@@ -46,7 +136,7 @@ export function setupDelegation(builder, agentRuntimes) {
|
|
|
46
136
|
yield {
|
|
47
137
|
...agentEvent,
|
|
48
138
|
type: "agent:sub-input",
|
|
49
|
-
meta: { ...agentEvent.meta, delegationId, agentName },
|
|
139
|
+
meta: { ...agentEvent.meta, delegationId, agentName: agentId },
|
|
50
140
|
};
|
|
51
141
|
continue;
|
|
52
142
|
}
|
|
@@ -55,7 +145,7 @@ export function setupDelegation(builder, agentRuntimes) {
|
|
|
55
145
|
yield {
|
|
56
146
|
...agentEvent,
|
|
57
147
|
type: "agent:sub-action",
|
|
58
|
-
meta: { ...agentEvent.meta, delegationId, agentName },
|
|
148
|
+
meta: { ...agentEvent.meta, delegationId, agentName: agentId },
|
|
59
149
|
data: { ...agentEvent.data, originalType: agentEvent.type },
|
|
60
150
|
};
|
|
61
151
|
continue;
|
|
@@ -65,7 +155,7 @@ export function setupDelegation(builder, agentRuntimes) {
|
|
|
65
155
|
yield {
|
|
66
156
|
...agentEvent,
|
|
67
157
|
type: "agent:sub-action-result",
|
|
68
|
-
meta: { ...agentEvent.meta, delegationId, agentName },
|
|
158
|
+
meta: { ...agentEvent.meta, delegationId, agentName: agentId },
|
|
69
159
|
};
|
|
70
160
|
continue;
|
|
71
161
|
}
|
|
@@ -74,42 +164,58 @@ export function setupDelegation(builder, agentRuntimes) {
|
|
|
74
164
|
yield {
|
|
75
165
|
...agentEvent,
|
|
76
166
|
type: "agent:sub-usage",
|
|
77
|
-
meta: { ...agentEvent.meta, delegationId, agentName },
|
|
167
|
+
meta: { ...agentEvent.meta, delegationId, agentName: agentId },
|
|
78
168
|
};
|
|
79
169
|
continue;
|
|
80
170
|
}
|
|
81
171
|
// Pass through other events but tag them with delegationId and agentName in meta
|
|
82
172
|
yield {
|
|
83
173
|
...agentEvent,
|
|
84
|
-
meta: { ...agentEvent.meta, delegationId, agentName },
|
|
174
|
+
meta: { ...agentEvent.meta, delegationId, agentName: agentId },
|
|
85
175
|
};
|
|
86
176
|
// accumulate agent output
|
|
87
177
|
if (agentEvent.type === "agent:output") {
|
|
88
178
|
const agentOutput = agentEvent.data;
|
|
89
|
-
//
|
|
90
|
-
const value = agentOutput?.result ?? agentOutput?.content ?? agentOutput?.message;
|
|
91
|
-
if (typeof value === "
|
|
179
|
+
// DETERMINISTIC SYNC: If agent returns structured data and stateKey is provided
|
|
180
|
+
const value = agentOutput?.result ?? agentOutput?.content ?? agentOutput?.message ?? agentOutput;
|
|
181
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
182
|
+
if (stateKey) {
|
|
183
|
+
context.state[stateKey] = value;
|
|
184
|
+
yield* maybeEmitWidget(stateKey, value);
|
|
185
|
+
}
|
|
92
186
|
if (lastAgentOutput)
|
|
93
187
|
lastAgentOutput += "\n\n";
|
|
94
|
-
lastAgentOutput += value;
|
|
188
|
+
lastAgentOutput += JSON.stringify(value, null, 2);
|
|
95
189
|
}
|
|
96
|
-
else if (typeof value === "
|
|
190
|
+
else if (typeof value === "string") {
|
|
97
191
|
if (lastAgentOutput)
|
|
98
192
|
lastAgentOutput += "\n\n";
|
|
99
|
-
lastAgentOutput +=
|
|
193
|
+
lastAgentOutput += value;
|
|
100
194
|
}
|
|
101
195
|
}
|
|
102
196
|
}
|
|
103
197
|
}
|
|
104
198
|
catch (error) {
|
|
105
|
-
console.error(`[delegation] Error running agent "${
|
|
199
|
+
console.error(`[delegation] Error running agent "${agentId}":`, error);
|
|
106
200
|
lastAgentOutput = `Error executing task: ${error.message}`;
|
|
107
201
|
}
|
|
202
|
+
// Option A behavior: if sub-agent suspended on approval,
|
|
203
|
+
// keep the manager tool call pending until approve/deny follow-up resolves it.
|
|
204
|
+
if (pendingApprovalId) {
|
|
205
|
+
state.pendingAgentTasks ?? (state.pendingAgentTasks = {});
|
|
206
|
+
state.pendingAgentTasks[pendingApprovalId] = {
|
|
207
|
+
toolCallId,
|
|
208
|
+
agentName: agentId,
|
|
209
|
+
delegationId,
|
|
210
|
+
stateKey: typeof stateKey === "string" ? stateKey : undefined,
|
|
211
|
+
};
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
108
214
|
// Signal delegation end for UI
|
|
109
215
|
yield {
|
|
110
216
|
type: "delegation:end",
|
|
111
|
-
meta: { delegationId, agentName },
|
|
112
|
-
data: { agent:
|
|
217
|
+
meta: { delegationId, agentName: agentId },
|
|
218
|
+
data: { agent: agentId, result: lastAgentOutput || "Task completed." },
|
|
113
219
|
};
|
|
114
220
|
// Feedback the result back to the manager
|
|
115
221
|
yield {
|
package/dist/core/manager.js
CHANGED
|
@@ -3,7 +3,7 @@ import { memoryPlugin, memoryToolDefinitions, createMemoryPromptBuilder } from "
|
|
|
3
3
|
import { topicAgent } from "../agents/topic-agent.js";
|
|
4
4
|
import { llmPlugin } from "../plugins/llm/index.js";
|
|
5
5
|
export function createManagerPlugin(model, resolvedModelId, resolvedBaseDir, registry) {
|
|
6
|
-
const
|
|
6
|
+
const agentIds = registry.getAgentIds();
|
|
7
7
|
const allAgents = registry.getAgents();
|
|
8
8
|
const buildMemoryPrompt = createMemoryPromptBuilder(resolvedBaseDir);
|
|
9
9
|
const agentDescriptions = allAgents
|
|
@@ -13,12 +13,14 @@ export function createManagerPlugin(model, resolvedModelId, resolvedBaseDir, reg
|
|
|
13
13
|
.map(([name, desc]) => ` - ${name}: ${desc}`)
|
|
14
14
|
.join("\n")
|
|
15
15
|
: "";
|
|
16
|
-
return `<agent name="${a.name}">
|
|
16
|
+
return `<agent id="${a.id}" name="${a.name}">
|
|
17
17
|
<description>${a.description}</description>
|
|
18
18
|
${tools ? ` <capabilities>\n${tools}\n </capabilities>` : ""}
|
|
19
19
|
</agent>`;
|
|
20
20
|
})
|
|
21
21
|
.join("\n\n");
|
|
22
|
+
console.log("agentIds", agentIds);
|
|
23
|
+
console.log("agentDescriptions", agentDescriptions);
|
|
22
24
|
return (builder) => {
|
|
23
25
|
builder
|
|
24
26
|
.use(memoryPlugin({
|
|
@@ -31,33 +33,47 @@ ${tools ? ` <capabilities>\n${tools}\n </capabilities>` : ""}
|
|
|
31
33
|
usageScope: "manager",
|
|
32
34
|
system: async (context) => {
|
|
33
35
|
const memoryPrompt = await buildMemoryPrompt(context);
|
|
34
|
-
|
|
36
|
+
const state = context.state;
|
|
37
|
+
// Deterministically inject any custom state keys into the prompt
|
|
38
|
+
const standardKeys = ["messages", "agentStates", "usage", "cwd", "workspaceRoot", "title", "sessionId", "pendingAgentTasks"];
|
|
39
|
+
const customState = {};
|
|
40
|
+
for (const key of Object.keys(state)) {
|
|
41
|
+
if (!standardKeys.includes(key)) {
|
|
42
|
+
customState[key] = state[key];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const statePrompt = Object.keys(customState).length > 0
|
|
46
|
+
? `\n\n<session_state>\n${JSON.stringify(customState, null, 2)}\n</session_state>`
|
|
47
|
+
: "";
|
|
48
|
+
const finalSystemPrompt = `
|
|
35
49
|
|
|
36
50
|
<orchestrator>
|
|
37
51
|
Your goal is to solve user requests by delegating tasks to expert sub-agents.
|
|
38
52
|
|
|
39
53
|
**Directives**:
|
|
40
54
|
1. **Delegate**: Use \`delegateTask\` for any task matching an agent's description.
|
|
41
|
-
2. **Plan**: For multi-step tasks, use \`planner-agent\` first to create a roadmap.
|
|
42
55
|
3. **Context**: Provide a clear, detailed task for the sub-agent. Pass any relevant user attachments.
|
|
43
56
|
4. **Report**: Summarize the sub-agent's work concisely for the user.
|
|
44
57
|
5. **Memory**: Use your memory tools (\`remember\`, \`recall\`) to maintain context across sessions.
|
|
58
|
+
6. **State**: Use \`updateSessionState\` to modify any value in the <session_state>.
|
|
45
59
|
</orchestrator>
|
|
46
60
|
|
|
61
|
+
${statePrompt}
|
|
62
|
+
|
|
47
63
|
<agents>
|
|
48
64
|
${agentDescriptions}
|
|
49
65
|
</agents>${memoryPrompt}`;
|
|
66
|
+
// console.log("finalSystemPrompt:::::", finalSystemPrompt);
|
|
67
|
+
return finalSystemPrompt;
|
|
50
68
|
},
|
|
51
|
-
promptInputType: "agent:input",
|
|
52
|
-
actionResultInputType: "action:result",
|
|
53
|
-
completionEventType: "agent:output",
|
|
54
69
|
toolDefinitions: {
|
|
55
70
|
...memoryToolDefinitions,
|
|
56
71
|
delegateTask: {
|
|
57
72
|
description: `Delegate a task to a specialized expert agent.`,
|
|
58
73
|
inputSchema: z.object({
|
|
59
|
-
|
|
74
|
+
agentId: z.enum(agentIds).describe("The ID of the agent to use"),
|
|
60
75
|
task: z.string().describe("The task for the agent to perform"),
|
|
76
|
+
stateKey: z.string().optional().describe("Optional key to store structured JSON result in the session state"),
|
|
61
77
|
attachments: z.array(z.object({
|
|
62
78
|
id: z.string(),
|
|
63
79
|
name: z.string(),
|
|
@@ -67,6 +83,13 @@ ${agentDescriptions}
|
|
|
67
83
|
})).optional().describe("Attachments to pass through to the agent"),
|
|
68
84
|
}),
|
|
69
85
|
},
|
|
86
|
+
updateSessionState: {
|
|
87
|
+
description: "Update a value in the session state using a JSON path.",
|
|
88
|
+
inputSchema: z.object({
|
|
89
|
+
path: z.string().describe("The JSON path to the value (e.g. 'project_plan.todos.0.status')"),
|
|
90
|
+
value: z.any().describe("The new value to set"),
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
70
93
|
},
|
|
71
94
|
}));
|
|
72
95
|
};
|
package/dist/core/plugins.js
CHANGED
|
@@ -3,7 +3,6 @@ import { fileSystemPlugin, fileSystemToolDefinitions } from "../plugins/file-sys
|
|
|
3
3
|
import { approvalPlugin } from "../plugins/approval/index.js";
|
|
4
4
|
import { osAgent } from "../agents/os-agent.js";
|
|
5
5
|
import { agentCreatorAgent } from "../agents/agent-creator.js";
|
|
6
|
-
import { plannerAgent } from "../agents/planner-agent.js";
|
|
7
6
|
import { PluginRegistry, discoverPlugins } from "../registry/index.js";
|
|
8
7
|
import path from "node:path";
|
|
9
8
|
/**
|
|
@@ -16,6 +15,7 @@ export async function setupPluginRegistry(resolvedBaseDir, model, options) {
|
|
|
16
15
|
const registry = new PluginRegistry();
|
|
17
16
|
// ── Built-in tools ───────────────────────────────────────────────
|
|
18
17
|
registry.register({
|
|
18
|
+
id: "shell",
|
|
19
19
|
name: "shell",
|
|
20
20
|
description: "Execute shell commands",
|
|
21
21
|
type: "tool",
|
|
@@ -24,6 +24,7 @@ export async function setupPluginRegistry(resolvedBaseDir, model, options) {
|
|
|
24
24
|
isBuiltIn: true,
|
|
25
25
|
});
|
|
26
26
|
registry.register({
|
|
27
|
+
id: "file-system",
|
|
27
28
|
name: "file-system",
|
|
28
29
|
description: "Read, write, list, and delete files",
|
|
29
30
|
type: "tool",
|
|
@@ -32,6 +33,7 @@ export async function setupPluginRegistry(resolvedBaseDir, model, options) {
|
|
|
32
33
|
isBuiltIn: true,
|
|
33
34
|
});
|
|
34
35
|
registry.register({
|
|
36
|
+
id: "approval",
|
|
35
37
|
name: "approval",
|
|
36
38
|
description: "Require user approval for specific actions",
|
|
37
39
|
type: "tool",
|
|
@@ -41,6 +43,7 @@ export async function setupPluginRegistry(resolvedBaseDir, model, options) {
|
|
|
41
43
|
});
|
|
42
44
|
// ── Built-in agents ──────────────────────────────────────────────
|
|
43
45
|
registry.register({
|
|
46
|
+
id: "os",
|
|
44
47
|
name: "os",
|
|
45
48
|
description: "Handles shell commands and file system operations",
|
|
46
49
|
type: "agent",
|
|
@@ -52,6 +55,7 @@ export async function setupPluginRegistry(resolvedBaseDir, model, options) {
|
|
|
52
55
|
isBuiltIn: true,
|
|
53
56
|
});
|
|
54
57
|
registry.register({
|
|
58
|
+
id: "agent-creator",
|
|
55
59
|
name: "agent-creator",
|
|
56
60
|
description: "Helps the user create and update custom OpenBot agents via natural language.",
|
|
57
61
|
type: "agent",
|
|
@@ -61,13 +65,6 @@ export async function setupPluginRegistry(resolvedBaseDir, model, options) {
|
|
|
61
65
|
plugin: agentCreatorAgent({ model }),
|
|
62
66
|
isBuiltIn: true,
|
|
63
67
|
});
|
|
64
|
-
registry.register({
|
|
65
|
-
name: "planner-agent",
|
|
66
|
-
description: "Creates concise execution plans from user intent for OpenBot to run.",
|
|
67
|
-
type: "agent",
|
|
68
|
-
plugin: plannerAgent({ model }),
|
|
69
|
-
isBuiltIn: true,
|
|
70
|
-
});
|
|
71
68
|
// ── Custom agents and plugins ────────────────────────────────────
|
|
72
69
|
const agentsDir = path.join(resolvedBaseDir, "agents");
|
|
73
70
|
const pluginsDir = path.join(resolvedBaseDir, "plugins");
|
package/dist/core/router.js
CHANGED
|
@@ -1,21 +1,131 @@
|
|
|
1
|
-
|
|
1
|
+
function summarizeAgentEventValue(event) {
|
|
2
|
+
if (!event)
|
|
3
|
+
return undefined;
|
|
4
|
+
const value = event?.data?.result ?? event?.data?.content ?? event?.data?.message ?? event?.data;
|
|
5
|
+
if (typeof value === "string")
|
|
6
|
+
return value;
|
|
7
|
+
if (typeof value === "object" && value !== null)
|
|
8
|
+
return JSON.stringify(value, null, 2);
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
export async function* runOpenBot(event, context, managerRuntime, agentRuntimes, registry) {
|
|
2
12
|
const { state } = context;
|
|
13
|
+
const allAgents = registry.getAgents();
|
|
3
14
|
// Initialize state
|
|
4
15
|
if (!state.messages)
|
|
5
16
|
state.messages = [];
|
|
6
17
|
if (!state.agentStates)
|
|
7
18
|
state.agentStates = {};
|
|
8
|
-
// 1.
|
|
19
|
+
// 1. Route non-user events directly (pluggable).
|
|
20
|
+
// If an event is tagged with meta.agentName, send it to that agent runtime.
|
|
21
|
+
// Otherwise, pass it to manager runtime unchanged.
|
|
22
|
+
if (event.type !== "agent:input") {
|
|
23
|
+
const targetAgent = event.meta?.agentName;
|
|
24
|
+
if (targetAgent) {
|
|
25
|
+
const runtime = agentRuntimes.get(targetAgent);
|
|
26
|
+
if (runtime) {
|
|
27
|
+
if (!state.agentStates[targetAgent])
|
|
28
|
+
state.agentStates[targetAgent] = {};
|
|
29
|
+
let resumedOutput = "";
|
|
30
|
+
for await (const agentChunk of runtime.run(event, {
|
|
31
|
+
runId: context.runId,
|
|
32
|
+
state: state.agentStates[targetAgent],
|
|
33
|
+
})) {
|
|
34
|
+
// Preserve sub-agent attribution when resuming after approval/deny
|
|
35
|
+
// so UI does not fall back to manager identity.
|
|
36
|
+
yield {
|
|
37
|
+
...agentChunk,
|
|
38
|
+
meta: {
|
|
39
|
+
...agentChunk?.meta,
|
|
40
|
+
...(event.meta?.delegationId ? { delegationId: event.meta.delegationId } : {}),
|
|
41
|
+
agentName: targetAgent,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
if (agentChunk.type === "agent:output" || agentChunk.type === "action:result") {
|
|
45
|
+
const summary = summarizeAgentEventValue(agentChunk);
|
|
46
|
+
if (summary) {
|
|
47
|
+
if (resumedOutput)
|
|
48
|
+
resumedOutput += "\n\n";
|
|
49
|
+
resumedOutput += summary;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Resolve pending delegated tool call after approval follow-up.
|
|
54
|
+
const maybeApprovalId = event?.data?.id;
|
|
55
|
+
const shouldResolvePending = (event.type === "action:approve" || event.type === "action:deny")
|
|
56
|
+
&& typeof maybeApprovalId === "string";
|
|
57
|
+
if (shouldResolvePending) {
|
|
58
|
+
const pending = state.pendingAgentTasks?.[maybeApprovalId];
|
|
59
|
+
if (pending) {
|
|
60
|
+
const wasDenied = event.type === "action:deny";
|
|
61
|
+
const delegateResult = wasDenied
|
|
62
|
+
? { error: "Action denied by user", denied: true }
|
|
63
|
+
: (resumedOutput || "Task completed with no output.");
|
|
64
|
+
delete state.pendingAgentTasks[maybeApprovalId];
|
|
65
|
+
yield {
|
|
66
|
+
type: "delegation:end",
|
|
67
|
+
meta: { delegationId: pending.delegationId, agentName: pending.agentName },
|
|
68
|
+
data: {
|
|
69
|
+
agent: pending.agentName,
|
|
70
|
+
result: wasDenied ? "Action denied by user." : (resumedOutput || "Task completed."),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
yield* managerRuntime.run({
|
|
74
|
+
type: "action:result",
|
|
75
|
+
data: {
|
|
76
|
+
action: "delegateTask",
|
|
77
|
+
result: delegateResult,
|
|
78
|
+
toolCallId: pending.toolCallId,
|
|
79
|
+
success: !wasDenied,
|
|
80
|
+
halt: wasDenied,
|
|
81
|
+
},
|
|
82
|
+
}, {
|
|
83
|
+
runId: context.runId,
|
|
84
|
+
state: state,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
yield* managerRuntime.run(event, {
|
|
92
|
+
runId: context.runId,
|
|
93
|
+
state: state,
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// 2. Direct agent routing for user input (e.g. "@os list files" or "@Codex Agent list files")
|
|
9
98
|
if (event.type === "agent:input") {
|
|
10
99
|
const content = event.data.content;
|
|
11
|
-
if (content?.startsWith("@")) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
100
|
+
if (content?.trim().startsWith("@")) {
|
|
101
|
+
const trimmedContent = content.trim();
|
|
102
|
+
const afterAt = trimmedContent.slice(1);
|
|
103
|
+
// Find the longest matching agent (by ID or Name) at the start of the message
|
|
104
|
+
// This handles agent names with spaces like "Codex Agent"
|
|
105
|
+
let bestMatch;
|
|
106
|
+
for (const agent of allAgents) {
|
|
107
|
+
const idMatches = afterAt.toLowerCase().startsWith(agent.id.toLowerCase());
|
|
108
|
+
const nameMatches = afterAt.toLowerCase().startsWith(agent.name.toLowerCase());
|
|
109
|
+
if (idMatches || nameMatches) {
|
|
110
|
+
const matchPrefix = idMatches ? agent.id : agent.name;
|
|
111
|
+
const prefixLength = matchPrefix.length;
|
|
112
|
+
// Next char must be space, end of string, or the match length must be at least
|
|
113
|
+
// the current best match length (prefer longer names like "Codex Agent" over "Codex")
|
|
114
|
+
const nextChar = afterAt[prefixLength];
|
|
115
|
+
if (!nextChar || nextChar === " ") {
|
|
116
|
+
if (!bestMatch || prefixLength > bestMatch.prefixLength) {
|
|
117
|
+
bestMatch = { id: agent.id, name: agent.name, prefixLength };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (bestMatch) {
|
|
123
|
+
const targetAgent = bestMatch.id;
|
|
124
|
+
const remaining = afterAt.slice(bestMatch.prefixLength).trim();
|
|
125
|
+
const runtime = agentRuntimes.get(targetAgent);
|
|
16
126
|
if (runtime) {
|
|
17
|
-
if (!state.agentStates[
|
|
18
|
-
state.agentStates[
|
|
127
|
+
if (!state.agentStates[targetAgent])
|
|
128
|
+
state.agentStates[targetAgent] = {};
|
|
19
129
|
const agentEvent = {
|
|
20
130
|
...event,
|
|
21
131
|
data: {
|
|
@@ -23,16 +133,57 @@ export async function* runOpenBot(event, context, managerRuntime, agentRuntimes)
|
|
|
23
133
|
attachments: event.data.attachments,
|
|
24
134
|
},
|
|
25
135
|
};
|
|
26
|
-
|
|
136
|
+
let lastAgentOutput = "";
|
|
137
|
+
for await (const agentChunk of runtime.run(agentEvent, {
|
|
27
138
|
runId: context.runId,
|
|
28
|
-
state: state.agentStates[
|
|
29
|
-
})
|
|
139
|
+
state: state.agentStates[targetAgent],
|
|
140
|
+
})) {
|
|
141
|
+
if (agentChunk.type === "agent:output" || agentChunk.type === "agent:output-delta") {
|
|
142
|
+
const summary = summarizeAgentEventValue(agentChunk);
|
|
143
|
+
if (summary)
|
|
144
|
+
lastAgentOutput = summary;
|
|
145
|
+
}
|
|
146
|
+
yield {
|
|
147
|
+
...agentChunk,
|
|
148
|
+
meta: {
|
|
149
|
+
...agentChunk?.meta,
|
|
150
|
+
agentName: targetAgent,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// Direct "@agent" routing bypasses manager handlers entirely.
|
|
155
|
+
// Trigger manager-side post-processing (e.g. topic/title generation)
|
|
156
|
+
// without producing a manager reply.
|
|
157
|
+
if (!state.title) {
|
|
158
|
+
for await (const _ of managerRuntime.run({
|
|
159
|
+
type: "agent:output",
|
|
160
|
+
meta: { agentName: targetAgent },
|
|
161
|
+
data: { content: lastAgentOutput || "" },
|
|
162
|
+
}, {
|
|
163
|
+
runId: context.runId,
|
|
164
|
+
state: state,
|
|
165
|
+
})) {
|
|
166
|
+
// side-effects only
|
|
167
|
+
}
|
|
168
|
+
}
|
|
30
169
|
return;
|
|
31
170
|
}
|
|
32
171
|
}
|
|
172
|
+
else {
|
|
173
|
+
// If the user used @ but the agent wasn't found, stop here to avoid
|
|
174
|
+
// falling back to the manager and burning tokens for a failed routing attempt.
|
|
175
|
+
const agentPrefixMatch = afterAt.split(" ")[0];
|
|
176
|
+
yield {
|
|
177
|
+
type: "agent:output",
|
|
178
|
+
data: {
|
|
179
|
+
content: `Agent "@${agentPrefixMatch}" not found. Available agents:\n${allAgents.map(a => `- ${a.name} (@${a.id})`).join("\n")}`
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
33
184
|
}
|
|
34
185
|
}
|
|
35
|
-
//
|
|
186
|
+
// 3. Default routing: translate user input to manager input
|
|
36
187
|
yield* managerRuntime.run({ ...event, type: "agent:input" }, {
|
|
37
188
|
runId: context.runId,
|
|
38
189
|
state: state,
|
package/dist/open-bot.js
CHANGED
|
@@ -24,7 +24,7 @@ export async function createOpenBot(options) {
|
|
|
24
24
|
for (const agent of registry.getAgents()) {
|
|
25
25
|
const builder = melony();
|
|
26
26
|
builder.use(agent.plugin);
|
|
27
|
-
agentRuntimes.set(agent.
|
|
27
|
+
agentRuntimes.set(agent.id, builder.build());
|
|
28
28
|
}
|
|
29
29
|
// 3. Initialize manager runtime
|
|
30
30
|
const managerBuilder = melony();
|
|
@@ -46,6 +46,6 @@ export async function createOpenBot(options) {
|
|
|
46
46
|
// 6. Return the runtime
|
|
47
47
|
return {
|
|
48
48
|
registry,
|
|
49
|
-
run: (event, context) => runOpenBot(event, context, managerRuntime, agentRuntimes),
|
|
49
|
+
run: (event, context) => runOpenBot(event, context, managerRuntime, agentRuntimes, registry),
|
|
50
50
|
};
|
|
51
51
|
}
|