@meetopenbot/openbot 1.0.3 → 1.1.0

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.
@@ -0,0 +1,125 @@
1
+ export const MAX_LISTED_MODELS = 8;
2
+ function trimmedString(value) {
3
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
4
+ }
5
+ function unique(values) {
6
+ return [...new Set(values)];
7
+ }
8
+ function optionFromSchema(value, schema) {
9
+ const match = schema?.options?.find((option) => option.value === value);
10
+ return {
11
+ id: value,
12
+ label: trimmedString(match?.label),
13
+ description: trimmedString(match?.description),
14
+ };
15
+ }
16
+ function schemaModelIds(schema) {
17
+ const fromOptions = schema?.options
18
+ ?.map((option) => option.value)
19
+ .filter((value) => typeof value === "string" && value.length > 0) ?? [];
20
+ if (fromOptions.length > 0)
21
+ return unique(fromOptions);
22
+ const fromEnum = Array.isArray(schema?.enum)
23
+ ? schema.enum.filter((value) => typeof value === "string" && value.length > 0)
24
+ : [];
25
+ return unique(fromEnum);
26
+ }
27
+ export function catalogForAgent(pluginConfig, schema) {
28
+ const allowedModels = schemaModelIds(schema);
29
+ const defaultModel = trimmedString(pluginConfig?.model) ?? trimmedString(schema?.default) ?? allowedModels[0];
30
+ const models = allowedModels.map((id) => optionFromSchema(id, schema));
31
+ return {
32
+ defaultModel,
33
+ allowedModels,
34
+ models,
35
+ constrained: allowedModels.length > 0,
36
+ };
37
+ }
38
+ export function listModelsForPrompt(catalog, limit = MAX_LISTED_MODELS) {
39
+ const byId = new Map(catalog.models.map((model) => [model.id, model]));
40
+ const ordered = unique([catalog.defaultModel, ...catalog.allowedModels].filter((value) => Boolean(value)));
41
+ return ordered.slice(0, limit).map((id) => byId.get(id) ?? { id });
42
+ }
43
+ function formatModelOption(model) {
44
+ const hint = model.description ?? (model.label && model.label !== model.id ? model.label : undefined);
45
+ return hint ? `${model.id} — ${hint}` : model.id;
46
+ }
47
+ export function formatInstalledAgentLine(agent, catalog) {
48
+ let line = `- ${agent.id}: ${agent.name}${agent.description ? ` - ${agent.description}` : ""}`;
49
+ if (!catalog?.defaultModel && !catalog?.constrained)
50
+ return line;
51
+ if (catalog.defaultModel) {
52
+ line += `\n default: ${catalog.defaultModel}`;
53
+ }
54
+ if (catalog.constrained && catalog.allowedModels.length > 0) {
55
+ const listed = listModelsForPrompt(catalog);
56
+ const truncated = catalog.allowedModels.length > listed.length;
57
+ const rows = listed.map((model) => ` - ${formatModelOption(model)}`);
58
+ if (truncated)
59
+ rows.push(" - …");
60
+ line += `\n models:\n${rows.join("\n")}`;
61
+ }
62
+ return line;
63
+ }
64
+ export function formatInstalledAgentsSection(agents, catalogs) {
65
+ const formatted = agents
66
+ .map((agent) => formatInstalledAgentLine(agent, catalogs.get(agent.id)))
67
+ .join("\n");
68
+ return `## INSTALLED AGENTS\n${formatted}\n\nWhen you ask_agent, pick \`model\` from that agent's list using the descriptions (cheaper/faster for simple work, stronger for complex coding or reasoning). Omit \`model\` to use their default. Do not reuse another agent's ids.\nAnswer questions about people from this list. Do not create a Space to inspect agents.`;
69
+ }
70
+ export function formatAvailableAgentsSection(agents) {
71
+ const formatted = agents
72
+ .map((agent) => `- ${agent.id}: ${agent.name}${agent.description ? ` - ${agent.description}` : ""}`)
73
+ .join("\n");
74
+ return `## AVAILABLE AGENTS\n${formatted}\n\nNot installed yet. Use \`install_agent\` with an id from this list when the human needs that person (they must approve). After install, ask them with \`ask_agent\`.`;
75
+ }
76
+ export function availableMarketplaceAgents(installed, marketplace) {
77
+ const installedIds = new Set(installed.map((agent) => agent.id));
78
+ return marketplace.filter((agent) => !installedIds.has(agent.id));
79
+ }
80
+ export function validateAskModel(model, catalog) {
81
+ const requested = trimmedString(model);
82
+ if (!requested)
83
+ return { ok: true };
84
+ if (!catalog?.constrained)
85
+ return { ok: true, model: requested };
86
+ if (catalog.allowedModels.includes(requested))
87
+ return { ok: true, model: requested };
88
+ const listed = listModelsForPrompt(catalog).map((option) => option.id);
89
+ const defaultHint = catalog.defaultModel ? ` Omit model to use default (${catalog.defaultModel}).` : "";
90
+ return {
91
+ ok: false,
92
+ error: `model "${requested}" is not in this agent's list. Use one of: ${listed.join(", ")}.${defaultHint}`,
93
+ };
94
+ }
95
+ function primaryPluginRef(pluginRefs) {
96
+ return pluginRefs?.find((ref) => typeof ref.id === "string" && ref.id.length > 0);
97
+ }
98
+ export async function loadAgentModelCatalogs(storage, agentIds) {
99
+ const catalogs = new Map();
100
+ if (!storage?.getAgentDetails || agentIds.length === 0)
101
+ return catalogs;
102
+ const [detailsList, plugins] = await Promise.all([
103
+ Promise.all(agentIds.map(async (agentId) => {
104
+ try {
105
+ const details = await storage.getAgentDetails({ agentId });
106
+ return { agentId, details };
107
+ }
108
+ catch {
109
+ return { agentId, details: undefined };
110
+ }
111
+ })),
112
+ storage.getPlugins
113
+ ? storage.getPlugins().catch(() => [])
114
+ : Promise.resolve([]),
115
+ ]);
116
+ const pluginsById = new Map(plugins.map((plugin) => [plugin.id, plugin]));
117
+ for (const { agentId, details } of detailsList) {
118
+ const ref = primaryPluginRef(details?.pluginRefs);
119
+ if (!ref)
120
+ continue;
121
+ const schema = pluginsById.get(ref.id)?.configSchema?.properties?.model;
122
+ catalogs.set(agentId, catalogForAgent(ref.config, schema));
123
+ }
124
+ return catalogs;
125
+ }
@@ -2,10 +2,13 @@ import { memoryTools } from "./tools/memory.js";
2
2
  import { todoTools } from "./tools/todo.js";
3
3
  import { storageTools } from "./tools/storage.js";
4
4
  import { askAgentTools } from "./tools/ask-agent.js";
5
+ import { installAgentTools } from "./tools/install-agent.js";
5
6
  import { startWorkTools } from "./tools/start-work.js";
6
7
  import { threadStatusTools } from "./tools/thread-status.js";
7
8
  import { threadTitleTools } from "./tools/thread-title.js";
8
9
  import { wrapToolsWithApproval } from "./tools/approval.js";
10
+ /** Always gated: installing an agent changes the workspace. */
11
+ const ALWAYS_APPROVE = ["install_agent"];
9
12
  /**
10
13
  * Merge first-party OpenBot coordinator tools and wrap configured actions
11
14
  * with approval.
@@ -16,11 +19,12 @@ export function buildOpenbotTools(args) {
16
19
  ...todoTools,
17
20
  ...storageTools,
18
21
  ...askAgentTools,
22
+ ...installAgentTools,
19
23
  ...startWorkTools,
20
24
  ...threadStatusTools,
21
25
  ...threadTitleTools,
22
26
  ...args.context.tools,
23
27
  };
24
28
  const approvalConfig = args.context.config?.approval ?? { actions: [] };
25
- return wrapToolsWithApproval(tools, approvalConfig.actions ?? [], args.context);
29
+ return wrapToolsWithApproval(tools, [...new Set([...ALWAYS_APPROVE, ...(approvalConfig.actions ?? [])])], args.context);
26
30
  }
package/dist/context.js CHANGED
@@ -1,5 +1,5 @@
1
+ import { formatAvailableAgentsSection, formatInstalledAgentsSection, loadAgentModelCatalogs, availableMarketplaceAgents, } from "./agent-models.js";
1
2
  import { GENERAL_SPACE_ID } from "./space-id.js";
2
- import { OPENBOT_SYSTEM_PROMPT } from "./system-prompt.js";
3
3
  import { todoService } from "./tools/todo-service.js";
4
4
  export const DEFAULT_CONTEXT_BUDGET = 8000;
5
5
  export const MAX_CONTEXT_FILES = 50;
@@ -19,14 +19,15 @@ export const getContextBudgetForModel = (modelString) => {
19
19
  "anthropic/claude-3-haiku-20240307": 200000,
20
20
  "deepseek/deepseek-chat": 64000,
21
21
  "deepseek/deepseek-reasoner": 64000,
22
+ "zai/glm-5.3-flash": 1000000,
22
23
  };
23
24
  return budgets[modelString] || DEFAULT_CONTEXT_BUDGET;
24
25
  };
25
26
  /**
26
27
  * Simplified context builder for MVP.
27
28
  */
28
- export async function buildContext(state, storage) {
29
- const { channelId, threadId, channelDetails, agentId, threadDetails, agentDetails, } = state;
29
+ export async function buildContext(state, storage, host) {
30
+ const { channelId, threadId, channelDetails, agentId, threadDetails, } = state;
30
31
  const sections = [];
31
32
  // Fetch agents once if storage is available
32
33
  const allAgents = storage?.getAgents
@@ -107,10 +108,21 @@ export async function buildContext(state, storage) {
107
108
  }
108
109
  // 2.6 Installed Agents
109
110
  if (allAgents.length > 0) {
110
- const formatted = allAgents
111
- .map((a) => `- ${a.id}: ${a.name}${a.description ? ` - ${a.description}` : ""}`)
112
- .join("\n");
113
- sections.push(`## INSTALLED AGENTS\n${formatted}\n\nAnswer questions about people from this list. Do not create a Space to inspect agents.`);
111
+ const catalogs = await loadAgentModelCatalogs(storage, allAgents.map((agent) => agent.id));
112
+ sections.push(formatInstalledAgentsSection(allAgents, catalogs));
113
+ }
114
+ // 2.7 Marketplace agents not yet installed
115
+ if (host?.listMarketplaceAgents) {
116
+ try {
117
+ const marketplace = await host.listMarketplaceAgents();
118
+ const available = availableMarketplaceAgents(allAgents, marketplace);
119
+ if (available.length > 0) {
120
+ sections.push(formatAvailableAgentsSection(available));
121
+ }
122
+ }
123
+ catch (error) {
124
+ console.warn("[context] Failed to fetch marketplace agents:", error);
125
+ }
114
126
  }
115
127
  if (storage?.getChannels) {
116
128
  try {
@@ -177,12 +189,7 @@ export async function buildContext(state, storage) {
177
189
  console.warn("[context] Failed to fetch files:", error);
178
190
  }
179
191
  }
180
- // 5. Agent Instructions
181
- const rawInstructions = agentDetails?.instructions?.trim();
182
- if (rawInstructions && rawInstructions !== OPENBOT_SYSTEM_PROMPT.trim()) {
183
- sections.push(`## Instructions\n${rawInstructions}`);
184
- }
185
- // 6. Memories
192
+ // 5. Memories
186
193
  if (storage?.listMemories) {
187
194
  try {
188
195
  const scopes = ["global", `agent:${agentId}`];
package/dist/images.js CHANGED
@@ -14,7 +14,7 @@ export function eventsHaveImages(events) {
14
14
  });
15
15
  }
16
16
  function hasAnyKey(env, keys) {
17
- return keys.some((key) => Boolean(env[key]?.trim()));
17
+ return Boolean(keys?.some((key) => Boolean(env[key]?.trim())));
18
18
  }
19
19
  /** Auto uses DeepSeek, which cannot see pictures. Pick a cheap vision model. */
20
20
  export function pickVisionModel(authMode, env = process.env) {
@@ -24,6 +24,8 @@ export function pickVisionModel(authMode, env = process.env) {
24
24
  return 'openai/gpt-4o-mini';
25
25
  if (hasAnyKey(env, BYOK_ENV_KEYS.google))
26
26
  return 'google/gemini-3.5-flash';
27
+ if (hasAnyKey(env, BYOK_ENV_KEYS.zai))
28
+ return 'zai/glm-5.3-flash';
27
29
  if (hasAnyKey(env, BYOK_ENV_KEYS.anthropic))
28
30
  return 'anthropic/claude-haiku-4-5';
29
31
  return undefined;
package/dist/index.js CHANGED
@@ -1,18 +1,20 @@
1
1
  import { defineOpenbotPlugin } from './types.js';
2
- import { cloudAuthModeProperties, modelConfigField, } from '@meetopenbot/plugin-sdk';
2
+ import { modelConfigField, } from '@meetopenbot/plugin-sdk';
3
3
  import { AUTO_MODEL_ID, AUTO_MODEL_OPTION } from './auto-model.js';
4
4
  import { handleOpenbotWidgetResponse, runOpenbotTurn } from './runtime.js';
5
+ import { MAX_USER_INSTRUCTIONS_LENGTH } from './system-prompt.js';
5
6
  import { memoryTools } from './tools/memory.js';
6
7
  import { todoTools } from './tools/todo.js';
7
8
  import { storageTools } from './tools/storage.js';
8
9
  import { askAgentTools } from './tools/ask-agent.js';
10
+ import { installAgentTools } from './tools/install-agent.js';
9
11
  import { startWorkTools } from './tools/start-work.js';
10
12
  import { threadStatusTools } from './tools/thread-status.js';
11
13
  import { threadTitleTools } from './tools/thread-title.js';
12
14
  import { buildOpenbotTools } from './build-tools.js';
13
15
  export const OPENBOT_PLUGIN_ID = '@meetopenbot/openbot';
14
16
  const modelField = await modelConfigField({
15
- providers: ['openai', 'anthropic', 'google', 'deepseek'],
17
+ providers: ['openai', 'anthropic', 'google', 'deepseek', 'zai'],
16
18
  extraOptions: [AUTO_MODEL_OPTION],
17
19
  defaultValue: AUTO_MODEL_ID,
18
20
  description: 'Auto, or a model from the OpenBot registry.',
@@ -31,8 +33,13 @@ export const openbotPlugin = defineOpenbotPlugin({
31
33
  configSchema: {
32
34
  type: 'object',
33
35
  properties: {
34
- ...cloudAuthModeProperties(),
35
36
  model: modelField,
37
+ instructions: {
38
+ type: 'string',
39
+ format: 'textarea',
40
+ description: 'Optional extra instructions injected into the coordinator system prompt.',
41
+ maxLength: MAX_USER_INSTRUCTIONS_LENGTH,
42
+ },
36
43
  },
37
44
  },
38
45
  tools: {
@@ -40,6 +47,7 @@ export const openbotPlugin = defineOpenbotPlugin({
40
47
  ...todoTools,
41
48
  ...storageTools,
42
49
  ...askAgentTools,
50
+ ...installAgentTools,
43
51
  ...startWorkTools,
44
52
  ...threadStatusTools,
45
53
  ...threadTitleTools,
@@ -53,6 +61,7 @@ export const openbotPlugin = defineOpenbotPlugin({
53
61
  yield* runOpenbotTurn(args, {
54
62
  model: context.config?.model || AUTO_MODEL_ID,
55
63
  authMode,
64
+ instructions: context.config?.instructions,
56
65
  agentId: context.agentId,
57
66
  storage: context.storage,
58
67
  toolDefinitions,
@@ -78,6 +87,7 @@ export const openbotPlugin = defineOpenbotPlugin({
78
87
  yield* handleOpenbotWidgetResponse(turnArgs, {
79
88
  model: context.config?.model || AUTO_MODEL_ID,
80
89
  authMode,
90
+ instructions: context.config?.instructions,
81
91
  agentId: context.agentId,
82
92
  storage: context.storage,
83
93
  toolDefinitions,
package/dist/model.js CHANGED
@@ -4,15 +4,24 @@ import { createDeepSeek, deepSeek } from '@ai-sdk/deepseek';
4
4
  import { createGoogle, google as defaultGoogle } from '@ai-sdk/google';
5
5
  import { expandModelString } from './auto-model.js';
6
6
  import { CREDITS_API_KEY_PLACEHOLDER, INTEGRATIONS_TOKEN_HEADER, creditsProviderBaseUrl, resolveByokApiKey, resolveCreditsAuthConfig, shouldUseCreditsAuth, } from '@meetopenbot/plugin-sdk';
7
+ const ZAI_BYOK_BASE_URL = 'https://api.z.ai/api/paas/v4';
7
8
  /** AI SDK default: OpenAI Responses API (`/v1/responses`), required for tools on Luna. */
8
9
  function openAiResponsesModel(options, modelId) {
9
10
  return createOpenAI(options)(modelId);
10
11
  }
12
+ /** Z.ai is OpenAI-compat chat completions, not the Responses API. */
13
+ function zaiChatModel(options, modelId) {
14
+ return createOpenAI({ ...options, name: 'zai' }).chat(modelId);
15
+ }
11
16
  /** `@ai-sdk/google` only reads `GOOGLE_GENERATIVE_AI_API_KEY`; accept `GEMINI_API_KEY` too. */
12
17
  function googleByok(modelId) {
13
18
  const apiKey = resolveByokApiKey('google');
14
19
  return apiKey ? createGoogle({ apiKey })(modelId) : defaultGoogle(modelId);
15
20
  }
21
+ function zaiByok(modelId) {
22
+ const apiKey = resolveByokApiKey('zai');
23
+ return zaiChatModel({ baseURL: ZAI_BYOK_BASE_URL, apiKey: apiKey ?? '' }, modelId);
24
+ }
16
25
  function resolveCreditsProvider(provider, modelId) {
17
26
  const config = resolveCreditsAuthConfig();
18
27
  if (!config) {
@@ -32,6 +41,8 @@ function resolveCreditsProvider(provider, modelId) {
32
41
  return createGoogle(options)(modelId);
33
42
  case 'deepseek':
34
43
  return createDeepSeek(options)(modelId);
44
+ case 'zai':
45
+ return zaiChatModel(options, modelId);
35
46
  }
36
47
  }
37
48
  export function resolveModel(modelString, options) {
@@ -51,6 +62,8 @@ export function resolveModel(modelString, options) {
51
62
  return useCredits ? resolveCreditsProvider('google', modelId) : googleByok(modelId);
52
63
  case 'deepseek':
53
64
  return useCredits ? resolveCreditsProvider('deepseek', modelId) : deepSeek(modelId);
65
+ case 'zai':
66
+ return useCredits ? resolveCreditsProvider('zai', modelId) : zaiByok(modelId);
54
67
  default:
55
68
  throw new Error(`Unsupported AI provider: "${provider}"`);
56
69
  }
package/dist/runtime.js CHANGED
@@ -1,17 +1,16 @@
1
1
  import { streamText, stepCountIs } from 'ai';
2
+ import { BYOK_ENV_KEYS, isAuthErrorMessage, isCreditsErrorMessage, fetchModelRegistry, getProviderModelOptions, listApiKeyProviders, PROVIDER_API_KEY_LINKS, } from '@meetopenbot/plugin-sdk';
2
3
  import { eventsToModelMessages } from './history.js';
3
4
  import { addImageParts, eventsHaveImages, pickVisionModel } from './images.js';
4
5
  import { buildContext } from './context.js';
5
- import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
6
- import { isAuthErrorMessage, isCreditsErrorMessage } from '@meetopenbot/plugin-sdk';
6
+ import { OPENBOT_SYSTEM_PROMPT, withUserInstructions } from './system-prompt.js';
7
7
  import { AUTO_MODEL_ID, isAutoModel, isRetryableModelError, listAutoModelCandidates, missingByokApiKeyMessage, } from './auto-model.js';
8
8
  import { reasoningForModel, resolveModel } from './model.js';
9
- import { fetchModelRegistry, getProviderModelOptions, listApiKeyProviders, PROVIDER_API_KEY_LINKS, } from '@meetopenbot/plugin-sdk';
10
9
  import { errorFromStreamPart } from './stream-error.js';
11
10
  import { createAgentOutputBuffer, isOutputBoundaryPart } from './output-buffer.js';
12
- async function buildSystemPrompt(state, storage) {
13
- const context = await buildContext(state, storage);
14
- const sections = [OPENBOT_SYSTEM_PROMPT, '', context];
11
+ async function buildSystemPrompt(state, storage, instructions, host) {
12
+ const context = await buildContext(state, storage, host);
13
+ const sections = [withUserInstructions(OPENBOT_SYSTEM_PROMPT, instructions), '', context];
15
14
  const threadState = state.threadDetails?.state;
16
15
  if (!threadState?.isSmartNamed) {
17
16
  sections.push('', '## SYSTEM HINT', 'This thread is unnamed. Please use the `set_thread_title` tool to set a concise, descriptive title (e.g., "Project Brainstorming" instead of "project-brainstorm"). Only do this once.');
@@ -85,7 +84,7 @@ function toAiTools(tools, args, flushPendingOutput) {
85
84
  return mapped;
86
85
  }
87
86
  export async function* runOpenbotTurn(args, options) {
88
- const { model: modelString = AUTO_MODEL_ID, authMode = 'byok', storage, toolDefinitions = {}, abortSignal, host, } = options;
87
+ const { model: modelString = AUTO_MODEL_ID, authMode = 'byok', instructions, storage, toolDefinitions = {}, abortSignal, host, } = options;
89
88
  let configuredModelString = modelString;
90
89
  const isCreditsCloudAgent = (id) => host.isCloudSystemAgent(id ?? '') && authMode === 'credits';
91
90
  if (!storage || abortSignal?.aborted)
@@ -93,7 +92,7 @@ export async function* runOpenbotTurn(args, options) {
93
92
  const state = args.state;
94
93
  state.model = configuredModelString;
95
94
  const threadId = args.threadId || state.threadId;
96
- const systemPrompt = await buildSystemPrompt(state, storage);
95
+ const systemPrompt = await buildSystemPrompt(state, storage, instructions, host);
97
96
  const events = await storage.getEvents({
98
97
  channelId: state.channelId ?? '',
99
98
  threadId,
@@ -395,10 +394,7 @@ export async function* handleOpenbotWidgetResponse(args, options) {
395
394
  if (!keepAuto && !modelId)
396
395
  return;
397
396
  const apiKey = String(values.apiKey);
398
- if (provider !== 'openai' &&
399
- provider !== 'anthropic' &&
400
- provider !== 'google' &&
401
- provider !== 'deepseek') {
397
+ if (!(provider in BYOK_ENV_KEYS)) {
402
398
  yield {
403
399
  type: 'agent:output',
404
400
  data: { content: `Unsupported provider: ${provider}` },
@@ -406,13 +402,7 @@ export async function* handleOpenbotWidgetResponse(args, options) {
406
402
  };
407
403
  return;
408
404
  }
409
- const envVar = provider === 'openai'
410
- ? 'OPENAI_API_KEY'
411
- : provider === 'anthropic'
412
- ? 'ANTHROPIC_API_KEY'
413
- : provider === 'deepseek'
414
- ? 'DEEPSEEK_API_KEY'
415
- : 'GOOGLE_GENERATIVE_AI_API_KEY';
405
+ const envVar = BYOK_ENV_KEYS[provider][0];
416
406
  const newModelString = keepAuto ? AUTO_MODEL_ID : `${provider}/${modelId}`;
417
407
  await storage.createVariable({ key: envVar, value: apiKey, secret: true });
418
408
  process.env[envVar] = apiKey;
@@ -8,7 +8,7 @@ export const OPENBOT_SYSTEM_PROMPT = [
8
8
  "",
9
9
  "# CORE MISSION",
10
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.",
11
+ "- Questions, listings, setup, routing, and status stay in `#general`. Answer them from `## SPACES`, `## INSTALLED AGENTS`, and `## AVAILABLE AGENTS`. Do not create a Space to look anything up.",
12
12
  "- For real project work (build, code, design, ops) you decide **where** (which Space) and **who** (which installed agent), then ask that agent, close without restating their work, and keep todos current.",
13
13
  "",
14
14
  "# THE AGENT LOOP",
@@ -42,8 +42,9 @@ export const OPENBOT_SYSTEM_PROMPT = [
42
42
  "# OPERATIONAL GUIDELINES",
43
43
  '- **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.',
44
44
  "- **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.",
45
+ "- **Models**: When you `ask_agent`, pick `model` from that agent's list in `## INSTALLED AGENTS` using the descriptions — cheaper/faster for simple work, stronger for complex coding or reasoning. Omit `model` to use their default. Do not reuse another agent's ids.",
45
46
  "- **Closer**: After `ask_agent`, you are the closer, not a second author of their work. Don't repeat. Add or close. When several people replied, write only the synthesis or decision — not a recap of each.",
46
- "- **Missing people**: If no installed agent can do the job, tell the human who to install. Do not attempt the specialist work yourself.",
47
+ "- **Missing people**: If no installed agent can do the job, pick a match from `## AVAILABLE AGENTS` and call `install_agent` (human must approve) — only when they asked for that capability or agreed. One agent per call. Then `ask_agent`. If nobody is listed, tell the human who to install from Settings. Do not attempt the specialist work yourself.",
47
48
  "- **Hub-and-spoke**: Specialists cannot talk to each other. You pass context from one to the next.",
48
49
  "- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.",
49
50
  "",
@@ -59,3 +60,29 @@ export const OPENBOT_SYSTEM_PROMPT = [
59
60
  "- If an asked agent fails, try a different agent or tell the human what blocked them.",
60
61
  "- Only ask the human for help after you cannot route or ask anyone who can do the work.",
61
62
  ].join("\n");
63
+ /** Max characters accepted from the OpenBot plugin `instructions` config field. */
64
+ export const MAX_USER_INSTRUCTIONS_LENGTH = 4000;
65
+ /** Trim and cap user-provided coordinator instructions. */
66
+ export function normalizeUserInstructions(value) {
67
+ if (typeof value !== "string")
68
+ return undefined;
69
+ const trimmed = value.trim();
70
+ if (!trimmed)
71
+ return undefined;
72
+ return trimmed.length > MAX_USER_INSTRUCTIONS_LENGTH
73
+ ? trimmed.slice(0, MAX_USER_INSTRUCTIONS_LENGTH)
74
+ : trimmed;
75
+ }
76
+ /** Append capped user instructions to the static system prompt. */
77
+ export function withUserInstructions(basePrompt, value) {
78
+ const instructions = normalizeUserInstructions(value);
79
+ if (!instructions)
80
+ return basePrompt;
81
+ return [
82
+ basePrompt,
83
+ "",
84
+ "# USER INSTRUCTIONS",
85
+ "The following are additional instructions from the user. Follow them when they do not conflict with the security policy or core mission above.",
86
+ instructions,
87
+ ].join("\n");
88
+ }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { loadAgentModelCatalogs, validateAskModel } from "../agent-models.js";
3
4
  /**
4
5
  * `ask_agent` — OpenBot sends a message to another agent in its own harness.
5
6
  * Only the orchestrator may ask. Child events stream into this thread so the
@@ -13,6 +14,10 @@ export const toolDefinitions = {
13
14
  .string()
14
15
  .describe("The ID of the agent to ask (from INSTALLED AGENTS)."),
15
16
  prompt: z.string().describe("The message or question for that agent."),
17
+ model: z
18
+ .string()
19
+ .optional()
20
+ .describe("Optional model id from that agent's list in INSTALLED AGENTS. Omit to use their default."),
16
21
  }),
17
22
  },
18
23
  };
@@ -34,6 +39,15 @@ async function runAskedAgent(ctx, args) {
34
39
  output: "agentId, prompt, and toolCallId are required",
35
40
  };
36
41
  }
42
+ const catalogs = await loadAgentModelCatalogs(ctx.storage, [agentId]);
43
+ const modelCheck = validateAskModel(args.model, catalogs.get(agentId));
44
+ if (!modelCheck.ok) {
45
+ return {
46
+ success: false,
47
+ error: modelCheck.error,
48
+ output: modelCheck.error,
49
+ };
50
+ }
37
51
  const runId = `ask_${randomUUID()}`;
38
52
  let lastAgentOutput = "";
39
53
  const eventQueue = [];
@@ -49,9 +63,10 @@ async function runAskedAgent(ctx, args) {
49
63
  role: "agent",
50
64
  content: prompt,
51
65
  agentId,
66
+ ...(modelCheck.model ? { config: { model: modelCheck.model } } : {}),
52
67
  },
53
68
  meta: {
54
- channelId: ctx.channelId ?? ctx.state.channelId ?? '',
69
+ channelId: ctx.channelId ?? ctx.state.channelId ?? "",
55
70
  threadId: ctx.threadId ?? ctx.state.threadId,
56
71
  parentAgentId: ctx.agentId,
57
72
  parentToolCallId: toolCallId,
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * `install_agent` — OpenBot installs a marketplace specialist so it can ask them.
4
+ * The host looks up the listing, installs plugins, and materializes AGENT.md.
5
+ * Approval wrapping happens in `buildOpenbotTools`.
6
+ */
7
+ export const toolDefinitions = {
8
+ install_agent: {
9
+ description: "Install a marketplace agent so you can ask them. Use an id from AVAILABLE AGENTS. The human must approve. After install, ask them with ask_agent.",
10
+ inputSchema: z.object({
11
+ agentId: z
12
+ .string()
13
+ .describe("The marketplace agent id to install (from AVAILABLE AGENTS)."),
14
+ }),
15
+ },
16
+ };
17
+ async function installMarketplaceAgent(ctx, args) {
18
+ if (ctx.agentId !== ctx.host.orchestratorAgentId) {
19
+ const message = "Only OpenBot can install agents.";
20
+ return { success: false, error: message, output: message };
21
+ }
22
+ const agentId = typeof args.agentId === "string" ? args.agentId.trim() : "";
23
+ if (!agentId) {
24
+ const message = "agentId is required";
25
+ return { success: false, error: message, output: message };
26
+ }
27
+ try {
28
+ const installed = await ctx.host.installMarketplaceAgent(agentId);
29
+ const output = `Installed ${installed.name} (${installed.agentId}). You can ask them with ask_agent.`;
30
+ return {
31
+ success: true,
32
+ agentId: installed.agentId,
33
+ name: installed.name,
34
+ output,
35
+ };
36
+ }
37
+ catch (error) {
38
+ const message = error instanceof Error ? error.message : "Failed to install agent";
39
+ return { success: false, error: message, output: message };
40
+ }
41
+ }
42
+ export const tools = {
43
+ install_agent: {
44
+ ...toolDefinitions.install_agent,
45
+ execute: async (rawArgs, ctx) => {
46
+ return installMarketplaceAgent(ctx, (rawArgs ?? {}));
47
+ },
48
+ },
49
+ };
50
+ export const installAgentTools = tools;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/openbot",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "OpenBot coordinator runtime: ask specialists, route work into Spaces, and track todos.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,7 +25,7 @@
25
25
  "@ai-sdk/openai": "^4.0.46",
26
26
  "ai": "^7.0.77",
27
27
  "zod": "^4.3.5",
28
- "@meetopenbot/plugin-sdk": "^1.0.2"
28
+ "@meetopenbot/plugin-sdk": "^1.0.4"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^20.10.1",