openbot 0.2.10 → 0.2.11
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/dist/agents/topic-agent.js +17 -3
- package/dist/cli.js +1 -1
- package/dist/core/delegation.js +128 -22
- package/dist/core/manager.js +31 -8
- package/dist/core/plugins.js +5 -8
- package/dist/core/router.js +164 -13
- package/dist/open-bot.js +2 -2
- package/dist/plugins/approval/index.js +75 -162
- package/dist/plugins/file-system/index.js +2 -2
- package/dist/plugins/llm/context-budget.js +139 -0
- package/dist/plugins/llm/index.js +85 -31
- package/dist/plugins/memory/index.js +2 -2
- package/dist/plugins/shell/index.js +2 -2
- package/dist/plugins/skills/index.js +4 -4
- package/dist/registry/plugin-loader.js +81 -9
- package/dist/registry/plugin-registry.js +5 -5
- package/dist/server.js +0 -1
- package/dist/session.js +23 -5
- package/dist/ui/block.js +12 -0
- package/dist/ui/widgets/action-list.js +2 -9
- package/dist/ui/widgets/approval-card.js +9 -35
- package/dist/ui/widgets/code-snippet.js +2 -2
- package/dist/ui/widgets/data-block.js +2 -5
- package/dist/ui/widgets/data-table.js +2 -8
- package/dist/ui/widgets/empty-state.js +2 -7
- package/dist/ui/widgets/index.js +4 -0
- package/dist/ui/widgets/inquiry.js +7 -0
- package/dist/ui/widgets/key-value.js +2 -12
- package/dist/ui/widgets/progress-step.js +2 -5
- package/dist/ui/widgets/resource-card.js +2 -10
- package/dist/ui/widgets/status.js +2 -5
- package/dist/ui/widgets/todo-list.js +2 -0
- package/package.json +1 -2
|
@@ -1,186 +1,99 @@
|
|
|
1
1
|
import { generateId } from "melony";
|
|
2
|
-
import {
|
|
2
|
+
import { uiEvent } from "../../ui/block.js";
|
|
3
3
|
import { widgets } from "../../ui/widgets/index.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
}));
|
|
4
|
+
function getActionName(eventType) {
|
|
5
|
+
return eventType.startsWith("action:")
|
|
6
|
+
? eventType.slice("action:".length)
|
|
7
|
+
: eventType;
|
|
92
8
|
}
|
|
93
9
|
function buildApprovalData(event, rule) {
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
{ label: "Action", value: buildActionLabel(eventType) },
|
|
98
|
-
{ label: "Event", value: eventType },
|
|
99
|
-
...deriveDetailEntries(data, rule),
|
|
100
|
-
];
|
|
10
|
+
const actionName = getActionName(String(event.type));
|
|
11
|
+
const toolCallId = event?.data?.toolCallId;
|
|
12
|
+
const data = event?.data ?? {};
|
|
101
13
|
return {
|
|
102
|
-
summary: rule.message
|
|
103
|
-
|
|
104
|
-
|
|
14
|
+
summary: rule.message ??
|
|
15
|
+
"The agent requested a protected action. Approve to continue or deny to block it.",
|
|
16
|
+
details: [
|
|
17
|
+
{ label: "Action", value: actionName },
|
|
18
|
+
...(toolCallId ? [{ label: "Tool call", value: String(toolCallId) }] : []),
|
|
19
|
+
],
|
|
20
|
+
rawPayload: JSON.stringify(data, null, 2),
|
|
105
21
|
};
|
|
106
22
|
}
|
|
107
23
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
24
|
+
* Minimal approval gate:
|
|
25
|
+
* - Intercept protected action events.
|
|
26
|
+
* - Suspend current request and show Approve/Deny UI.
|
|
27
|
+
* - Resume only when user sends action:approve or action:deny.
|
|
111
28
|
*/
|
|
112
29
|
export const approvalPlugin = (options) => (builder) => {
|
|
113
|
-
const
|
|
114
|
-
// Register an interceptor that runs before any handlers.
|
|
115
|
-
// This is the correct way to handle HITL/Approval in Melony.
|
|
30
|
+
const rules = options?.rules ?? [];
|
|
116
31
|
builder.intercept(async (event, { state, suspend }) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (meta
|
|
121
|
-
event.type === "action:approve" ||
|
|
122
|
-
event.type === "action:deny" ||
|
|
123
|
-
event.type === "ui" ||
|
|
124
|
-
event.type.endsWith(":status")) {
|
|
32
|
+
const type = String(event.type ?? "");
|
|
33
|
+
const meta = event.meta ?? {};
|
|
34
|
+
// Never intercept internal approval events or already-approved replays.
|
|
35
|
+
if (type === "action:approve" || type === "action:deny" || meta.approved === true) {
|
|
125
36
|
return;
|
|
126
37
|
}
|
|
127
|
-
const rule = rules.find(r =>
|
|
38
|
+
const rule = rules.find((r) => type.startsWith(r.action));
|
|
128
39
|
if (!rule)
|
|
129
40
|
return;
|
|
130
41
|
const approvalId = `approve_${generateId()}`;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
})));
|
|
42
|
+
state.pendingApprovals ?? (state.pendingApprovals = {});
|
|
43
|
+
state.pendingApprovals[approvalId] = {
|
|
44
|
+
originalEvent: event,
|
|
45
|
+
createdAt: Date.now(),
|
|
46
|
+
};
|
|
47
|
+
suspend({
|
|
48
|
+
type: "suspend",
|
|
49
|
+
data: {
|
|
50
|
+
reason: "approval",
|
|
51
|
+
id: approvalId,
|
|
52
|
+
event: uiEvent(widgets.approvalCard("Approval Required", buildApprovalData(event, rule), { type: "action:approve", data: { id: approvalId } }, { type: "action:deny", data: { id: approvalId } }, { placement: "attention", id: approvalId })),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
145
55
|
});
|
|
146
|
-
// Handle Approval response from user
|
|
147
56
|
builder.on("action:approve", async function* (event, { state }) {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// The interceptor will see it, but bypass because of meta.approved.
|
|
155
|
-
// Then the appropriate handlers for the event will finally run.
|
|
156
|
-
yield {
|
|
157
|
-
...originalEvent,
|
|
158
|
-
meta: {
|
|
159
|
-
...originalEvent.meta,
|
|
160
|
-
approved: true,
|
|
161
|
-
},
|
|
162
|
-
};
|
|
57
|
+
const id = event?.data?.id;
|
|
58
|
+
if (!id)
|
|
59
|
+
return;
|
|
60
|
+
const pending = state.pendingApprovals?.[id];
|
|
61
|
+
if (!pending) {
|
|
62
|
+
return;
|
|
163
63
|
}
|
|
64
|
+
delete state.pendingApprovals[id];
|
|
65
|
+
// Re-emit the original action with approval marker.
|
|
66
|
+
yield {
|
|
67
|
+
...pending.originalEvent,
|
|
68
|
+
meta: {
|
|
69
|
+
...(pending.originalEvent?.meta ?? {}),
|
|
70
|
+
approved: true,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
164
73
|
});
|
|
165
|
-
// Handle Denial response from user
|
|
166
74
|
builder.on("action:deny", async function* (event, { state }) {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
75
|
+
const id = event?.data?.id;
|
|
76
|
+
if (!id)
|
|
77
|
+
return;
|
|
78
|
+
const pending = state.pendingApprovals?.[id];
|
|
79
|
+
if (!pending) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
delete state.pendingApprovals[id];
|
|
83
|
+
yield uiEvent(widgets.status("Action denied", "error", { placement: "attention", id }));
|
|
84
|
+
const originalEvent = pending.originalEvent;
|
|
85
|
+
const toolCallId = originalEvent?.data?.toolCallId;
|
|
86
|
+
if (toolCallId) {
|
|
87
|
+
yield {
|
|
88
|
+
type: "action:result",
|
|
89
|
+
data: {
|
|
90
|
+
action: getActionName(String(originalEvent.type ?? "")),
|
|
91
|
+
toolCallId,
|
|
92
|
+
result: { error: "Action denied by user", denied: true },
|
|
93
|
+
success: false,
|
|
94
|
+
halt: true,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
184
97
|
}
|
|
185
98
|
});
|
|
186
99
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { uiEvent } from "../../ui/block.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
@@ -166,6 +166,6 @@ export const fileSystemPlugin = (options = {}) => (builder) => {
|
|
|
166
166
|
}
|
|
167
167
|
});
|
|
168
168
|
builder.on("file-system:status", async function* (event) {
|
|
169
|
-
yield
|
|
169
|
+
yield uiEvent(statusWidget(event.data.message, event.data.severity));
|
|
170
170
|
});
|
|
171
171
|
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
function truncateMiddle(value, maxChars) {
|
|
2
|
+
if (value.length <= maxChars)
|
|
3
|
+
return value;
|
|
4
|
+
const half = Math.floor(maxChars / 2);
|
|
5
|
+
const removed = value.length - maxChars;
|
|
6
|
+
return `${value.slice(0, half)}\n\n[... ${removed} characters truncated ...]\n\n${value.slice(-half)}`;
|
|
7
|
+
}
|
|
8
|
+
function estimateStringTokens(value) {
|
|
9
|
+
if (!value)
|
|
10
|
+
return 0;
|
|
11
|
+
return Math.max(1, Math.ceil(value.length / 4));
|
|
12
|
+
}
|
|
13
|
+
function estimateMessageTokens(message) {
|
|
14
|
+
if (typeof message.content === "string") {
|
|
15
|
+
return estimateStringTokens(message.content);
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(message.content)) {
|
|
18
|
+
let total = 0;
|
|
19
|
+
for (const part of message.content) {
|
|
20
|
+
if (typeof part === "string") {
|
|
21
|
+
total += estimateStringTokens(part);
|
|
22
|
+
}
|
|
23
|
+
else if (part && typeof part === "object") {
|
|
24
|
+
total += estimateStringTokens(JSON.stringify(part));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return total;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
function cloneWithTrimmedSystemMessages(messages, maxSystemChars) {
|
|
32
|
+
return messages.map((message) => {
|
|
33
|
+
if (message.role !== "system" || typeof message.content !== "string") {
|
|
34
|
+
return message;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
...message,
|
|
38
|
+
content: truncateMiddle(message.content, maxSystemChars),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function getLeadingSystemCount(messages) {
|
|
43
|
+
let count = 0;
|
|
44
|
+
for (const message of messages) {
|
|
45
|
+
if (message.role !== "system")
|
|
46
|
+
break;
|
|
47
|
+
count++;
|
|
48
|
+
}
|
|
49
|
+
return count;
|
|
50
|
+
}
|
|
51
|
+
function collectAssistantToolPairings(messages) {
|
|
52
|
+
const toolCallToAssistant = new Map();
|
|
53
|
+
const assistantToToolResults = new Map();
|
|
54
|
+
messages.forEach((message, index) => {
|
|
55
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
56
|
+
return;
|
|
57
|
+
for (const part of message.content) {
|
|
58
|
+
if (part
|
|
59
|
+
&& typeof part === "object"
|
|
60
|
+
&& part.type === "tool-call"
|
|
61
|
+
&& typeof part.toolCallId === "string") {
|
|
62
|
+
toolCallToAssistant.set(part.toolCallId, index);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
messages.forEach((message, index) => {
|
|
67
|
+
if (message.role !== "tool" || !Array.isArray(message.content))
|
|
68
|
+
return;
|
|
69
|
+
for (const part of message.content) {
|
|
70
|
+
if (!part || typeof part !== "object")
|
|
71
|
+
continue;
|
|
72
|
+
const toolCallId = part.toolCallId;
|
|
73
|
+
if (typeof toolCallId !== "string")
|
|
74
|
+
continue;
|
|
75
|
+
const assistantIndex = toolCallToAssistant.get(toolCallId);
|
|
76
|
+
if (typeof assistantIndex !== "number")
|
|
77
|
+
continue;
|
|
78
|
+
const bucket = assistantToToolResults.get(assistantIndex) ?? new Set();
|
|
79
|
+
bucket.add(index);
|
|
80
|
+
assistantToToolResults.set(assistantIndex, bucket);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return assistantToToolResults;
|
|
84
|
+
}
|
|
85
|
+
function buildAtomicGroupIndices(index, messages, assistantToToolResults) {
|
|
86
|
+
const message = messages[index];
|
|
87
|
+
if (!message)
|
|
88
|
+
return [];
|
|
89
|
+
if (message.role === "assistant") {
|
|
90
|
+
const results = assistantToToolResults.get(index);
|
|
91
|
+
if (!results || results.size === 0)
|
|
92
|
+
return [index];
|
|
93
|
+
return [index, ...Array.from(results)].sort((a, b) => a - b);
|
|
94
|
+
}
|
|
95
|
+
if (message.role === "tool") {
|
|
96
|
+
for (const [assistantIndex, resultIndices] of assistantToToolResults.entries()) {
|
|
97
|
+
if (resultIndices.has(index)) {
|
|
98
|
+
return [assistantIndex, ...Array.from(resultIndices)].sort((a, b) => a - b);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return [index];
|
|
103
|
+
}
|
|
104
|
+
export function buildBudgetedMessages(messages, options) {
|
|
105
|
+
if (messages.length === 0)
|
|
106
|
+
return messages;
|
|
107
|
+
const normalized = cloneWithTrimmedSystemMessages(messages, options.maxSystemChars);
|
|
108
|
+
const systemCount = getLeadingSystemCount(normalized);
|
|
109
|
+
const leadingSystem = normalized.slice(0, systemCount);
|
|
110
|
+
const history = normalized.slice(systemCount);
|
|
111
|
+
const usableBudget = Math.max(1, options.maxContextTokens - options.reserveOutputTokens);
|
|
112
|
+
let used = leadingSystem.reduce((sum, message) => sum + estimateMessageTokens(message), 0);
|
|
113
|
+
const requiredIndices = new Set();
|
|
114
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
115
|
+
if (history[i].role === "user") {
|
|
116
|
+
requiredIndices.add(i);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const assistantToToolResults = collectAssistantToolPairings(history);
|
|
121
|
+
const included = new Set();
|
|
122
|
+
const visited = new Set();
|
|
123
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
124
|
+
if (visited.has(i))
|
|
125
|
+
continue;
|
|
126
|
+
const group = buildAtomicGroupIndices(i, history, assistantToToolResults);
|
|
127
|
+
for (const idx of group)
|
|
128
|
+
visited.add(idx);
|
|
129
|
+
const groupTokens = group.reduce((sum, idx) => sum + estimateMessageTokens(history[idx]), 0);
|
|
130
|
+
const isRequired = group.some((idx) => requiredIndices.has(idx));
|
|
131
|
+
if (isRequired || used + groupTokens <= usableBudget) {
|
|
132
|
+
for (const idx of group)
|
|
133
|
+
included.add(idx);
|
|
134
|
+
used += groupTokens;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const selectedHistory = history.filter((_, index) => included.has(index));
|
|
138
|
+
return [...leadingSystem, ...selectedHistory];
|
|
139
|
+
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { streamText } from "ai";
|
|
1
|
+
import { streamText, Output } from "ai";
|
|
2
|
+
async function toInlineDataUrl(url, mimeType) {
|
|
3
|
+
const response = await fetch(url);
|
|
4
|
+
if (!response.ok) {
|
|
5
|
+
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
|
|
6
|
+
}
|
|
7
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
8
|
+
const base64 = Buffer.from(arrayBuffer).toString("base64");
|
|
9
|
+
return `data:${mimeType};base64,${base64}`;
|
|
10
|
+
}
|
|
2
11
|
async function toModelMessages(messages) {
|
|
3
|
-
return messages.map((message) => {
|
|
12
|
+
return Promise.all(messages.map(async (message) => {
|
|
4
13
|
if (message.role === "tool") {
|
|
5
14
|
return {
|
|
6
15
|
role: "tool",
|
|
@@ -50,23 +59,33 @@ async function toModelMessages(messages) {
|
|
|
50
59
|
}
|
|
51
60
|
if (message.role === "user") {
|
|
52
61
|
if (message.attachments && message.attachments.length > 0) {
|
|
62
|
+
const attachmentParts = await Promise.all(message.attachments.map(async (a) => {
|
|
63
|
+
if (a.mimeType.startsWith("image/")) {
|
|
64
|
+
try {
|
|
65
|
+
return {
|
|
66
|
+
type: "image",
|
|
67
|
+
image: await toInlineDataUrl(a.url, a.mimeType),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.warn(`Failed to inline image attachment (${a.name}). Falling back to URL: ${a.url}`, error);
|
|
72
|
+
return {
|
|
73
|
+
type: "image",
|
|
74
|
+
image: a.url,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
type: "file",
|
|
80
|
+
data: a.url,
|
|
81
|
+
mimeType: a.mimeType,
|
|
82
|
+
};
|
|
83
|
+
}));
|
|
53
84
|
return {
|
|
54
85
|
role: "user",
|
|
55
86
|
content: [
|
|
56
87
|
{ type: "text", text: message.content },
|
|
57
|
-
...
|
|
58
|
-
if (a.mimeType.startsWith("image/")) {
|
|
59
|
-
return {
|
|
60
|
-
type: "image",
|
|
61
|
-
image: a.url,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
type: "file",
|
|
66
|
-
data: a.url,
|
|
67
|
-
mimeType: a.mimeType,
|
|
68
|
-
};
|
|
69
|
-
}),
|
|
88
|
+
...attachmentParts,
|
|
70
89
|
],
|
|
71
90
|
};
|
|
72
91
|
}
|
|
@@ -79,7 +98,7 @@ async function toModelMessages(messages) {
|
|
|
79
98
|
role: message.role,
|
|
80
99
|
content: message.content,
|
|
81
100
|
};
|
|
82
|
-
});
|
|
101
|
+
}));
|
|
83
102
|
}
|
|
84
103
|
// Helper to find pending tool calls in history
|
|
85
104
|
function getPendingToolCalls(messages) {
|
|
@@ -156,7 +175,7 @@ function insertToolResult(messages, toolResultMsg) {
|
|
|
156
175
|
* It can also automatically trigger events based on tool calls.
|
|
157
176
|
*/
|
|
158
177
|
export const llmPlugin = (options) => (builder) => {
|
|
159
|
-
const { model, system, toolDefinitions = {}, actionEventPrefix = "action:", promptInputType = "agent:input", actionResultInputType = "action:result", completionEventType = "agent:output", usageEventType = "usage:update", usageScope = "default", modelId, } = options;
|
|
178
|
+
const { model, system, toolDefinitions = {}, actionEventPrefix = "action:", promptInputType = "agent:input", actionResultInputType = "action:result", completionEventType = "agent:output", usageEventType = "usage:update", usageScope = "default", modelId, outputSchema, } = options;
|
|
160
179
|
async function* routeToLLM(newMessage, context, silent = false) {
|
|
161
180
|
const state = context.state;
|
|
162
181
|
if (!state.messages) {
|
|
@@ -217,19 +236,40 @@ export const llmPlugin = (options) => (builder) => {
|
|
|
217
236
|
system: systemPrompt,
|
|
218
237
|
messages: modelMessages,
|
|
219
238
|
tools: toolDefinitions,
|
|
239
|
+
output: outputSchema ? Output.object({ schema: outputSchema }) : undefined,
|
|
220
240
|
onError: (error) => {
|
|
221
241
|
console.error("streamText error:::::", JSON.stringify(error, null, 2));
|
|
222
242
|
},
|
|
223
243
|
});
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
244
|
+
if (outputSchema) {
|
|
245
|
+
for await (const delta of result.partialOutputStream) {
|
|
246
|
+
if (!silent) {
|
|
247
|
+
yield {
|
|
248
|
+
type: "agent:output-delta",
|
|
249
|
+
data: { delta: "", content: JSON.stringify(delta) },
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const finalObject = await result.output;
|
|
254
|
+
assistantMessage.content = JSON.stringify(finalObject);
|
|
255
|
+
if (completionEventType && !silent) {
|
|
227
256
|
yield {
|
|
228
|
-
type:
|
|
229
|
-
data:
|
|
257
|
+
type: completionEventType,
|
|
258
|
+
data: finalObject,
|
|
230
259
|
};
|
|
231
260
|
}
|
|
232
261
|
}
|
|
262
|
+
else {
|
|
263
|
+
for await (const delta of result.textStream) {
|
|
264
|
+
assistantMessage.content += delta;
|
|
265
|
+
if (!silent) {
|
|
266
|
+
yield {
|
|
267
|
+
type: "agent:output-delta",
|
|
268
|
+
data: { delta, content: assistantMessage.content },
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
233
273
|
const assistantText = assistantMessage.content;
|
|
234
274
|
// Wait for tool calls to complete
|
|
235
275
|
const toolCalls = await result.toolCalls;
|
|
@@ -252,10 +292,13 @@ export const llmPlugin = (options) => (builder) => {
|
|
|
252
292
|
}
|
|
253
293
|
else {
|
|
254
294
|
if (completionEventType && !silent) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
295
|
+
// If it's structured output, we already yielded the final object
|
|
296
|
+
if (!outputSchema) {
|
|
297
|
+
yield {
|
|
298
|
+
type: completionEventType,
|
|
299
|
+
data: { content: assistantText },
|
|
300
|
+
};
|
|
301
|
+
}
|
|
259
302
|
}
|
|
260
303
|
}
|
|
261
304
|
const usage = await result.usage;
|
|
@@ -307,20 +350,31 @@ export const llmPlugin = (options) => (builder) => {
|
|
|
307
350
|
});
|
|
308
351
|
// Feed action results back as system-role feedback to the model.
|
|
309
352
|
builder.on(actionResultInputType, async function* (event, context) {
|
|
310
|
-
const { action, result, toolCallId } = event.data;
|
|
353
|
+
const { action, result, toolCallId, halt } = event.data;
|
|
311
354
|
const normalizedAction = typeof action === "string" ? action : "unknown";
|
|
312
355
|
const summary = typeof result === "string" ? result : JSON.stringify(result);
|
|
313
|
-
|
|
356
|
+
const toolResultMessage = {
|
|
314
357
|
role: "tool",
|
|
315
358
|
content: [{
|
|
316
|
-
type:
|
|
359
|
+
type: "tool-result",
|
|
317
360
|
toolCallId,
|
|
318
361
|
toolName: normalizedAction,
|
|
319
362
|
output: {
|
|
320
|
-
type:
|
|
363
|
+
type: "text",
|
|
321
364
|
value: summary,
|
|
322
365
|
},
|
|
323
366
|
}],
|
|
324
|
-
}
|
|
367
|
+
};
|
|
368
|
+
// Hard-stop mode: record tool result to unblock pending calls, but do not
|
|
369
|
+
// continue the autonomous tool loop in this turn.
|
|
370
|
+
if (halt === true) {
|
|
371
|
+
const state = context.state;
|
|
372
|
+
if (!state.messages) {
|
|
373
|
+
state.messages = [];
|
|
374
|
+
}
|
|
375
|
+
insertToolResult(state.messages, toolResultMessage);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
yield* routeToLLM(toolResultMessage, context);
|
|
325
379
|
});
|
|
326
380
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { uiEvent } from "../../ui/block.js";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { createMemoryModule } from "./memory.js";
|
|
@@ -214,7 +214,7 @@ export const memoryPlugin = (options) => (builder) => {
|
|
|
214
214
|
}
|
|
215
215
|
});
|
|
216
216
|
builder.on("memory:status", async function* (event) {
|
|
217
|
-
yield
|
|
217
|
+
yield uiEvent(statusWidget(event.data.message, event.data.severity));
|
|
218
218
|
});
|
|
219
219
|
};
|
|
220
220
|
export default memoryPlugin;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { uiEvent } from "../../ui/block.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { exec } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
@@ -95,6 +95,6 @@ export const shellPlugin = (options = {}) => (builder) => {
|
|
|
95
95
|
}
|
|
96
96
|
});
|
|
97
97
|
builder.on("shell:status", async function* (event) {
|
|
98
|
-
yield
|
|
98
|
+
yield uiEvent(statusWidget(event.data.message, event.data.severity));
|
|
99
99
|
});
|
|
100
100
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { uiEvent, block } from "../../ui/block.js";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import matter from "gray-matter";
|
|
@@ -275,11 +275,11 @@ export const skillsPlugin = (options) => (builder) => {
|
|
|
275
275
|
}
|
|
276
276
|
});
|
|
277
277
|
builder.on("skills:status", async function* (event) {
|
|
278
|
-
yield
|
|
278
|
+
yield uiEvent(statusWidget(event.data.message, event.data.severity));
|
|
279
279
|
});
|
|
280
280
|
builder.on("skills:loaded", async function* (event) {
|
|
281
|
-
yield
|
|
282
|
-
|
|
281
|
+
yield uiEvent(resourceCardWidget(event.data.title, "", [
|
|
282
|
+
block('text', { value: event.data.instructions }),
|
|
283
283
|
]));
|
|
284
284
|
});
|
|
285
285
|
};
|