openbot 0.2.2 → 0.2.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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/agent-creator.js +58 -19
  3. package/dist/agents/os-agent.js +1 -4
  4. package/dist/agents/planner-agent.js +32 -0
  5. package/dist/agents/topic-agent.js +1 -1
  6. package/dist/architecture/contracts.js +1 -0
  7. package/dist/architecture/execution-engine.js +151 -0
  8. package/dist/architecture/intent-classifier.js +26 -0
  9. package/dist/architecture/planner.js +106 -0
  10. package/dist/automation-worker.js +121 -0
  11. package/dist/automations.js +52 -0
  12. package/dist/cli.js +54 -141
  13. package/dist/config.js +20 -0
  14. package/dist/core/agents.js +41 -0
  15. package/dist/core/delegation.js +124 -0
  16. package/dist/core/manager.js +73 -0
  17. package/dist/core/plugins.js +77 -0
  18. package/dist/core/router.js +40 -0
  19. package/dist/installers.js +170 -0
  20. package/dist/marketplace.js +80 -0
  21. package/dist/open-bot.js +34 -157
  22. package/dist/orchestrator.js +247 -51
  23. package/dist/plugins/approval/index.js +107 -3
  24. package/dist/plugins/brain/index.js +17 -86
  25. package/dist/plugins/brain/memory.js +1 -1
  26. package/dist/plugins/brain/prompt.js +8 -13
  27. package/dist/plugins/brain/types.js +0 -15
  28. package/dist/plugins/file-system/index.js +8 -8
  29. package/dist/plugins/llm/context-shaping.js +177 -0
  30. package/dist/plugins/llm/index.js +223 -49
  31. package/dist/plugins/memory/index.js +220 -0
  32. package/dist/plugins/memory/memory.js +122 -0
  33. package/dist/plugins/memory/prompt.js +55 -0
  34. package/dist/plugins/memory/types.js +45 -0
  35. package/dist/plugins/shell/index.js +3 -3
  36. package/dist/plugins/skills/index.js +9 -9
  37. package/dist/registry/index.js +1 -4
  38. package/dist/registry/plugin-loader.js +339 -56
  39. package/dist/registry/plugin-registry.js +21 -4
  40. package/dist/registry/ts-agent-loader.js +4 -4
  41. package/dist/registry/yaml-agent-loader.js +78 -20
  42. package/dist/runtime/execution-trace.js +41 -0
  43. package/dist/runtime/intent-routing.js +26 -0
  44. package/dist/runtime/openbot-runtime.js +354 -0
  45. package/dist/server.js +549 -31
  46. package/dist/ui/widgets/approval-card.js +22 -2
  47. package/dist/ui/widgets/delegation.js +29 -0
  48. package/dist/version.js +62 -0
  49. package/package.json +8 -7
@@ -1,15 +1,47 @@
1
1
  import { melony } from "melony";
2
2
  const AGENT_TEXT_TYPES = new Set(["assistant:text-delta", "assistant:text"]);
3
+ const MAX_DELEGATIONS_PER_MANAGER_RUN = 6;
4
+ const DIRECT_TITLE_CONTEXT_LIMIT = 20;
5
+ const TASK_HINT_REGEX = /\b(create|build|write|fix|update|implement|run|execute|open|edit|delete|list|plan|analyze|research|refactor|install|configure|debug|deploy)\b/i;
3
6
  function isAgentTextEvent(event) {
4
7
  return AGENT_TEXT_TYPES.has(event.type);
5
8
  }
6
- /**
7
- * Orchestrator coordinates isolated runtimes for the manager and each agent.
8
- *
9
- * Instead of composing all plugins into a single shared runtime (which causes
10
- * duplicate handlers and shared state bugs), each agent gets its own melony
11
- * runtime. The orchestrator routes events between them.
12
- */
9
+ function createTraceId(runId) {
10
+ return `trace_${runId}_${Date.now()}`;
11
+ }
12
+ function nowIso() {
13
+ return new Date().toISOString();
14
+ }
15
+ function hasPendingApprovals(state) {
16
+ const agentStates = state.agentStates || {};
17
+ return Object.values(agentStates).some((agentState) => !!agentState.pendingApprovals &&
18
+ Object.keys(agentState.pendingApprovals).length > 0);
19
+ }
20
+ function parseDirectAgent(content, knownAgents) {
21
+ const raw = content.trim();
22
+ if (!raw || (!raw.startsWith("/") && !raw.startsWith("@")))
23
+ return null;
24
+ const firstSpace = raw.indexOf(" ");
25
+ const candidate = firstSpace === -1 ? raw.slice(1) : raw.slice(1, firstSpace);
26
+ if (!knownAgents.has(candidate))
27
+ return null;
28
+ const task = firstSpace === -1 ? "" : raw.slice(firstSpace + 1).trim();
29
+ return { agentName: candidate, task };
30
+ }
31
+ function classifyIntent(content, knownAgents) {
32
+ const raw = content.trim();
33
+ const direct = parseDirectAgent(raw, knownAgents);
34
+ if (direct) {
35
+ return { type: "agent_direct", targetAgent: direct.agentName };
36
+ }
37
+ if (!raw) {
38
+ return { type: "chat" };
39
+ }
40
+ if (TASK_HINT_REGEX.test(raw)) {
41
+ return { type: "task" };
42
+ }
43
+ return { type: "chat" };
44
+ }
13
45
  export class Orchestrator {
14
46
  constructor(options) {
15
47
  this.managerPlugin = options.managerPlugin;
@@ -43,11 +75,97 @@ export class Orchestrator {
43
75
  agentState.cwd = sessionState.cwd;
44
76
  return agentState;
45
77
  }
78
+ appendSessionMessage(sessionState, message) {
79
+ if (!sessionState.messages)
80
+ sessionState.messages = [];
81
+ sessionState.messages.push(message);
82
+ if (sessionState.messages.length > DIRECT_TITLE_CONTEXT_LIMIT) {
83
+ sessionState.messages = sessionState.messages.slice(-DIRECT_TITLE_CONTEXT_LIMIT);
84
+ }
85
+ }
86
+ setExecutionState(state, patch) {
87
+ const hasCurrentStepId = Object.prototype.hasOwnProperty.call(patch, "currentStepId");
88
+ const hasError = Object.prototype.hasOwnProperty.call(patch, "error");
89
+ const hasIntentType = Object.prototype.hasOwnProperty.call(patch, "intentType");
90
+ const hasPlanSteps = Object.prototype.hasOwnProperty.call(patch, "planSteps");
91
+ const next = {
92
+ traceId: patch.traceId ?? state.execution?.traceId ?? `trace_${Date.now()}`,
93
+ state: patch.state ?? state.execution?.state ?? "RECEIVED",
94
+ currentStepId: hasCurrentStepId ? patch.currentStepId : state.execution?.currentStepId,
95
+ error: hasError ? patch.error : state.execution?.error,
96
+ intentType: hasIntentType ? patch.intentType : state.execution?.intentType,
97
+ planSteps: hasPlanSteps ? patch.planSteps : state.execution?.planSteps,
98
+ updatedAt: nowIso(),
99
+ };
100
+ state.execution = next;
101
+ return next;
102
+ }
103
+ executionStateEvent(trace) {
104
+ return {
105
+ type: "execution:state",
106
+ data: {
107
+ traceId: trace?.traceId,
108
+ state: trace?.state,
109
+ currentStepId: trace?.currentStepId,
110
+ error: trace?.error,
111
+ intentType: trace?.intentType,
112
+ planSteps: trace?.planSteps,
113
+ },
114
+ };
115
+ }
116
+ async *triggerTopicRefresh(sessionState, runId) {
117
+ const runtime = this.buildManagerRuntime();
118
+ for await (const yielded of runtime.run({
119
+ type: "manager:completion",
120
+ data: { content: "" },
121
+ }, { state: sessionState, runId })) {
122
+ yield yielded;
123
+ }
124
+ }
125
+ estimatePlanSteps(planText) {
126
+ const trimmed = planText.trim();
127
+ if (!trimmed)
128
+ return undefined;
129
+ try {
130
+ const parsed = JSON.parse(trimmed);
131
+ if (Array.isArray(parsed?.steps))
132
+ return parsed.steps.length;
133
+ }
134
+ catch {
135
+ // Best-effort parsing only.
136
+ }
137
+ return undefined;
138
+ }
139
+ buildManagerTaskInput(userIntent, planText) {
140
+ return `User intent:\n${userIntent}\n\nPlanner output (treat as proposed execution plan):\n${planText}\n\nExecute this pragmatically. Delegate concrete subtasks to relevant specialist agents when needed. Keep user-facing updates concise.`;
141
+ }
142
+ async *runPlannerAgent(content, attachments, sessionState, runId) {
143
+ const plannerName = "planner-agent";
144
+ if (!this.agents.has(plannerName)) {
145
+ return "";
146
+ }
147
+ let plannerOutput = "";
148
+ for await (const yielded of this.runAgentInternal(plannerName, content, attachments, sessionState, runId)) {
149
+ if (yielded.type === `agent:${plannerName}:output`) {
150
+ plannerOutput = yielded.data.content || "";
151
+ }
152
+ // Planner output is consumed by OpenBot orchestration and should not be
153
+ // streamed directly to the user.
154
+ if (!isAgentTextEvent(yielded) && yielded.type !== `agent:${plannerName}:output`) {
155
+ yield yielded;
156
+ }
157
+ }
158
+ return plannerOutput;
159
+ }
46
160
  async *run(event, options) {
47
161
  const { state, runId = `run_${Date.now()}` } = options;
48
- // Yield the input event so it gets logged and the client can render it
49
162
  yield event;
50
163
  if (event.type === "action:approve" || event.type === "action:deny") {
164
+ const trace = this.setExecutionState(state, {
165
+ state: "EXECUTING",
166
+ error: undefined,
167
+ });
168
+ yield this.executionStateEvent(trace);
51
169
  yield* this.routeApproval(event, state, runId);
52
170
  return;
53
171
  }
@@ -57,43 +175,127 @@ export class Orchestrator {
57
175
  const attachments = Array.isArray(event.data.attachments)
58
176
  ? event.data.attachments
59
177
  : undefined;
60
- if (content.startsWith("/") || content.startsWith("@")) {
61
- const firstSpace = content.indexOf(" ");
62
- const prefix = firstSpace === -1 ? content.slice(1) : content.slice(1, firstSpace);
63
- const task = firstSpace === -1 ? "" : content.slice(firstSpace + 1).trim();
64
- if (this.agents.has(prefix)) {
65
- state.lastDirectAgent = prefix;
66
- yield* this.runAgentDirect(prefix, task, attachments, state, runId);
178
+ const knownAgents = new Set(this.agents.keys());
179
+ const direct = parseDirectAgent(content, knownAgents);
180
+ const intent = classifyIntent(content, knownAgents);
181
+ let trace = this.setExecutionState(state, {
182
+ traceId: createTraceId(runId),
183
+ state: "RECEIVED",
184
+ intentType: intent.type,
185
+ error: undefined,
186
+ currentStepId: undefined,
187
+ planSteps: undefined,
188
+ });
189
+ yield this.executionStateEvent(trace);
190
+ try {
191
+ if (direct) {
192
+ this.appendSessionMessage(state, {
193
+ role: "user",
194
+ content,
195
+ attachments,
196
+ });
197
+ trace = this.setExecutionState(state, {
198
+ state: "EXECUTING",
199
+ currentStepId: `delegate:${direct.agentName}`,
200
+ });
201
+ yield this.executionStateEvent(trace);
202
+ yield* this.runAgentDirect(direct.agentName, direct.task, attachments, state, runId);
203
+ const finalTrace = this.setExecutionState(state, {
204
+ state: hasPendingApprovals(state) ? "WAITING_APPROVAL" : "COMPLETED",
205
+ currentStepId: hasPendingApprovals(state)
206
+ ? `delegate:${direct.agentName}`
207
+ : undefined,
208
+ error: undefined,
209
+ });
210
+ yield this.executionStateEvent(finalTrace);
67
211
  return;
68
212
  }
213
+ trace = this.setExecutionState(state, {
214
+ state: "EXECUTING",
215
+ currentStepId: intent.type === "task" ? "planner" : "manager",
216
+ });
217
+ yield this.executionStateEvent(trace);
218
+ let managerInput = content;
219
+ if (intent.type === "task" && this.agents.has("planner-agent")) {
220
+ const plannerResult = yield* this.runPlannerAgent(content, attachments, state, runId);
221
+ if (plannerResult.trim()) {
222
+ managerInput = this.buildManagerTaskInput(content, plannerResult);
223
+ const planSteps = this.estimatePlanSteps(plannerResult);
224
+ trace = this.setExecutionState(state, {
225
+ currentStepId: "manager",
226
+ planSteps,
227
+ });
228
+ yield this.executionStateEvent(trace);
229
+ }
230
+ }
231
+ yield* this.runManagerLoop({
232
+ type: "manager:input",
233
+ data: { content: managerInput, attachments },
234
+ }, state, runId);
235
+ const waiting = hasPendingApprovals(state);
236
+ const finalTrace = this.setExecutionState(state, {
237
+ state: waiting ? "WAITING_APPROVAL" : "COMPLETED",
238
+ currentStepId: waiting ? (state.execution?.currentStepId ?? "manager") : undefined,
239
+ error: undefined,
240
+ });
241
+ yield this.executionStateEvent(finalTrace);
242
+ return;
243
+ }
244
+ catch (error) {
245
+ const finalTrace = this.setExecutionState(state, {
246
+ state: "FAILED",
247
+ error: error instanceof Error ? error.message : String(error),
248
+ });
249
+ yield this.executionStateEvent(finalTrace);
250
+ throw error;
69
251
  }
70
- state.lastDirectAgent = undefined;
71
- yield* this.runManagerLoop({ type: "manager:input", data: { content, attachments } }, state, runId);
72
- return;
73
252
  }
74
253
  yield* this.runManagerLoop(event, state, runId);
75
254
  }
76
- /**
77
- * Runs the manager runtime. When it yields `action:delegateTask`,
78
- * the orchestrator intercepts, runs the target agent to completion,
79
- * then feeds the result back to the manager in a new run.
80
- */
81
255
  async *runManagerLoop(event, state, runId) {
82
256
  const runtime = this.buildManagerRuntime();
83
- for await (const yielded of runtime.run(event, { state, runId })) {
84
- if (yielded.type === "action:delegateTask") {
257
+ const delegationSignatures = new Set();
258
+ let delegationCount = 0;
259
+ let nextManagerEvent = event;
260
+ while (nextManagerEvent) {
261
+ const managerEvent = nextManagerEvent;
262
+ nextManagerEvent = undefined;
263
+ for await (const yielded of runtime.run(managerEvent, { state, runId })) {
264
+ if (yielded.type !== "action:delegateTask") {
265
+ yield yielded;
266
+ continue;
267
+ }
85
268
  const { agent: agentName, task, attachments, toolCallId } = yielded.data;
269
+ const normalizedTask = typeof task === "string"
270
+ ? task.replace(/\s+/g, " ").trim().toLowerCase()
271
+ : "";
272
+ const signature = `${agentName}::${normalizedTask}`;
273
+ if (delegationCount >= MAX_DELEGATIONS_PER_MANAGER_RUN ||
274
+ (normalizedTask && delegationSignatures.has(signature))) {
275
+ nextManagerEvent = {
276
+ type: "manager:result",
277
+ data: {
278
+ action: "delegateTask",
279
+ toolCallId,
280
+ result: `Error: delegation loop detected for agent "${agentName}". Summarize current progress and stop delegating.`,
281
+ },
282
+ };
283
+ break;
284
+ }
86
285
  if (!this.agents.has(agentName)) {
87
- yield* this.runManagerLoop({
286
+ nextManagerEvent = {
88
287
  type: "manager:result",
89
288
  data: {
90
289
  action: "delegateTask",
91
290
  toolCallId,
92
291
  result: `Error: Agent "${agentName}" not found`,
93
292
  },
94
- }, state, runId);
95
- return;
293
+ };
294
+ break;
96
295
  }
296
+ delegationCount += 1;
297
+ if (normalizedTask)
298
+ delegationSignatures.add(signature);
97
299
  if (!state.pendingAgentTasks)
98
300
  state.pendingAgentTasks = {};
99
301
  state.pendingAgentTasks[agentName] = { toolCallId };
@@ -105,9 +307,6 @@ export class Orchestrator {
105
307
  agentOutput = agentEvent.data.content;
106
308
  agentCompleted = true;
107
309
  }
108
- // During delegation, suppress the agent's LLM text — the manager
109
- // will summarize the result for the user. Still pass through
110
- // operational events (status, UI/approval cards, etc.).
111
310
  if (!isAgentTextEvent(agentEvent)) {
112
311
  yield agentEvent;
113
312
  }
@@ -119,18 +318,17 @@ export class Orchestrator {
119
318
  }
120
319
  if (agentCompleted) {
121
320
  delete state.pendingAgentTasks[agentName];
122
- yield* this.runManagerLoop({
321
+ nextManagerEvent = {
123
322
  type: "manager:result",
124
323
  data: {
125
324
  action: "delegateTask",
126
325
  toolCallId,
127
326
  result: agentOutput,
128
327
  },
129
- }, state, runId);
328
+ };
130
329
  }
131
- return;
330
+ break;
132
331
  }
133
- yield yielded;
134
332
  }
135
333
  }
136
334
  async *runAgentInternal(agentName, task, attachments, sessionState, runId) {
@@ -148,28 +346,26 @@ export class Orchestrator {
148
346
  yield yielded;
149
347
  }
150
348
  }
151
- /**
152
- * Direct agent invocation via prefix commands (/agent task).
153
- * Wraps the agent's output event as assistant:text for the client.
154
- */
155
349
  async *runAgentDirect(agentName, task, attachments, sessionState, runId) {
156
350
  for await (const yielded of this.runAgentInternal(agentName, task, attachments, sessionState, runId)) {
157
351
  if (yielded.type === `agent:${agentName}:output`) {
352
+ const content = yielded.data.content;
353
+ this.appendSessionMessage(sessionState, {
354
+ role: "assistant",
355
+ content,
356
+ });
158
357
  yield {
159
358
  type: "assistant:text",
160
- data: { content: yielded.data.content },
359
+ data: { content },
161
360
  meta: { agent: agentName },
162
361
  };
362
+ yield* this.triggerTopicRefresh(sessionState, runId);
163
363
  }
164
364
  else {
165
365
  yield yielded;
166
366
  }
167
367
  }
168
368
  }
169
- /**
170
- * Routes approval/deny events to the agent that owns the pending approval.
171
- * After the agent resumes, bridges back to the manager if this was a delegation.
172
- */
173
369
  async *routeApproval(event, state, runId) {
174
370
  const approvalId = event.data.id;
175
371
  const agentStates = state.agentStates || {};
@@ -215,12 +411,12 @@ export class Orchestrator {
215
411
  },
216
412
  }, state, runId);
217
413
  }
218
- else if (agentCompleted && state.lastDirectAgent === targetAgent) {
219
- yield {
220
- type: "assistant:text",
221
- data: { content: agentOutput },
222
- meta: { agent: targetAgent },
223
- };
224
- }
414
+ const waiting = hasPendingApprovals(state);
415
+ const trace = this.setExecutionState(state, {
416
+ state: waiting ? "WAITING_APPROVAL" : "COMPLETED",
417
+ currentStepId: waiting ? state.execution?.currentStepId : undefined,
418
+ error: undefined,
419
+ });
420
+ yield this.executionStateEvent(trace);
225
421
  }
226
422
  }
@@ -1,6 +1,109 @@
1
1
  import { generateId } from "melony";
2
2
  import { ui } from "@melony/ui-kit/server";
3
3
  import { widgets } from "../../ui/widgets/index.js";
4
+ const DEFAULT_REDACTED_KEY_PATTERNS = [
5
+ /toolcallid/i,
6
+ /content/i,
7
+ /stdout/i,
8
+ /stderr/i,
9
+ /password/i,
10
+ /secret/i,
11
+ /token/i,
12
+ /api[_-]?key/i,
13
+ /authorization/i,
14
+ /cookie/i,
15
+ ];
16
+ const MAX_VALUE_LENGTH = 240;
17
+ const MAX_DETAILS = 8;
18
+ function serializeValue(value) {
19
+ if (value === undefined || value === null)
20
+ return "-";
21
+ const serialized = typeof value === "string"
22
+ ? value
23
+ : typeof value === "number" || typeof value === "boolean"
24
+ ? String(value)
25
+ : JSON.stringify(value);
26
+ if (serialized.length <= MAX_VALUE_LENGTH)
27
+ return serialized;
28
+ return `${serialized.slice(0, MAX_VALUE_LENGTH - 3)}...`;
29
+ }
30
+ function buildActionLabel(eventType) {
31
+ const action = eventType.startsWith("action:") ? eventType.slice("action:".length) : eventType;
32
+ return action.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase()).trim();
33
+ }
34
+ function toTitleCaseKey(key) {
35
+ return key
36
+ .replace(/([A-Z])/g, " $1")
37
+ .replace(/[_-]+/g, " ")
38
+ .replace(/^./, (c) => c.toUpperCase())
39
+ .trim();
40
+ }
41
+ function isRedactedKey(key, hiddenKeys = []) {
42
+ if (hiddenKeys.includes(key))
43
+ return true;
44
+ return DEFAULT_REDACTED_KEY_PATTERNS.some((pattern) => pattern.test(key));
45
+ }
46
+ function sanitizePayload(value, hiddenKeys = []) {
47
+ if (Array.isArray(value)) {
48
+ return value.map((item) => sanitizePayload(item, hiddenKeys));
49
+ }
50
+ if (value && typeof value === "object") {
51
+ const obj = value;
52
+ const sanitizedEntries = Object.entries(obj).map(([key, v]) => {
53
+ if (isRedactedKey(key, hiddenKeys))
54
+ return [key, "[REDACTED]"];
55
+ return [key, sanitizePayload(v, hiddenKeys)];
56
+ });
57
+ return Object.fromEntries(sanitizedEntries);
58
+ }
59
+ if (typeof value === "string" && value.length > 1000) {
60
+ return `${value.slice(0, 997)}...`;
61
+ }
62
+ return value;
63
+ }
64
+ function summarizeData(data = {}, hiddenKeys = []) {
65
+ const safeData = sanitizePayload(data, hiddenKeys);
66
+ return JSON.stringify(safeData, null, 2);
67
+ }
68
+ function isRenderableDetailValue(value) {
69
+ return value !== undefined && value !== null;
70
+ }
71
+ function deriveDetailEntries(data, rule) {
72
+ const hiddenKeys = rule.hiddenKeys ?? [];
73
+ if (rule.detailKeys?.length) {
74
+ return rule.detailKeys
75
+ .filter((key) => key in data)
76
+ .filter((key) => !isRedactedKey(key, hiddenKeys))
77
+ .filter((key) => isRenderableDetailValue(data[key]))
78
+ .map((key) => ({
79
+ label: toTitleCaseKey(key),
80
+ value: serializeValue(data[key]),
81
+ }))
82
+ .slice(0, MAX_DETAILS);
83
+ }
84
+ return Object.entries(data)
85
+ .filter(([key]) => !isRedactedKey(key, hiddenKeys))
86
+ .filter(([_, value]) => isRenderableDetailValue(value))
87
+ .slice(0, MAX_DETAILS)
88
+ .map(([key, value]) => ({
89
+ label: toTitleCaseKey(key),
90
+ value: serializeValue(value),
91
+ }));
92
+ }
93
+ function buildApprovalData(event, rule) {
94
+ const eventType = event.type;
95
+ const data = (event.data ?? {});
96
+ const details = [
97
+ { label: "Action", value: buildActionLabel(eventType) },
98
+ { label: "Event", value: eventType },
99
+ ...deriveDetailEntries(data, rule),
100
+ ];
101
+ return {
102
+ summary: rule.message || "The agent wants to execute an action. Review details before approving.",
103
+ details,
104
+ rawPayload: summarizeData(data, rule.hiddenKeys ?? []),
105
+ };
106
+ }
4
107
  /**
5
108
  * Approval Plugin for OpenBot.
6
109
  * Intercepts specific actions and requires user approval before proceeding.
@@ -31,7 +134,8 @@ export const approvalPlugin = (options) => (builder) => {
31
134
  state.pendingApprovals[approvalId] = event;
32
135
  // Use suspend(event) to emit the UI and halt execution of any handlers for this event.
33
136
  // This effectively "pauses" the run for user input.
34
- suspend(ui.event(widgets.approvalCard("Approval Required", rule.message || `Approval required for: ${event.type}\n\n${JSON.stringify(event.data, null, 2)}`, {
137
+ const approvalData = buildApprovalData(event, rule);
138
+ suspend(ui.event(widgets.approvalCard("Approval Required", approvalData, {
35
139
  type: "action:approve",
36
140
  data: { id: approvalId }
37
141
  }, {
@@ -65,10 +169,10 @@ export const approvalPlugin = (options) => (builder) => {
65
169
  if (originalEvent) {
66
170
  delete state.pendingApprovals[id];
67
171
  yield ui.event(widgets.status("Action denied", "error"));
68
- // If it was a tool call (action:*), return a taskResult error so the LLM knows it failed
172
+ // If it was a tool call (action:*), return a result error so the LLM knows it failed
69
173
  if (originalEvent.data?.toolCallId) {
70
174
  yield {
71
- type: "action:taskResult",
175
+ type: "action:result",
72
176
  data: {
73
177
  action: originalEvent.type.replace("action:", ""),
74
178
  toolCallId: originalEvent.data.toolCallId,