openbot 0.2.3 → 0.2.6
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.
- package/README.md +1 -1
- package/dist/agents/agent-creator.js +58 -19
- package/dist/agents/os-agent.js +1 -4
- package/dist/agents/planner-agent.js +32 -0
- package/dist/agents/topic-agent.js +1 -1
- package/dist/architecture/contracts.js +1 -0
- package/dist/architecture/execution-engine.js +151 -0
- package/dist/architecture/intent-classifier.js +26 -0
- package/dist/architecture/planner.js +106 -0
- package/dist/automation-worker.js +121 -0
- package/dist/automations.js +52 -0
- package/dist/cli.js +116 -146
- package/dist/config.js +20 -0
- package/dist/core/agents.js +41 -0
- package/dist/core/delegation.js +124 -0
- package/dist/core/manager.js +73 -0
- package/dist/core/plugins.js +77 -0
- package/dist/core/router.js +40 -0
- package/dist/installers.js +156 -0
- package/dist/marketplace.js +80 -0
- package/dist/open-bot.js +34 -157
- package/dist/orchestrator.js +247 -51
- package/dist/plugins/approval/index.js +107 -3
- package/dist/plugins/brain/index.js +17 -86
- package/dist/plugins/brain/memory.js +1 -1
- package/dist/plugins/brain/prompt.js +8 -13
- package/dist/plugins/brain/types.js +0 -15
- package/dist/plugins/file-system/index.js +8 -8
- package/dist/plugins/llm/context-shaping.js +177 -0
- package/dist/plugins/llm/index.js +223 -49
- package/dist/plugins/memory/index.js +220 -0
- package/dist/plugins/memory/memory.js +122 -0
- package/dist/plugins/memory/prompt.js +55 -0
- package/dist/plugins/memory/types.js +45 -0
- package/dist/plugins/shell/index.js +3 -3
- package/dist/plugins/skills/index.js +9 -9
- package/dist/registry/index.js +1 -4
- package/dist/registry/plugin-loader.js +361 -56
- package/dist/registry/plugin-registry.js +21 -4
- package/dist/registry/ts-agent-loader.js +4 -4
- package/dist/registry/yaml-agent-loader.js +78 -20
- package/dist/runtime/execution-trace.js +41 -0
- package/dist/runtime/intent-routing.js +26 -0
- package/dist/runtime/openbot-runtime.js +354 -0
- package/dist/server.js +513 -41
- package/dist/ui/widgets/approval-card.js +22 -2
- package/dist/ui/widgets/delegation.js +29 -0
- package/dist/version.js +62 -0
- package/package.json +4 -1
package/dist/orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
95
|
-
|
|
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
|
-
|
|
321
|
+
nextManagerEvent = {
|
|
123
322
|
type: "manager:result",
|
|
124
323
|
data: {
|
|
125
324
|
action: "delegateTask",
|
|
126
325
|
toolCallId,
|
|
127
326
|
result: agentOutput,
|
|
128
327
|
},
|
|
129
|
-
}
|
|
328
|
+
};
|
|
130
329
|
}
|
|
131
|
-
|
|
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
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
175
|
+
type: "action:result",
|
|
72
176
|
data: {
|
|
73
177
|
action: originalEvent.type.replace("action:", ""),
|
|
74
178
|
toolCallId: originalEvent.data.toolCallId,
|