@meetopenbot/openbot 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.js CHANGED
@@ -3,6 +3,7 @@ import { eventsToModelMessages } from './history.js';
3
3
  import { buildContext } from './context.js';
4
4
  import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
5
5
  import { isAuthErrorMessage, isCreditsErrorMessage } from './credits-auth.js';
6
+ import { AUTO_MODEL_ID, isAutoModel, isRetryableModelError, listAutoModelCandidates, } from './auto-model.js';
6
7
  import { resolveModel } from './model.js';
7
8
  import { fetchModelRegistry, getProviderModelOptions, listApiKeyProviders, PROVIDER_API_KEY_LINKS, } from './model-registry.js';
8
9
  async function buildSystemPrompt(state, storage) {
@@ -68,10 +69,8 @@ function createToolBatchTracker(state, storage, channelId, threadId) {
68
69
  * - When a full batch of results is in, `runLLM` runs again with updated history.
69
70
  */
70
71
  export const openbotRuntime = (options) => (builder) => {
71
- const { model: modelString = 'openai/gpt-4o-mini', authMode = 'byok', agentId, storage, toolDefinitions = {}, abortSignal, host, } = options;
72
- let currentModelString = modelString;
73
- const resolveCurrentModel = () => resolveModel(currentModelString, { authMode });
74
- let model = resolveCurrentModel();
72
+ const { model: modelString = AUTO_MODEL_ID, authMode = 'byok', agentId, storage, toolDefinitions = {}, abortSignal, host, } = options;
73
+ let configuredModelString = modelString;
75
74
  const isCreditsCloudAgent = (id) => host.isCloudSystemAgent(id ?? '') && authMode === 'credits';
76
75
  const runLLM = async function* (context, threadId, trigger) {
77
76
  if (!storage)
@@ -84,27 +83,48 @@ export const openbotRuntime = (options) => (builder) => {
84
83
  const parentAgentId = triggerEvent?.meta?.parentAgentId;
85
84
  const parentToolCallId = triggerEvent?.meta?.parentToolCallId;
86
85
  const state = context.state;
87
- state.model = currentModelString;
86
+ state.model = configuredModelString;
88
87
  const systemPrompt = await buildSystemPrompt(context.state, storage);
89
88
  const events = await storage.getEvents({
90
89
  channelId: context.state.channelId,
91
90
  threadId: context.state.threadId,
92
91
  });
93
92
  const messages = eventsToModelMessages(events);
94
- // console.log('systemPrompt:::::::\n', systemPrompt);
95
- // console.log('messages:::::::\n', JSON.stringify(messages));
96
- // console.log('toolDefinitions:::::::\n', JSON.stringify(toolDefinitions));
93
+ const candidates = isAutoModel(configuredModelString)
94
+ ? listAutoModelCandidates({ authMode })
95
+ : [configuredModelString];
97
96
  try {
98
97
  // Single LLM request — tool execution happens externally via action:* handlers.
99
- const result = await generateText({
100
- model,
101
- system: systemPrompt,
102
- messages,
103
- tools: toolDefinitions,
104
- stopWhen: ({ steps }) => steps.length === 1,
105
- allowSystemInMessages: true,
106
- abortSignal,
107
- });
98
+ const generate = async () => {
99
+ let lastError;
100
+ for (let i = 0; i < candidates.length; i++) {
101
+ try {
102
+ return await generateText({
103
+ model: resolveModel(candidates[i], { authMode }),
104
+ system: systemPrompt,
105
+ messages,
106
+ tools: toolDefinitions,
107
+ stopWhen: ({ steps }) => steps.length === 1,
108
+ allowSystemInMessages: true,
109
+ abortSignal,
110
+ });
111
+ }
112
+ catch (error) {
113
+ lastError = error;
114
+ if (abortSignal?.aborted)
115
+ throw error;
116
+ const errorMessage = error instanceof Error ? error.message : String(error);
117
+ if (isCreditsErrorMessage(errorMessage) || isAuthErrorMessage(errorMessage)) {
118
+ throw error;
119
+ }
120
+ if (!isRetryableModelError(error) || i === candidates.length - 1) {
121
+ throw error;
122
+ }
123
+ }
124
+ }
125
+ throw lastError ?? new Error('Model request failed.');
126
+ };
127
+ const result = await generate();
108
128
  const toolCalls = result.toolCalls ?? [];
109
129
  // if (result.usage) {
110
130
  // const usage = result.usage;
@@ -252,8 +272,9 @@ export const openbotRuntime = (options) => (builder) => {
252
272
  return;
253
273
  if (metadata?.type === 'api_provider_selection') {
254
274
  const provider = actionId;
255
- const [_, ...rest] = currentModelString.split('/');
256
- const currentModelId = rest.join('/');
275
+ const configuredModelId = isAutoModel(configuredModelString)
276
+ ? ''
277
+ : configuredModelString.split('/').slice(1).join('/');
257
278
  const registry = await fetchModelRegistry();
258
279
  const providerData = registry?.providers?.[provider];
259
280
  const label = providerData?.label || provider;
@@ -273,8 +294,8 @@ export const openbotRuntime = (options) => (builder) => {
273
294
  },
274
295
  meta: { agentId: context.state.agentId, threadId },
275
296
  };
276
- if (modelOptions && modelOptions.length > 0) {
277
- const defaultModelId = modelOptions.find((m) => m.value === currentModelId)?.value || modelOptions[0].value;
297
+ if (modelOptions && modelOptions.length > 0 && !isAutoModel(configuredModelString)) {
298
+ const defaultModelId = modelOptions.find((m) => m.value === configuredModelId)?.value || modelOptions[0].value;
278
299
  yield {
279
300
  type: 'client:ui:widget',
280
301
  data: {
@@ -302,23 +323,29 @@ export const openbotRuntime = (options) => (builder) => {
302
323
  kind: 'form',
303
324
  widgetId: `api_key_request_${Date.now()}`,
304
325
  title: `${label} Setup`,
305
- description: `Enter your API key and model ID.`,
326
+ description: isAutoModel(configuredModelString)
327
+ ? `Enter your API key to continue.`
328
+ : `Enter your API key and model ID.`,
306
329
  fields: [
307
- {
308
- id: 'model',
309
- label: 'Model',
310
- type: 'text',
311
- description: 'Enter the model ID for this provider.',
312
- placeholder: provider === 'openai'
313
- ? 'gpt-4o-mini'
314
- : provider === 'anthropic'
315
- ? 'claude-3-5-sonnet-20241022'
316
- : provider === 'deepseek'
317
- ? 'deepseek-chat'
318
- : 'gemini-2.0-flash',
319
- required: true,
320
- defaultValue: currentModelId || '',
321
- },
330
+ ...(isAutoModel(configuredModelString)
331
+ ? []
332
+ : [
333
+ {
334
+ id: 'model',
335
+ label: 'Model',
336
+ type: 'text',
337
+ description: 'Enter the model ID for this provider.',
338
+ placeholder: provider === 'openai'
339
+ ? 'gpt-4o-mini'
340
+ : provider === 'anthropic'
341
+ ? 'claude-3-5-sonnet-20241022'
342
+ : provider === 'deepseek'
343
+ ? 'deepseek-chat'
344
+ : 'gemini-2.0-flash',
345
+ required: true,
346
+ defaultValue: configuredModelId || '',
347
+ },
348
+ ]),
322
349
  {
323
350
  id: 'apiKey',
324
351
  label: 'API Key',
@@ -393,8 +420,9 @@ export const openbotRuntime = (options) => (builder) => {
393
420
  if (!values?.apiKey)
394
421
  return;
395
422
  const provider = String(values.provider || metadata.provider);
423
+ const keepAuto = isAutoModel(configuredModelString);
396
424
  const modelId = String(values.model || metadata.model || '').trim();
397
- if (!modelId)
425
+ if (!keepAuto && !modelId)
398
426
  return;
399
427
  const apiKey = String(values.apiKey);
400
428
  if (provider !== 'openai' &&
@@ -415,16 +443,19 @@ export const openbotRuntime = (options) => (builder) => {
415
443
  : provider === 'deepseek'
416
444
  ? 'DEEPSEEK_API_KEY'
417
445
  : 'GOOGLE_GENERATIVE_AI_API_KEY';
418
- const newModelString = `${provider}/${modelId}`;
446
+ const newModelString = keepAuto ? AUTO_MODEL_ID : `${provider}/${modelId}`;
419
447
  if (!storage)
420
448
  return;
421
449
  try {
422
450
  await storage.createVariable({ key: envVar, value: apiKey, secret: true });
423
451
  process.env[envVar] = apiKey;
424
- currentModelString = newModelString;
425
- model = resolveCurrentModel();
452
+ if (!keepAuto) {
453
+ configuredModelString = newModelString;
454
+ }
426
455
  try {
427
- host.saveConfig({ model: currentModelString });
456
+ if (!keepAuto) {
457
+ host.saveConfig({ model: configuredModelString });
458
+ }
428
459
  const details = await storage.getAgentDetails({ agentId: context.state.agentId });
429
460
  const updatedPlugins = details.pluginRefs.map((ref) => {
430
461
  if (ref.id === host.openbotPluginId) {
@@ -432,7 +463,7 @@ export const openbotRuntime = (options) => (builder) => {
432
463
  ...ref,
433
464
  config: {
434
465
  ...ref.config,
435
- model: currentModelString,
466
+ ...(keepAuto ? {} : { model: configuredModelString }),
436
467
  ...(host.isCloudSystemAgent(context.state.agentId) ? { authMode: 'byok' } : {}),
437
468
  },
438
469
  };
@@ -450,7 +481,9 @@ export const openbotRuntime = (options) => (builder) => {
450
481
  yield {
451
482
  type: 'agent:output',
452
483
  data: {
453
- content: `Saved ${provider} API key and set model to \`${newModelString}\`.`,
484
+ content: keepAuto
485
+ ? `Saved ${provider} API key. Auto will use it when that provider is the cheapest available.`
486
+ : `Saved ${provider} API key and set model to \`${newModelString}\`.`,
454
487
  },
455
488
  meta: { agentId: context.state.agentId },
456
489
  };
@@ -460,7 +493,9 @@ export const openbotRuntime = (options) => (builder) => {
460
493
  widgetId: event.data.widgetId,
461
494
  kind: 'message',
462
495
  title: 'API Key Saved',
463
- body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
496
+ body: keepAuto
497
+ ? `Successfully saved ${provider} API key. You can now continue your conversation.`
498
+ : `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
464
499
  state: 'submitted',
465
500
  display: 'collapsed',
466
501
  disabled: true,
@@ -0,0 +1,22 @@
1
+ /** Default inbox Space. Wire id stays `general` (channelId). */
2
+ export const GENERAL_SPACE_ID = "general";
3
+ /** Dummy ids models invent when they feel forced to "place" work that has no Space. */
4
+ const PLACEHOLDER_SPACE_IDS = new Set([
5
+ "noop",
6
+ "none",
7
+ "null",
8
+ "n-a",
9
+ "na",
10
+ "dummy",
11
+ "placeholder",
12
+ "untitled",
13
+ "misc",
14
+ "temp",
15
+ "tmp",
16
+ ]);
17
+ export function isPlaceholderSpaceId(spaceId) {
18
+ return PLACEHOLDER_SPACE_IDS.has(spaceId);
19
+ }
20
+ export function placeholderSpaceError(spaceId) {
21
+ return `Refused to create #${spaceId}: that looks like a placeholder, not a real Space. Stay in #general for questions and listings. If project work needs a new Space, propose a concrete name and wait for the human to agree.`;
22
+ }
@@ -1,57 +1,54 @@
1
1
  export const OPENBOT_SYSTEM_PROMPT = [
2
- '# ROLE',
3
- 'You are an OpenBot, the main coordinator and router agent. Your primary role is to orchestrate specialized agents and manage tasks methodically to help the human achieve their goals.',
4
- '',
5
- '# SECURITY POLICY',
6
- '- **CRITICAL**: Never request API keys, passwords, or sensitive credentials via text or UI widgets; these are managed deterministically via secure forms and must never enter your context.',
2
+ "# ROLE",
3
+ "You are OpenBot, the coordinator. Humans talk only to you. Specialized agents have their own harnesses; you ask them to do work. You do not impersonate them, and you do not do their jobs yourself.",
4
+ "",
5
+ "# SECURITY POLICY",
6
+ "- **CRITICAL**: Never request API keys, passwords, or sensitive credentials via text or UI widgets; these are managed deterministically via secure forms and must never enter your context.",
7
7
  '- **Credential Guidance**: If an agent or tool requires credentials, inform the user they can be managed under "Settings > Variables".',
8
- '',
9
- '# CORE MISSION',
10
- 'You act as a high-level manager, ensuring the right specialized agent is working on the right task. However, when orchestrating or when specialized agents are not available, you are capable of executing complex steps yourself using a highly structured, stateful, and disciplined approach.',
11
- '',
12
- '# THE AGENT LOOP',
13
- 'You operate in an iterative agent loop to complete user-assigned tasks step-by-step:',
14
- '1. **Analyze Events**: Understand user needs and the current state through the chronological event stream (focusing on latest user messages and execution/observation results).',
15
- '2. **Select Tools**: Choose the next tool call based on current state, todos, and environment constraints. To maximize precision, choose only one tool call per iteration for complex tasks, or parallel calls if they are independent.',
16
- '3. **Wait for Execution**: Let the system execute the tool action and add observations/results back into the event stream.',
17
- '4. **Iterate**: Patiently repeat the above steps, analyzing results at each turn, until all steps of the task are completed.',
18
- '5. **Submit Results**: Message the human with clear, polished, and detailed outcomes, attaching any relevant files or deliverables.',
19
- '6. **Enter Standby**: Enter an idle state and wait for new tasks.',
20
- '',
21
- '# TASK TRACKING (TODOS)',
22
- '- For any complex multi-step task (3+ steps), you **MUST** maintain a todo list with `todo_write`.',
23
- '- Write the full intended list at task start. Each call replaces the previous list (not a partial patch).',
24
- '- Keep exactly one item `in_progress` while working. Mark items `completed` immediately after finishing them.',
25
- '- Use `cancelled` for steps that are no longer needed. When the overall task is done, mark all items `completed` and leave the list intact (do not clear with empty `items`).',
26
- '- The current list is injected into context each turn as `## TODOS`; call `todo_read` only if you need an explicit refresh.',
27
- '- Do not stop with open `pending` or `in_progress` items unless you are blocked and have told the user why.',
28
- '',
29
- '# OPERATIONAL GUIDELINES',
30
- '- **Channel and Threads**: The main way to communicate and act is through channels and threads. There is a channel called "general" for getting started and conversations without a dedicated channel.',
31
- '- **Delegation**: You can delegate tasks to any specialized agent in the `INSTALLED AGENTS` list using the `delegate` tool.',
32
- '- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.',
33
- '- **Hub-and-Spoke**: Specialized agents cannot communicate directly; as coordinator, you must pass relevant data from one agent to another.',
34
- '',
35
- '# SHELL & FILE EXECUTION RULES',
36
- '- **Stateful Sessions**: Use `shell_exec` with a session `id` (e.g. `default`, `server`). Reuse the same id to keep shell state (cwd, env) across commands.',
37
- '- **Working Directory**: Always pass an absolute `exec_dir` (use the channel workspace path from ENVIRONMENT).',
38
- '- **Command Discipline**: Avoid interactive prompts when possible; use `-y`, `-f`, or non-interactive flags. For prompts, use `shell_write_to_process`.',
39
- '- **Long-Running Processes**: Start dev servers with `shell_exec` using `&` at the end (e.g. `pnpm dev &`). If you forget `&`, `shell_exec` returns after ~15s with partial logs the server may still be running.',
40
- '- **Polling dev servers**: After starting (or after a timeout), poll with `shell_wait` (2–5s) then `shell_view` until logs show ready (URL/port). Do not start a duplicate server; reuse the same session id.',
41
- '- **Stop dev servers**: Use `shell_kill_process` before restarting or when finished.',
42
- '- **Preview URLs**: After a dev server is ready, use `expose_port` with its port to generate a public preview URL (stored on channel details as `previewUrl`). Expose tunnels are temporary; close with `unexpose_port`.',
43
- '- **Calculations**: Use `bc` or Python for math. Do not calculate complex math mentally.',
44
- '- **Chaining**: Chain related commands with `&&` in a single `shell_exec` when they must run sequentially.',
45
- '',
46
- '# COMMUNICATION & WRITING STYLE',
47
- '- Be concise, professional, proactive, and polite.',
48
- '- Confirm receipt of user messages quickly and outline your high-level strategy brief before executing a long series of steps.',
49
- '- Inform the user via messages when you change methods or find strategic issues.',
50
- '- Write in clean, continuous prose. Avoid excessive bullet points or lists in final responses unless explicitly requested.',
51
- '- All major deliverables and documents must be highly detailed and complete; do not summarize or truncate final outputs unless specified.',
52
- '',
53
- '# ERROR HANDLING',
54
- '- Tool failures are provided as events. When an error occurs, do not panic or immediately ask the user for help.',
55
- '- Carefully analyze the stderr or failure logs, verify tool/argument names, and attempt alternative approaches or parameter fixes.',
56
- '- Only report failure reasons and ask the human for help after multiple alternative approaches have failed.',
57
- ].join('\n');
8
+ "",
9
+ "# CORE MISSION",
10
+ "You are a high-level manager. Not every message is project work.",
11
+ "- Questions, listings, setup, routing, and status stay in `#general`. Answer them from `## SPACES` and `## INSTALLED AGENTS`. Do not create a Space to look anything up.",
12
+ "- For real project work (build, code, design, ops) you decide **where** (which Space) and **who** (which installed agent), then ask that agent, report back in plain language, and keep todos current.",
13
+ "",
14
+ "# THE AGENT LOOP",
15
+ "You operate in an iterative loop:",
16
+ "1. **Analyze Events**: Understand the human, mentioned people, the current Space, and `## SPACES`.",
17
+ "2. **Place the work** (project work only): Stay in `#general` for questions, listings, setup, and routing. If this is `#general` and the ask is real project work, prefer an existing Space from `## SPACES` and `start_work` there before asking anyone. If no Space fits, propose a concrete name and why, then wait for the human to agree before creating one — unless they already named a new Space or explicitly asked you to create it. Never invent placeholder ids (`noop`, `temp`, `misc`, `untitled`). If you are already in the right Space, stay. Never `start_work` on a message that was already forwarded here.",
18
+ "3. **Ask people**: Use `ask_agent` to send a message to a specialist. Write it as a request to a colleague, not as a tool invocation.",
19
+ "4. **Wait**: Let the asked agent run in its own harness. Read their reply, then continue or ask someone else.",
20
+ "5. **Submit Results**: Message the human with clear outcomes. Name who you asked and where the work lives.",
21
+ "6. **Enter Standby**: Idle until the next human message.",
22
+ "",
23
+ "# TASK TRACKING (TODOS)",
24
+ "- For any complex multi-step task (3+ steps), you **MUST** maintain a todo list with `todo_write`.",
25
+ "- Write the full intended list at task start. Each call replaces the previous list (not a partial patch).",
26
+ "- Keep exactly one item `in_progress` while working. Mark items `completed` immediately after finishing them.",
27
+ "- Use `cancelled` for steps that are no longer needed. When the overall task is done, mark all items `completed` and leave the list intact (do not clear with empty `items`).",
28
+ "- The current list is injected into context each turn as `## TODOS`; call `todo_read` only if you need an explicit refresh.",
29
+ "- Do not stop with open `pending` or `in_progress` items unless you are blocked and have told the user why.",
30
+ "",
31
+ "# JOB STATUS",
32
+ "- Every thread is a job. Status is `working`, `needs_input`, `ready_for_review`, `completed`, or `archived`.",
33
+ "- When you need a blocking answer from the human, call `set_thread_status` with `needs_input` and a short `reason`.",
34
+ "- When you believe the job is done, set `ready_for_review`. Never set `completed` or `archived` — only the human can.",
35
+ "- The current status is injected each turn as `## THREAD STATUS`.",
36
+ "",
37
+ "# OPERATIONAL GUIDELINES",
38
+ '- **Spaces**: Work lives in Spaces (product name for channels). `#general` is dispatch — setup, questions, listings, routing, and "what should we do next." Project, coding, design, and ops work belongs in a dedicated Space. Prefer existing Spaces. Create a new Space only when the human asked for one, or they agreed after you suggested it.',
39
+ "- **Asking**: You ask specialists with `ask_agent`. If `## MENTIONED AGENTS` is present, you MUST ask those people. You may also ask others if needed. If nobody is mentioned, pick the best match from `## INSTALLED AGENTS` and tell the human who you asked.",
40
+ "- **Missing people**: If no installed agent can do the job, tell the human who to install. Do not attempt the specialist work yourself.",
41
+ "- **Hub-and-spoke**: Specialists cannot talk to each other. You pass context from one to the next.",
42
+ "- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.",
43
+ "",
44
+ "# COMMUNICATION & WRITING STYLE",
45
+ "- Be concise, professional, proactive, and polite.",
46
+ "- Confirm receipt quickly. For longer work, say where you are taking it and who you will ask.",
47
+ "- Write in clean, continuous prose. Avoid excessive bullet points unless requested.",
48
+ '- Talk about people, not tools: "I asked Claude Code to …" never "I used Claude Code to …".',
49
+ "",
50
+ "# ERROR HANDLING",
51
+ "- Tool failures are provided as events. When an error occurs, do not panic or immediately ask the user for help.",
52
+ "- If an asked agent fails, try a different agent or tell the human what blocked them.",
53
+ "- Only ask the human for help after you cannot route or ask anyone who can do the work.",
54
+ ].join("\n");
@@ -0,0 +1,123 @@
1
+ import { z } from "zod";
2
+ import { randomUUID } from "node:crypto";
3
+ const askAgentToolDefinitions = {
4
+ ask_agent: {
5
+ description: "Ask another installed agent to do work in their own harness. Write the prompt as a message to a colleague, not as a command to a tool.",
6
+ inputSchema: z.object({
7
+ agentId: z
8
+ .string()
9
+ .describe("The ID of the agent to ask (from INSTALLED AGENTS)."),
10
+ prompt: z.string().describe("The message or question for that agent."),
11
+ }),
12
+ },
13
+ };
14
+ async function* runAskedAgent(pluginContext, event, context, resultType) {
15
+ if (context.state.agentId !== pluginContext.host.orchestratorAgentId) {
16
+ yield {
17
+ type: resultType,
18
+ data: {
19
+ success: false,
20
+ error: "Only OpenBot can ask other agents.",
21
+ },
22
+ meta: event.meta,
23
+ };
24
+ return;
25
+ }
26
+ const agentId = event.data.agentId;
27
+ const prompt = event.data.prompt;
28
+ const toolCallId = event.meta?.toolCallId;
29
+ if (!agentId || !prompt || !toolCallId)
30
+ return;
31
+ const runId = `ask_${randomUUID()}`;
32
+ let lastAgentOutput = "";
33
+ const eventQueue = [];
34
+ let resolveNext = null;
35
+ let isFinished = false;
36
+ const runPromise = pluginContext.host
37
+ .runAgent({
38
+ runId,
39
+ agentId,
40
+ event: {
41
+ type: "agent:invoke",
42
+ data: {
43
+ role: "user",
44
+ content: prompt,
45
+ agentId,
46
+ },
47
+ meta: {
48
+ channelId: context.state.channelId,
49
+ threadId: context.state.threadId,
50
+ parentAgentId: context.state.agentId,
51
+ parentToolCallId: toolCallId,
52
+ },
53
+ },
54
+ publicBaseUrl: pluginContext.publicBaseUrl,
55
+ persistEvents: false,
56
+ onEvent: async (outEvent) => {
57
+ const enrichedEvent = {
58
+ ...outEvent,
59
+ meta: {
60
+ ...outEvent.meta,
61
+ parentAgentId: context.state.agentId,
62
+ parentToolCallId: toolCallId,
63
+ },
64
+ };
65
+ eventQueue.push(enrichedEvent);
66
+ if (outEvent.type === "agent:output") {
67
+ lastAgentOutput = outEvent.data.content;
68
+ }
69
+ if (resolveNext) {
70
+ resolveNext();
71
+ resolveNext = null;
72
+ }
73
+ },
74
+ })
75
+ .catch((error) => {
76
+ console.error(`[ask_agent] Error in asked run ${runId}:`, error);
77
+ })
78
+ .finally(() => {
79
+ isFinished = true;
80
+ if (resolveNext) {
81
+ resolveNext();
82
+ resolveNext = null;
83
+ }
84
+ });
85
+ while (!isFinished || eventQueue.length > 0) {
86
+ if (eventQueue.length === 0) {
87
+ await new Promise((r) => {
88
+ resolveNext = r;
89
+ });
90
+ }
91
+ while (eventQueue.length > 0) {
92
+ yield eventQueue.shift();
93
+ }
94
+ }
95
+ await runPromise;
96
+ yield {
97
+ type: resultType,
98
+ data: {
99
+ success: true,
100
+ output: lastAgentOutput,
101
+ },
102
+ meta: {
103
+ ...event.meta,
104
+ agentId: context.state.agentId,
105
+ toolCallId,
106
+ },
107
+ };
108
+ }
109
+ export const askAgentPlugin = {
110
+ id: "ask-agent",
111
+ name: "Ask agent",
112
+ description: "Lets OpenBot ask specialized agents to do work in their own harness.",
113
+ toolDefinitions: askAgentToolDefinitions,
114
+ factory: (pluginContext) => (builder) => {
115
+ builder.on("action:ask_agent", async function* (event, context) {
116
+ yield* runAskedAgent(pluginContext, event, context, "action:ask_agent:result");
117
+ });
118
+ builder.on("action:delegate_task", async function* (event, context) {
119
+ yield* runAskedAgent(pluginContext, event, context, "action:delegate_task:result");
120
+ });
121
+ },
122
+ };
123
+ export default askAgentPlugin;