openbot 0.2.9 → 0.2.11

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