openbot 0.2.3 → 0.2.6

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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/agent-creator.js +58 -19
  3. package/dist/agents/os-agent.js +1 -4
  4. package/dist/agents/planner-agent.js +32 -0
  5. package/dist/agents/topic-agent.js +1 -1
  6. package/dist/architecture/contracts.js +1 -0
  7. package/dist/architecture/execution-engine.js +151 -0
  8. package/dist/architecture/intent-classifier.js +26 -0
  9. package/dist/architecture/planner.js +106 -0
  10. package/dist/automation-worker.js +121 -0
  11. package/dist/automations.js +52 -0
  12. package/dist/cli.js +116 -146
  13. package/dist/config.js +20 -0
  14. package/dist/core/agents.js +41 -0
  15. package/dist/core/delegation.js +124 -0
  16. package/dist/core/manager.js +73 -0
  17. package/dist/core/plugins.js +77 -0
  18. package/dist/core/router.js +40 -0
  19. package/dist/installers.js +156 -0
  20. package/dist/marketplace.js +80 -0
  21. package/dist/open-bot.js +34 -157
  22. package/dist/orchestrator.js +247 -51
  23. package/dist/plugins/approval/index.js +107 -3
  24. package/dist/plugins/brain/index.js +17 -86
  25. package/dist/plugins/brain/memory.js +1 -1
  26. package/dist/plugins/brain/prompt.js +8 -13
  27. package/dist/plugins/brain/types.js +0 -15
  28. package/dist/plugins/file-system/index.js +8 -8
  29. package/dist/plugins/llm/context-shaping.js +177 -0
  30. package/dist/plugins/llm/index.js +223 -49
  31. package/dist/plugins/memory/index.js +220 -0
  32. package/dist/plugins/memory/memory.js +122 -0
  33. package/dist/plugins/memory/prompt.js +55 -0
  34. package/dist/plugins/memory/types.js +45 -0
  35. package/dist/plugins/shell/index.js +3 -3
  36. package/dist/plugins/skills/index.js +9 -9
  37. package/dist/registry/index.js +1 -4
  38. package/dist/registry/plugin-loader.js +361 -56
  39. package/dist/registry/plugin-registry.js +21 -4
  40. package/dist/registry/ts-agent-loader.js +4 -4
  41. package/dist/registry/yaml-agent-loader.js +78 -20
  42. package/dist/runtime/execution-trace.js +41 -0
  43. package/dist/runtime/intent-routing.js +26 -0
  44. package/dist/runtime/openbot-runtime.js +354 -0
  45. package/dist/server.js +513 -41
  46. package/dist/ui/widgets/approval-card.js +22 -2
  47. package/dist/ui/widgets/delegation.js +29 -0
  48. package/dist/version.js +62 -0
  49. package/package.json +4 -1
@@ -1,11 +1,11 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import yaml from "js-yaml";
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.yaml file from a directory.
29
+ * Read and parse an agent configuration from AGENT.md with frontmatter.
30
30
  */
31
31
  export async function readAgentConfig(agentDir) {
32
- const yamlPath = path.join(agentDir, "agent.yaml");
33
- const content = await fs.readFile(yamlPath, "utf-8");
34
- return yaml.load(content);
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 `agent.yaml` file,
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 || !config.plugins?.length || !config.systemPrompt) {
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
- const { plugin, toolDefinitions } = composeAgentFromYaml(config, scopedRegistry, agentModel);
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
- // Invalid or missing agent.yaml — silently skip if missing, warn if invalid
200
+ // Skip invalid agents
140
201
  if (err.code !== 'ENOENT') {
141
- console.warn(`[agents] Error loading "${entry.name}/agent.yaml":`, err);
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 a YAML agent configuration.
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 composeAgentFromYaml(config, pluginRegistry, model) {
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
+ }