openbot 0.2.11 → 0.2.13
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/.prettierrc +8 -0
- package/AGENTS.md +68 -0
- package/CONTRIBUTING.md +74 -0
- package/LICENSE +21 -0
- package/README.md +117 -14
- package/dist/agents/system.js +106 -0
- package/dist/app/cli.js +27 -0
- package/dist/app/config.js +64 -0
- package/dist/app/server.js +237 -0
- package/dist/app/utils.js +35 -0
- package/dist/harness/agent-harness.js +45 -0
- package/dist/harness/mcp.js +61 -0
- package/dist/harness/orchestrator.js +273 -0
- package/dist/harness/process.js +7 -0
- package/dist/plugins/ai-sdk.js +141 -0
- package/dist/plugins/delegation.js +52 -0
- package/dist/plugins/mcp.js +140 -0
- package/dist/plugins/storage.js +502 -0
- package/dist/plugins/ui.js +47 -0
- package/dist/registry/plugins.js +73 -0
- package/dist/services/storage.js +724 -0
- package/docs/README.md +7 -0
- package/docs/agents.md +83 -0
- package/docs/architecture.md +34 -0
- package/docs/plugins.md +77 -0
- package/logo-black.png +0 -0
- package/{dist/assets/logo.js → logo-black.svg} +24 -24
- package/{dist/ui/sidebar.js → logo-white.svg} +23 -88
- package/package.json +10 -9
- package/src/agents/system.ts +112 -0
- package/src/app/cli.ts +38 -0
- package/src/app/config.ts +104 -0
- package/src/app/server.ts +284 -0
- package/src/app/types.ts +476 -0
- package/src/app/utils.ts +43 -0
- package/src/assets/icon.svg +1 -0
- package/src/harness/agent-harness.ts +58 -0
- package/src/harness/mcp.ts +78 -0
- package/src/harness/orchestrator.ts +342 -0
- package/src/harness/process.ts +9 -0
- package/src/harness/types.ts +34 -0
- package/src/plugins/ai-sdk.ts +197 -0
- package/src/plugins/delegation.ts +60 -0
- package/src/plugins/mcp.ts +154 -0
- package/src/plugins/storage.ts +725 -0
- package/src/plugins/ui.ts +57 -0
- package/src/registry/plugins.ts +85 -0
- package/src/services/storage.ts +957 -0
- package/tsconfig.json +18 -0
- package/dist/agents/agent-creator.js +0 -74
- package/dist/agents/browser-agent.js +0 -31
- package/dist/agents/os-agent.js +0 -32
- package/dist/agents/planner-agent.js +0 -32
- package/dist/agents/topic-agent.js +0 -46
- package/dist/architecture/execution-engine.js +0 -151
- package/dist/architecture/intent-classifier.js +0 -26
- package/dist/architecture/planner.js +0 -106
- package/dist/automation-worker.js +0 -121
- package/dist/automations.js +0 -52
- package/dist/cli.js +0 -275
- package/dist/config.js +0 -53
- package/dist/core/agents.js +0 -41
- package/dist/core/delegation.js +0 -230
- package/dist/core/manager.js +0 -96
- package/dist/core/plugins.js +0 -74
- package/dist/core/router.js +0 -191
- package/dist/handlers/init.js +0 -29
- package/dist/handlers/session-change.js +0 -21
- package/dist/handlers/settings.js +0 -47
- package/dist/handlers/tab-change.js +0 -14
- package/dist/installers.js +0 -156
- package/dist/marketplace.js +0 -80
- package/dist/model-catalog.js +0 -132
- package/dist/model-defaults.js +0 -25
- package/dist/models.js +0 -47
- package/dist/open-bot.js +0 -51
- package/dist/orchestrator/direct-invocation.js +0 -13
- package/dist/orchestrator/events.js +0 -36
- package/dist/orchestrator/state.js +0 -54
- package/dist/orchestrator.js +0 -422
- package/dist/plugins/agent/index.js +0 -81
- package/dist/plugins/approval/index.js +0 -100
- package/dist/plugins/brain/identity.js +0 -77
- package/dist/plugins/brain/index.js +0 -204
- package/dist/plugins/brain/memory.js +0 -120
- package/dist/plugins/brain/prompt.js +0 -46
- package/dist/plugins/brain/types.js +0 -45
- package/dist/plugins/brain/ui.js +0 -7
- package/dist/plugins/browser/index.js +0 -629
- package/dist/plugins/browser/ui.js +0 -13
- package/dist/plugins/file-system/index.js +0 -171
- package/dist/plugins/file-system/ui.js +0 -6
- package/dist/plugins/llm/context-budget.js +0 -139
- package/dist/plugins/llm/context-shaping.js +0 -177
- package/dist/plugins/llm/index.js +0 -380
- package/dist/plugins/memory/index.js +0 -220
- package/dist/plugins/memory/memory.js +0 -122
- package/dist/plugins/memory/prompt.js +0 -55
- package/dist/plugins/memory/types.js +0 -45
- package/dist/plugins/meta-agent/index.js +0 -570
- package/dist/plugins/meta-agent/ui.js +0 -11
- package/dist/plugins/shell/index.js +0 -100
- package/dist/plugins/shell/ui.js +0 -6
- package/dist/plugins/skills/index.js +0 -286
- package/dist/plugins/skills/types.js +0 -50
- package/dist/plugins/skills/ui.js +0 -12
- package/dist/registry/agent-registry.js +0 -35
- package/dist/registry/index.js +0 -2
- package/dist/registry/plugin-loader.js +0 -499
- package/dist/registry/plugin-registry.js +0 -44
- package/dist/registry/ts-agent-loader.js +0 -82
- package/dist/registry/yaml-agent-loader.js +0 -246
- package/dist/runtime/execution-trace.js +0 -41
- package/dist/runtime/intent-routing.js +0 -26
- package/dist/runtime/openbot-runtime.js +0 -354
- package/dist/server.js +0 -890
- package/dist/session.js +0 -179
- package/dist/ui/block.js +0 -12
- package/dist/ui/header.js +0 -52
- package/dist/ui/layout.js +0 -26
- package/dist/ui/navigation.js +0 -15
- package/dist/ui/settings.js +0 -106
- package/dist/ui/skills.js +0 -7
- package/dist/ui/thread.js +0 -16
- package/dist/ui/widgets/action-list.js +0 -2
- package/dist/ui/widgets/approval-card.js +0 -9
- package/dist/ui/widgets/code-snippet.js +0 -2
- package/dist/ui/widgets/data-block.js +0 -2
- package/dist/ui/widgets/data-table.js +0 -2
- package/dist/ui/widgets/delegation.js +0 -29
- package/dist/ui/widgets/empty-state.js +0 -2
- package/dist/ui/widgets/index.js +0 -23
- package/dist/ui/widgets/inquiry.js +0 -7
- package/dist/ui/widgets/key-value.js +0 -2
- package/dist/ui/widgets/progress-step.js +0 -2
- package/dist/ui/widgets/resource-card.js +0 -2
- package/dist/ui/widgets/status.js +0 -2
- package/dist/ui/widgets/todo-list.js +0 -2
- package/dist/version.js +0 -62
- /package/dist/{types.js → app/types.js} +0 -0
- /package/dist/{architecture/contracts.js → harness/types.js} +0 -0
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import matter from "gray-matter";
|
|
4
|
-
import { llmPlugin } from "../plugins/llm/index.js";
|
|
5
|
-
import { PluginRegistry } from "./plugin-registry.js";
|
|
6
|
-
import { createModel } from "../models.js";
|
|
7
|
-
import { loadPluginsFromDir } from "./plugin-loader.js";
|
|
8
|
-
import { resolvePath, DEFAULT_AGENT_MD } from "../config.js";
|
|
9
|
-
/**
|
|
10
|
-
* Recursively resolve tilde paths in a configuration object.
|
|
11
|
-
*/
|
|
12
|
-
function resolveConfigPaths(config) {
|
|
13
|
-
if (typeof config === "string") {
|
|
14
|
-
return resolvePath(config);
|
|
15
|
-
}
|
|
16
|
-
if (Array.isArray(config)) {
|
|
17
|
-
return config.map(resolveConfigPaths);
|
|
18
|
-
}
|
|
19
|
-
if (config !== null && typeof config === "object") {
|
|
20
|
-
const resolved = {};
|
|
21
|
-
for (const [key, value] of Object.entries(config)) {
|
|
22
|
-
resolved[key] = resolveConfigPaths(value);
|
|
23
|
-
}
|
|
24
|
-
return resolved;
|
|
25
|
-
}
|
|
26
|
-
return config;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Read and parse an agent configuration from AGENT.md with frontmatter.
|
|
30
|
-
*/
|
|
31
|
-
export async function readAgentConfig(agentDir) {
|
|
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
|
-
};
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Discover YAML-defined agents from a directory without loading plugins.
|
|
55
|
-
*
|
|
56
|
-
* @param agentsDir Absolute path to the agents directory (e.g. ~/.openbot/agents)
|
|
57
|
-
* @returns Array of agent metadata
|
|
58
|
-
*/
|
|
59
|
-
export async function listYamlAgents(agentsDir) {
|
|
60
|
-
const agents = [];
|
|
61
|
-
const seenNames = new Set();
|
|
62
|
-
try {
|
|
63
|
-
const entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
64
|
-
for (const entry of entries) {
|
|
65
|
-
if (!entry.isDirectory())
|
|
66
|
-
continue;
|
|
67
|
-
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
68
|
-
continue;
|
|
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
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
const config = await readAgentConfig(agentDir);
|
|
99
|
-
if (config.name && config.description && !seenNames.has(config.name)) {
|
|
100
|
-
agents.push({
|
|
101
|
-
name: config.name,
|
|
102
|
-
description: config.description,
|
|
103
|
-
folder: agentDir,
|
|
104
|
-
});
|
|
105
|
-
seenNames.add(config.name);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// Skip invalid agents
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// Agents directory doesn't exist
|
|
115
|
-
}
|
|
116
|
-
return agents;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Discover and load YAML-defined agents from a directory.
|
|
120
|
-
*
|
|
121
|
-
* Scans `agentsDir` for subdirectories containing an `AGENT.md` file,
|
|
122
|
-
* parses each one, and composes a Melony plugin from the referenced plugins.
|
|
123
|
-
*
|
|
124
|
-
* @param agentsDir Absolute path to the agents directory (e.g. ~/.openbot/agents)
|
|
125
|
-
* @param pluginRegistry Registry of available plugins
|
|
126
|
-
* @param defaultModel Language model to use for agent LLMs if not specified in YAML
|
|
127
|
-
* @param options Optional API keys for creating specific models
|
|
128
|
-
* @returns Array of discovered agent entries ready for registration
|
|
129
|
-
*/
|
|
130
|
-
export async function discoverYamlAgents(agentsDir, pluginRegistry, defaultModel, options) {
|
|
131
|
-
const agents = [];
|
|
132
|
-
const seenNames = new Set();
|
|
133
|
-
// Ensure the agents directory exists
|
|
134
|
-
try {
|
|
135
|
-
await fs.mkdir(agentsDir, { recursive: true });
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
// Best effort
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
const entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
142
|
-
for (const entry of entries) {
|
|
143
|
-
if (!entry.isDirectory())
|
|
144
|
-
continue;
|
|
145
|
-
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
146
|
-
continue;
|
|
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;
|
|
153
|
-
try {
|
|
154
|
-
const config = await readAgentConfig(agentDir);
|
|
155
|
-
// Validate required fields and avoid duplicates
|
|
156
|
-
if (!config.name || !config.description || seenNames.has(config.name)) {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
seenNames.add(config.name);
|
|
160
|
-
const agentModel = config.model
|
|
161
|
-
? createModel({ ...options, model: config.model })
|
|
162
|
-
: defaultModel;
|
|
163
|
-
// 1. Load local plugins from agents/<name>/plugins/
|
|
164
|
-
const localPluginsDir = path.join(agentDir, "plugins");
|
|
165
|
-
const localPlugins = await loadPluginsFromDir(localPluginsDir);
|
|
166
|
-
// 2. Create a scoped registry for this agent: global + local
|
|
167
|
-
const scopedRegistry = new PluginRegistry();
|
|
168
|
-
// Add all global plugins
|
|
169
|
-
for (const p of pluginRegistry.getAll()) {
|
|
170
|
-
scopedRegistry.register(p);
|
|
171
|
-
}
|
|
172
|
-
// Add local plugins (overwriting globals if names conflict)
|
|
173
|
-
for (const p of localPlugins) {
|
|
174
|
-
scopedRegistry.register(p);
|
|
175
|
-
}
|
|
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);
|
|
187
|
-
agents.push({
|
|
188
|
-
name: config.name,
|
|
189
|
-
description: config.description,
|
|
190
|
-
plugin,
|
|
191
|
-
capabilities: Object.fromEntries(Object.entries(toolDefinitions).map(([name, def]) => [
|
|
192
|
-
name,
|
|
193
|
-
def.description,
|
|
194
|
-
])),
|
|
195
|
-
subscribe: config.subscribe,
|
|
196
|
-
});
|
|
197
|
-
console.log(`[agents] Loaded: ${config.name} — ${config.description}${config.model ? ` (model: ${config.model})` : ""}`);
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
// Skip invalid agents
|
|
201
|
-
if (err.code !== 'ENOENT') {
|
|
202
|
-
console.warn(`[agents] Error loading "${entry.name}":`, err);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
// Agents directory doesn't exist or can't be read — that's fine
|
|
209
|
-
}
|
|
210
|
-
return agents;
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Compose a Melony plugin from an agent configuration.
|
|
214
|
-
*
|
|
215
|
-
* Resolves each plugin name against the registry, collects their tool definitions,
|
|
216
|
-
* and wires them with an agent-scoped LLM plugin.
|
|
217
|
-
*/
|
|
218
|
-
function composeAgentFromConfig(config, pluginRegistry, model) {
|
|
219
|
-
const allToolDefinitions = {};
|
|
220
|
-
const pluginFactories = [];
|
|
221
|
-
for (const pluginItem of config.plugins) {
|
|
222
|
-
const isString = typeof pluginItem === "string";
|
|
223
|
-
const pluginName = isString ? pluginItem : pluginItem.name;
|
|
224
|
-
const pluginConfig = isString ? {} : (pluginItem.config || {});
|
|
225
|
-
const resolvedConfig = resolveConfigPaths(pluginConfig);
|
|
226
|
-
const entry = pluginRegistry.get(pluginName);
|
|
227
|
-
if (!entry) {
|
|
228
|
-
console.warn(`[agents] "${config.name}": plugin "${pluginName}" not found in registry — skipping`);
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
pluginFactories.push({ factory: entry.factory, config: resolvedConfig });
|
|
232
|
-
Object.assign(allToolDefinitions, entry.toolDefinitions);
|
|
233
|
-
}
|
|
234
|
-
const plugin = (builder) => {
|
|
235
|
-
for (const { factory, config: resolvedConfig } of pluginFactories) {
|
|
236
|
-
builder.use(factory({ ...resolvedConfig, model }));
|
|
237
|
-
}
|
|
238
|
-
// Wire up the LLM with agent-scoped event channels
|
|
239
|
-
builder.use(llmPlugin({
|
|
240
|
-
model,
|
|
241
|
-
system: config.systemPrompt,
|
|
242
|
-
toolDefinitions: allToolDefinitions,
|
|
243
|
-
}));
|
|
244
|
-
};
|
|
245
|
-
return { plugin, toolDefinitions: allToolDefinitions };
|
|
246
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,354 +0,0 @@
|
|
|
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
|
-
}
|