openbot 0.3.6 → 0.4.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.
Files changed (96) hide show
  1. package/README.md +15 -16
  2. package/dist/app/agent-ids.js +4 -0
  3. package/dist/app/cli.js +1 -1
  4. package/dist/app/config.js +0 -19
  5. package/dist/app/server.js +8 -14
  6. package/dist/bus/services.js +34 -124
  7. package/dist/harness/agent-invoke-run.js +44 -0
  8. package/dist/harness/agent-turn.js +99 -0
  9. package/dist/harness/channel-participants.js +40 -0
  10. package/dist/harness/constants.js +2 -0
  11. package/dist/harness/context-meter.js +97 -0
  12. package/dist/harness/context.js +95 -47
  13. package/dist/harness/dispatch.js +144 -0
  14. package/dist/harness/dispatcher.js +45 -156
  15. package/dist/harness/history.js +177 -0
  16. package/dist/harness/index.js +91 -0
  17. package/dist/harness/orchestration.js +88 -0
  18. package/dist/harness/participants.js +22 -0
  19. package/dist/harness/run-harness.js +154 -0
  20. package/dist/harness/run.js +98 -0
  21. package/dist/harness/runtime-factory.js +0 -34
  22. package/dist/harness/runtime.js +57 -0
  23. package/dist/harness/todo-dispatch.js +51 -0
  24. package/dist/harness/todos.js +5 -0
  25. package/dist/harness/turn.js +79 -0
  26. package/dist/plugins/approval/index.js +105 -149
  27. package/dist/plugins/delegation/index.js +119 -32
  28. package/dist/plugins/memory/index.js +103 -14
  29. package/dist/plugins/memory/service.js +152 -0
  30. package/dist/plugins/openbot/context.js +80 -0
  31. package/dist/plugins/openbot/history.js +98 -0
  32. package/dist/plugins/openbot/index.js +31 -0
  33. package/dist/plugins/openbot/runtime.js +317 -0
  34. package/dist/plugins/openbot/system-prompt.js +5 -0
  35. package/dist/plugins/plugin-manager/index.js +105 -0
  36. package/dist/plugins/storage/index.js +573 -0
  37. package/dist/plugins/storage/service.js +1159 -0
  38. package/dist/plugins/storage-tools/index.js +2 -2
  39. package/dist/plugins/thread-namer/index.js +72 -0
  40. package/dist/plugins/thread-naming/generate-title.js +44 -0
  41. package/dist/plugins/thread-naming/index.js +103 -0
  42. package/dist/plugins/threads/index.js +114 -0
  43. package/dist/plugins/todo/index.js +24 -25
  44. package/dist/plugins/ui/index.js +2 -32
  45. package/dist/registry/plugins.js +3 -9
  46. package/dist/services/plugins/domain.js +1 -0
  47. package/dist/services/plugins/plugin-cache.js +9 -0
  48. package/dist/services/plugins/registry.js +110 -0
  49. package/dist/services/plugins/service.js +177 -0
  50. package/dist/services/plugins/types.js +1 -0
  51. package/dist/services/process.js +29 -0
  52. package/dist/services/storage.js +11 -10
  53. package/dist/services/thread-naming.js +81 -0
  54. package/docs/agents.md +16 -10
  55. package/docs/architecture.md +2 -2
  56. package/docs/plugins.md +6 -15
  57. package/docs/templates/AGENT.example.md +7 -13
  58. package/package.json +1 -2
  59. package/src/app/agent-ids.ts +5 -0
  60. package/src/app/cli.ts +1 -1
  61. package/src/app/config.ts +1 -31
  62. package/src/app/server.ts +8 -16
  63. package/src/app/types.ts +63 -189
  64. package/src/harness/index.ts +145 -0
  65. package/src/plugins/approval/index.ts +91 -189
  66. package/src/plugins/delegation/index.ts +136 -39
  67. package/src/plugins/memory/index.ts +112 -15
  68. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  69. package/src/plugins/openbot/context.ts +91 -0
  70. package/src/plugins/openbot/history.ts +107 -0
  71. package/src/plugins/openbot/index.ts +37 -0
  72. package/src/plugins/openbot/runtime.ts +384 -0
  73. package/src/plugins/openbot/system-prompt.ts +7 -0
  74. package/src/plugins/plugin-manager/index.ts +122 -0
  75. package/src/plugins/shell/index.ts +1 -1
  76. package/src/plugins/storage/index.ts +633 -0
  77. package/src/{services/storage.ts → plugins/storage/service.ts} +224 -67
  78. package/src/{bus/types.ts → services/plugins/domain.ts} +16 -7
  79. package/src/services/plugins/plugin-cache.ts +13 -0
  80. package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
  81. package/src/services/{plugins.ts → plugins/service.ts} +96 -2
  82. package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
  83. package/src/bus/services.ts +0 -954
  84. package/src/harness/context.ts +0 -365
  85. package/src/harness/dispatcher.ts +0 -379
  86. package/src/harness/mcp.ts +0 -78
  87. package/src/harness/runtime-factory.ts +0 -129
  88. package/src/harness/todo-advance.ts +0 -128
  89. package/src/plugins/ai-sdk/index.ts +0 -41
  90. package/src/plugins/ai-sdk/runtime.ts +0 -468
  91. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  92. package/src/plugins/mcp/index.ts +0 -128
  93. package/src/plugins/storage-tools/index.ts +0 -90
  94. package/src/plugins/todo/index.ts +0 -64
  95. package/src/plugins/ui/index.ts +0 -227
  96. /package/src/{harness → services}/process.ts +0 -0
@@ -1,228 +1,130 @@
1
- import { MelonyPlugin } from 'melony';
2
- import type { Plugin } from '../../bus/plugin.js';
3
- import { OpenBotEvent, OpenBotState } from '../../app/types.js';
4
- import { storageService } from '../../services/storage.js';
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { Plugin } from '../../services/plugins/types.js';
3
+ import { OpenBotEvent } from '../../app/types.js';
5
4
 
6
5
  /**
7
6
  * `approval` — gates protected tool calls behind a UI confirmation widget.
8
- *
9
- * Configuration is read from the per-agent plugin config in AGENT.md:
10
- * ```yaml
11
- * plugins:
12
- * - id: approval
13
- * config:
14
- * rules:
15
- * - action: action:shell_exec
16
- * message: The agent wants to run a terminal command.
17
- * detailKeys: [command, cwd, shell, timeoutMs]
18
- * ```
7
+ *
8
+ * This is a simplified version that intercepts specified actions (default: shell_exec)
9
+ * and requires user approval before they are allowed to proceed.
19
10
  */
20
11
 
21
- export type ApprovalRule = {
22
- action: string;
23
- message?: string;
24
- detailKeys?: string[];
25
- hiddenKeys?: string[];
26
- executeEvent?: string;
27
- denyEvent?: string;
28
- denyData?: Record<string, unknown>;
29
- };
30
-
31
- export const DEFAULT_APPROVAL_RULES: ApprovalRule[] = [
32
- {
33
- action: 'action:shell_exec',
34
- denyEvent: 'action:shell_exec:result',
35
- message: 'The agent wants to run a terminal command.',
36
- detailKeys: ['command', 'cwd', 'shell', 'timeoutMs'],
37
- hiddenKeys: ['env'],
38
- denyData: {
39
- exitCode: null,
40
- stdout: '',
41
- stderr: 'Command execution was denied by the user.',
42
- timedOut: false,
43
- },
44
- },
45
- ];
46
-
47
- type PendingApproval = {
48
- id: string;
49
- action: string;
50
- executeEvent: string;
51
- denyEvent: string;
52
- denyData: Record<string, unknown>;
53
- payload: Record<string, unknown>;
54
- meta?: Record<string, unknown>;
55
- message: string;
56
- createdAt: string;
57
- status: 'pending' | 'approved' | 'denied';
58
- };
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
59
15
 
60
- const asRecord = (value: unknown): Record<string, unknown> =>
61
- value && typeof value === 'object' && !Array.isArray(value)
62
- ? (value as Record<string, unknown>)
63
- : {};
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 }) => (builder) => {
21
+ // Actions that require approval. Defaults to shell_exec.
22
+ const actionsToApprove = (config.actions as string[]) || ['action:shell_exec'];
64
23
 
65
- const getApprovalsFromState = (state: OpenBotState): Record<string, PendingApproval> => {
66
- const source = state.threadDetails?.state ?? state.channelDetails?.state;
67
- const stateRecord = asRecord(source);
68
- return asRecord(stateRecord.approvals) as Record<string, PendingApproval>;
69
- };
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;
70
28
 
71
- const persistApprovals = async (
72
- state: OpenBotState,
73
- approvals: Record<string, PendingApproval>,
74
- ): Promise<void> => {
75
- if (state.threadId) {
76
- await storageService.patchThreadState({
77
- channelId: state.channelId,
78
- threadId: state.threadId,
79
- state: { approvals },
80
- });
81
- return;
82
- }
83
- await storageService.patchChannelState({
84
- channelId: state.channelId,
85
- state: { approvals },
86
- });
87
- };
29
+ // Otherwise, intercept and ask for approval via a UI widget
30
+ const displayData = action === 'action:shell_exec'
31
+ ? `\`\`\`bash\n${(event as any).data.command}\n\`\`\``
32
+ : `\`\`\`json\n${JSON.stringify((event as any).data, null, 2)}\n\`\`\``;
88
33
 
89
- const buildApprovalPlugin =
90
- (rules: ApprovalRule[]): MelonyPlugin<OpenBotState, OpenBotEvent> =>
91
- (builder) => {
92
- for (const rule of rules) {
93
- builder.on(rule.action as OpenBotEvent['type'], async function* (event, context) {
94
- const meta = asRecord(event.meta);
95
- if (meta.approvalStatus === 'approved') return;
96
-
97
- const eventData = asRecord((event as { data?: unknown }).data);
98
- const eventMeta = meta;
99
-
100
- const approvalId = `approval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
101
- const widgetId = `widget_${approvalId}`;
102
- const executeEvent = rule.executeEvent || rule.action;
103
- const denyEvent = rule.denyEvent || `${rule.action}:result`;
104
- const denyData = rule.denyData || {};
105
- const hiddenKeys = new Set(rule.hiddenKeys || []);
106
- const detailKeys = rule.detailKeys || Object.keys(eventData);
107
- const details = detailKeys
108
- .filter((key) => !hiddenKeys.has(key))
109
- .map((key) => `- ${key}: ${String(eventData[key] ?? '')}`)
110
- .join('\n');
111
-
112
- const pendingApprovals = getApprovalsFromState(context.state);
113
- pendingApprovals[approvalId] = {
114
- id: approvalId,
115
- action: rule.action,
116
- executeEvent,
117
- denyEvent,
118
- denyData,
119
- payload: eventData,
120
- meta: eventMeta,
121
- message: rule.message || `Approval required for ${rule.action}.`,
122
- createdAt: new Date().toISOString(),
123
- status: 'pending',
124
- };
125
- await persistApprovals(context.state, pendingApprovals);
34
+ const widgetId = randomUUID();
35
+ pendingApprovals.set(widgetId, Date.now());
126
36
 
127
- yield {
37
+ context.suspend({
128
38
  type: 'client:ui:widget',
129
39
  data: {
130
- kind: 'choice',
131
40
  widgetId,
132
- title: 'Approval Required',
133
- body: `${rule.message || 'A protected action requires approval.'}${
134
- details ? `\n\n${details}` : ''
135
- }`,
136
- metadata: { type: 'approval:request', approvalId, action: rule.action },
41
+ kind: 'message',
42
+ title: `The agent wants to perform \`${action}\``,
43
+ body: displayData,
44
+ metadata: {
45
+ type: 'approval:request',
46
+ originalEvent: event,
47
+ },
137
48
  actions: [
138
49
  { id: 'approve', label: 'Approve', variant: 'primary' },
139
50
  { id: 'deny', label: 'Deny', variant: 'danger' },
140
51
  ],
141
52
  },
142
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
143
- } as OpenBotEvent;
144
-
145
- yield {
146
- type: 'agent:output',
147
- data: { content: `Waiting for approval before running \`${rule.action}\`.` },
148
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
149
- } as OpenBotEvent;
150
-
151
- context.suspend();
53
+ meta: { agentId: context.state.agentId, threadId: context.state.threadId },
54
+ } as OpenBotEvent);
152
55
  });
153
56
  }
154
57
 
58
+ // Handle the user's response from the UI widget
155
59
  builder.on('client:ui:widget:response', async function* (event, context) {
156
- const metadata = asRecord(event.data?.metadata);
157
- if (metadata.type !== 'approval:request') return;
60
+ const { widgetId, actionId } = event.data;
61
+ const metadata = event.data?.metadata;
62
+ if (metadata?.type !== 'approval:request') return;
158
63
 
159
- const approvalId = String(metadata.approvalId || '');
160
- if (!approvalId) return;
64
+ // Verify the widget is still pending and hasn't expired
65
+ if (!widgetId || !pendingApprovals.has(widgetId)) {
66
+ console.warn(`[approval] Received response for unknown or already handled widget: ${widgetId}`);
67
+ return;
68
+ }
161
69
 
162
- const approvals = getApprovalsFromState(context.state);
163
- const approval = approvals[approvalId];
164
- if (!approval || approval.status !== 'pending') {
165
- yield {
166
- type: 'agent:output',
167
- data: { content: 'Approval request not found or already resolved.' },
168
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
169
- } as OpenBotEvent;
70
+ const timestamp = pendingApprovals.get(widgetId)!;
71
+ if (Date.now() - timestamp > TTL_MS) {
72
+ pendingApprovals.delete(widgetId);
73
+ console.warn(`[approval] Received response for expired widget: ${widgetId}`);
170
74
  return;
171
75
  }
172
76
 
173
- const approved = event.data.actionId === 'approve';
174
- approvals[approvalId] = {
175
- ...approval,
176
- status: approved ? 'approved' : 'denied',
177
- };
178
- await persistApprovals(context.state, approvals);
77
+ // Mark as handled
78
+ pendingApprovals.delete(widgetId);
79
+
80
+ const originalEvent = metadata.originalEvent as OpenBotEvent;
81
+ const approved = actionId === 'approve';
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: `The request for \`${originalEvent.type}\` was ${approved ? 'approved' : 'denied'}.`,
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;
179
98
 
180
99
  if (approved) {
100
+ // Re-emit the original event with approved status so the actual handler can run
181
101
  yield {
182
- type: approval.executeEvent as OpenBotEvent['type'],
183
- data: approval.payload,
102
+ ...originalEvent,
184
103
  meta: {
185
- ...(approval.meta || {}),
186
- approvalId,
104
+ ...(originalEvent.meta || {}),
187
105
  approvalStatus: 'approved',
188
106
  },
107
+ };
108
+ } else {
109
+ // Emit a failure result event for the denied action
110
+ // yield {
111
+ // type: `${originalEvent.type}:result` as OpenBotEvent['type'],
112
+ // data: {
113
+ // success: false,
114
+ // error: 'Action denied by user.',
115
+ // stderr: 'Action denied by user.',
116
+ // },
117
+ // meta: originalEvent.meta,
118
+ // } as OpenBotEvent;
119
+
120
+ yield {
121
+ type: 'agent:output',
122
+ data: { content: `Action \`${originalEvent.type}\` was denied.` },
123
+ meta: { agentId: context.state.agentId },
189
124
  } as OpenBotEvent;
190
- return;
191
125
  }
192
-
193
- yield {
194
- type: approval.denyEvent as OpenBotEvent['type'],
195
- data: {
196
- success: false,
197
- approved: false,
198
- error: 'Action denied by user approval.',
199
- ...approval.denyData,
200
- },
201
- meta: { ...(approval.meta || {}), approvalId },
202
- } as OpenBotEvent;
203
-
204
- yield {
205
- type: 'agent:output',
206
- data: { content: 'Action denied by user approval.' },
207
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
208
- } as OpenBotEvent;
209
126
  });
210
- };
211
-
212
- const readRules = (config: Record<string, unknown>): ApprovalRule[] => {
213
- const raw = config.rules;
214
- if (!Array.isArray(raw)) return DEFAULT_APPROVAL_RULES;
215
- return raw.filter(
216
- (entry): entry is ApprovalRule =>
217
- !!entry && typeof entry === 'object' && typeof (entry as { action?: unknown }).action === 'string',
218
- );
219
- };
220
-
221
- export const approvalPlugin: Plugin = {
222
- id: 'approval',
223
- name: 'Approval',
224
- description: 'Gate protected tool calls (e.g. shell_exec) behind a UI confirmation prompt.',
225
- factory: ({ config }) => buildApprovalPlugin(readRules(config)),
127
+ },
226
128
  };
227
129
 
228
130
  export default approvalPlugin;
@@ -1,51 +1,148 @@
1
- import { MelonyPlugin } from 'melony';
2
- import z from 'zod';
3
- import type { Plugin } from '../../bus/plugin.js';
4
- import { OpenBotEvent, OpenBotState } from '../../app/types.js';
5
-
6
- const handoffToolDefinitions = {
7
- handoff: {
8
- description:
9
- 'Transfer control to another agent. The target agent continues the task in this thread.',
1
+ import { z } from 'zod';
2
+ import { generateId } from 'melony';
3
+ import type { Plugin } from '../../services/plugins/types.js';
4
+ import { runAgent } from '../../harness/index.js';
5
+ import {
6
+ OpenBotEvent,
7
+ DelegateTaskEvent,
8
+ } from '../../app/types.js';
9
+
10
+ /**
11
+ * `delegation` — allows agents to delegate tasks to other agents.
12
+ *
13
+ * Only the 'system' agent is allowed to delegate by default.
14
+ * It uses runAgent to execute the delegated agent in its own isolated runtime,
15
+ * bridging events back to the caller's stream.
16
+ */
17
+
18
+ const delegationToolDefinitions = {
19
+ delegate_task: {
20
+ description: 'Delegate a specific task or question to another specialized agent.',
10
21
  inputSchema: z.object({
11
- agentId: z.string().describe('The ID of the target agent.'),
12
- content: z.string().describe('The message or task to hand off.'),
22
+ agentId: z.string().describe('The ID of the agent to delegate to (e.g., "researcher", "coder").'),
23
+ prompt: z.string().describe('The instructions or question for the delegated agent.'),
13
24
  }),
14
25
  },
15
26
  };
16
27
 
17
- const handoffPluginRuntime = (): MelonyPlugin<OpenBotState, OpenBotEvent> => (builder) => {
18
- builder.on('action:handoff', async function* (event, context) {
19
- const { agentId, content } = event.data;
28
+ export const delegationPlugin: Plugin = {
29
+ id: 'delegation',
30
+ name: 'Delegation',
31
+ description: 'Allows agents to call upon other agents to solve sub-tasks.',
32
+ toolDefinitions: delegationToolDefinitions,
33
+ factory: () => (builder) => {
20
34
 
21
- yield {
22
- type: 'agent:output',
23
- data: { content: `Handing off to **${agentId}**: ${content}` },
24
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
25
- };
35
+ // Handle the tool execution
36
+ builder.on('action:delegate_task', async function* (event, context) {
37
+ const delegateEvent = event as DelegateTaskEvent;
26
38
 
27
- yield {
28
- type: 'handoff:request',
29
- data: { agentId, content },
30
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
31
- };
39
+ // POLICY: Only the 'system' agent can delegate
40
+ if (context.state.agentId !== 'system') {
41
+ yield {
42
+ type: 'action:delegate_task:result',
43
+ data: {
44
+ success: false,
45
+ error: 'Only the system agent can delegate.'
46
+ },
47
+ meta: delegateEvent.meta,
48
+ } as OpenBotEvent;
49
+ return;
50
+ }
32
51
 
33
- if (event.meta?.toolCallId) {
34
- yield {
35
- type: 'action:handoff:result',
36
- data: { success: true, agentId, accepted: true },
37
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
38
- };
39
- }
40
- });
41
- };
52
+ const { agentId, prompt } = delegateEvent.data;
53
+ const toolCallId = delegateEvent.meta?.toolCallId;
42
54
 
43
- export const delegationPlugin: Plugin = {
44
- id: 'delegation',
45
- name: 'Handoff',
46
- description: 'Hand off tasks to other agents on the bus.',
47
- toolDefinitions: handoffToolDefinitions,
48
- factory: () => handoffPluginRuntime(),
55
+ if (!toolCallId) return;
56
+
57
+ const runId = `dg_${generateId()}`;
58
+ let lastAgentOutput = '';
59
+
60
+ // Queue to bridge the async onEvent callback to this generator
61
+ const eventQueue: OpenBotEvent[] = [];
62
+ let resolveNext: (() => void) | null = null;
63
+ let isFinished = false;
64
+
65
+ // Start the delegated agent in its own runtime.
66
+ // We don't await this immediately so we can yield events as they arrive.
67
+ const runPromise = runAgent({
68
+ runId,
69
+ agentId,
70
+ event: {
71
+ type: 'agent:invoke',
72
+ data: {
73
+ role: 'user',
74
+ content: prompt,
75
+ agentId: agentId,
76
+ },
77
+ meta: {
78
+ threadId: context.state.threadId,
79
+ parentAgentId: context.state.agentId,
80
+ parentToolCallId: toolCallId,
81
+ },
82
+ } as OpenBotEvent,
83
+ channelId: context.state.channelId,
84
+ threadId: context.state.threadId,
85
+ onEvent: async (outEvent) => {
86
+ // Enrich events with parent metadata so the UI can track the hierarchy
87
+ const enrichedEvent = {
88
+ ...outEvent,
89
+ meta: {
90
+ ...outEvent.meta,
91
+ parentAgentId: context.state.agentId,
92
+ parentToolCallId: toolCallId,
93
+ }
94
+ };
95
+
96
+ eventQueue.push(enrichedEvent);
97
+
98
+ if (outEvent.type === 'agent:output') {
99
+ lastAgentOutput = outEvent.data.content;
100
+ }
101
+
102
+ // Wake up the generator loop if it's waiting
103
+ if (resolveNext) {
104
+ resolveNext();
105
+ resolveNext = null;
106
+ }
107
+ }
108
+ }).catch(error => {
109
+ console.error(`[delegation] Error in delegated run ${runId}:`, error);
110
+ }).finally(() => {
111
+ isFinished = true;
112
+ if (resolveNext) {
113
+ resolveNext();
114
+ resolveNext = null;
115
+ }
116
+ });
117
+
118
+ // Yield events from the delegated agent as they arrive
119
+ while (!isFinished || eventQueue.length > 0) {
120
+ if (eventQueue.length === 0) {
121
+ await new Promise<void>(r => { resolveNext = r; });
122
+ }
123
+ while (eventQueue.length > 0) {
124
+ yield eventQueue.shift()!;
125
+ }
126
+ }
127
+
128
+ // Ensure the run is fully complete (though isFinished already implies this)
129
+ await runPromise;
130
+
131
+ // Yield the result back to our own LLM runtime.
132
+ yield {
133
+ type: 'action:delegate_task:result',
134
+ data: {
135
+ success: true,
136
+ output: lastAgentOutput,
137
+ },
138
+ meta: {
139
+ ...delegateEvent.meta,
140
+ agentId: context.state.agentId,
141
+ toolCallId: toolCallId,
142
+ },
143
+ } as OpenBotEvent;
144
+ });
145
+ },
49
146
  };
50
147
 
51
148
  export default delegationPlugin;
@@ -1,19 +1,42 @@
1
1
  import z from 'zod';
2
- import type { Plugin } from '../../bus/plugin.js';
2
+ import type { Plugin } from '../../services/plugins/types.js';
3
+ import { OpenBotEvent, MemoryScopeAlias } from '../../app/types.js';
3
4
 
4
5
  /**
5
- * `memory` exposes the global memory store as agent tools.
6
- *
7
- * The actual handlers live in `bus/services.ts` because memory is platform
8
- * infrastructure (shared across every agent on the bus); this plugin only
9
- * contributes the tool definitions so a runtime plugin (e.g. `ai-sdk`) can
10
- * surface them to the LLM.
11
- *
12
- * Scopes
13
- * ------
14
- * - `global` (default) — visible to every agent and channel.
15
- * - `agent` — visible only to the agent that wrote it.
16
- * - `channel` — visible only inside the active channel.
6
+ * Resolve a scope alias to a concrete scope string. Aliases let tools accept
7
+ * `agent`/`channel`/`global` without knowing the active ids; the bus rewrites
8
+ * them using `context.state`.
9
+ */
10
+ function resolveMemoryScope(
11
+ alias: MemoryScopeAlias | undefined,
12
+ state: any,
13
+ ): string {
14
+ switch (alias) {
15
+ case 'agent':
16
+ return `agent:${state.agentId}`;
17
+ case 'channel':
18
+ return `channel:${state.channelId}`;
19
+ case 'global':
20
+ case undefined:
21
+ return 'global';
22
+ default:
23
+ return 'global';
24
+ }
25
+ }
26
+
27
+ function resolveMemoryScopeFilter(
28
+ alias: MemoryScopeAlias | 'all' | undefined,
29
+ state: any,
30
+ ): string[] | undefined {
31
+ if (alias === 'all' || alias === undefined) {
32
+ return ['global', `agent:${state.agentId}`, `channel:${state.channelId}`];
33
+ }
34
+ return [resolveMemoryScope(alias, state)];
35
+ }
36
+
37
+ /**
38
+ * `memory` — exposes the global memory store as agent tools and provides
39
+ * platform-level memory handlers.
17
40
  */
18
41
  const memoryToolDefinitions = {
19
42
  remember: {
@@ -77,8 +100,82 @@ export const memoryPlugin: Plugin = {
77
100
  description:
78
101
  'Global long-term memory: remember/recall/forget facts across runs and agents.',
79
102
  toolDefinitions: memoryToolDefinitions,
80
- factory: () => () => {
81
- // Handlers live in bus/services.ts; this plugin only contributes tool definitions.
103
+ factory: ({ storage }) => (builder) => {
104
+ builder.on('action:remember', async function* (event, context) {
105
+ const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
106
+ try {
107
+ const { content, scope, tags } = event.data;
108
+ const record = await storage.appendMemory({
109
+ scope: resolveMemoryScope(scope, context.state),
110
+ content,
111
+ tags,
112
+ });
113
+ yield {
114
+ type: 'action:remember:result',
115
+ data: { success: true, record },
116
+ meta: resultMeta,
117
+ } as OpenBotEvent;
118
+ } catch (error) {
119
+ yield {
120
+ type: 'action:remember:result',
121
+ data: {
122
+ success: false,
123
+ error: error instanceof Error ? error.message : 'Unknown error',
124
+ },
125
+ meta: resultMeta,
126
+ } as OpenBotEvent;
127
+ }
128
+ });
129
+
130
+ builder.on('action:recall', async function* (event, context) {
131
+ const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
132
+ try {
133
+ const { query, tag, scope, limit } = event.data;
134
+ const records = await storage.listMemories({
135
+ scopes: resolveMemoryScopeFilter(scope, context.state),
136
+ query,
137
+ tag,
138
+ limit,
139
+ });
140
+ yield {
141
+ type: 'action:recall:result',
142
+ data: { success: true, records },
143
+ meta: resultMeta,
144
+ } as OpenBotEvent;
145
+ } catch (error) {
146
+ yield {
147
+ type: 'action:recall:result',
148
+ data: {
149
+ success: false,
150
+ records: [],
151
+ error: error instanceof Error ? error.message : 'Unknown error',
152
+ },
153
+ meta: resultMeta,
154
+ } as OpenBotEvent;
155
+ }
156
+ });
157
+
158
+ builder.on('action:forget', async function* (event, context) {
159
+ const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
160
+ try {
161
+ const deleted = await storage.deleteMemory({ id: event.data.id });
162
+ yield {
163
+ type: 'action:forget:result',
164
+ data: { success: true, deleted },
165
+ meta: resultMeta,
166
+ } as OpenBotEvent;
167
+ } catch (error) {
168
+ yield {
169
+ type: 'action:forget:result',
170
+ data: {
171
+ success: false,
172
+ deleted: false,
173
+ error: error instanceof Error ? error.message : 'Unknown error',
174
+ },
175
+ meta: resultMeta,
176
+ } as OpenBotEvent;
177
+ }
178
+ });
82
179
  },
83
180
  };
84
181
 
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
- import { DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
4
+ import { DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../../app/config.js';
5
5
 
6
6
  /**
7
7
  * Global memory service.