openbot 0.4.7 → 0.5.5

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 (139) hide show
  1. package/.claude/agents/runtime-security-reviewer.md +32 -0
  2. package/.claude/hooks/format.sh +18 -0
  3. package/.claude/hooks/protect-secrets.sh +23 -0
  4. package/.claude/settings.json +26 -0
  5. package/.claude/skills/deploy/SKILL.md +26 -0
  6. package/.claude/skills/new-plugin/SKILL.md +46 -0
  7. package/.dockerignore +9 -0
  8. package/.mcp.json +8 -0
  9. package/AGENTS.md +2 -2
  10. package/CLAUDE.md +51 -0
  11. package/Dockerfile +41 -0
  12. package/deploy/README.md +59 -0
  13. package/deploy/entrypoint.sh +6 -0
  14. package/deploy/fly.toml +36 -0
  15. package/dist/app/bootstrap.js +29 -0
  16. package/dist/app/cli.js +5 -1
  17. package/dist/app/cloud-mode.js +15 -0
  18. package/dist/app/config.js +61 -16
  19. package/dist/app/ensure-default-stack.js +54 -0
  20. package/dist/app/openbot-plugin.js +4 -0
  21. package/dist/app/server.js +37 -6
  22. package/dist/harness/index.js +3 -0
  23. package/dist/plugins/approval/index.js +1 -1
  24. package/dist/plugins/bash/index.js +386 -164
  25. package/dist/plugins/memory/service.js +2 -5
  26. package/dist/plugins/openbot/context.js +17 -1
  27. package/dist/plugins/openbot/index.js +23 -7
  28. package/dist/plugins/openbot/model.js +20 -0
  29. package/dist/plugins/openbot/runtime.js +45 -57
  30. package/dist/plugins/openbot/system-prompt.js +43 -8
  31. package/dist/plugins/preview/index.js +267 -0
  32. package/dist/plugins/storage/index.js +3 -325
  33. package/dist/plugins/storage/service.js +61 -18
  34. package/dist/plugins/todo/index.js +131 -38
  35. package/dist/plugins/todo/service.js +93 -0
  36. package/dist/plugins/ui/index.js +8 -1
  37. package/dist/services/plugins/host.js +21 -0
  38. package/dist/services/plugins/model-registry.js +126 -0
  39. package/dist/services/plugins/registry.js +28 -42
  40. package/dist/services/plugins/service.js +2 -6
  41. package/docs/agents.md +20 -2
  42. package/docs/architecture.md +1 -1
  43. package/docs/plugins.md +13 -3
  44. package/docs/templates/AGENT.example.md +8 -14
  45. package/package.json +5 -2
  46. package/pnpm-workspace.yaml +2 -0
  47. package/src/app/bootstrap.ts +43 -0
  48. package/src/app/cli.ts +5 -1
  49. package/src/app/cloud-mode.ts +26 -0
  50. package/src/app/config.ts +71 -19
  51. package/src/app/ensure-default-stack.ts +63 -0
  52. package/src/app/openbot-plugin.ts +5 -0
  53. package/src/app/server.ts +42 -6
  54. package/src/app/types.ts +158 -32
  55. package/src/harness/index.ts +4 -0
  56. package/src/plugins/memory/service.ts +2 -5
  57. package/src/plugins/storage/index.ts +3 -371
  58. package/src/plugins/storage/service.ts +81 -22
  59. package/src/services/plugins/domain.ts +2 -0
  60. package/src/services/plugins/host.ts +36 -0
  61. package/src/services/plugins/model-registry.ts +178 -0
  62. package/src/services/plugins/registry.ts +30 -43
  63. package/src/services/plugins/service.ts +8 -17
  64. package/src/services/plugins/types.ts +36 -2
  65. package/src/services/todo/types.ts +12 -0
  66. package/dist/agents/openbot/index.js +0 -76
  67. package/dist/agents/openbot/middleware/approval.js +0 -132
  68. package/dist/agents/openbot/runtime.js +0 -289
  69. package/dist/agents/openbot/system-prompt.js +0 -32
  70. package/dist/agents/openbot/tools/delegation.js +0 -78
  71. package/dist/agents/openbot/tools/mcp.js +0 -99
  72. package/dist/agents/openbot/tools/shell.js +0 -91
  73. package/dist/agents/openbot/tools/storage.js +0 -75
  74. package/dist/agents/openbot/tools/ui.js +0 -176
  75. package/dist/agents/system.js +0 -33
  76. package/dist/bus/agent-package.js +0 -1
  77. package/dist/bus/plugin.js +0 -1
  78. package/dist/bus/services.js +0 -754
  79. package/dist/harness/agent-harness.js +0 -45
  80. package/dist/harness/agent-invoke-run.js +0 -44
  81. package/dist/harness/agent-turn.js +0 -99
  82. package/dist/harness/channel-participants.js +0 -40
  83. package/dist/harness/constants.js +0 -2
  84. package/dist/harness/context-meter.js +0 -97
  85. package/dist/harness/context.js +0 -363
  86. package/dist/harness/dispatch.js +0 -144
  87. package/dist/harness/dispatcher.js +0 -156
  88. package/dist/harness/event-normalizer.js +0 -59
  89. package/dist/harness/history.js +0 -177
  90. package/dist/harness/mcp.js +0 -61
  91. package/dist/harness/orchestration.js +0 -88
  92. package/dist/harness/orchestrator.js +0 -149
  93. package/dist/harness/participants.js +0 -22
  94. package/dist/harness/process.js +0 -29
  95. package/dist/harness/queue-processor.js +0 -187
  96. package/dist/harness/run-harness.js +0 -154
  97. package/dist/harness/run.js +0 -98
  98. package/dist/harness/runtime-factory.js +0 -73
  99. package/dist/harness/runtime.js +0 -57
  100. package/dist/harness/todo-advance.js +0 -93
  101. package/dist/harness/todo-dispatch.js +0 -51
  102. package/dist/harness/todos.js +0 -5
  103. package/dist/harness/turn.js +0 -79
  104. package/dist/harness/types.js +0 -1
  105. package/dist/plugins/ai-sdk/index.js +0 -34
  106. package/dist/plugins/ai-sdk/runtime.js +0 -395
  107. package/dist/plugins/ai-sdk/system-prompt.js +0 -18
  108. package/dist/plugins/ai-sdk.js +0 -331
  109. package/dist/plugins/approval.js +0 -163
  110. package/dist/plugins/delegation.js +0 -108
  111. package/dist/plugins/mcp/index.js +0 -108
  112. package/dist/plugins/mcp.js +0 -140
  113. package/dist/plugins/shell/index.js +0 -100
  114. package/dist/plugins/shell.js +0 -123
  115. package/dist/plugins/storage-tools/index.js +0 -76
  116. package/dist/plugins/storage.js +0 -737
  117. package/dist/plugins/thread-namer/index.js +0 -72
  118. package/dist/plugins/threads/index.js +0 -114
  119. package/dist/plugins/ui.js +0 -211
  120. package/dist/plugins/workflow/index.js +0 -65
  121. package/dist/registry/agents.js +0 -138
  122. package/dist/registry/plugins.js +0 -110
  123. package/dist/services/agent-packages.js +0 -103
  124. package/dist/services/memory.js +0 -152
  125. package/dist/services/plugins.js +0 -98
  126. package/dist/services/storage.js +0 -1028
  127. package/dist/workflow/service.js +0 -106
  128. package/dist/workflow/types.js +0 -3
  129. package/src/plugins/approval/index.ts +0 -147
  130. package/src/plugins/bash/index.ts +0 -232
  131. package/src/plugins/delegation/index.ts +0 -153
  132. package/src/plugins/memory/index.ts +0 -182
  133. package/src/plugins/openbot/context.ts +0 -121
  134. package/src/plugins/openbot/history.ts +0 -158
  135. package/src/plugins/openbot/index.ts +0 -80
  136. package/src/plugins/openbot/runtime.ts +0 -526
  137. package/src/plugins/openbot/system-prompt.ts +0 -22
  138. package/src/plugins/ui/index.ts +0 -123
  139. /package/dist/{bus → services/todo}/types.js +0 -0
@@ -1,106 +0,0 @@
1
- import { generateId } from 'melony';
2
- import { storageService } from '../services/storage.js';
3
- import { WORKFLOW_SCHEMA_VERSION, WORKFLOW_THREAD_STATE_KEY, } from './types.js';
4
- const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value)
5
- ? value
6
- : {};
7
- const isTodoStatus = (value) => value === 'pending' || value === 'done' || value === 'failed';
8
- const parseTodo = (value) => {
9
- const record = asRecord(value);
10
- if (typeof record.id !== 'string' || !record.id)
11
- return null;
12
- if (typeof record.text !== 'string' || !record.text)
13
- return null;
14
- if (!isTodoStatus(record.status))
15
- return null;
16
- return {
17
- id: record.id,
18
- text: record.text,
19
- status: record.status,
20
- summary: typeof record.summary === 'string' ? record.summary : undefined,
21
- };
22
- };
23
- export const parseThreadWorkflow = (state) => {
24
- const record = asRecord(state);
25
- const raw = record[WORKFLOW_THREAD_STATE_KEY];
26
- const workflow = asRecord(raw);
27
- if (typeof workflow.id !== 'string' || !workflow.id)
28
- return null;
29
- if (!Array.isArray(workflow.todos))
30
- return null;
31
- const todos = workflow.todos
32
- .map((todo) => parseTodo(todo))
33
- .filter((todo) => todo !== null);
34
- if (todos.length === 0 && !workflow.title)
35
- return null;
36
- return {
37
- version: typeof workflow.version === 'number' ? workflow.version : WORKFLOW_SCHEMA_VERSION,
38
- id: workflow.id,
39
- title: typeof workflow.title === 'string' ? workflow.title : undefined,
40
- todos,
41
- updatedAt: typeof workflow.updatedAt === 'string' ? workflow.updatedAt : new Date().toISOString(),
42
- };
43
- };
44
- export const loadThreadWorkflow = async (channelId, threadId) => {
45
- const details = await storageService.getThreadDetails({ channelId, threadId });
46
- return parseThreadWorkflow(details.state);
47
- };
48
- export const persistWorkflow = async (channelId, threadId, workflow) => {
49
- const next = {
50
- ...workflow,
51
- version: WORKFLOW_SCHEMA_VERSION,
52
- updatedAt: new Date().toISOString(),
53
- };
54
- await storageService.patchThreadState({
55
- channelId,
56
- threadId,
57
- state: { [WORKFLOW_THREAD_STATE_KEY]: next },
58
- });
59
- return next;
60
- };
61
- export const setWorkflowPlan = async (options) => {
62
- const { channelId, threadId, plan } = options;
63
- const workflow = {
64
- version: WORKFLOW_SCHEMA_VERSION,
65
- id: `wf_${generateId()}`,
66
- title: plan.title,
67
- todos: plan.todos.map((t) => ({
68
- id: t.id || `todo_${generateId()}`,
69
- text: t.text,
70
- status: 'pending',
71
- })),
72
- updatedAt: new Date().toISOString(),
73
- };
74
- return persistWorkflow(channelId, threadId, workflow);
75
- };
76
- export const updateWorkflowTodo = async (options) => {
77
- const { channelId, threadId, update } = options;
78
- const workflow = await loadThreadWorkflow(channelId, threadId);
79
- if (!workflow)
80
- throw new Error('No active workflow found.');
81
- const todos = workflow.todos.map((todo) => {
82
- if (todo.id !== update.todoId)
83
- return todo;
84
- return {
85
- ...todo,
86
- ...(update.status ? { status: update.status } : {}),
87
- ...(update.summary !== undefined ? { summary: update.summary } : {}),
88
- };
89
- });
90
- return persistWorkflow(channelId, threadId, { ...workflow, todos });
91
- };
92
- export const formatWorkflowForPrompt = (workflow) => {
93
- const lines = [
94
- '## Current Workflow Plan',
95
- workflow.title ? `Title: ${workflow.title}` : '',
96
- 'This plan is for your internal coordination. Update it as you progress.',
97
- '',
98
- ].filter(Boolean);
99
- for (const todo of workflow.todos) {
100
- lines.push(`- [${todo.status}] ${todo.id}: ${todo.text}`);
101
- if (todo.summary) {
102
- lines.push(` Result: ${todo.summary}`);
103
- }
104
- }
105
- return lines.join('\n');
106
- };
@@ -1,3 +0,0 @@
1
- export const WORKFLOW_SCHEMA_VERSION = 3;
2
- export const WORKFLOW_THREAD_STATE_KEY = 'workflow';
3
- export const OPENBOT_SYSTEM_AGENT_ID = 'system';
@@ -1,147 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import type { Plugin } from '../../services/plugins/types.js';
3
- import { OpenBotEvent } from '../../app/types.js';
4
-
5
- /**
6
- * `approval` — gates protected tool calls behind a UI confirmation widget.
7
- *
8
- * This is a simplified version that intercepts specified actions (default: bash)
9
- * and requires user approval before they are allowed to proceed.
10
- */
11
-
12
- // In-memory tracking for pending approval IDs with TTL (shared across plugin instances)
13
- const pendingApprovals = new Map<string, number>();
14
- const TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
15
-
16
- export const approvalPlugin: Plugin = {
17
- id: 'approval',
18
- name: 'Approval',
19
- description: 'Gate protected tool calls behind a UI confirmation widget.',
20
- factory: ({ config, storage }) => (builder) => {
21
- // Actions that require approval. Defaults to bash.
22
- const actionsToApprove = (config.actions as string[]) || ['action:bash'];
23
-
24
- for (const action of actionsToApprove) {
25
- builder.intercept(action as OpenBotEvent['type'], (event, context) => {
26
- // If already approved in this flow, let it pass to the actual handler
27
- if (event.meta?.approvalStatus === 'approved') return event;
28
-
29
- // Otherwise, intercept and ask for approval via a UI widget
30
- const displayData = JSON.stringify((event as any)?.data) || '';
31
-
32
- const widgetId = randomUUID();
33
- pendingApprovals.set(widgetId, Date.now());
34
-
35
- return {
36
- type: 'client:ui:widget',
37
- data: {
38
- widgetId,
39
- kind: 'message',
40
- title: `The agent wants to perform \`${action}\``,
41
- body: displayData,
42
- metadata: {
43
- type: 'approval:request',
44
- originalEvent: event,
45
- },
46
- actions: [
47
- { id: 'approve', label: 'Approve', variant: 'primary' },
48
- { id: 'deny', label: 'Deny', variant: 'danger' },
49
- ],
50
- },
51
- meta: { agentId: context.state.agentId, threadId: context.state.threadId },
52
- } as OpenBotEvent;
53
- });
54
- }
55
-
56
- // Handle the user's response from the UI widget
57
- builder.on('client:ui:widget:response', async function* (event, context) {
58
- const { widgetId, actionId } = event.data;
59
- const metadata = event.data?.metadata;
60
- if (metadata?.type !== 'approval:request') return;
61
-
62
- // Verify the widget is still pending and hasn't expired
63
- if (!widgetId || !pendingApprovals.has(widgetId)) {
64
- console.warn(`[approval] Received response for unknown or already handled widget: ${widgetId}`);
65
- return;
66
- }
67
-
68
- const timestamp = pendingApprovals.get(widgetId)!;
69
- if (Date.now() - timestamp > TTL_MS) {
70
- pendingApprovals.delete(widgetId);
71
- console.warn(`[approval] Received response for expired widget: ${widgetId}`);
72
- return;
73
- }
74
-
75
- // Mark as handled
76
- pendingApprovals.delete(widgetId);
77
-
78
- const originalEvent = metadata.originalEvent as OpenBotEvent;
79
- const approved = actionId === 'approve';
80
-
81
- const displayData = JSON.stringify((event as any)?.data) || '';
82
-
83
- // Yield a "responded" widget update to the UI
84
- yield {
85
- type: 'client:ui:widget',
86
- data: {
87
- widgetId,
88
- kind: 'message',
89
- title: `Action ${approved ? 'Approved' : 'Denied'}`,
90
- body: displayData,
91
- state: approved ? 'submitted' : 'cancelled',
92
- display: 'collapsed',
93
- disabled: true,
94
- actions: [], // Clear actions to disable buttons in UI
95
- },
96
- meta: { agentId: context.state.agentId, threadId: context.state.threadId },
97
- } as OpenBotEvent;
98
-
99
- if (approved) {
100
- // Re-emit the original event with approved status so the actual handler can run
101
- yield {
102
- ...originalEvent,
103
- meta: {
104
- ...(originalEvent.meta || {}),
105
- approvalStatus: 'approved',
106
- },
107
- };
108
- } else {
109
- // Manually store the original event with denied status so it's recorded in history
110
- // but NOT re-emitted to the pipeline (to avoid actual execution).
111
- if (storage) {
112
- await storage.storeEvent({
113
- channelId: context.state.channelId,
114
- threadId: context.state.threadId,
115
- event: {
116
- ...originalEvent,
117
- meta: {
118
- ...(originalEvent.meta || {}),
119
- approvalStatus: 'denied',
120
- },
121
- },
122
- });
123
- }
124
-
125
- // Emit a failure result event for the denied action to clear the pending tool batch
126
- yield {
127
- type: `${originalEvent.type}:result` as OpenBotEvent['type'],
128
- data: {
129
- success: false,
130
- error: 'Action denied by user.',
131
- stderr: 'Action denied by user.',
132
- output: 'Action denied by user.',
133
- },
134
- meta: originalEvent.meta,
135
- } as OpenBotEvent;
136
-
137
- yield {
138
- type: 'agent:output',
139
- data: { content: `Action \`${originalEvent.type}\` was denied.` },
140
- meta: { agentId: context.state.agentId },
141
- } as OpenBotEvent;
142
- }
143
- });
144
- },
145
- };
146
-
147
- export default approvalPlugin;
@@ -1,232 +0,0 @@
1
- import { MelonyPlugin } from 'melony';
2
- import { z } from 'zod';
3
- import { spawn, ChildProcess } from 'node:child_process';
4
- import { randomUUID } from 'node:crypto';
5
- import type { Plugin } from '../../services/plugins/types.js';
6
- import { OpenBotEvent, OpenBotState } from '../../app/types.js';
7
- import { resolvePath } from '../../app/config.js';
8
-
9
- const bashToolDefinitions = {
10
- bash: {
11
- description:
12
- 'Execute a bash command in a stateful session. The working directory and environment variables persist between calls. Use this for all system tasks, file operations, and running development servers.',
13
- inputSchema: z.object({
14
- command: z.string().describe('The bash command to execute.'),
15
- restart: z
16
- .boolean()
17
- .optional()
18
- .describe('Restart the bash session before running the command.'),
19
- }),
20
- },
21
- bash_stop: {
22
- description: 'Stop the bash session for the current or specified channel.',
23
- inputSchema: z.object({
24
- channelId: z.string().optional().describe('The channel ID to stop the session for.'),
25
- }),
26
- },
27
- bash_list_sessions: {
28
- description: 'List all active bash sessions.',
29
- inputSchema: z.object({}),
30
- },
31
- };
32
-
33
- interface BashSession {
34
- process: ChildProcess;
35
- cwd: string;
36
- lastActivity: number;
37
- }
38
-
39
- const sessions = new Map<string, BashSession>();
40
-
41
- const getSession = (channelId: string, initialCwd: string): BashSession => {
42
- let session = sessions.get(channelId);
43
- if (!session) {
44
- const childProcess = spawn('bash', ['--login'], {
45
- cwd: initialCwd,
46
- env: { ...process.env, PS1: '' },
47
- stdio: ['pipe', 'pipe', 'pipe'],
48
- });
49
-
50
- session = {
51
- process: childProcess,
52
- cwd: initialCwd,
53
- lastActivity: Date.now(),
54
- };
55
- sessions.set(channelId, session);
56
-
57
- // Basic error handling for the process
58
- childProcess.on('error', (err: Error) => {
59
- console.error(`[bash] Session error for channel ${channelId}:`, err);
60
- sessions.delete(channelId);
61
- });
62
-
63
- childProcess.on('exit', () => {
64
- sessions.delete(channelId);
65
- });
66
- }
67
- return session;
68
- };
69
-
70
- const bashPluginRuntime = (): MelonyPlugin<OpenBotState, OpenBotEvent> => (builder) => {
71
- builder.on('action:bash', async function* (event, context) {
72
- const { command, restart } = event.data;
73
- const channelId = context.state.channelId;
74
- const initialCwd = resolvePath(context.state.channelDetails?.cwd || process.cwd());
75
-
76
- if (restart) {
77
- const oldSession = sessions.get(channelId);
78
- if (oldSession) {
79
- oldSession.process.kill();
80
- sessions.delete(channelId);
81
- }
82
- }
83
-
84
- const session = getSession(channelId, initialCwd);
85
- session.lastActivity = Date.now();
86
-
87
- try {
88
- const result = await new Promise<{
89
- exitCode: number | null;
90
- stdout: string;
91
- stderr: string;
92
- timedOut: boolean;
93
- }>((resolve) => {
94
- let stdout = '';
95
- let stderr = '';
96
- let timedOut = false;
97
- const sentinel = `__OPENBOT_BASH_DONE_${Math.random().toString(36).substring(7)}__`;
98
-
99
- const timeoutMs = 60000; // 1 minute timeout for tool calls
100
- const timer = setTimeout(() => {
101
- timedOut = true;
102
- // We don't kill the session on timeout, just return what we have
103
- resolve({ exitCode: null, stdout, stderr, timedOut });
104
- }, timeoutMs);
105
-
106
- const onStdout = (data: Buffer) => {
107
- const str = data.toString();
108
- if (str.includes(sentinel)) {
109
- const parts = str.split(sentinel);
110
- stdout += parts[0];
111
- const exitCodeMatch = parts[1].match(/EXIT:(\d+)/);
112
- const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 0;
113
-
114
- cleanup();
115
- resolve({ exitCode, stdout, stderr, timedOut: false });
116
- } else {
117
- stdout += str;
118
- }
119
- };
120
-
121
- const onStderr = (data: Buffer) => {
122
- stderr += data.toString();
123
- };
124
-
125
- const cleanup = () => {
126
- clearTimeout(timer);
127
- session.process.stdout?.removeListener('data', onStdout);
128
- session.process.stderr?.removeListener('data', onStderr);
129
- };
130
-
131
- session.process.stdout?.on('data', onStdout);
132
- session.process.stderr?.on('data', onStderr);
133
-
134
- // Execute command and then echo the sentinel with exit code
135
- session.process.stdin?.write(`${command}\necho "${sentinel}EXIT:$?"\n`);
136
- });
137
-
138
- yield {
139
- type: 'action:bash:result',
140
- data: {
141
- success: result.exitCode === 0 && !result.timedOut,
142
- exitCode: result.exitCode,
143
- stdout: result.stdout.trim(),
144
- stderr: result.stderr.trim(),
145
- timedOut: result.timedOut,
146
- output: result.stderr.trim() ? result.stderr.trim() : result.stdout.trim(),
147
- },
148
- meta: event.meta,
149
- } as OpenBotEvent;
150
- } catch (error) {
151
- const message = error instanceof Error ? error.message : 'Unknown bash error';
152
- yield {
153
- type: 'action:bash:result',
154
- data: {
155
- success: false,
156
- exitCode: -1,
157
- stdout: '',
158
- stderr: message,
159
- timedOut: false,
160
- error: message,
161
- output: message,
162
- },
163
- meta: event.meta,
164
- } as OpenBotEvent;
165
- }
166
- });
167
-
168
- // Add a tool to stop/kill the session
169
- builder.on('action:bash_stop', async function* (event, context) {
170
- const channelId = event.data?.channelId || context.state.channelId;
171
- const session = sessions.get(channelId);
172
- if (session) {
173
- session.process.kill();
174
- sessions.delete(channelId);
175
- }
176
- yield {
177
- type: 'action:bash_stop:result',
178
- data: { success: true, output: `Bash session for channel ${channelId} stopped.` },
179
- meta: event.meta,
180
- } as OpenBotEvent;
181
- });
182
-
183
- // Add a tool to list all active sessions
184
- builder.on('action:bash_list_sessions', async function* (event, context) {
185
- const activeSessions = Array.from(sessions.entries()).map(([channelId, session]) => ({
186
- channelId,
187
- cwd: session.cwd,
188
- lastActivity: session.lastActivity,
189
- }));
190
-
191
- yield {
192
- type: 'client:ui:widget',
193
- data: {
194
- widgetId: randomUUID(),
195
- kind: 'list',
196
- title: 'Active Bash Sessions',
197
- description: `Found ${activeSessions.length} active bash session${activeSessions.length === 1 ? '' : 's'}.`,
198
- items: activeSessions.map((s) => ({
199
- id: s.channelId,
200
- label: s.channelId,
201
- description: `CWD: ${s.cwd}`,
202
- status: 'done',
203
- metadata: {
204
- cwd: s.cwd,
205
- lastActivity: s.lastActivity,
206
- },
207
- })),
208
- },
209
- meta: event.meta,
210
- } as OpenBotEvent;
211
-
212
- yield {
213
- type: 'action:bash_list_sessions:result',
214
- data: {
215
- success: true,
216
- sessions: activeSessions,
217
- output: JSON.stringify(activeSessions),
218
- },
219
- meta: event.meta,
220
- } as OpenBotEvent;
221
- });
222
- };
223
-
224
- export const bashPlugin: Plugin = {
225
- id: 'bash',
226
- name: 'Bash',
227
- description: 'Stateful bash session for the channel.',
228
- toolDefinitions: bashToolDefinitions,
229
- factory: () => bashPluginRuntime(),
230
- };
231
-
232
- export default bashPlugin;
@@ -1,153 +0,0 @@
1
- import { z } from 'zod';
2
- import { generateId } from 'melony';
3
- import type { Plugin } from '../../services/plugins/types.js';
4
- import {
5
- OpenBotEvent,
6
- DelegateTaskEvent,
7
- } from '../../app/types.js';
8
-
9
- /**
10
- * `delegation` — allows agents to delegate tasks to other agents.
11
- *
12
- * Only the 'system' agent is allowed to delegate by default.
13
- * It uses runAgent to execute the delegated agent in its own isolated runtime,
14
- * bridging events back to the caller's stream.
15
- */
16
-
17
- const delegationToolDefinitions = {
18
- delegate_task: {
19
- description: 'Delegate a specific task or question to another specialized agent.',
20
- inputSchema: z.object({
21
- agentId: z.string().describe('The ID of the agent to delegate to (e.g., "researcher", "coder").'),
22
- prompt: z.string().describe('The instructions or question for the delegated agent.'),
23
- }),
24
- },
25
- };
26
-
27
- export const delegationPlugin: Plugin = {
28
- id: 'delegation',
29
- name: 'Delegation',
30
- description: 'Allows agents to call upon other agents to solve sub-tasks.',
31
- toolDefinitions: delegationToolDefinitions,
32
- factory: (pluginContext) => (builder) => {
33
-
34
- // Handle the tool execution
35
- builder.on('action:delegate_task', async function* (event, context) {
36
- const delegateEvent = event as DelegateTaskEvent;
37
-
38
- // POLICY: Only the 'system' agent can delegate
39
- if (context.state.agentId !== 'system') {
40
- yield {
41
- type: 'action:delegate_task:result',
42
- data: {
43
- success: false,
44
- error: 'Only the system agent can delegate.'
45
- },
46
- meta: delegateEvent.meta,
47
- } as OpenBotEvent;
48
- return;
49
- }
50
-
51
- const { agentId, prompt } = delegateEvent.data;
52
- const toolCallId = delegateEvent.meta?.toolCallId;
53
-
54
- if (!toolCallId) return;
55
-
56
- // Break circular dependency by dynamic import
57
- const { runAgent } = await import('../../harness/index.js');
58
-
59
- const runId = `dg_${generateId()}`;
60
- let lastAgentOutput = '';
61
-
62
- // Queue to bridge the async onEvent callback to this generator
63
- const eventQueue: OpenBotEvent[] = [];
64
- let resolveNext: (() => void) | null = null;
65
- let isFinished = false;
66
-
67
- // Start the delegated agent in its own runtime.
68
- // We don't await this immediately so we can yield events as they arrive.
69
- const runPromise = runAgent({
70
- runId,
71
- agentId,
72
- event: {
73
- type: 'agent:invoke',
74
- data: {
75
- role: 'user',
76
- content: prompt,
77
- agentId: agentId,
78
- },
79
- meta: {
80
- threadId: context.state.threadId,
81
- parentAgentId: context.state.agentId,
82
- parentToolCallId: toolCallId,
83
- },
84
- } as OpenBotEvent,
85
- channelId: context.state.channelId,
86
- threadId: context.state.threadId,
87
- publicBaseUrl: pluginContext.publicBaseUrl,
88
- // Child events are re-yielded to the parent harness, which persists them once.
89
- persistEvents: false,
90
- onEvent: async (outEvent) => {
91
- // Enrich events with parent metadata so the UI can track the hierarchy
92
- const enrichedEvent = {
93
- ...outEvent,
94
- meta: {
95
- ...outEvent.meta,
96
- parentAgentId: context.state.agentId,
97
- parentToolCallId: toolCallId,
98
- }
99
- };
100
-
101
- eventQueue.push(enrichedEvent);
102
-
103
- if (outEvent.type === 'agent:output') {
104
- lastAgentOutput = outEvent.data.content;
105
- }
106
-
107
- // Wake up the generator loop if it's waiting
108
- if (resolveNext) {
109
- resolveNext();
110
- resolveNext = null;
111
- }
112
- }
113
- }).catch(error => {
114
- console.error(`[delegation] Error in delegated run ${runId}:`, error);
115
- }).finally(() => {
116
- isFinished = true;
117
- if (resolveNext) {
118
- resolveNext();
119
- resolveNext = null;
120
- }
121
- });
122
-
123
- // Yield events from the delegated agent as they arrive
124
- while (!isFinished || eventQueue.length > 0) {
125
- if (eventQueue.length === 0) {
126
- await new Promise<void>(r => { resolveNext = r; });
127
- }
128
- while (eventQueue.length > 0) {
129
- yield eventQueue.shift()!;
130
- }
131
- }
132
-
133
- // Ensure the run is fully complete (though isFinished already implies this)
134
- await runPromise;
135
-
136
- // Yield the result back to our own LLM runtime.
137
- yield {
138
- type: 'action:delegate_task:result',
139
- data: {
140
- success: true,
141
- output: lastAgentOutput,
142
- },
143
- meta: {
144
- ...delegateEvent.meta,
145
- agentId: context.state.agentId,
146
- toolCallId: toolCallId,
147
- },
148
- } as OpenBotEvent;
149
- });
150
- },
151
- };
152
-
153
- export default delegationPlugin;