openbot 0.2.14 → 0.3.1

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 (84) hide show
  1. package/dist/agents/openbot/index.js +76 -0
  2. package/dist/agents/openbot/middleware/approval.js +132 -0
  3. package/dist/agents/openbot/runtime.js +289 -0
  4. package/dist/agents/openbot/system-prompt.js +32 -0
  5. package/dist/agents/openbot/tools/delegation.js +78 -0
  6. package/dist/agents/openbot/tools/mcp.js +99 -0
  7. package/dist/agents/openbot/tools/shell.js +91 -0
  8. package/dist/agents/openbot/tools/storage.js +75 -0
  9. package/dist/agents/openbot/tools/ui.js +176 -0
  10. package/dist/agents/system.js +20 -93
  11. package/dist/app/cli.js +1 -1
  12. package/dist/app/config.js +4 -1
  13. package/dist/app/server.js +15 -8
  14. package/dist/bus/agent-package.js +1 -0
  15. package/dist/bus/plugin.js +1 -0
  16. package/dist/bus/services.js +711 -0
  17. package/dist/bus/types.js +1 -0
  18. package/dist/harness/context.js +250 -0
  19. package/dist/harness/event-normalizer.js +59 -0
  20. package/dist/harness/orchestrator.js +27 -227
  21. package/dist/harness/process.js +25 -3
  22. package/dist/harness/queue-processor.js +227 -0
  23. package/dist/harness/runtime-factory.js +103 -0
  24. package/dist/plugins/ai-sdk/index.js +37 -0
  25. package/dist/plugins/ai-sdk/runtime.js +402 -0
  26. package/dist/plugins/ai-sdk/system-prompt.js +3 -0
  27. package/dist/plugins/ai-sdk.js +277 -87
  28. package/dist/plugins/approval/index.js +159 -0
  29. package/dist/plugins/approval.js +163 -0
  30. package/dist/plugins/delegation/index.js +79 -0
  31. package/dist/plugins/delegation.js +67 -11
  32. package/dist/plugins/mcp/index.js +108 -0
  33. package/dist/plugins/memory/index.js +71 -0
  34. package/dist/plugins/shell/index.js +99 -0
  35. package/dist/plugins/shell.js +123 -0
  36. package/dist/plugins/storage-tools/index.js +85 -0
  37. package/dist/plugins/storage.js +240 -5
  38. package/dist/plugins/ui/index.js +184 -0
  39. package/dist/plugins/ui.js +185 -21
  40. package/dist/registry/agents.js +138 -0
  41. package/dist/registry/plugins.js +93 -50
  42. package/dist/services/agent-packages.js +103 -0
  43. package/dist/services/memory.js +152 -0
  44. package/dist/services/plugins.js +98 -0
  45. package/dist/services/storage.js +366 -94
  46. package/docs/agents.md +52 -65
  47. package/docs/architecture.md +1 -1
  48. package/docs/plugins.md +70 -58
  49. package/docs/templates/AGENT.example.md +57 -0
  50. package/package.json +8 -7
  51. package/src/app/cli.ts +1 -1
  52. package/src/app/config.ts +14 -4
  53. package/src/app/server.ts +23 -10
  54. package/src/app/types.ts +445 -16
  55. package/src/assets/icon.svg +4 -1
  56. package/src/bus/plugin.ts +67 -0
  57. package/src/bus/services.ts +786 -0
  58. package/src/bus/types.ts +160 -0
  59. package/src/harness/context.ts +293 -0
  60. package/src/harness/event-normalizer.ts +82 -0
  61. package/src/harness/orchestrator.ts +35 -273
  62. package/src/harness/process.ts +28 -4
  63. package/src/harness/queue-processor.ts +309 -0
  64. package/src/harness/runtime-factory.ts +125 -0
  65. package/src/plugins/ai-sdk/index.ts +44 -0
  66. package/src/plugins/ai-sdk/runtime.ts +484 -0
  67. package/src/plugins/ai-sdk/system-prompt.ts +4 -0
  68. package/src/plugins/approval/index.ts +228 -0
  69. package/src/plugins/delegation/index.ts +94 -0
  70. package/src/plugins/mcp/index.ts +128 -0
  71. package/src/plugins/memory/index.ts +85 -0
  72. package/src/plugins/shell/index.ts +123 -0
  73. package/src/plugins/storage-tools/index.ts +101 -0
  74. package/src/plugins/ui/index.ts +227 -0
  75. package/src/registry/plugins.ts +108 -55
  76. package/src/services/memory.ts +213 -0
  77. package/src/services/plugins.ts +133 -0
  78. package/src/services/storage.ts +472 -137
  79. package/src/agents/system.ts +0 -112
  80. package/src/plugins/ai-sdk.ts +0 -197
  81. package/src/plugins/delegation.ts +0 -60
  82. package/src/plugins/mcp.ts +0 -154
  83. package/src/plugins/storage.ts +0 -725
  84. package/src/plugins/ui.ts +0 -57
@@ -1,6 +1,7 @@
1
1
  import { generateText } from 'ai';
2
2
  import { openai } from '@ai-sdk/openai';
3
3
  import { anthropic } from '@ai-sdk/anthropic';
4
+ import { createDefaultContextEngine } from '../harness/context.js';
4
5
  /**
5
6
  * Resolves a standardized model string to an AI SDK LanguageModel.
6
7
  */
@@ -19,61 +20,41 @@ function resolveModel(modelString) {
19
20
  throw new Error(`Unsupported AI provider: "${provider}"`);
20
21
  }
21
22
  }
22
- async function buildSystemPrompt(state, system, context, storage) {
23
- const sections = [];
24
- if (state.agentDetails) {
25
- sections.push(`## AGENT NAME\n${state.agentDetails.name}`);
26
- sections.push(`## AGENT SPECIFICATION\n${state.agentDetails.instructions}`);
27
- }
28
- if (state.channelDetails) {
29
- sections.push(`## CHANNEL NAME\n${state.channelDetails.name}`);
30
- sections.push(`## CHANNEL SPECIFICATION\n${state.channelDetails.spec}`);
31
- // sections.push(`## CHANNEL STATE\n${JSON.stringify(state.channelDetails.state, null, 2)}`);
32
- if (storage) {
33
- try {
34
- const channelEvents = await storage.getEvents({ channelId: state.channelId });
35
- if (channelEvents.length > 0) {
36
- const formattedEvents = channelEvents
37
- .slice(-20)
38
- .map((e) => `- ${e.type}: ${JSON.stringify(e.data || {})}`)
39
- .join('\n');
40
- sections.push(`## CHANNEL RECENT ACTIVITIES (events)\n${formattedEvents}`);
41
- }
42
- }
43
- catch (error) {
44
- console.warn(`[ai-sdk] Failed to fetch channel events for ${state.channelId}`, error);
45
- }
46
- }
47
- }
48
- if (state.threadDetails) {
49
- sections.push(`## THREAD NAME\n${state.threadDetails.name}`);
50
- sections.push(`## THREAD SPECIFICATION\n${state.threadDetails.spec}`);
51
- // sections.push(`## THREAD STATE\n${JSON.stringify(state.threadDetails.state, null, 2)}`);
52
- if (storage && state.threadId) {
53
- try {
54
- const threadEvents = await storage.getEvents({
55
- channelId: state.channelId,
56
- threadId: state.threadId,
57
- });
58
- if (threadEvents.length > 0) {
59
- const formattedEvents = threadEvents
60
- .slice(-20)
61
- .map((e) => `- ${e.type}: ${JSON.stringify(e.data || {})}`)
62
- .join('\n');
63
- sections.push(`## THREAD RECENT ACTIVITIES (events)\n${formattedEvents}`);
64
- }
65
- }
66
- catch (error) {
67
- console.warn(`[ai-sdk] Failed to fetch thread events for channel ${state.channelId} thread ${state.threadId}`, error);
68
- }
69
- }
23
+ const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
24
+ const readPersistedShortTermMessages = (state) => {
25
+ const source = state.threadDetails?.state ?? state.channelDetails?.state;
26
+ const record = asRecord(source);
27
+ const raw = record.shortTermMessages;
28
+ return Array.isArray(raw) ? raw : [];
29
+ };
30
+ const persistShortTermMessages = async (state, storage) => {
31
+ if (!storage)
32
+ return;
33
+ const shortTermMessages = state.shortTermMessages ?? [];
34
+ if (state.threadId) {
35
+ await storage.patchThreadState({
36
+ channelId: state.channelId,
37
+ threadId: state.threadId,
38
+ state: { shortTermMessages },
39
+ });
40
+ return;
70
41
  }
42
+ await storage.patchChannelState({
43
+ channelId: state.channelId,
44
+ state: { shortTermMessages },
45
+ });
46
+ };
47
+ async function buildSystemPrompt(state, system, context, storage, contextEngine) {
48
+ const sections = [];
71
49
  if (system && typeof system === 'string') {
72
- sections.push(`## SYSTEM INSTRUCTIONS\n${system}`);
50
+ sections.push(system);
73
51
  }
74
52
  if (system && typeof system === 'function' && context) {
75
53
  sections.push(await system(context));
76
54
  }
55
+ if (contextEngine) {
56
+ sections.push(await contextEngine.buildContext(state, storage));
57
+ }
77
58
  return sections.join('\n\n');
78
59
  }
79
60
  /**
@@ -82,60 +63,269 @@ async function buildSystemPrompt(state, system, context, storage) {
82
63
  * It can also automatically trigger events based on tool calls.
83
64
  */
84
65
  export const aiSdkPlugin = (options) => (builder) => {
85
- const { model: modelString = 'openai/gpt-4o-mini', system, storage, toolDefinitions = {}, } = options;
66
+ const { model: modelString = 'openai/gpt-4o-mini', system, storage, contextEngine = createDefaultContextEngine(), toolDefinitions = {}, } = options;
86
67
  const model = resolveModel(modelString);
68
+ const ensureShortTermMessages = (state) => {
69
+ if (!state.shortTermMessages || state.shortTermMessages.length === 0) {
70
+ state.shortTermMessages = readPersistedShortTermMessages(state);
71
+ }
72
+ };
73
+ const mapToCoreMessages = (messages) => {
74
+ return messages.map((m) => {
75
+ if (m.role === 'assistant' && m.toolCalls) {
76
+ const assistantContent = [
77
+ {
78
+ role: 'assistant',
79
+ content: [
80
+ { type: 'text', text: m.content || '' },
81
+ ...m.toolCalls.map((tc) => ({
82
+ type: 'tool-call',
83
+ toolCallId: tc.id,
84
+ toolName: tc.function.name,
85
+ input: JSON.parse(tc.function.arguments),
86
+ })),
87
+ ],
88
+ },
89
+ ];
90
+ return assistantContent[0];
91
+ }
92
+ if (m.role === 'assistant') {
93
+ return {
94
+ role: 'assistant',
95
+ content: m.content || '',
96
+ };
97
+ }
98
+ if (m.role === 'tool') {
99
+ return {
100
+ role: 'tool',
101
+ content: [
102
+ {
103
+ type: 'tool-result',
104
+ toolCallId: m.toolCallId,
105
+ toolName: m.toolName,
106
+ output: {
107
+ type: 'text',
108
+ value: JSON.stringify(m.content),
109
+ },
110
+ },
111
+ ],
112
+ };
113
+ }
114
+ return m;
115
+ });
116
+ };
117
+ const runLLM = async function* (context, threadId) {
118
+ ensureShortTermMessages(context.state);
119
+ const systemPrompt = await buildSystemPrompt(context.state, system, context, storage, contextEngine);
120
+ const coreMessages = mapToCoreMessages(context.state.shortTermMessages || []);
121
+ try {
122
+ const result = await generateText({
123
+ model,
124
+ system: systemPrompt,
125
+ messages: coreMessages,
126
+ tools: toolDefinitions,
127
+ });
128
+ const toolCalls = result.toolCalls ?? [];
129
+ if (toolCalls.length > 0) {
130
+ // Add assistant message with tool calls to state
131
+ context.state.shortTermMessages = [
132
+ ...(context.state.shortTermMessages ?? []),
133
+ {
134
+ role: 'assistant',
135
+ content: result.text || '',
136
+ toolCalls: toolCalls.map((tc) => ({
137
+ id: tc.toolCallId,
138
+ type: 'function',
139
+ function: {
140
+ name: tc.toolName,
141
+ arguments: JSON.stringify(tc.input),
142
+ },
143
+ })),
144
+ },
145
+ ];
146
+ await persistShortTermMessages(context.state, storage);
147
+ for (const toolCall of toolCalls) {
148
+ yield {
149
+ type: `action:${toolCall.toolName}`,
150
+ data: toolCall.input,
151
+ meta: {
152
+ toolCallId: toolCall.toolCallId,
153
+ agentId: context.state.agentId,
154
+ threadId,
155
+ },
156
+ };
157
+ }
158
+ }
159
+ if (result.text) {
160
+ if (toolCalls.length === 0) {
161
+ context.state.shortTermMessages = [
162
+ ...(context.state.shortTermMessages ?? []),
163
+ { role: 'assistant', content: result.text },
164
+ ];
165
+ await persistShortTermMessages(context.state, storage);
166
+ }
167
+ yield {
168
+ type: 'agent:output',
169
+ data: {
170
+ content: result.text,
171
+ },
172
+ meta: {
173
+ agentId: context.state.agentId,
174
+ threadId,
175
+ },
176
+ };
177
+ }
178
+ }
179
+ catch (error) {
180
+ const errorMessage = error?.message || String(error);
181
+ const isApiKeyError = errorMessage.includes('API key') ||
182
+ errorMessage.includes('401') ||
183
+ errorMessage.includes('Unauthorized') ||
184
+ errorMessage.includes('authentication');
185
+ if (isApiKeyError) {
186
+ const provider = modelString.split('/')[0];
187
+ const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
188
+ yield {
189
+ type: 'client:ui:widget',
190
+ data: {
191
+ kind: 'form',
192
+ widgetId: `api_key_request_${Date.now()}`,
193
+ title: `${provider.toUpperCase()} API Key Required`,
194
+ description: `The ${provider} API returned an authentication error. Please provide a valid API key to continue. The key provided here never leaves your runtime Computer.`,
195
+ fields: [
196
+ {
197
+ id: 'apiKey',
198
+ label: 'API Key',
199
+ type: 'text',
200
+ placeholder: `sk-...`,
201
+ required: true,
202
+ },
203
+ ],
204
+ submitLabel: 'Save API Key',
205
+ metadata: {
206
+ type: 'api_key_request',
207
+ provider,
208
+ envVar,
209
+ },
210
+ },
211
+ meta: {
212
+ agentId: context.state.agentId,
213
+ threadId,
214
+ },
215
+ };
216
+ return;
217
+ }
218
+ // Re-throw other errors
219
+ throw error;
220
+ }
221
+ };
87
222
  builder.on('agent:invoke', async function* (event, context) {
223
+ // if the agent:invoke is routed to a different agent, don't process it, it prevents infinite loops
224
+ const routedTo = event.data?.agentId;
225
+ if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
226
+ return;
227
+ }
88
228
  // extract threadId if model decides to reply in a thread
89
229
  const threadId = event.meta?.threadId || context.state.threadId;
90
- const systemPrompt = await buildSystemPrompt(context.state, system, context, storage);
230
+ ensureShortTermMessages(context.state);
91
231
  context.state.shortTermMessages = [
92
232
  ...(context.state.shortTermMessages ?? []),
93
233
  {
94
234
  role: event.data?.role || 'user',
95
- content: event?.data?.content || '',
235
+ content: (event)?.data?.content || '',
96
236
  },
97
237
  ];
98
- const result = await generateText({
99
- model,
100
- system: systemPrompt,
101
- messages: context.state.shortTermMessages,
102
- tools: toolDefinitions,
103
- });
104
- const toolCalls = result.toolCalls ?? [];
105
- if (toolCalls.length > 0) {
106
- for (const toolCall of toolCalls) {
107
- const toolEvent = {
108
- type: `action:${toolCall.toolName}`,
109
- data: toolCall.input,
110
- meta: {
111
- toolCallId: toolCall.toolCallId,
112
- agentId: context.state.agentId,
113
- threadId,
114
- },
115
- };
116
- yield toolEvent;
238
+ await persistShortTermMessages(context.state, storage);
239
+ yield* runLLM(context, threadId);
240
+ });
241
+ builder.on('*', async function* (event, context) {
242
+ if (!event.type.endsWith(':result'))
243
+ return;
244
+ if (event.meta?.agentId !== context.state.agentId)
245
+ return;
246
+ const toolCallId = event.meta?.toolCallId;
247
+ if (!toolCallId)
248
+ return;
249
+ ensureShortTermMessages(context.state);
250
+ // Extract tool name from event type (e.g., action:shell_exec:result -> shell_exec)
251
+ const toolName = event.type.replace(/^action:/, '').replace(/:result$/, '');
252
+ // Add tool result to state
253
+ const resultData = event.data;
254
+ const content = typeof resultData === 'string' ? resultData : JSON.stringify(resultData);
255
+ context.state.shortTermMessages = [
256
+ ...(context.state.shortTermMessages ?? []),
257
+ { role: 'tool', content, toolCallId, toolName },
258
+ ];
259
+ await persistShortTermMessages(context.state, storage);
260
+ // Check if we should re-trigger LLM
261
+ // We re-trigger if the last assistant message's tool calls are all fulfilled
262
+ const lastAssistant = [...(context.state.shortTermMessages ?? [])]
263
+ .reverse()
264
+ .find((m) => m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0);
265
+ if (lastAssistant && lastAssistant.toolCalls) {
266
+ const allFulfilled = lastAssistant.toolCalls.every((tc) => context.state.shortTermMessages?.some((m) => m.role === 'tool' && m.toolCallId === tc.id));
267
+ if (allFulfilled) {
268
+ // Some tools intentionally terminate the current agent path (e.g., handoff).
269
+ // We still persist their tool result for provider consistency, but do not continue this agent.
270
+ if (toolName === 'handoff')
271
+ return;
272
+ const threadId = event.meta?.threadId || context.state.threadId;
273
+ yield* runLLM(context, threadId);
117
274
  }
118
275
  }
119
- if (result.text) {
120
- context.state.shortTermMessages = [
121
- ...(context.state.shortTermMessages ?? []),
122
- { role: 'assistant', content: result.text },
123
- ];
124
- yield {
125
- type: 'agent:output',
126
- data: {
127
- content: result.text,
128
- },
129
- meta: {
130
- agentId: context.state.agentId,
131
- },
132
- };
276
+ });
277
+ builder.on('client:ui:widget:response', async function* (event, context) {
278
+ const { metadata, values } = event.data;
279
+ if (metadata?.type === 'api_key_request' && values?.apiKey) {
280
+ const key = metadata.envVar;
281
+ const value = values.apiKey;
282
+ if (storage) {
283
+ try {
284
+ await storage.createVariable({ key, value, secret: true });
285
+ yield {
286
+ type: 'agent:output',
287
+ data: {
288
+ content: `Successfully saved ${metadata.provider} API key to workspace variables.`,
289
+ },
290
+ meta: {
291
+ agentId: context.state.agentId,
292
+ },
293
+ };
294
+ // Update the widget to show success
295
+ yield {
296
+ type: 'client:ui:widget',
297
+ data: {
298
+ widgetId: event.data.widgetId,
299
+ kind: 'message',
300
+ title: 'API Key Saved',
301
+ body: `Successfully saved ${metadata.provider} API key. You can now continue your conversation.`,
302
+ state: 'submitted',
303
+ actions: [{ id: 'ok', label: 'Got it', variant: 'primary' }],
304
+ },
305
+ meta: {
306
+ agentId: context.state.agentId,
307
+ },
308
+ };
309
+ }
310
+ catch (error) {
311
+ yield {
312
+ type: 'agent:output',
313
+ data: {
314
+ content: `Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'}`,
315
+ },
316
+ meta: {
317
+ agentId: context.state.agentId,
318
+ },
319
+ };
320
+ }
321
+ }
133
322
  }
134
323
  });
135
324
  };
136
325
  export const plugin = {
137
- name: 'ai-sdk',
326
+ id: 'ai-sdk',
327
+ name: 'AI SDK',
138
328
  description: 'Built-in AI SDK plugin',
139
329
  kind: 'runtime',
140
- factory: aiSdkPlugin,
330
+ factory: () => aiSdkPlugin({}),
141
331
  };
@@ -0,0 +1,159 @@
1
+ import { storageService } from '../../services/storage.js';
2
+ export const DEFAULT_APPROVAL_RULES = [
3
+ {
4
+ action: 'action:shell_exec',
5
+ denyEvent: 'action:shell_exec:result',
6
+ message: 'The agent wants to run a terminal command.',
7
+ detailKeys: ['command', 'cwd', 'shell', 'timeoutMs'],
8
+ hiddenKeys: ['env'],
9
+ denyData: {
10
+ exitCode: null,
11
+ stdout: '',
12
+ stderr: 'Command execution was denied by the user.',
13
+ timedOut: false,
14
+ },
15
+ },
16
+ ];
17
+ const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value)
18
+ ? value
19
+ : {};
20
+ const getApprovalsFromState = (state) => {
21
+ const source = state.threadDetails?.state ?? state.channelDetails?.state;
22
+ const stateRecord = asRecord(source);
23
+ return asRecord(stateRecord.approvals);
24
+ };
25
+ const persistApprovals = async (state, approvals) => {
26
+ if (state.threadId) {
27
+ await storageService.patchThreadState({
28
+ channelId: state.channelId,
29
+ threadId: state.threadId,
30
+ state: { approvals },
31
+ });
32
+ return;
33
+ }
34
+ await storageService.patchChannelState({
35
+ channelId: state.channelId,
36
+ state: { approvals },
37
+ });
38
+ };
39
+ const buildApprovalPlugin = (rules) => (builder) => {
40
+ for (const rule of rules) {
41
+ builder.on(rule.action, async function* (event, context) {
42
+ const meta = asRecord(event.meta);
43
+ if (meta.approvalStatus === 'approved')
44
+ return;
45
+ const eventData = asRecord(event.data);
46
+ const eventMeta = meta;
47
+ const approvalId = `approval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
48
+ const widgetId = `widget_${approvalId}`;
49
+ const executeEvent = rule.executeEvent || rule.action;
50
+ const denyEvent = rule.denyEvent || `${rule.action}:result`;
51
+ const denyData = rule.denyData || {};
52
+ const hiddenKeys = new Set(rule.hiddenKeys || []);
53
+ const detailKeys = rule.detailKeys || Object.keys(eventData);
54
+ const details = detailKeys
55
+ .filter((key) => !hiddenKeys.has(key))
56
+ .map((key) => `- ${key}: ${String(eventData[key] ?? '')}`)
57
+ .join('\n');
58
+ const pendingApprovals = getApprovalsFromState(context.state);
59
+ pendingApprovals[approvalId] = {
60
+ id: approvalId,
61
+ action: rule.action,
62
+ executeEvent,
63
+ denyEvent,
64
+ denyData,
65
+ payload: eventData,
66
+ meta: eventMeta,
67
+ message: rule.message || `Approval required for ${rule.action}.`,
68
+ createdAt: new Date().toISOString(),
69
+ status: 'pending',
70
+ };
71
+ await persistApprovals(context.state, pendingApprovals);
72
+ yield {
73
+ type: 'client:ui:widget',
74
+ data: {
75
+ kind: 'choice',
76
+ widgetId,
77
+ title: 'Approval Required',
78
+ body: `${rule.message || 'A protected action requires approval.'}${details ? `\n\n${details}` : ''}`,
79
+ metadata: { type: 'approval:request', approvalId, action: rule.action },
80
+ actions: [
81
+ { id: 'approve', label: 'Approve', variant: 'primary' },
82
+ { id: 'deny', label: 'Deny', variant: 'danger' },
83
+ ],
84
+ },
85
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
86
+ };
87
+ yield {
88
+ type: 'agent:output',
89
+ data: { content: `Waiting for approval before running \`${rule.action}\`.` },
90
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
91
+ };
92
+ context.suspend();
93
+ });
94
+ }
95
+ builder.on('client:ui:widget:response', async function* (event, context) {
96
+ const metadata = asRecord(event.data?.metadata);
97
+ if (metadata.type !== 'approval:request')
98
+ return;
99
+ const approvalId = String(metadata.approvalId || '');
100
+ if (!approvalId)
101
+ return;
102
+ const approvals = getApprovalsFromState(context.state);
103
+ const approval = approvals[approvalId];
104
+ if (!approval || approval.status !== 'pending') {
105
+ yield {
106
+ type: 'agent:output',
107
+ data: { content: 'Approval request not found or already resolved.' },
108
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
109
+ };
110
+ return;
111
+ }
112
+ const approved = event.data.actionId === 'approve';
113
+ approvals[approvalId] = {
114
+ ...approval,
115
+ status: approved ? 'approved' : 'denied',
116
+ };
117
+ await persistApprovals(context.state, approvals);
118
+ if (approved) {
119
+ yield {
120
+ type: approval.executeEvent,
121
+ data: approval.payload,
122
+ meta: {
123
+ ...(approval.meta || {}),
124
+ approvalId,
125
+ approvalStatus: 'approved',
126
+ },
127
+ };
128
+ return;
129
+ }
130
+ yield {
131
+ type: approval.denyEvent,
132
+ data: {
133
+ success: false,
134
+ approved: false,
135
+ error: 'Action denied by user approval.',
136
+ ...approval.denyData,
137
+ },
138
+ meta: { ...(approval.meta || {}), approvalId },
139
+ };
140
+ yield {
141
+ type: 'agent:output',
142
+ data: { content: 'Action denied by user approval.' },
143
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
144
+ };
145
+ });
146
+ };
147
+ const readRules = (config) => {
148
+ const raw = config.rules;
149
+ if (!Array.isArray(raw))
150
+ return DEFAULT_APPROVAL_RULES;
151
+ return raw.filter((entry) => !!entry && typeof entry === 'object' && typeof entry.action === 'string');
152
+ };
153
+ export const approvalPlugin = {
154
+ id: 'approval',
155
+ name: 'Approval',
156
+ description: 'Gate protected tool calls (e.g. shell_exec) behind a UI confirmation prompt.',
157
+ factory: ({ config }) => buildApprovalPlugin(readRules(config)),
158
+ };
159
+ export default approvalPlugin;