openbot 0.4.0 → 0.4.2

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 (50) hide show
  1. package/dist/app/cli.js +1 -1
  2. package/dist/app/config.js +10 -0
  3. package/dist/app/server.js +200 -3
  4. package/dist/harness/index.js +18 -0
  5. package/dist/plugins/approval/index.js +35 -20
  6. package/dist/plugins/bash/index.js +195 -0
  7. package/dist/plugins/delegation/index.js +4 -2
  8. package/dist/plugins/openbot/context.js +54 -9
  9. package/dist/plugins/openbot/history.js +47 -1
  10. package/dist/plugins/openbot/index.js +43 -3
  11. package/dist/plugins/openbot/runtime.js +91 -27
  12. package/dist/plugins/openbot/system-prompt.js +21 -1
  13. package/dist/plugins/plugin-manager/index.js +87 -3
  14. package/dist/plugins/shell/index.js +2 -1
  15. package/dist/plugins/storage/files.js +67 -0
  16. package/dist/plugins/storage/index.js +184 -7
  17. package/dist/plugins/storage/service.js +201 -44
  18. package/dist/plugins/ui/index.js +109 -150
  19. package/dist/services/abort.js +43 -0
  20. package/dist/services/plugins/registry.js +5 -3
  21. package/dist/services/plugins/service.js +66 -11
  22. package/docs/agents.md +5 -8
  23. package/docs/architecture.md +1 -1
  24. package/docs/plugins.md +28 -7
  25. package/docs/templates/AGENT.example.md +4 -4
  26. package/package.json +1 -1
  27. package/src/app/cli.ts +1 -1
  28. package/src/app/config.ts +13 -0
  29. package/src/app/server.ts +235 -3
  30. package/src/app/types.ts +284 -14
  31. package/src/harness/index.ts +21 -0
  32. package/src/plugins/approval/index.ts +37 -20
  33. package/src/plugins/bash/index.ts +232 -0
  34. package/src/plugins/delegation/index.ts +5 -2
  35. package/src/plugins/openbot/context.ts +58 -9
  36. package/src/plugins/openbot/history.ts +52 -1
  37. package/src/plugins/openbot/index.ts +45 -3
  38. package/src/plugins/openbot/runtime.ts +121 -27
  39. package/src/plugins/openbot/system-prompt.ts +21 -1
  40. package/src/plugins/plugin-manager/index.ts +105 -3
  41. package/src/plugins/storage/files.ts +81 -0
  42. package/src/plugins/storage/index.ts +198 -8
  43. package/src/plugins/storage/service.ts +267 -44
  44. package/src/plugins/ui/index.ts +123 -0
  45. package/src/services/abort.ts +46 -0
  46. package/src/services/plugins/domain.ts +34 -1
  47. package/src/services/plugins/registry.ts +5 -3
  48. package/src/services/plugins/service.ts +136 -45
  49. package/src/services/plugins/types.ts +5 -1
  50. package/src/plugins/shell/index.ts +0 -123
@@ -1,6 +1,5 @@
1
1
  import { z } from 'zod';
2
2
  import { generateId } from 'melony';
3
- import { runAgent } from '../../harness/index.js';
4
3
  /**
5
4
  * `delegation` — allows agents to delegate tasks to other agents.
6
5
  *
@@ -22,7 +21,7 @@ export const delegationPlugin = {
22
21
  name: 'Delegation',
23
22
  description: 'Allows agents to call upon other agents to solve sub-tasks.',
24
23
  toolDefinitions: delegationToolDefinitions,
25
- factory: () => (builder) => {
24
+ factory: (pluginContext) => (builder) => {
26
25
  // Handle the tool execution
27
26
  builder.on('action:delegate_task', async function* (event, context) {
28
27
  const delegateEvent = event;
@@ -42,6 +41,8 @@ export const delegationPlugin = {
42
41
  const toolCallId = delegateEvent.meta?.toolCallId;
43
42
  if (!toolCallId)
44
43
  return;
44
+ // Break circular dependency by dynamic import
45
+ const { runAgent } = await import('../../harness/index.js');
45
46
  const runId = `dg_${generateId()}`;
46
47
  let lastAgentOutput = '';
47
48
  // Queue to bridge the async onEvent callback to this generator
@@ -68,6 +69,7 @@ export const delegationPlugin = {
68
69
  },
69
70
  channelId: context.state.channelId,
70
71
  threadId: context.state.threadId,
72
+ publicBaseUrl: pluginContext.publicBaseUrl,
71
73
  onEvent: async (outEvent) => {
72
74
  // Enrich events with parent metadata so the UI can track the hierarchy
73
75
  const enrichedEvent = {
@@ -1,4 +1,6 @@
1
+ import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
1
2
  export const DEFAULT_CONTEXT_BUDGET = 8000;
3
+ export const MAX_CONTEXT_FILES = 50;
2
4
  /**
3
5
  * Returns the known context window budget (in tokens) for a given model string.
4
6
  */
@@ -32,7 +34,13 @@ export async function buildContext(state, storage) {
32
34
  const participants = channelDetails?.participants || [];
33
35
  const isDm = isDmSoloChannel(participants, agentId);
34
36
  const sections = [];
35
- // 1. Environment
37
+ // Fetch agents once if storage is available
38
+ const allAgents = storage?.getAgents ? await storage.getAgents().catch(() => []) : [];
39
+ // 1. User
40
+ if (state.currentUser?.userName) {
41
+ sections.push(`## HUMAN\n- Name: ${state.currentUser.userName}`);
42
+ }
43
+ // 2. Environment
36
44
  let env = '## ENVIRONMENT\n';
37
45
  if (isDm) {
38
46
  env += '- Mode: Direct Message (Solo)\n';
@@ -40,25 +48,62 @@ export async function buildContext(state, storage) {
40
48
  else {
41
49
  const channelName = channelDetails?.name || channelId;
42
50
  env += `- Mode: Channel (#${channelName})\n`;
51
+ if (channelDetails?.cwd) {
52
+ env += `- Workspace: ${channelDetails.cwd}\n`;
53
+ }
43
54
  if (threadId) {
44
55
  env += `- Thread: ${threadDetails?.name || threadId}\n`;
45
56
  }
46
57
  const peerIds = participants.filter((id) => id !== agentId);
47
- if (peerIds.length > 0) {
48
- env += `- Participants: ${peerIds.join(', ')}\n`;
49
- }
58
+ const participantLabels = peerIds.map((id) => {
59
+ const agent = allAgents.find((a) => a.id === id);
60
+ return agent ? `${agent.name} (${id})` : id;
61
+ });
62
+ env += `- Participants: ${participantLabels.length > 0 ? participantLabels.join(', ') : 'None'}\n`;
50
63
  }
51
64
  sections.push(env);
52
- // 2. Channel Spec
65
+ // 2.5 Installed Agents
66
+ if (allAgents.length > 0) {
67
+ const formatted = allAgents
68
+ .map((a) => `- ${a.id}: ${a.name}${a.description ? ` - ${a.description}` : ''}`)
69
+ .join('\n');
70
+ sections.push(`## INSTALLED AGENTS\n${formatted}`);
71
+ }
72
+ // 3. Channel Spec
53
73
  const spec = channelDetails?.spec?.trim();
54
74
  if (spec) {
55
75
  sections.push(`## CHANNEL SPECIFICATION\n${spec}`);
56
76
  }
57
- // 3. Agent Instructions
58
- if (agentDetails?.instructions) {
59
- sections.push(`## AGENT: ${agentDetails?.name}\n${agentDetails.instructions}`);
77
+ // 4. Files
78
+ if (storage?.listFiles && channelId && channelDetails?.cwd) {
79
+ try {
80
+ const files = await storage.listFiles({ channelId });
81
+ if (files.length > 0) {
82
+ const limited = files.slice(0, MAX_CONTEXT_FILES);
83
+ const formatted = limited
84
+ .map((f) => `- ${f.name}${f.isDirectory ? '/' : ''}`)
85
+ .join('\n');
86
+ let fileSection = `## FILES\n${formatted}`;
87
+ if (files.length > MAX_CONTEXT_FILES) {
88
+ fileSection += `\n- ... and ${files.length - MAX_CONTEXT_FILES} more files`;
89
+ }
90
+ sections.push(fileSection);
91
+ }
92
+ else {
93
+ sections.push('## FILES\n- (No files in workspace)');
94
+ }
95
+ }
96
+ catch (error) {
97
+ console.warn('[context] Failed to fetch files:', error);
98
+ }
99
+ }
100
+ // 5. Agent Instructions
101
+ const rawInstructions = agentDetails?.instructions?.trim();
102
+ if (rawInstructions &&
103
+ rawInstructions !== OPENBOT_SYSTEM_PROMPT.trim()) {
104
+ sections.push(`## Instructions\n${rawInstructions}`);
60
105
  }
61
- // 4. Memories
106
+ // 6. Memories
62
107
  if (storage?.listMemories) {
63
108
  try {
64
109
  const scopes = ['global', `agent:${agentId}`];
@@ -1,3 +1,49 @@
1
+ /**
2
+ * Ensures every tool-call has a matching tool-result before calling the LLM.
3
+ * Orphaned calls (interrupted run, missing :result event, etc.) get an empty
4
+ * result so the conversation can resume instead of failing validation.
5
+ */
6
+ function fillMissingToolResults(messages) {
7
+ const filled = [];
8
+ const pending = new Map();
9
+ const flushPending = () => {
10
+ if (pending.size === 0)
11
+ return;
12
+ filled.push({
13
+ role: 'tool',
14
+ content: [...pending.entries()].map(([toolCallId, toolName]) => ({
15
+ type: 'tool-result',
16
+ toolCallId,
17
+ toolName,
18
+ output: { type: 'text', value: '' },
19
+ })),
20
+ });
21
+ pending.clear();
22
+ };
23
+ for (const message of messages) {
24
+ if (message.role === 'tool' && Array.isArray(message.content)) {
25
+ filled.push(message);
26
+ for (const part of message.content) {
27
+ if (part.type === 'tool-result') {
28
+ pending.delete(part.toolCallId);
29
+ }
30
+ }
31
+ continue;
32
+ }
33
+ flushPending();
34
+ filled.push(message);
35
+ if (message.role === 'assistant' && Array.isArray(message.content)) {
36
+ for (const part of message.content) {
37
+ if (part.type === 'tool-call') {
38
+ const toolCall = part;
39
+ pending.set(toolCall.toolCallId, toolCall.toolName);
40
+ }
41
+ }
42
+ }
43
+ }
44
+ flushPending();
45
+ return filled;
46
+ }
1
47
  /**
2
48
  * Converts a raw event log into a valid chain of ModelMessages for the AI SDK.
3
49
  *
@@ -94,5 +140,5 @@ export function eventsToModelMessages(events) {
94
140
  break;
95
141
  }
96
142
  }
97
- return messages;
143
+ return fillMissingToolResults(messages);
98
144
  }
@@ -1,15 +1,24 @@
1
1
  import { openbotRuntime } from './runtime.js';
2
+ import { bashPlugin } from '../bash/index.js';
3
+ import { memoryPlugin } from '../memory/index.js';
4
+ import { approvalPlugin } from '../approval/index.js';
5
+ import { storagePlugin } from '../storage/index.js';
6
+ import { delegationPlugin } from '../delegation/index.js';
7
+ import { uiPlugin } from '../ui/index.js';
2
8
  /**
3
9
  * `openbot` — the standard, opinionated OpenBot agent runtime.
4
10
  *
5
11
  * This is the canonical execution loop for OpenBot agents. It handles
6
12
  * `agent:invoke`, manages short-term memory, assembles context, and
7
13
  * orchestrates tool calls.
14
+ *
15
+ * It comes with a "batteries-included" set of inbuilt tools: bash, memory,
16
+ * storage, delegation, and approval.
8
17
  */
9
18
  export const openbotPlugin = {
10
19
  id: 'openbot',
11
20
  name: 'OpenBot Agent',
12
- description: 'The standard, opinionated OpenBot agent runtime. Handles the core execution loop and tool orchestration.',
21
+ description: 'The standard OpenBot agent runtime with inbuilt tools (bash, memory, storage, delegation, and approval).',
13
22
  configSchema: {
14
23
  type: 'object',
15
24
  properties: {
@@ -18,14 +27,45 @@ export const openbotPlugin = {
18
27
  description: 'Provider model string, e.g. openai/gpt-4o-mini, anthropic/claude-3-5-sonnet-20240620',
19
28
  default: 'openai/gpt-4o-mini',
20
29
  },
30
+ approval: {
31
+ type: 'object',
32
+ description: 'Configuration for the inbuilt approval plugin.',
33
+ properties: {
34
+ actions: {
35
+ type: 'array',
36
+ items: { type: 'string' },
37
+ description: 'List of actions that require manual approval.',
38
+ },
39
+ },
40
+ },
21
41
  },
22
42
  },
23
- factory: ({ config, storage, tools }) => {
43
+ toolDefinitions: {
44
+ ...bashPlugin.toolDefinitions,
45
+ ...memoryPlugin.toolDefinitions,
46
+ ...storagePlugin.toolDefinitions,
47
+ ...delegationPlugin.toolDefinitions,
48
+ ...uiPlugin.toolDefinitions,
49
+ },
50
+ factory: (context) => (builder) => {
51
+ const { config, storage, tools, abortSignal } = context;
52
+ // Register inbuilt plugins
53
+ bashPlugin.factory(context)(builder);
54
+ memoryPlugin.factory(context)(builder);
55
+ storagePlugin.factory(context)(builder);
56
+ delegationPlugin.factory(context)(builder);
57
+ uiPlugin.factory(context)(builder);
58
+ // Approval plugin configuration
59
+ const approvalConfig = config?.approval || {
60
+ actions: ['action:bash', 'action:create_channel', 'action:delete_channel'],
61
+ };
62
+ approvalPlugin.factory({ ...context, config: approvalConfig })(builder);
24
63
  return openbotRuntime({
25
64
  model: config?.model,
26
65
  storage,
27
66
  toolDefinitions: tools,
28
- });
67
+ abortSignal,
68
+ })(builder);
29
69
  },
30
70
  };
31
71
  export default openbotPlugin;
@@ -2,7 +2,7 @@ import { generateText } from 'ai';
2
2
  import { openai } from '@ai-sdk/openai';
3
3
  import { anthropic } from '@ai-sdk/anthropic';
4
4
  import { eventsToModelMessages } from './history.js';
5
- import { ORCHESTRATOR_AGENT_ID, buildContext, } from './context.js';
5
+ import { buildContext, } from './context.js';
6
6
  import { saveConfig } from '../../app/config.js';
7
7
  import { API_KEY_SETUP_MESSAGE, OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
8
8
  function resolveModel(modelString) {
@@ -22,10 +22,7 @@ function resolveModel(modelString) {
22
22
  }
23
23
  async function buildSystemPrompt(state, storage) {
24
24
  const context = await buildContext(state, storage);
25
- const instructions = state.agentId === ORCHESTRATOR_AGENT_ID
26
- ? (state.agentDetails?.instructions?.trim() || OPENBOT_SYSTEM_PROMPT)
27
- : OPENBOT_SYSTEM_PROMPT;
28
- const sections = [instructions, '', context];
25
+ const sections = [OPENBOT_SYSTEM_PROMPT, '', context];
29
26
  // Hardcoded naming hint logic
30
27
  const threadState = state.threadDetails?.state;
31
28
  if (!threadState?.isSmartNamed) {
@@ -40,24 +37,41 @@ async function buildSystemPrompt(state, storage) {
40
37
  * a single `generateText` response execute one-by-one. We must wait for every ID
41
38
  * in the batch before calling the LLM again — not after the first result.
42
39
  */
43
- function createToolBatchTracker() {
44
- let pending = null;
40
+ function createToolBatchTracker(state, storage, channelId, threadId) {
41
+ const save = async (ids) => {
42
+ if (!storage || !channelId || !threadId)
43
+ return;
44
+ try {
45
+ await storage.patchThreadState({
46
+ channelId,
47
+ threadId,
48
+ state: { pendingToolCallIds: ids },
49
+ });
50
+ }
51
+ catch (error) {
52
+ console.error('[openbot] Failed to persist pendingToolCallIds:', error);
53
+ }
54
+ };
45
55
  return {
46
- startBatch(toolCallIds) {
47
- pending = new Set(toolCallIds);
56
+ async startBatch(toolCallIds) {
57
+ state.pendingToolCallIds = [...toolCallIds];
58
+ await save(state.pendingToolCallIds);
48
59
  },
49
- clear() {
50
- pending = null;
60
+ async clear() {
61
+ state.pendingToolCallIds = undefined;
62
+ await save(undefined);
51
63
  },
52
64
  /** Returns true when this result completes the batch (time to call the LLM again). */
53
- recordResult(toolCallId) {
54
- if (!pending?.has(toolCallId))
55
- return false;
56
- pending.delete(toolCallId);
57
- if (pending.size > 0)
65
+ async recordResult(toolCallId) {
66
+ if (!state.pendingToolCallIds?.includes(toolCallId))
58
67
  return false;
59
- pending = null;
60
- return true;
68
+ state.pendingToolCallIds = state.pendingToolCallIds.filter((id) => id !== toolCallId);
69
+ const done = state.pendingToolCallIds.length === 0;
70
+ if (done) {
71
+ state.pendingToolCallIds = undefined;
72
+ }
73
+ await save(state.pendingToolCallIds);
74
+ return done;
61
75
  },
62
76
  };
63
77
  }
@@ -69,13 +83,15 @@ function createToolBatchTracker() {
69
83
  * - When a full batch of results is in, `runLLM` runs again with updated history.
70
84
  */
71
85
  export const openbotRuntime = (options) => (builder) => {
72
- const { model: modelString = 'openai/gpt-4o-mini', storage, toolDefinitions = {}, } = options;
86
+ const { model: modelString = 'openai/gpt-4o-mini', storage, toolDefinitions = {}, abortSignal, } = options;
73
87
  let currentModelString = modelString;
74
88
  let model = resolveModel(currentModelString);
75
- const toolBatch = createToolBatchTracker();
76
89
  const runLLM = async function* (context, threadId, trigger) {
77
90
  if (!storage)
78
91
  return;
92
+ if (abortSignal?.aborted)
93
+ return;
94
+ const toolBatch = createToolBatchTracker(context.state, storage, context.state.channelId, threadId || context.state.threadId);
79
95
  // Capture parent metadata for event enrichment
80
96
  const triggerEvent = trigger || context.state.triggerEvent;
81
97
  const parentAgentId = triggerEvent?.meta?.parentAgentId;
@@ -88,8 +104,8 @@ export const openbotRuntime = (options) => (builder) => {
88
104
  });
89
105
  const messages = eventsToModelMessages(events);
90
106
  // console.log('systemPrompt:::::::\n', systemPrompt);
91
- // console.log('messages:::::::\n', JSON.stringify(messages, null, 2));
92
- // console.log('toolDefinitions:::::::\n', JSON.stringify(toolDefinitions, null, 2));
107
+ // console.log('messages:::::::\n', JSON.stringify(messages));
108
+ // console.log('toolDefinitions:::::::\n', JSON.stringify(toolDefinitions));
93
109
  try {
94
110
  // Single LLM request — tool execution happens externally via action:* handlers.
95
111
  const result = await generateText({
@@ -99,6 +115,7 @@ export const openbotRuntime = (options) => (builder) => {
99
115
  tools: toolDefinitions,
100
116
  stopWhen: ({ steps }) => steps.length === 1,
101
117
  allowSystemInMessages: true,
118
+ abortSignal,
102
119
  });
103
120
  const toolCalls = result.toolCalls ?? [];
104
121
  // if (result.usage) {
@@ -138,7 +155,7 @@ export const openbotRuntime = (options) => (builder) => {
138
155
  }
139
156
  if (toolCalls.length > 0) {
140
157
  // when multiple tool calls are made, Melony runtime handles them one by one, thats why we need to start a new batch
141
- toolBatch.startBatch(toolCalls.map((tc) => tc.toolCallId));
158
+ await toolBatch.startBatch(toolCalls.map((tc) => tc.toolCallId));
142
159
  for (const toolCall of toolCalls) {
143
160
  yield {
144
161
  type: `action:${toolCall.toolName}`,
@@ -152,10 +169,13 @@ export const openbotRuntime = (options) => (builder) => {
152
169
  }
153
170
  else {
154
171
  // clear the tool batch if there are no tool calls
155
- toolBatch.clear();
172
+ await toolBatch.clear();
156
173
  }
157
174
  }
158
175
  catch (error) {
176
+ // Run was stopped — unwind quietly without surfacing an error.
177
+ if (abortSignal?.aborted)
178
+ return;
159
179
  const errorMessage = error instanceof Error ? error.message : String(error);
160
180
  const isApiKeyError = errorMessage.includes('API key') ||
161
181
  errorMessage.includes('401') ||
@@ -217,10 +237,53 @@ export const openbotRuntime = (options) => (builder) => {
217
237
  if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
218
238
  return;
219
239
  }
240
+ // Capture user info from meta if available
241
+ if (event.meta?.userName) {
242
+ context.state.currentUser = {
243
+ userName: event.meta.userName,
244
+ };
245
+ }
246
+ const threadId = event.meta?.threadId || context.state.threadId;
247
+ // Auto-add participants if tagged in the prompt
248
+ const content = event.data?.content;
249
+ if (content && storage) {
250
+ try {
251
+ const allAgents = await storage.getAgents();
252
+ const tags = content.match(/@([\w-]+)/g);
253
+ if (tags) {
254
+ const taggedAgentIds = tags.map((t) => t.slice(1));
255
+ const validAgentIds = taggedAgentIds.filter((id) => allAgents.some((a) => a.id === id));
256
+ const currentParticipants = context.state.channelDetails?.participants || [];
257
+ const newParticipants = [...new Set([...currentParticipants, ...validAgentIds])];
258
+ if (newParticipants.length > currentParticipants.length) {
259
+ // Update storage
260
+ await storage.patchChannelState({
261
+ channelId: context.state.channelId,
262
+ state: { participants: newParticipants },
263
+ });
264
+ // Refresh local state
265
+ context.state.channelDetails = await storage.getChannelDetails({
266
+ channelId: context.state.channelId,
267
+ });
268
+ // Notify UI/others about the change
269
+ yield {
270
+ type: 'action:patch_channel_details:result',
271
+ data: { success: true, updatedFields: ['participants'] },
272
+ meta: {
273
+ agentId: context.state.agentId,
274
+ threadId,
275
+ },
276
+ };
277
+ }
278
+ }
279
+ }
280
+ catch (error) {
281
+ console.warn('[openbot] Failed to auto-add participants from tags:', error);
282
+ }
283
+ }
220
284
  // clear the tool batch if the agent is invoked
221
285
  // this is to prevent the tool batch from being used for a new agent invocation
222
- toolBatch.clear();
223
- const threadId = event.meta?.threadId || context.state.threadId;
286
+ await createToolBatchTracker(context.state, storage, context.state.channelId, threadId).clear();
224
287
  yield* runLLM(context, threadId, event);
225
288
  });
226
289
  // this is to handle the tool results from the tool calls
@@ -232,7 +295,8 @@ export const openbotRuntime = (options) => (builder) => {
232
295
  return;
233
296
  const toolCallId = event.meta?.toolCallId;
234
297
  // record the result of the tool call
235
- if (!toolCallId || !toolBatch.recordResult(toolCallId))
298
+ if (!toolCallId ||
299
+ !(await createToolBatchTracker(context.state, storage, context.state.channelId, event.meta?.threadId || context.state.threadId).recordResult(toolCallId)))
236
300
  return;
237
301
  const threadId = event.meta?.threadId || context.state.threadId;
238
302
  yield* runLLM(context, threadId);
@@ -1,5 +1,25 @@
1
1
  export const OPENBOT_SYSTEM_PROMPT = [
2
- 'You are a helpful AI assistant for your human. Your job is to help the user with their questions and tasks.',
2
+ '# ROLE',
3
+ 'You are an OpenBot, the main coordinator and router agent. Your primary role is to orchestrate specialized agents 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.',
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 almost never execute tasks yourself. Instead, you delegate tasks to specialized agents (channel participants). You act as a high-level manager, ensuring the right agent is working on the right task.',
11
+ '',
12
+ '# OPERATIONAL GUIDELINES',
13
+ '- **Channel and Threads**: The main and only way to communicate and act is through channels and threads. There might be a channel called "uncategorized" for general purpose communication.',
14
+ '- **Agent Participation**: ONLY add an agent via `patch_channel_details` if the user manually tags them (e.g., `@name`) AND they are missing from the `Participants` list in `ENVIRONMENT`.',
15
+ '- **Delegation**: NEVER delegate to an agent who is not a participant. Only if existing participants clearly cannot handle a task should you suggest relevant agents from the `INSTALLED AGENTS` list.',
16
+ '- **Bash Tool Usage**: You should use the `bash` tool very rarely. Only use it when the user explicitly requests a command to be run or when it is absolutely necessary for a task that no other participant can handle.',
17
+ '- **Context Awareness**: Use the provided ENVIRONMENT, CHANNEL SPECIFICATION, and MEMORIES to maintain continuity. Do not ask for information already present in these sections.',
18
+ '- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.',
19
+ '- **Structured Interaction**: Use the `render_widget` tool to collect information via forms, offer choices, or display lists. This is preferred over asking multiple separate questions in plain text.',
20
+ '',
21
+ '# COMMUNICATION STYLE',
22
+ '- Be always concise, professional, and proactive.',
3
23
  ].join('\n');
4
24
  /** Shown in the API key setup form when no provider credentials are configured. */
5
25
  export const API_KEY_SETUP_MESSAGE = 'OpenBot runs AI agents locally with tools, memory, and delegation. Bring your own OpenAI or Anthropic key — it stays on your machine. Use the form below to get started.';
@@ -1,5 +1,5 @@
1
1
  import { STATE_AGENT_ID } from '../../app/agent-ids.js';
2
- import { pluginService, resolveMarketplaceAgentList, } from '../../services/plugins/service.js';
2
+ import { pluginService, resolveMarketplaceRegistry, } from '../../services/plugins/service.js';
3
3
  /**
4
4
  * `plugin-manager` — marketplace listing, npm plugin install/uninstall, and
5
5
  * installing agents from the registry. Wired on the **`state`** built-in agent
@@ -46,12 +46,96 @@ export const pluginManagerPlugin = {
46
46
  }
47
47
  });
48
48
  builder.on('action:marketplace:list', async function* () {
49
- const agents = await resolveMarketplaceAgentList();
49
+ const { agents, channels } = await resolveMarketplaceRegistry();
50
50
  yield {
51
51
  type: 'action:marketplace:list:result',
52
- data: { success: true, agents },
52
+ data: { success: true, agents, channels },
53
53
  };
54
54
  });
55
+ builder.on('action:channel:install', async function* (event) {
56
+ try {
57
+ const { channelId: instanceId, name: templateName, participants: customParticipants, initialState: customInitialState, } = event.data;
58
+ const { agents: marketplaceAgents, channels } = await resolveMarketplaceRegistry();
59
+ // Try to find the template by ID or Name
60
+ const channelListing = channels.find((c) => c.id === instanceId) ||
61
+ channels.find((c) => c.name === templateName);
62
+ const channelId = instanceId;
63
+ const participants = customParticipants || channelListing?.participants || [];
64
+ const initialState = {
65
+ ...(channelListing?.initialState || {}),
66
+ ...(customInitialState || {}),
67
+ };
68
+ const spec = channelListing?.spec || '';
69
+ // 1. Auto-install participant agents if missing
70
+ for (const agentId of participants) {
71
+ const existingAgents = await storage.getAgents();
72
+ if (existingAgents.some((a) => a.id === agentId)) {
73
+ continue;
74
+ }
75
+ // Not found locally, look in marketplace
76
+ const agentListing = marketplaceAgents.find((a) => a.id === agentId);
77
+ if (agentListing) {
78
+ console.log(`[plugin-manager] Auto-installing agent ${agentId} for channel ${channelId}`);
79
+ // Install plugins for this agent
80
+ for (const ref of agentListing.plugins) {
81
+ const installed = await pluginService.isInstalled(ref.id);
82
+ if (!installed &&
83
+ ref.id.includes('/') === false &&
84
+ ref.id.includes('-plugin-') === false) {
85
+ continue;
86
+ }
87
+ if (!installed) {
88
+ try {
89
+ await pluginService.install({ packageName: ref.id });
90
+ }
91
+ catch (err) {
92
+ console.warn(`[plugins] Failed to pre-install plugin ${ref.id}`, err);
93
+ }
94
+ }
95
+ }
96
+ // Create the agent
97
+ await storage.createAgent({
98
+ agentId: agentListing.id,
99
+ name: agentListing.name,
100
+ description: agentListing.description,
101
+ image: agentListing.image,
102
+ instructions: agentListing.instructions,
103
+ plugins: agentListing.plugins,
104
+ });
105
+ }
106
+ }
107
+ // 2. Create the channel
108
+ await storage.createChannel({
109
+ channelId,
110
+ spec,
111
+ initialState: {
112
+ ...initialState,
113
+ participants,
114
+ },
115
+ });
116
+ const channelUrl = `/channels/${channelId}`;
117
+ yield {
118
+ type: 'action:channel:install:result',
119
+ data: { success: true, channelId, channelUrl },
120
+ };
121
+ yield {
122
+ type: 'agent:output',
123
+ data: {
124
+ content: `Successfully installed channel **${channelListing?.name || templateName || channelId}** and created channel \`${channelId}\`.`,
125
+ },
126
+ meta: { agentId: 'system' },
127
+ };
128
+ }
129
+ catch (error) {
130
+ yield {
131
+ type: 'action:channel:install:result',
132
+ data: {
133
+ success: false,
134
+ error: error instanceof Error ? error.message : 'Unknown error',
135
+ },
136
+ };
137
+ }
138
+ });
55
139
  builder.on('action:agent:install', async function* (event) {
56
140
  try {
57
141
  const { agentId: newAgentId, name, description, image, instructions, plugins, } = event.data;
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { spawn } from 'node:child_process';
3
+ import { resolvePath } from '../../app/config.js';
3
4
  const shellToolDefinitions = {
4
5
  shell_exec: {
5
6
  description: 'Execute a shell command in the terminal. Use this for file operations, running scripts, or system tasks.',
@@ -22,7 +23,7 @@ const shellPluginRuntime = () => (builder) => {
22
23
  builder.on('action:shell_exec', async function* (event, context) {
23
24
  const { command, cwd, shell = 'bash', timeoutMs = 30000 } = event.data;
24
25
  const actualTimeout = Math.max(1000, Math.min(timeoutMs, 60000));
25
- const actualCwd = cwd || context.state.channelDetails?.cwd || process.cwd();
26
+ const actualCwd = resolvePath(cwd || context.state.channelDetails?.cwd || process.cwd());
26
27
  try {
27
28
  const result = await new Promise((resolve) => {
28
29
  const child = spawn(command, {