openbot 0.2.3 → 0.2.5
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 +1 -1
- package/dist/agents/agent-creator.js +58 -19
- package/dist/agents/os-agent.js +1 -4
- package/dist/agents/planner-agent.js +32 -0
- package/dist/agents/topic-agent.js +1 -1
- package/dist/architecture/contracts.js +1 -0
- package/dist/architecture/execution-engine.js +151 -0
- package/dist/architecture/intent-classifier.js +26 -0
- package/dist/architecture/planner.js +106 -0
- package/dist/automation-worker.js +121 -0
- package/dist/automations.js +52 -0
- package/dist/cli.js +54 -141
- package/dist/config.js +20 -0
- package/dist/core/agents.js +41 -0
- package/dist/core/delegation.js +124 -0
- package/dist/core/manager.js +73 -0
- package/dist/core/plugins.js +77 -0
- package/dist/core/router.js +40 -0
- package/dist/installers.js +170 -0
- package/dist/marketplace.js +80 -0
- package/dist/open-bot.js +34 -157
- package/dist/orchestrator.js +247 -51
- package/dist/plugins/approval/index.js +107 -3
- package/dist/plugins/brain/index.js +17 -86
- package/dist/plugins/brain/memory.js +1 -1
- package/dist/plugins/brain/prompt.js +8 -13
- package/dist/plugins/brain/types.js +0 -15
- package/dist/plugins/file-system/index.js +8 -8
- package/dist/plugins/llm/context-shaping.js +177 -0
- package/dist/plugins/llm/index.js +223 -49
- package/dist/plugins/memory/index.js +220 -0
- package/dist/plugins/memory/memory.js +122 -0
- package/dist/plugins/memory/prompt.js +55 -0
- package/dist/plugins/memory/types.js +45 -0
- package/dist/plugins/shell/index.js +3 -3
- package/dist/plugins/skills/index.js +9 -9
- package/dist/registry/index.js +1 -4
- package/dist/registry/plugin-loader.js +339 -56
- package/dist/registry/plugin-registry.js +21 -4
- package/dist/registry/ts-agent-loader.js +4 -4
- package/dist/registry/yaml-agent-loader.js +78 -20
- package/dist/runtime/execution-trace.js +41 -0
- package/dist/runtime/intent-routing.js +26 -0
- package/dist/runtime/openbot-runtime.js +354 -0
- package/dist/server.js +489 -40
- package/dist/ui/widgets/approval-card.js +22 -2
- package/dist/ui/widgets/delegation.js +29 -0
- package/dist/version.js +62 -0
- package/package.json +7 -7
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plugin Registry
|
|
2
|
+
* Unified Plugin Registry
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Built-in
|
|
6
|
-
*
|
|
4
|
+
* Holds both tool plugins and agent plugins in a single registry.
|
|
5
|
+
* Built-in entries are registered at startup; community plugins
|
|
6
|
+
* are discovered from ~/.openbot/plugins/.
|
|
7
7
|
*/
|
|
8
8
|
export class PluginRegistry {
|
|
9
9
|
constructor() {
|
|
10
10
|
this.plugins = new Map();
|
|
11
11
|
}
|
|
12
12
|
register(entry) {
|
|
13
|
+
if (this.plugins.has(entry.name)) {
|
|
14
|
+
console.warn(`Plugin "${entry.name}" is already registered — overwriting`);
|
|
15
|
+
}
|
|
13
16
|
this.plugins.set(entry.name, entry);
|
|
14
17
|
}
|
|
15
18
|
get(name) {
|
|
@@ -24,4 +27,18 @@ export class PluginRegistry {
|
|
|
24
27
|
getNames() {
|
|
25
28
|
return Array.from(this.plugins.keys());
|
|
26
29
|
}
|
|
30
|
+
getAgents() {
|
|
31
|
+
return this.getAll().filter(p => p.type === "agent");
|
|
32
|
+
}
|
|
33
|
+
getTools() {
|
|
34
|
+
return this.getAll().filter(p => p.type === "tool");
|
|
35
|
+
}
|
|
36
|
+
/** Returns agent names as a tuple suitable for z.enum(). */
|
|
37
|
+
getAgentNames() {
|
|
38
|
+
const names = this.getAgents().map(a => a.name);
|
|
39
|
+
if (names.length === 0) {
|
|
40
|
+
throw new Error("No agents registered — at least one agent is required");
|
|
41
|
+
}
|
|
42
|
+
return names;
|
|
43
|
+
}
|
|
27
44
|
}
|
|
@@ -22,11 +22,11 @@ export async function discoverTsAgents(agentsDir, defaultModel, options) {
|
|
|
22
22
|
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
23
23
|
continue;
|
|
24
24
|
const agentDir = path.join(agentsDir, entry.name);
|
|
25
|
-
// We only consider it a TS agent if it doesn't have an
|
|
25
|
+
// We only consider it a TS agent if it doesn't have an AGENT.md
|
|
26
26
|
// (This avoids double-loading if someone has both for some reason)
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
27
|
+
const mdPath = path.join(agentDir, "AGENT.md");
|
|
28
|
+
const hasMd = await fs.access(mdPath).then(() => true).catch(() => false);
|
|
29
|
+
if (hasMd)
|
|
30
30
|
continue;
|
|
31
31
|
// Check for package.json to see if it's a package
|
|
32
32
|
const pkgPath = path.join(agentDir, "package.json");
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import matter from "gray-matter";
|
|
4
4
|
import { llmPlugin } from "../plugins/llm/index.js";
|
|
5
5
|
import { PluginRegistry } from "./plugin-registry.js";
|
|
6
6
|
import { createModel } from "../models.js";
|
|
7
7
|
import { loadPluginsFromDir } from "./plugin-loader.js";
|
|
8
|
-
import { resolvePath } from "../config.js";
|
|
8
|
+
import { resolvePath, DEFAULT_AGENT_MD } from "../config.js";
|
|
9
9
|
/**
|
|
10
10
|
* Recursively resolve tilde paths in a configuration object.
|
|
11
11
|
*/
|
|
@@ -26,12 +26,29 @@ function resolveConfigPaths(config) {
|
|
|
26
26
|
return config;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Read and parse an agent
|
|
29
|
+
* Read and parse an agent configuration from AGENT.md with frontmatter.
|
|
30
30
|
*/
|
|
31
31
|
export async function readAgentConfig(agentDir) {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
32
|
+
const mdPath = path.join(agentDir, "AGENT.md");
|
|
33
|
+
const folderName = path.basename(agentDir);
|
|
34
|
+
let mdContent = "";
|
|
35
|
+
try {
|
|
36
|
+
mdContent = await fs.readFile(mdPath, "utf-8");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Fallback to a default template if AGENT.md is missing
|
|
40
|
+
mdContent = DEFAULT_AGENT_MD.replace("name: Agent", `name: ${folderName}`);
|
|
41
|
+
}
|
|
42
|
+
const parsed = matter(mdContent);
|
|
43
|
+
const config = (parsed.data || {});
|
|
44
|
+
return {
|
|
45
|
+
name: config.name || folderName,
|
|
46
|
+
description: config.description || `The ${folderName} agent`,
|
|
47
|
+
model: config.model,
|
|
48
|
+
plugins: config.plugins || [],
|
|
49
|
+
systemPrompt: parsed.content.trim() || "",
|
|
50
|
+
subscribe: config.subscribe,
|
|
51
|
+
};
|
|
35
52
|
}
|
|
36
53
|
/**
|
|
37
54
|
* Discover YAML-defined agents from a directory without loading plugins.
|
|
@@ -41,6 +58,7 @@ export async function readAgentConfig(agentDir) {
|
|
|
41
58
|
*/
|
|
42
59
|
export async function listYamlAgents(agentsDir) {
|
|
43
60
|
const agents = [];
|
|
61
|
+
const seenNames = new Set();
|
|
44
62
|
try {
|
|
45
63
|
const entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
46
64
|
for (const entry of entries) {
|
|
@@ -49,14 +67,42 @@ export async function listYamlAgents(agentsDir) {
|
|
|
49
67
|
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
50
68
|
continue;
|
|
51
69
|
const agentDir = path.join(agentsDir, entry.name);
|
|
70
|
+
// Check if it's a TS agent (has package.json but NO AGENT.md)
|
|
71
|
+
const hasMd = await fs.access(path.join(agentDir, "AGENT.md")).then(() => true).catch(() => false);
|
|
72
|
+
const hasPkg = await fs.access(path.join(agentDir, "package.json")).then(() => true).catch(() => false);
|
|
73
|
+
if (!hasMd && hasPkg) {
|
|
74
|
+
let description = "TypeScript Agent (editable via files only)";
|
|
75
|
+
let name = entry.name;
|
|
76
|
+
try {
|
|
77
|
+
const pkg = JSON.parse(await fs.readFile(path.join(agentDir, "package.json"), "utf-8"));
|
|
78
|
+
if (pkg.description)
|
|
79
|
+
description = pkg.description;
|
|
80
|
+
if (pkg.name)
|
|
81
|
+
name = pkg.name;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Ignore
|
|
85
|
+
}
|
|
86
|
+
if (!seenNames.has(name)) {
|
|
87
|
+
agents.push({
|
|
88
|
+
name,
|
|
89
|
+
description,
|
|
90
|
+
folder: agentDir,
|
|
91
|
+
isTs: true,
|
|
92
|
+
});
|
|
93
|
+
seenNames.add(name);
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
52
97
|
try {
|
|
53
98
|
const config = await readAgentConfig(agentDir);
|
|
54
|
-
if (config.name && config.description) {
|
|
99
|
+
if (config.name && config.description && !seenNames.has(config.name)) {
|
|
55
100
|
agents.push({
|
|
56
101
|
name: config.name,
|
|
57
102
|
description: config.description,
|
|
58
103
|
folder: agentDir,
|
|
59
104
|
});
|
|
105
|
+
seenNames.add(config.name);
|
|
60
106
|
}
|
|
61
107
|
}
|
|
62
108
|
catch {
|
|
@@ -72,7 +118,7 @@ export async function listYamlAgents(agentsDir) {
|
|
|
72
118
|
/**
|
|
73
119
|
* Discover and load YAML-defined agents from a directory.
|
|
74
120
|
*
|
|
75
|
-
* Scans `agentsDir` for subdirectories containing an `
|
|
121
|
+
* Scans `agentsDir` for subdirectories containing an `AGENT.md` file,
|
|
76
122
|
* parses each one, and composes a Melony plugin from the referenced plugins.
|
|
77
123
|
*
|
|
78
124
|
* @param agentsDir Absolute path to the agents directory (e.g. ~/.openbot/agents)
|
|
@@ -83,6 +129,7 @@ export async function listYamlAgents(agentsDir) {
|
|
|
83
129
|
*/
|
|
84
130
|
export async function discoverYamlAgents(agentsDir, pluginRegistry, defaultModel, options) {
|
|
85
131
|
const agents = [];
|
|
132
|
+
const seenNames = new Set();
|
|
86
133
|
// Ensure the agents directory exists
|
|
87
134
|
try {
|
|
88
135
|
await fs.mkdir(agentsDir, { recursive: true });
|
|
@@ -97,15 +144,19 @@ export async function discoverYamlAgents(agentsDir, pluginRegistry, defaultModel
|
|
|
97
144
|
continue;
|
|
98
145
|
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
99
146
|
continue;
|
|
100
|
-
const yamlPath = path.join(agentsDir, entry.name, "agent.yaml");
|
|
101
147
|
const agentDir = path.join(agentsDir, entry.name);
|
|
148
|
+
// Skip TS agents (they don't need AGENT.md)
|
|
149
|
+
const hasMd = await fs.access(path.join(agentDir, "AGENT.md")).then(() => true).catch(() => false);
|
|
150
|
+
const hasPkg = await fs.access(path.join(agentDir, "package.json")).then(() => true).catch(() => false);
|
|
151
|
+
if (!hasMd && hasPkg)
|
|
152
|
+
continue;
|
|
102
153
|
try {
|
|
103
154
|
const config = await readAgentConfig(agentDir);
|
|
104
|
-
// Validate required fields
|
|
105
|
-
if (!config.name || !config.description ||
|
|
106
|
-
console.warn(`[agents] "${entry.name}/agent.yaml": missing required fields (name, description, plugins, systemPrompt) — skipping`);
|
|
155
|
+
// Validate required fields and avoid duplicates
|
|
156
|
+
if (!config.name || !config.description || seenNames.has(config.name)) {
|
|
107
157
|
continue;
|
|
108
158
|
}
|
|
159
|
+
seenNames.add(config.name);
|
|
109
160
|
const agentModel = config.model
|
|
110
161
|
? createModel({ ...options, model: config.model })
|
|
111
162
|
: defaultModel;
|
|
@@ -122,7 +173,17 @@ export async function discoverYamlAgents(agentsDir, pluginRegistry, defaultModel
|
|
|
122
173
|
for (const p of localPlugins) {
|
|
123
174
|
scopedRegistry.register(p);
|
|
124
175
|
}
|
|
125
|
-
|
|
176
|
+
// Initialize AGENT.md if it doesn't exist (using the template)
|
|
177
|
+
const agentMdPath = path.join(agentDir, "AGENT.md");
|
|
178
|
+
try {
|
|
179
|
+
await fs.access(agentMdPath);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
const content = DEFAULT_AGENT_MD.replace("name: Agent", `name: ${config.name}`);
|
|
183
|
+
await fs.writeFile(agentMdPath, content, "utf-8");
|
|
184
|
+
console.log(`[agents] Initialized ${config.name}/AGENT.md`);
|
|
185
|
+
}
|
|
186
|
+
const { plugin, toolDefinitions } = composeAgentFromConfig(config, scopedRegistry, agentModel);
|
|
126
187
|
agents.push({
|
|
127
188
|
name: config.name,
|
|
128
189
|
description: config.description,
|
|
@@ -136,9 +197,9 @@ export async function discoverYamlAgents(agentsDir, pluginRegistry, defaultModel
|
|
|
136
197
|
console.log(`[agents] Loaded: ${config.name} — ${config.description}${config.model ? ` (model: ${config.model})` : ""}`);
|
|
137
198
|
}
|
|
138
199
|
catch (err) {
|
|
139
|
-
//
|
|
200
|
+
// Skip invalid agents
|
|
140
201
|
if (err.code !== 'ENOENT') {
|
|
141
|
-
console.warn(`[agents] Error loading "${entry.name}
|
|
202
|
+
console.warn(`[agents] Error loading "${entry.name}":`, err);
|
|
142
203
|
}
|
|
143
204
|
}
|
|
144
205
|
}
|
|
@@ -149,12 +210,12 @@ export async function discoverYamlAgents(agentsDir, pluginRegistry, defaultModel
|
|
|
149
210
|
return agents;
|
|
150
211
|
}
|
|
151
212
|
/**
|
|
152
|
-
* Compose a Melony plugin from
|
|
213
|
+
* Compose a Melony plugin from an agent configuration.
|
|
153
214
|
*
|
|
154
215
|
* Resolves each plugin name against the registry, collects their tool definitions,
|
|
155
216
|
* and wires them with an agent-scoped LLM plugin.
|
|
156
217
|
*/
|
|
157
|
-
function
|
|
218
|
+
function composeAgentFromConfig(config, pluginRegistry, model) {
|
|
158
219
|
const allToolDefinitions = {};
|
|
159
220
|
const pluginFactories = [];
|
|
160
221
|
for (const pluginItem of config.plugins) {
|
|
@@ -179,9 +240,6 @@ function composeAgentFromYaml(config, pluginRegistry, model) {
|
|
|
179
240
|
model,
|
|
180
241
|
system: config.systemPrompt,
|
|
181
242
|
toolDefinitions: allToolDefinitions,
|
|
182
|
-
promptInputType: `agent:${config.name}:input`,
|
|
183
|
-
actionResultInputType: `agent:${config.name}:result`,
|
|
184
|
-
completionEventType: `agent:${config.name}:output`,
|
|
185
243
|
}));
|
|
186
244
|
};
|
|
187
245
|
return { plugin, toolDefinitions: allToolDefinitions };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function nowIso() {
|
|
2
|
+
return new Date().toISOString();
|
|
3
|
+
}
|
|
4
|
+
export function createTraceId(runId) {
|
|
5
|
+
return `trace_${runId}_${Date.now()}`;
|
|
6
|
+
}
|
|
7
|
+
export function hasPendingApprovals(state) {
|
|
8
|
+
const agentStates = state.agentStates || {};
|
|
9
|
+
return Object.values(agentStates).some((agentState) => !!agentState.pendingApprovals &&
|
|
10
|
+
Object.keys(agentState.pendingApprovals).length > 0);
|
|
11
|
+
}
|
|
12
|
+
export function setExecutionState(state, patch) {
|
|
13
|
+
const hasCurrentStepId = Object.prototype.hasOwnProperty.call(patch, "currentStepId");
|
|
14
|
+
const hasError = Object.prototype.hasOwnProperty.call(patch, "error");
|
|
15
|
+
const hasIntentType = Object.prototype.hasOwnProperty.call(patch, "intentType");
|
|
16
|
+
const hasPlanSteps = Object.prototype.hasOwnProperty.call(patch, "planSteps");
|
|
17
|
+
const next = {
|
|
18
|
+
traceId: patch.traceId ?? state.execution?.traceId ?? `trace_${Date.now()}`,
|
|
19
|
+
state: patch.state ?? state.execution?.state ?? "RECEIVED",
|
|
20
|
+
currentStepId: hasCurrentStepId ? patch.currentStepId : state.execution?.currentStepId,
|
|
21
|
+
error: hasError ? patch.error : state.execution?.error,
|
|
22
|
+
intentType: hasIntentType ? patch.intentType : state.execution?.intentType,
|
|
23
|
+
planSteps: hasPlanSteps ? patch.planSteps : state.execution?.planSteps,
|
|
24
|
+
updatedAt: nowIso(),
|
|
25
|
+
};
|
|
26
|
+
state.execution = next;
|
|
27
|
+
return next;
|
|
28
|
+
}
|
|
29
|
+
export function executionStateEvent(trace) {
|
|
30
|
+
return {
|
|
31
|
+
type: "execution:state",
|
|
32
|
+
data: {
|
|
33
|
+
traceId: trace?.traceId,
|
|
34
|
+
state: trace?.state,
|
|
35
|
+
currentStepId: trace?.currentStepId,
|
|
36
|
+
error: trace?.error,
|
|
37
|
+
intentType: trace?.intentType,
|
|
38
|
+
planSteps: trace?.planSteps,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const TASK_HINT_REGEX = /\b(create|build|write|fix|update|implement|run|execute|open|edit|delete|list|plan|analyze|research|refactor|install|configure|debug|deploy)\b/i;
|
|
2
|
+
export function parseDirectAgent(content, knownAgents) {
|
|
3
|
+
const raw = content.trim();
|
|
4
|
+
if (!raw || (!raw.startsWith("/") && !raw.startsWith("@")))
|
|
5
|
+
return null;
|
|
6
|
+
const firstSpace = raw.indexOf(" ");
|
|
7
|
+
const candidate = firstSpace === -1 ? raw.slice(1) : raw.slice(1, firstSpace);
|
|
8
|
+
if (!knownAgents.has(candidate))
|
|
9
|
+
return null;
|
|
10
|
+
const task = firstSpace === -1 ? "" : raw.slice(firstSpace + 1).trim();
|
|
11
|
+
return { agentName: candidate, task };
|
|
12
|
+
}
|
|
13
|
+
export function classifyIntent(content, knownAgents) {
|
|
14
|
+
const raw = content.trim();
|
|
15
|
+
const direct = parseDirectAgent(raw, knownAgents);
|
|
16
|
+
if (direct) {
|
|
17
|
+
return { type: "agent_direct", targetAgent: direct.agentName };
|
|
18
|
+
}
|
|
19
|
+
if (!raw) {
|
|
20
|
+
return { type: "chat" };
|
|
21
|
+
}
|
|
22
|
+
if (TASK_HINT_REGEX.test(raw)) {
|
|
23
|
+
return { type: "task" };
|
|
24
|
+
}
|
|
25
|
+
return { type: "chat" };
|
|
26
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { melony } from "melony";
|
|
2
|
+
import { classifyIntent, parseDirectAgent } from "./intent-routing.js";
|
|
3
|
+
import { createTraceId, executionStateEvent, hasPendingApprovals, setExecutionState, } from "./execution-trace.js";
|
|
4
|
+
const AGENT_TEXT_TYPES = new Set(["assistant:text-delta", "assistant:text"]);
|
|
5
|
+
const MAX_DELEGATIONS_PER_MANAGER_RUN = 6;
|
|
6
|
+
const DIRECT_TITLE_CONTEXT_LIMIT = 20;
|
|
7
|
+
function isAgentTextEvent(event) {
|
|
8
|
+
return AGENT_TEXT_TYPES.has(event.type);
|
|
9
|
+
}
|
|
10
|
+
export class OpenBotRuntime {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.managerPlugin = options.managerPlugin;
|
|
13
|
+
this.agents = new Map(options.agents.map((a) => [a.name, a]));
|
|
14
|
+
}
|
|
15
|
+
buildManagerRuntime() {
|
|
16
|
+
const builder = melony();
|
|
17
|
+
builder.use(this.managerPlugin);
|
|
18
|
+
builder.on("action:taskResult", async function* (event) {
|
|
19
|
+
yield { type: "manager:result", data: event.data };
|
|
20
|
+
});
|
|
21
|
+
return builder.build();
|
|
22
|
+
}
|
|
23
|
+
buildAgentRuntime(agent) {
|
|
24
|
+
const builder = melony();
|
|
25
|
+
builder.use(agent.plugin);
|
|
26
|
+
const name = agent.name;
|
|
27
|
+
builder.on("action:taskResult", async function* (event) {
|
|
28
|
+
yield { type: `agent:${name}:result`, data: event.data };
|
|
29
|
+
});
|
|
30
|
+
return builder.build();
|
|
31
|
+
}
|
|
32
|
+
getAgentState(agentName, sessionState) {
|
|
33
|
+
if (!sessionState.agentStates)
|
|
34
|
+
sessionState.agentStates = {};
|
|
35
|
+
if (!sessionState.agentStates[agentName]) {
|
|
36
|
+
sessionState.agentStates[agentName] = {};
|
|
37
|
+
}
|
|
38
|
+
const agentState = sessionState.agentStates[agentName];
|
|
39
|
+
if (!agentState.cwd)
|
|
40
|
+
agentState.cwd = sessionState.cwd;
|
|
41
|
+
return agentState;
|
|
42
|
+
}
|
|
43
|
+
appendSessionMessage(sessionState, message) {
|
|
44
|
+
if (!sessionState.messages)
|
|
45
|
+
sessionState.messages = [];
|
|
46
|
+
sessionState.messages.push(message);
|
|
47
|
+
if (sessionState.messages.length > DIRECT_TITLE_CONTEXT_LIMIT) {
|
|
48
|
+
sessionState.messages = sessionState.messages.slice(-DIRECT_TITLE_CONTEXT_LIMIT);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async *triggerTopicRefresh(sessionState, runId) {
|
|
52
|
+
const runtime = this.buildManagerRuntime();
|
|
53
|
+
for await (const yielded of runtime.run({
|
|
54
|
+
type: "manager:completion",
|
|
55
|
+
data: { content: "" },
|
|
56
|
+
}, { state: sessionState, runId })) {
|
|
57
|
+
yield yielded;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
estimatePlanSteps(planText) {
|
|
61
|
+
const trimmed = planText.trim();
|
|
62
|
+
if (!trimmed)
|
|
63
|
+
return undefined;
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(trimmed);
|
|
66
|
+
if (Array.isArray(parsed?.steps))
|
|
67
|
+
return parsed.steps.length;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Best-effort parse only.
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
buildManagerTaskInput(userIntent, planText) {
|
|
75
|
+
return `User intent:\n${userIntent}\n\nPlanner output (treat as proposed execution plan):\n${planText}\n\nExecute this pragmatically. Delegate concrete subtasks to relevant specialist agents when needed. Keep user-facing updates concise.`;
|
|
76
|
+
}
|
|
77
|
+
async *runPlannerAgent(content, attachments, sessionState, runId) {
|
|
78
|
+
const plannerName = "planner-agent";
|
|
79
|
+
if (!this.agents.has(plannerName)) {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
let plannerOutput = "";
|
|
83
|
+
for await (const yielded of this.runAgentInternal(plannerName, content, attachments, sessionState, runId)) {
|
|
84
|
+
if (yielded.type === `agent:${plannerName}:output`) {
|
|
85
|
+
plannerOutput = yielded.data.content || "";
|
|
86
|
+
}
|
|
87
|
+
if (!isAgentTextEvent(yielded) && yielded.type !== `agent:${plannerName}:output`) {
|
|
88
|
+
yield yielded;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return plannerOutput;
|
|
92
|
+
}
|
|
93
|
+
async *run(event, options) {
|
|
94
|
+
const { state, runId = `run_${Date.now()}` } = options;
|
|
95
|
+
yield event;
|
|
96
|
+
if (event.type === "action:approve" || event.type === "action:deny") {
|
|
97
|
+
const trace = setExecutionState(state, {
|
|
98
|
+
state: "EXECUTING",
|
|
99
|
+
error: undefined,
|
|
100
|
+
});
|
|
101
|
+
yield executionStateEvent(trace);
|
|
102
|
+
yield* this.routeApproval(event, state, runId);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (event.type === "user:text" || event.type === "user:multimodal") {
|
|
106
|
+
const rawContent = event.data.content;
|
|
107
|
+
const content = typeof rawContent === "string" ? rawContent.trim() : "";
|
|
108
|
+
const attachments = Array.isArray(event.data.attachments)
|
|
109
|
+
? event.data.attachments
|
|
110
|
+
: undefined;
|
|
111
|
+
const knownAgents = new Set(this.agents.keys());
|
|
112
|
+
const direct = parseDirectAgent(content, knownAgents);
|
|
113
|
+
const intent = classifyIntent(content, knownAgents);
|
|
114
|
+
let trace = setExecutionState(state, {
|
|
115
|
+
traceId: createTraceId(runId),
|
|
116
|
+
state: "RECEIVED",
|
|
117
|
+
intentType: intent.type,
|
|
118
|
+
error: undefined,
|
|
119
|
+
currentStepId: undefined,
|
|
120
|
+
planSteps: undefined,
|
|
121
|
+
});
|
|
122
|
+
yield executionStateEvent(trace);
|
|
123
|
+
try {
|
|
124
|
+
if (direct) {
|
|
125
|
+
this.appendSessionMessage(state, {
|
|
126
|
+
role: "user",
|
|
127
|
+
content,
|
|
128
|
+
attachments,
|
|
129
|
+
});
|
|
130
|
+
trace = setExecutionState(state, {
|
|
131
|
+
state: "EXECUTING",
|
|
132
|
+
currentStepId: `delegate:${direct.agentName}`,
|
|
133
|
+
});
|
|
134
|
+
yield executionStateEvent(trace);
|
|
135
|
+
yield* this.runAgentDirect(direct.agentName, direct.task, attachments, state, runId);
|
|
136
|
+
const finalTrace = setExecutionState(state, {
|
|
137
|
+
state: hasPendingApprovals(state) ? "WAITING_APPROVAL" : "COMPLETED",
|
|
138
|
+
currentStepId: hasPendingApprovals(state)
|
|
139
|
+
? `delegate:${direct.agentName}`
|
|
140
|
+
: undefined,
|
|
141
|
+
error: undefined,
|
|
142
|
+
});
|
|
143
|
+
yield executionStateEvent(finalTrace);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
trace = setExecutionState(state, {
|
|
147
|
+
state: "EXECUTING",
|
|
148
|
+
currentStepId: intent.type === "task" ? "planner" : "manager",
|
|
149
|
+
});
|
|
150
|
+
yield executionStateEvent(trace);
|
|
151
|
+
let managerInput = content;
|
|
152
|
+
if (intent.type === "task" && this.agents.has("planner-agent")) {
|
|
153
|
+
const plannerResult = yield* this.runPlannerAgent(content, attachments, state, runId);
|
|
154
|
+
if (plannerResult.trim()) {
|
|
155
|
+
managerInput = this.buildManagerTaskInput(content, plannerResult);
|
|
156
|
+
trace = setExecutionState(state, {
|
|
157
|
+
currentStepId: "manager",
|
|
158
|
+
planSteps: this.estimatePlanSteps(plannerResult),
|
|
159
|
+
});
|
|
160
|
+
yield executionStateEvent(trace);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
yield* this.runManagerLoop({
|
|
164
|
+
type: "manager:input",
|
|
165
|
+
data: { content: managerInput, attachments },
|
|
166
|
+
}, state, runId);
|
|
167
|
+
const waiting = hasPendingApprovals(state);
|
|
168
|
+
const finalTrace = setExecutionState(state, {
|
|
169
|
+
state: waiting ? "WAITING_APPROVAL" : "COMPLETED",
|
|
170
|
+
currentStepId: waiting ? (state.execution?.currentStepId ?? "manager") : undefined,
|
|
171
|
+
error: undefined,
|
|
172
|
+
});
|
|
173
|
+
yield executionStateEvent(finalTrace);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
const finalTrace = setExecutionState(state, {
|
|
178
|
+
state: "FAILED",
|
|
179
|
+
error: error instanceof Error ? error.message : String(error),
|
|
180
|
+
});
|
|
181
|
+
yield executionStateEvent(finalTrace);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
yield* this.runManagerLoop(event, state, runId);
|
|
186
|
+
}
|
|
187
|
+
async *runManagerLoop(event, state, runId) {
|
|
188
|
+
const runtime = this.buildManagerRuntime();
|
|
189
|
+
const delegationSignatures = new Set();
|
|
190
|
+
let delegationCount = 0;
|
|
191
|
+
let nextManagerEvent = event;
|
|
192
|
+
while (nextManagerEvent) {
|
|
193
|
+
const managerEvent = nextManagerEvent;
|
|
194
|
+
nextManagerEvent = undefined;
|
|
195
|
+
for await (const yielded of runtime.run(managerEvent, { state, runId })) {
|
|
196
|
+
if (yielded.type !== "action:delegateTask") {
|
|
197
|
+
yield yielded;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const { agent: agentName, task, attachments, toolCallId } = yielded.data;
|
|
201
|
+
const normalizedTask = typeof task === "string"
|
|
202
|
+
? task.replace(/\s+/g, " ").trim().toLowerCase()
|
|
203
|
+
: "";
|
|
204
|
+
const signature = `${agentName}::${normalizedTask}`;
|
|
205
|
+
if (delegationCount >= MAX_DELEGATIONS_PER_MANAGER_RUN ||
|
|
206
|
+
(normalizedTask && delegationSignatures.has(signature))) {
|
|
207
|
+
nextManagerEvent = {
|
|
208
|
+
type: "manager:result",
|
|
209
|
+
data: {
|
|
210
|
+
action: "delegateTask",
|
|
211
|
+
toolCallId,
|
|
212
|
+
result: `Error: delegation loop detected for agent "${agentName}". Summarize current progress and stop delegating.`,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
if (!this.agents.has(agentName)) {
|
|
218
|
+
nextManagerEvent = {
|
|
219
|
+
type: "manager:result",
|
|
220
|
+
data: {
|
|
221
|
+
action: "delegateTask",
|
|
222
|
+
toolCallId,
|
|
223
|
+
result: `Error: Agent "${agentName}" not found`,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
delegationCount += 1;
|
|
229
|
+
if (normalizedTask)
|
|
230
|
+
delegationSignatures.add(signature);
|
|
231
|
+
if (!state.pendingAgentTasks)
|
|
232
|
+
state.pendingAgentTasks = {};
|
|
233
|
+
state.pendingAgentTasks[agentName] = { toolCallId };
|
|
234
|
+
let agentOutput = "";
|
|
235
|
+
let agentCompleted = false;
|
|
236
|
+
try {
|
|
237
|
+
for await (const agentEvent of this.runAgentInternal(agentName, task, attachments, state, runId)) {
|
|
238
|
+
if (agentEvent.type === `agent:${agentName}:output`) {
|
|
239
|
+
agentOutput = agentEvent.data.content;
|
|
240
|
+
agentCompleted = true;
|
|
241
|
+
}
|
|
242
|
+
if (!isAgentTextEvent(agentEvent)) {
|
|
243
|
+
yield agentEvent;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
agentOutput = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
249
|
+
agentCompleted = true;
|
|
250
|
+
}
|
|
251
|
+
if (agentCompleted) {
|
|
252
|
+
delete state.pendingAgentTasks[agentName];
|
|
253
|
+
nextManagerEvent = {
|
|
254
|
+
type: "manager:result",
|
|
255
|
+
data: {
|
|
256
|
+
action: "delegateTask",
|
|
257
|
+
toolCallId,
|
|
258
|
+
result: agentOutput,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async *runAgentInternal(agentName, task, attachments, sessionState, runId) {
|
|
267
|
+
const agent = this.agents.get(agentName);
|
|
268
|
+
const agentState = this.getAgentState(agentName, sessionState);
|
|
269
|
+
const runtime = this.buildAgentRuntime(agent);
|
|
270
|
+
const inputEvent = {
|
|
271
|
+
type: `agent:${agentName}:input`,
|
|
272
|
+
data: { content: task, attachments },
|
|
273
|
+
};
|
|
274
|
+
for await (const yielded of runtime.run(inputEvent, {
|
|
275
|
+
state: agentState,
|
|
276
|
+
runId,
|
|
277
|
+
})) {
|
|
278
|
+
yield yielded;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async *runAgentDirect(agentName, task, attachments, sessionState, runId) {
|
|
282
|
+
for await (const yielded of this.runAgentInternal(agentName, task, attachments, sessionState, runId)) {
|
|
283
|
+
if (yielded.type === `agent:${agentName}:output`) {
|
|
284
|
+
const content = yielded.data.content;
|
|
285
|
+
this.appendSessionMessage(sessionState, {
|
|
286
|
+
role: "assistant",
|
|
287
|
+
content,
|
|
288
|
+
});
|
|
289
|
+
yield {
|
|
290
|
+
type: "assistant:text",
|
|
291
|
+
data: { content },
|
|
292
|
+
meta: { agent: agentName },
|
|
293
|
+
};
|
|
294
|
+
yield* this.triggerTopicRefresh(sessionState, runId);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
yield yielded;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async *routeApproval(event, state, runId) {
|
|
302
|
+
const approvalId = event.data.id;
|
|
303
|
+
const agentStates = state.agentStates || {};
|
|
304
|
+
let targetAgent;
|
|
305
|
+
for (const [name, agentState] of Object.entries(agentStates)) {
|
|
306
|
+
if (agentState.pendingApprovals?.[approvalId]) {
|
|
307
|
+
targetAgent = name;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!targetAgent) {
|
|
312
|
+
console.warn("[openbot-runtime] No agent found for approval event:", approvalId);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const agent = this.agents.get(targetAgent);
|
|
316
|
+
const agentState = this.getAgentState(targetAgent, state);
|
|
317
|
+
const runtime = this.buildAgentRuntime(agent);
|
|
318
|
+
let agentOutput = "";
|
|
319
|
+
let agentCompleted = false;
|
|
320
|
+
const isDelegation = !!state.pendingAgentTasks?.[targetAgent];
|
|
321
|
+
for await (const yielded of runtime.run(event, {
|
|
322
|
+
state: agentState,
|
|
323
|
+
runId,
|
|
324
|
+
})) {
|
|
325
|
+
if (yielded.type === `agent:${targetAgent}:output`) {
|
|
326
|
+
agentOutput = yielded.data.content;
|
|
327
|
+
agentCompleted = true;
|
|
328
|
+
}
|
|
329
|
+
if (isDelegation && isAgentTextEvent(yielded)) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
yield yielded;
|
|
333
|
+
}
|
|
334
|
+
if (agentCompleted && state.pendingAgentTasks?.[targetAgent]) {
|
|
335
|
+
const { toolCallId } = state.pendingAgentTasks[targetAgent];
|
|
336
|
+
delete state.pendingAgentTasks[targetAgent];
|
|
337
|
+
yield* this.runManagerLoop({
|
|
338
|
+
type: "manager:result",
|
|
339
|
+
data: {
|
|
340
|
+
action: "delegateTask",
|
|
341
|
+
toolCallId,
|
|
342
|
+
result: agentOutput,
|
|
343
|
+
},
|
|
344
|
+
}, state, runId);
|
|
345
|
+
}
|
|
346
|
+
const waiting = hasPendingApprovals(state);
|
|
347
|
+
const trace = setExecutionState(state, {
|
|
348
|
+
state: waiting ? "WAITING_APPROVAL" : "COMPLETED",
|
|
349
|
+
currentStepId: waiting ? state.execution?.currentStepId : undefined,
|
|
350
|
+
error: undefined,
|
|
351
|
+
});
|
|
352
|
+
yield executionStateEvent(trace);
|
|
353
|
+
}
|
|
354
|
+
}
|