@aparte/runner-ag-ui 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/enforce.d.ts +19 -0
- package/dist/enforce.d.ts.map +1 -0
- package/dist/index.js +569 -19
- package/dist/index.js.map +1 -1
- package/dist/messages.d.ts +4 -3
- package/dist/messages.d.ts.map +1 -1
- package/dist/runner.d.ts +24 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/state.d.ts +13 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/translate.d.ts +18 -0
- package/dist/translate.d.ts.map +1 -0
- package/dist/verify.d.ts +26 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/wire.d.ts +15 -2
- package/dist/wire.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import { contentToText,
|
|
2
|
-
function
|
|
1
|
+
import { contentToText, AparteError, AparteErrorCode, uuid } from "@aparte/core";
|
|
2
|
+
function imagePart(part) {
|
|
3
|
+
const data = /^data:([^;,]*)(?:;[^;,]*)*;base64,(.*)$/s.exec(part.image);
|
|
4
|
+
if (data) return { type: "image", source: { type: "data", value: data[2], mimeType: part.mimeType ?? (data[1] || "application/octet-stream") } };
|
|
5
|
+
return { type: "image", source: { type: "url", value: part.image, ...part.mimeType ? { mimeType: part.mimeType } : {} } };
|
|
6
|
+
}
|
|
7
|
+
function toAgUiMessages(messages, flatten, warn) {
|
|
3
8
|
const out = [];
|
|
4
|
-
messages.forEach((m,
|
|
5
|
-
const id = `aparte-${
|
|
9
|
+
messages.forEach((m, index2) => {
|
|
10
|
+
const id = `aparte-${index2}`;
|
|
6
11
|
const content = contentToText(m.content);
|
|
7
|
-
|
|
12
|
+
const images = Array.isArray(m.content) && m.content.some((part) => part.type === "image");
|
|
13
|
+
if (m.role === "user" && images && !flatten) {
|
|
14
|
+
out.push({
|
|
15
|
+
id,
|
|
16
|
+
role: "user",
|
|
17
|
+
content: m.content.map((part) => part.type === "image" ? imagePart(part) : { type: "text", text: part.text })
|
|
18
|
+
});
|
|
19
|
+
} else if (m.role === "system" || m.role === "user") {
|
|
20
|
+
if (images) warn(`the agent's AG-UI version takes text only, which DROPS non-text parts: the images of message ${index2} are not sent.`);
|
|
8
21
|
out.push({ id, role: m.role, content });
|
|
9
22
|
} else if (m.role === "assistant") {
|
|
10
23
|
out.push({
|
|
@@ -28,6 +41,465 @@ function toAgUiMessages(messages) {
|
|
|
28
41
|
function toAgUiTools(tools) {
|
|
29
42
|
return (tools ?? []).map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.inputSchema }));
|
|
30
43
|
}
|
|
44
|
+
const BASE = ["type", "timestamp", "rawEvent", "metadata"];
|
|
45
|
+
const EVENT_KEYS = {
|
|
46
|
+
RUN_STARTED: ["threadId", "runId", "parentRunId", "protocolVersion", "input"],
|
|
47
|
+
RUN_FINISHED: ["threadId", "runId", "outcome", "result", "usage"],
|
|
48
|
+
RUN_ERROR: ["message", "code", "usage"],
|
|
49
|
+
STEP_STARTED: ["stepName", "subagentRunId"],
|
|
50
|
+
STEP_FINISHED: ["stepName", "subagentRunId"],
|
|
51
|
+
TEXT_MESSAGE_START: ["messageId", "role", "name", "subagentRunId"],
|
|
52
|
+
TEXT_MESSAGE_CONTENT: ["messageId", "delta", "subagentRunId"],
|
|
53
|
+
TEXT_MESSAGE_END: ["messageId", "subagentRunId"],
|
|
54
|
+
TEXT_MESSAGE_CHUNK: ["messageId", "role", "name", "delta", "subagentRunId"],
|
|
55
|
+
TOOL_CALL_START: ["toolCallId", "toolCallName", "parentMessageId", "subagentRunId"],
|
|
56
|
+
TOOL_CALL_ARGS: ["toolCallId", "delta", "subagentRunId"],
|
|
57
|
+
TOOL_CALL_END: ["toolCallId", "subagentRunId"],
|
|
58
|
+
TOOL_CALL_CHUNK: ["toolCallId", "toolCallName", "parentMessageId", "delta", "subagentRunId"],
|
|
59
|
+
TOOL_CALL_RESULT: ["messageId", "toolCallId", "content", "role", "subagentRunId"],
|
|
60
|
+
REASONING_START: ["messageId", "subagentRunId"],
|
|
61
|
+
REASONING_END: ["messageId", "subagentRunId"],
|
|
62
|
+
REASONING_MESSAGE_START: ["messageId", "role", "subagentRunId"],
|
|
63
|
+
REASONING_MESSAGE_CONTENT: ["messageId", "delta", "subagentRunId"],
|
|
64
|
+
REASONING_MESSAGE_END: ["messageId", "subagentRunId"],
|
|
65
|
+
REASONING_MESSAGE_CHUNK: ["messageId", "delta", "subagentRunId"],
|
|
66
|
+
REASONING_ENCRYPTED_VALUE: ["subtype", "entityId", "encryptedValue", "subagentRunId"],
|
|
67
|
+
STATE_SNAPSHOT: ["snapshot", "subagentRunId"],
|
|
68
|
+
STATE_DELTA: ["delta", "subagentRunId"],
|
|
69
|
+
MESSAGES_SNAPSHOT: ["messages"],
|
|
70
|
+
ACTIVITY_SNAPSHOT: ["messageId", "activityType", "content", "replace", "subagentRunId"],
|
|
71
|
+
ACTIVITY_DELTA: ["messageId", "activityType", "patch", "subagentRunId"],
|
|
72
|
+
RAW: ["event", "source", "subagentRunId"],
|
|
73
|
+
CUSTOM: ["name", "value", "subagentRunId"],
|
|
74
|
+
SUBAGENT_STARTED: ["subagentRunId", "name", "description", "parentMessageId", "parentToolCallId", "parentSubagentRunId"],
|
|
75
|
+
SUBAGENT_FINISHED: ["subagentRunId", "outcome", "result"],
|
|
76
|
+
SUBAGENT_ERROR: ["subagentRunId", "message", "code"]
|
|
77
|
+
};
|
|
78
|
+
const TEXT_ROLES = /* @__PURE__ */ new Set(["developer", "system", "assistant", "user"]);
|
|
79
|
+
const OUTCOMES = /* @__PURE__ */ new Set(["success", "interrupt", "cancelled"]);
|
|
80
|
+
const CONTENT_PARTS = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "document"]);
|
|
81
|
+
function enforce(event, warn) {
|
|
82
|
+
const keys = EVENT_KEYS[event.type];
|
|
83
|
+
if (!keys) {
|
|
84
|
+
warn(`dropped an event of a type this runner does not know: ${event.type}.`);
|
|
85
|
+
return void 0;
|
|
86
|
+
}
|
|
87
|
+
const out = { ...event };
|
|
88
|
+
for (const key of Object.keys(out)) {
|
|
89
|
+
if (!BASE.includes(key) && !keys.includes(key)) {
|
|
90
|
+
delete out[key];
|
|
91
|
+
warn(`stripped /${key} from ${event.type}: AG-UI 1.0 does not describe it.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if ((out.type === "TEXT_MESSAGE_START" || out.type === "TEXT_MESSAGE_CHUNK") && typeof out["role"] === "string" && !TEXT_ROLES.has(out["role"])) {
|
|
95
|
+
warn(`stripped /role from ${out.type}: "${out["role"]}" is not a message role.`);
|
|
96
|
+
delete out["role"];
|
|
97
|
+
}
|
|
98
|
+
const outcome = out["outcome"];
|
|
99
|
+
if (out.type === "RUN_FINISHED" && outcome && typeof outcome === "object" && !Array.isArray(outcome) && !OUTCOMES.has(String(outcome.type))) {
|
|
100
|
+
warn(`stripped /outcome from RUN_FINISHED: "${String(outcome.type)}" is not an outcome AG-UI 1.0 describes; the run ends as a plain one.`);
|
|
101
|
+
delete out["outcome"];
|
|
102
|
+
}
|
|
103
|
+
if (out.type === "MESSAGES_SNAPSHOT" && Array.isArray(out["messages"])) {
|
|
104
|
+
out["messages"] = out["messages"].map((message, i) => {
|
|
105
|
+
const content = message?.content;
|
|
106
|
+
if (!Array.isArray(content)) return message;
|
|
107
|
+
const kept = content.filter((part, j) => {
|
|
108
|
+
const known = CONTENT_PARTS.has(String(part?.type));
|
|
109
|
+
if (!known) warn(`stripped /messages/${i}/content/${j} from MESSAGES_SNAPSHOT: "${String(part?.type)}" is not a content part.`);
|
|
110
|
+
return known;
|
|
111
|
+
});
|
|
112
|
+
return { ...message, content: kept };
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
const OPS = /* @__PURE__ */ new Set(["add", "remove", "replace", "move", "copy", "test"]);
|
|
118
|
+
function tokens(pointer) {
|
|
119
|
+
if (pointer === "") return [];
|
|
120
|
+
if (!pointer.startsWith("/")) throw new Error(`"${pointer}" is not a JSON Pointer`);
|
|
121
|
+
return pointer.slice(1).split("/").map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
122
|
+
}
|
|
123
|
+
function isContainer(value) {
|
|
124
|
+
return typeof value === "object" && value !== null;
|
|
125
|
+
}
|
|
126
|
+
function index(list, token, append) {
|
|
127
|
+
if (append && token === "-") return list.length;
|
|
128
|
+
if (!/^(0|[1-9]\d*)$/.test(token)) throw new Error(`"${token}" is not an array index`);
|
|
129
|
+
const i = Number(token);
|
|
130
|
+
if (i > list.length || !append && i === list.length) throw new Error(`index ${i} is out of range`);
|
|
131
|
+
return i;
|
|
132
|
+
}
|
|
133
|
+
function parentOf(doc, path) {
|
|
134
|
+
const all = tokens(path);
|
|
135
|
+
const key = all.pop();
|
|
136
|
+
let node = doc;
|
|
137
|
+
for (const t of all) {
|
|
138
|
+
if (!isContainer(node)) throw new Error(`"${path}" does not resolve`);
|
|
139
|
+
node = Array.isArray(node) ? node[index(node, t, false)] : Object.hasOwn(node, t) ? node[t] : void 0;
|
|
140
|
+
if (node === void 0) throw new Error(`"${path}" does not resolve`);
|
|
141
|
+
}
|
|
142
|
+
if (!isContainer(node)) throw new Error(`"${path}" does not resolve`);
|
|
143
|
+
return { parent: node, key };
|
|
144
|
+
}
|
|
145
|
+
function get(doc, path) {
|
|
146
|
+
if (path === "") return doc;
|
|
147
|
+
const { parent, key } = parentOf(doc, path);
|
|
148
|
+
const value = Array.isArray(parent) ? parent[index(parent, key, false)] : Object.hasOwn(parent, key) ? parent[key] : void 0;
|
|
149
|
+
if (value === void 0) throw new Error(`"${path}" does not resolve`);
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
function add(doc, path, value) {
|
|
153
|
+
if (path === "") return value;
|
|
154
|
+
const { parent, key } = parentOf(doc, path);
|
|
155
|
+
if (Array.isArray(parent)) parent.splice(index(parent, key, true), 0, value);
|
|
156
|
+
else parent[key] = value;
|
|
157
|
+
return doc;
|
|
158
|
+
}
|
|
159
|
+
function remove(doc, path) {
|
|
160
|
+
if (path === "") throw new Error("the whole document cannot be removed");
|
|
161
|
+
const { parent, key } = parentOf(doc, path);
|
|
162
|
+
if (Array.isArray(parent)) parent.splice(index(parent, key, false), 1);
|
|
163
|
+
else if (Object.hasOwn(parent, key)) delete parent[key];
|
|
164
|
+
else throw new Error(`"${path}" does not resolve`);
|
|
165
|
+
return doc;
|
|
166
|
+
}
|
|
167
|
+
function replace(doc, path, value) {
|
|
168
|
+
if (path === "") return value;
|
|
169
|
+
const { parent, key } = parentOf(doc, path);
|
|
170
|
+
if (Array.isArray(parent)) parent[index(parent, key, false)] = value;
|
|
171
|
+
else if (Object.hasOwn(parent, key)) parent[key] = value;
|
|
172
|
+
else throw new Error(`"${path}" does not resolve`);
|
|
173
|
+
return doc;
|
|
174
|
+
}
|
|
175
|
+
function equal(a, b) {
|
|
176
|
+
if (a === b) return true;
|
|
177
|
+
if (!isContainer(a) || !isContainer(b) || Array.isArray(a) !== Array.isArray(b)) return false;
|
|
178
|
+
if (Array.isArray(a)) return a.length === b.length && a.every((v, i) => equal(v, b[i]));
|
|
179
|
+
const ka = Object.keys(a);
|
|
180
|
+
const kb = Object.keys(b);
|
|
181
|
+
return ka.length === kb.length && ka.every((k) => Object.hasOwn(b, k) && equal(a[k], b[k]));
|
|
182
|
+
}
|
|
183
|
+
function step(doc, op) {
|
|
184
|
+
const path = op["path"];
|
|
185
|
+
if (typeof path !== "string") throw new Error("an operation has no path");
|
|
186
|
+
const from = typeof op["from"] === "string" ? op["from"] : void 0;
|
|
187
|
+
if ((op["op"] === "add" || op["op"] === "replace" || op["op"] === "test") && !("value" in op)) throw new Error(`${String(op["op"])} has no value`);
|
|
188
|
+
switch (op["op"]) {
|
|
189
|
+
case "add":
|
|
190
|
+
return add(doc, path, structuredClone(op["value"]));
|
|
191
|
+
case "remove":
|
|
192
|
+
return remove(doc, path);
|
|
193
|
+
case "replace":
|
|
194
|
+
return replace(doc, path, structuredClone(op["value"]));
|
|
195
|
+
case "move": {
|
|
196
|
+
if (from === void 0) throw new Error("move has no from");
|
|
197
|
+
if (path.startsWith(`${from}/`)) throw new Error(`"${from}" cannot move into its own child "${path}"`);
|
|
198
|
+
const value = get(doc, from);
|
|
199
|
+
return add(remove(doc, from), path, value);
|
|
200
|
+
}
|
|
201
|
+
case "copy": {
|
|
202
|
+
if (from === void 0) throw new Error("copy has no from");
|
|
203
|
+
return add(doc, path, structuredClone(get(doc, from)));
|
|
204
|
+
}
|
|
205
|
+
case "test":
|
|
206
|
+
if (!equal(get(doc, path), op["value"])) throw new Error(`test failed at "${path}"`);
|
|
207
|
+
return doc;
|
|
208
|
+
default:
|
|
209
|
+
throw new Error(`unknown operation "${String(op["op"])}"`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function applyStateDelta(state, delta, warn) {
|
|
213
|
+
if (!Array.isArray(delta)) {
|
|
214
|
+
warn("Failed to apply state patch: STATE_DELTA carried no list of operations; the state is left as it was.");
|
|
215
|
+
return state;
|
|
216
|
+
}
|
|
217
|
+
const ops = [];
|
|
218
|
+
delta.forEach((op, i) => {
|
|
219
|
+
if (isContainer(op) && !Array.isArray(op) && OPS.has(String(op["op"]))) ops.push(op);
|
|
220
|
+
else warn(`dropped /delta/${i}: ${JSON.stringify(op)} is not a JSON Patch operation.`);
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
let doc = structuredClone(state);
|
|
224
|
+
for (const op of ops) doc = step(doc, op);
|
|
225
|
+
return doc;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
warn(`Failed to apply state patch: ${error.message}; the state is left as it was.`);
|
|
228
|
+
return state;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function compare(a, b) {
|
|
232
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
233
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
234
|
+
if (d !== 0) return d;
|
|
235
|
+
}
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
function createTranslator(peer, warn) {
|
|
239
|
+
const beforeSubagents = peer !== void 0 && compare(peer, [0, 0, 57]) <= 0;
|
|
240
|
+
let thought = 0;
|
|
241
|
+
let span;
|
|
242
|
+
let message;
|
|
243
|
+
const warned = /* @__PURE__ */ new Set();
|
|
244
|
+
const once = (key, text) => {
|
|
245
|
+
if (warned.has(key)) return;
|
|
246
|
+
warned.add(key);
|
|
247
|
+
warn(text);
|
|
248
|
+
};
|
|
249
|
+
return (event) => {
|
|
250
|
+
switch (event.type) {
|
|
251
|
+
case "THINKING_START":
|
|
252
|
+
span = `thinking-${++thought}`;
|
|
253
|
+
once("THINKING_START", `translated the retired THINKING_START into REASONING_START${typeof event["title"] === "string" ? `, dropping its title "${event["title"]}"` : ""}; the agent should send REASONING_* events.`);
|
|
254
|
+
return [{ type: "REASONING_START", messageId: span }];
|
|
255
|
+
case "THINKING_END": {
|
|
256
|
+
const id = span ?? `thinking-${++thought}`;
|
|
257
|
+
span = void 0;
|
|
258
|
+
return [{ type: "REASONING_END", messageId: id }];
|
|
259
|
+
}
|
|
260
|
+
case "THINKING_TEXT_MESSAGE_START":
|
|
261
|
+
message = `thinking-message-${++thought}`;
|
|
262
|
+
return [{ type: "REASONING_MESSAGE_START", messageId: message, role: "reasoning" }];
|
|
263
|
+
case "THINKING_TEXT_MESSAGE_CONTENT":
|
|
264
|
+
return [{ type: "REASONING_MESSAGE_CONTENT", messageId: message ?? `thinking-message-${thought}`, delta: event["delta"] }];
|
|
265
|
+
case "THINKING_TEXT_MESSAGE_END": {
|
|
266
|
+
const id = message ?? `thinking-message-${thought}`;
|
|
267
|
+
message = void 0;
|
|
268
|
+
return [{ type: "REASONING_MESSAGE_END", messageId: id }];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (beforeSubagents) {
|
|
272
|
+
if (event.type.startsWith("SUBAGENT_")) {
|
|
273
|
+
once(event.type, `dropping ${event.type}: the agent's AG-UI version predates subagents (0.0.57 or older).`);
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
if ("subagentRunId" in event) {
|
|
277
|
+
const { subagentRunId: _dropped, ...rest } = event;
|
|
278
|
+
return [rest];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return [event];
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function malformed(reason) {
|
|
285
|
+
return new AparteError(`The agent stream is malformed: ${reason}.`, AparteErrorCode.PROVIDER_ERROR, { agUi: "malformed" });
|
|
286
|
+
}
|
|
287
|
+
const STRING_DELTA = /* @__PURE__ */ new Set([
|
|
288
|
+
"TEXT_MESSAGE_CONTENT",
|
|
289
|
+
"TEXT_MESSAGE_CHUNK",
|
|
290
|
+
"TOOL_CALL_ARGS",
|
|
291
|
+
"TOOL_CALL_CHUNK",
|
|
292
|
+
"REASONING_MESSAGE_CONTENT",
|
|
293
|
+
"REASONING_MESSAGE_CHUNK"
|
|
294
|
+
]);
|
|
295
|
+
const STRING_FIELDS = [
|
|
296
|
+
"messageId",
|
|
297
|
+
"toolCallId",
|
|
298
|
+
"toolCallName",
|
|
299
|
+
"parentMessageId",
|
|
300
|
+
"stepName",
|
|
301
|
+
"subagentRunId",
|
|
302
|
+
"threadId",
|
|
303
|
+
"runId",
|
|
304
|
+
"parentRunId",
|
|
305
|
+
"protocolVersion",
|
|
306
|
+
"name",
|
|
307
|
+
"role",
|
|
308
|
+
"message",
|
|
309
|
+
"code",
|
|
310
|
+
"activityType",
|
|
311
|
+
"entityId",
|
|
312
|
+
"encryptedValue",
|
|
313
|
+
"subtype",
|
|
314
|
+
"description",
|
|
315
|
+
"parentToolCallId",
|
|
316
|
+
"parentSubagentRunId"
|
|
317
|
+
];
|
|
318
|
+
const REQUIRED = {
|
|
319
|
+
TEXT_MESSAGE_START: ["messageId"],
|
|
320
|
+
TEXT_MESSAGE_CONTENT: ["messageId", "delta"],
|
|
321
|
+
TEXT_MESSAGE_END: ["messageId"],
|
|
322
|
+
TOOL_CALL_START: ["toolCallId", "toolCallName"],
|
|
323
|
+
TOOL_CALL_ARGS: ["toolCallId", "delta"],
|
|
324
|
+
TOOL_CALL_END: ["toolCallId"],
|
|
325
|
+
TOOL_CALL_RESULT: ["toolCallId"],
|
|
326
|
+
REASONING_START: ["messageId"],
|
|
327
|
+
REASONING_END: ["messageId"],
|
|
328
|
+
REASONING_MESSAGE_START: ["messageId"],
|
|
329
|
+
REASONING_MESSAGE_CONTENT: ["messageId", "delta"],
|
|
330
|
+
REASONING_MESSAGE_END: ["messageId"],
|
|
331
|
+
STEP_STARTED: ["stepName"],
|
|
332
|
+
STEP_FINISHED: ["stepName"],
|
|
333
|
+
SUBAGENT_STARTED: ["subagentRunId"],
|
|
334
|
+
SUBAGENT_FINISHED: ["subagentRunId"],
|
|
335
|
+
SUBAGENT_ERROR: ["subagentRunId"]
|
|
336
|
+
};
|
|
337
|
+
const FRAMING_HINT = " (TanStack AI frames runs this way: give createAgUiRunner relaxedRunFraming: true to read it)";
|
|
338
|
+
function createVerifier(relaxed) {
|
|
339
|
+
let seen = false;
|
|
340
|
+
let running = false;
|
|
341
|
+
let closed;
|
|
342
|
+
const text = /* @__PURE__ */ new Map();
|
|
343
|
+
const reasoning = /* @__PURE__ */ new Map();
|
|
344
|
+
const tools = /* @__PURE__ */ new Map();
|
|
345
|
+
const steps = /* @__PURE__ */ new Set();
|
|
346
|
+
const subagents = /* @__PURE__ */ new Set();
|
|
347
|
+
const chunks = /* @__PURE__ */ new Map();
|
|
348
|
+
const ownerOf = (event) => typeof event["subagentRunId"] === "string" ? event["subagentRunId"] : "";
|
|
349
|
+
const stepKey = (owner, name) => `${owner}\0${name}`;
|
|
350
|
+
const nothingOpen = () => {
|
|
351
|
+
text.clear();
|
|
352
|
+
reasoning.clear();
|
|
353
|
+
tools.clear();
|
|
354
|
+
steps.clear();
|
|
355
|
+
subagents.clear();
|
|
356
|
+
chunks.clear();
|
|
357
|
+
};
|
|
358
|
+
const stillOpen = () => [...text.keys()].map((id) => `text message "${id}"`).concat([...reasoning.keys()].map((id) => `reasoning message "${id}"`)).concat([...tools.keys()].map((id) => `tool call "${id}"`)).concat([...steps].map((key) => `step "${key.split("\0")[1]}"`)).concat([...subagents].map((id) => `subagent "${id}"`));
|
|
359
|
+
const agrees = (id, opened, event) => {
|
|
360
|
+
const by = event["subagentRunId"];
|
|
361
|
+
if (by === void 0) return;
|
|
362
|
+
const at = opened.get(id);
|
|
363
|
+
if (at !== void 0 && at !== by) throw malformed(`${event.type} for "${id}" is attributed to "${String(by)}", but "${id}" started attributed to "${String(at)}"`);
|
|
364
|
+
};
|
|
365
|
+
const verify = ((event) => {
|
|
366
|
+
const type = event.type;
|
|
367
|
+
const first = !seen;
|
|
368
|
+
seen = true;
|
|
369
|
+
for (const key of REQUIRED[type] ?? []) if (event[key] === void 0) throw malformed(`${type} needs "${key}"`);
|
|
370
|
+
for (const key of STRING_FIELDS) if (key in event && event[key] !== void 0 && typeof event[key] !== "string") throw malformed(`"${key}" must be a string on ${type}`);
|
|
371
|
+
if (STRING_DELTA.has(type) && "delta" in event && typeof event["delta"] !== "string") throw malformed(`"delta" must be a string on ${type}`);
|
|
372
|
+
if (type === "RUN_STARTED") {
|
|
373
|
+
if (running && !relaxed) throw malformed("RUN_STARTED while a run is still active");
|
|
374
|
+
running = true;
|
|
375
|
+
closed = void 0;
|
|
376
|
+
nothingOpen();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (!relaxed) {
|
|
380
|
+
if (first && type !== "RUN_ERROR") throw malformed(`the first event must be RUN_STARTED, not ${type}${FRAMING_HINT}`);
|
|
381
|
+
if (closed === "finished" && type !== "RUN_ERROR") throw malformed(`${type} after the run finished${FRAMING_HINT}`);
|
|
382
|
+
if (closed === "error") throw malformed(`${type} after the run ended with RUN_ERROR`);
|
|
383
|
+
}
|
|
384
|
+
if (type === "RUN_ERROR") {
|
|
385
|
+
running = false;
|
|
386
|
+
if (closed !== "finished") closed = "error";
|
|
387
|
+
nothingOpen();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const owner = ownerOf(event);
|
|
391
|
+
const open = chunks.get(owner);
|
|
392
|
+
if (open && type !== open.type && type !== "RAW") chunks.delete(owner);
|
|
393
|
+
if (type === "SUBAGENT_FINISHED" || type === "SUBAGENT_ERROR") chunks.delete(String(event["subagentRunId"]));
|
|
394
|
+
switch (type) {
|
|
395
|
+
case "TEXT_MESSAGE_START":
|
|
396
|
+
text.set(String(event["messageId"]), event["subagentRunId"]);
|
|
397
|
+
break;
|
|
398
|
+
case "TEXT_MESSAGE_CONTENT":
|
|
399
|
+
case "TEXT_MESSAGE_END": {
|
|
400
|
+
const id = String(event["messageId"]);
|
|
401
|
+
if (!text.has(id)) throw malformed(`${type} for "${id}", but no open text message has that id`);
|
|
402
|
+
agrees(id, text, event);
|
|
403
|
+
if (type === "TEXT_MESSAGE_END") text.delete(id);
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
case "TEXT_MESSAGE_CHUNK":
|
|
407
|
+
case "REASONING_MESSAGE_CHUNK": {
|
|
408
|
+
const id = typeof event["messageId"] === "string" ? event["messageId"] : void 0;
|
|
409
|
+
const role = typeof event["role"] === "string" ? event["role"] : void 0;
|
|
410
|
+
const current = chunks.get(owner);
|
|
411
|
+
if (!current || id !== void 0 && id !== current.id) {
|
|
412
|
+
if (id === void 0) throw malformed(`the first ${type} of a stream needs a messageId`);
|
|
413
|
+
chunks.set(owner, { type, id, ...role !== void 0 ? { label: role } : {} });
|
|
414
|
+
} else if (role !== void 0 && current.label !== void 0 && role !== current.label) {
|
|
415
|
+
throw malformed(`${type} changes its role from "${current.label}" to "${role}" in the middle of message "${current.id}"`);
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
case "TOOL_CALL_CHUNK": {
|
|
420
|
+
const id = typeof event["toolCallId"] === "string" ? event["toolCallId"] : void 0;
|
|
421
|
+
const name = typeof event["toolCallName"] === "string" ? event["toolCallName"] : void 0;
|
|
422
|
+
const current = chunks.get(owner);
|
|
423
|
+
if (!current || id !== void 0 && id !== current.id) {
|
|
424
|
+
if (id === void 0 || name === void 0) throw malformed("the first TOOL_CALL_CHUNK of a call needs a toolCallId and a toolCallName");
|
|
425
|
+
chunks.set(owner, { type, id, label: name });
|
|
426
|
+
} else if (name !== void 0 && name !== current.label) {
|
|
427
|
+
throw malformed(`TOOL_CALL_CHUNK changes its toolCallName from "${current.label}" to "${name}" in the middle of call "${current.id}"`);
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
case "TOOL_CALL_START":
|
|
432
|
+
tools.set(String(event["toolCallId"]), event["subagentRunId"]);
|
|
433
|
+
break;
|
|
434
|
+
case "TOOL_CALL_ARGS":
|
|
435
|
+
case "TOOL_CALL_END": {
|
|
436
|
+
const id = String(event["toolCallId"]);
|
|
437
|
+
if (!tools.has(id)) throw malformed(`${type} for "${id}", but no open tool call has that id`);
|
|
438
|
+
agrees(id, tools, event);
|
|
439
|
+
if (type === "TOOL_CALL_END") tools.delete(id);
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
case "REASONING_MESSAGE_START": {
|
|
443
|
+
const id = String(event["messageId"]);
|
|
444
|
+
if (event["role"] !== void 0 && event["role"] !== "reasoning") {
|
|
445
|
+
throw malformed(`REASONING_MESSAGE_START "${id}" has the role "${String(event["role"])}", where only role "reasoning" is allowed`);
|
|
446
|
+
}
|
|
447
|
+
reasoning.set(id, event["subagentRunId"]);
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
case "REASONING_MESSAGE_CONTENT":
|
|
451
|
+
case "REASONING_MESSAGE_END": {
|
|
452
|
+
const id = String(event["messageId"]);
|
|
453
|
+
if (!reasoning.has(id)) throw malformed(`${type} for "${id}", but no open reasoning message has that id`);
|
|
454
|
+
agrees(id, reasoning, event);
|
|
455
|
+
if (type === "REASONING_MESSAGE_END") reasoning.delete(id);
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
case "STEP_STARTED":
|
|
459
|
+
steps.add(stepKey(owner, String(event["stepName"])));
|
|
460
|
+
break;
|
|
461
|
+
case "STEP_FINISHED": {
|
|
462
|
+
const key = stepKey(owner, String(event["stepName"]));
|
|
463
|
+
if (!steps.has(key)) throw malformed(`STEP_FINISHED for step "${String(event["stepName"])}", which was not started${owner ? ` by "${owner}"` : ""}`);
|
|
464
|
+
steps.delete(key);
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
case "SUBAGENT_STARTED": {
|
|
468
|
+
const id = String(event["subagentRunId"]);
|
|
469
|
+
if (typeof event["name"] !== "string" || !event["name"]) throw malformed(`SUBAGENT_STARTED "${id}" needs a name`);
|
|
470
|
+
subagents.add(id);
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
case "SUBAGENT_FINISHED":
|
|
474
|
+
case "SUBAGENT_ERROR": {
|
|
475
|
+
const id = String(event["subagentRunId"]);
|
|
476
|
+
if (!subagents.has(id)) throw malformed(`${type} for subagent "${id}", which was not started`);
|
|
477
|
+
subagents.delete(id);
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
case "RUN_FINISHED": {
|
|
481
|
+
const outcome = event["outcome"];
|
|
482
|
+
if (outcome !== void 0 && (typeof outcome !== "object" || outcome === null || Array.isArray(outcome))) {
|
|
483
|
+
throw malformed("RUN_FINISHED.outcome must be an object");
|
|
484
|
+
}
|
|
485
|
+
if (!relaxed) {
|
|
486
|
+
const left = stillOpen();
|
|
487
|
+
if (left.length) throw malformed(`${left.join(", ")} still running at RUN_FINISHED`);
|
|
488
|
+
closed = "finished";
|
|
489
|
+
}
|
|
490
|
+
chunks.clear();
|
|
491
|
+
running = false;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
verify.end = () => {
|
|
497
|
+
if (!relaxed) return;
|
|
498
|
+
const left = stillOpen();
|
|
499
|
+
if (left.length) throw malformed(`${left.join(", ")} still running at the end of the stream`);
|
|
500
|
+
};
|
|
501
|
+
return verify;
|
|
502
|
+
}
|
|
31
503
|
async function* readAgUiEvents(body, contentType) {
|
|
32
504
|
const ndjson = Boolean(contentType && /ndjson|jsonl/i.test(contentType));
|
|
33
505
|
const reader = body.getReader();
|
|
@@ -81,6 +553,17 @@ async function* readAgUiEvents(body, contentType) {
|
|
|
81
553
|
reader.releaseLock();
|
|
82
554
|
}
|
|
83
555
|
}
|
|
556
|
+
const PROTOCOL_VERSION = "1.0";
|
|
557
|
+
function versionParts(version) {
|
|
558
|
+
return /^\d+(\.\d+)*$/.test(version) ? version.split(".").map(Number) : void 0;
|
|
559
|
+
}
|
|
560
|
+
function compareVersions(a, b) {
|
|
561
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
562
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
563
|
+
if (d !== 0) return d;
|
|
564
|
+
}
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
84
567
|
function readTicket(token) {
|
|
85
568
|
if (!token) return void 0;
|
|
86
569
|
try {
|
|
@@ -119,6 +602,10 @@ function parseArgs(args) {
|
|
|
119
602
|
function resultText(content) {
|
|
120
603
|
if (typeof content === "string") return content;
|
|
121
604
|
if (content === void 0 || content === null) return "";
|
|
605
|
+
if (Array.isArray(content)) {
|
|
606
|
+
const text = content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text);
|
|
607
|
+
if (text.length) return text.join("\n");
|
|
608
|
+
}
|
|
122
609
|
return JSON.stringify(content);
|
|
123
610
|
}
|
|
124
611
|
function rejectionText(decision) {
|
|
@@ -152,16 +639,24 @@ function resumeItem(interrupt, outcome) {
|
|
|
152
639
|
};
|
|
153
640
|
}
|
|
154
641
|
function createAgUiRunner(options) {
|
|
155
|
-
|
|
642
|
+
const warn = options.onWarning ?? ((message) => console.warn(`[aparte] AG-UI: ${message}`));
|
|
643
|
+
const peer = options.peerProtocolVersion ? versionParts(options.peerProtocolVersion) : void 0;
|
|
644
|
+
const declare = !peer || compareVersions(peer, [1, 0]) >= 0;
|
|
645
|
+
const flatten = peer !== void 0 && compareVersions(peer, [0, 0, 39]) <= 0;
|
|
646
|
+
const states = /* @__PURE__ */ new Map();
|
|
647
|
+
const stableThread = options.threadId !== void 0;
|
|
648
|
+
const unanswered = /* @__PURE__ */ new Map();
|
|
649
|
+
const runOnce = async (opts, track) => {
|
|
156
650
|
const doFetch = options.fetch ?? fetch;
|
|
157
651
|
const headers = typeof options.headers === "function" ? await options.headers() : options.headers ?? {};
|
|
158
652
|
const request = opts.baseRequest;
|
|
159
653
|
const ticket = readTicket(opts.deferral?.token);
|
|
160
654
|
const outcomes = opts.deferral?.outcomes ?? {};
|
|
161
655
|
const threadId = ticket?.threadId ?? (typeof options.threadId === "function" ? options.threadId(opts) : options.threadId) ?? `aparte-${uuid()}`;
|
|
656
|
+
track.threadId = threadId;
|
|
162
657
|
const runId = `aparte-run-${uuid()}`;
|
|
163
658
|
const interrupted = new Set((ticket?.interrupts ?? []).map((i) => i.toolCallId));
|
|
164
|
-
const messages = toAgUiMessages(request.messages);
|
|
659
|
+
const messages = toAgUiMessages(request.messages, flatten, warn);
|
|
165
660
|
for (const [toolCallId, outcome] of Object.entries(outcomes)) {
|
|
166
661
|
if (interrupted.has(toolCallId)) continue;
|
|
167
662
|
if (outcome.kind === "decision" && outcome.approved) continue;
|
|
@@ -172,12 +667,15 @@ function createAgUiRunner(options) {
|
|
|
172
667
|
content: outcome.kind === "failed" ? `Error: ${outcome.error}` : outcome.kind === "decision" ? rejectionText(outcome) : outcome.content
|
|
173
668
|
});
|
|
174
669
|
}
|
|
175
|
-
const
|
|
670
|
+
const left = ticket ? [] : unanswered.get(threadId) ?? [];
|
|
671
|
+
unanswered.delete(threadId);
|
|
672
|
+
const resume = ticket?.interrupts.length ? ticket.interrupts.map((interrupt) => resumeItem(interrupt, interrupt.kind === "unanswerable" ? void 0 : outcomes[interrupt.toolCallId])) : left.length ? left.map((interruptId) => ({ interruptId, status: "cancelled" })) : void 0;
|
|
176
673
|
const body = {
|
|
177
674
|
threadId,
|
|
178
675
|
runId,
|
|
676
|
+
...declare ? { protocolVersion: PROTOCOL_VERSION } : {},
|
|
179
677
|
...ticket ? { parentRunId: ticket.runId } : {},
|
|
180
|
-
state: {},
|
|
678
|
+
state: states.has(threadId) ? states.get(threadId) : {},
|
|
181
679
|
messages,
|
|
182
680
|
tools: toAgUiTools(request.tools),
|
|
183
681
|
context: options.context ?? [],
|
|
@@ -210,6 +708,7 @@ function createAgUiRunner(options) {
|
|
|
210
708
|
const rejected = /* @__PURE__ */ new Set();
|
|
211
709
|
const settledByPage = /* @__PURE__ */ new Set();
|
|
212
710
|
let textOpen = false;
|
|
711
|
+
let runError;
|
|
213
712
|
let usage;
|
|
214
713
|
let finished;
|
|
215
714
|
const emit = opts.emitter;
|
|
@@ -274,12 +773,39 @@ function createAgUiRunner(options) {
|
|
|
274
773
|
}
|
|
275
774
|
}
|
|
276
775
|
};
|
|
277
|
-
|
|
776
|
+
const verify = createVerifier(!declare || options.relaxedRunFraming === true);
|
|
777
|
+
const translate = createTranslator(peer, warn);
|
|
778
|
+
async function* incoming(body2, contentType) {
|
|
779
|
+
for await (const raw of readAgUiEvents(body2, contentType)) {
|
|
780
|
+
for (const translated of translate(raw)) {
|
|
781
|
+
const event = enforce(translated, warn);
|
|
782
|
+
if (event) yield event;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
for await (const event of incoming(response.body, response.headers.get("content-type"))) {
|
|
787
|
+
verify(event);
|
|
278
788
|
begin();
|
|
279
789
|
if (/^(TEXT_MESSAGE|REASONING|TOOL_CALL)_/.test(event.type)) endedOnFinish = false;
|
|
280
790
|
switch (event.type) {
|
|
281
|
-
case "RUN_STARTED":
|
|
791
|
+
case "RUN_STARTED": {
|
|
792
|
+
if (runError) {
|
|
793
|
+
warn(`the agent's run failed ("${runError.message}") and the stream ran it again; the reply shows the new run.`);
|
|
794
|
+
for (const [toolCallId] of started) {
|
|
795
|
+
if (answered.has(toolCallId)) continue;
|
|
796
|
+
emit({ type: "tool-aborted", toolCallId });
|
|
797
|
+
answered.add(toolCallId);
|
|
798
|
+
}
|
|
799
|
+
runError = void 0;
|
|
800
|
+
}
|
|
801
|
+
const declared = event["protocolVersion"];
|
|
802
|
+
if (typeof declared === "string") {
|
|
803
|
+
const parts = versionParts(declared);
|
|
804
|
+
if (!parts) warn(`the agent declares protocol version "${declared}", which this runner cannot interpret; reading its stream as ${PROTOCOL_VERSION}.`);
|
|
805
|
+
else if (compareVersions(parts, versionParts(PROTOCOL_VERSION)) > 0) warn(`the agent speaks AG-UI ${declared}, newer than the ${PROTOCOL_VERSION} this runner speaks; reading its stream as ${PROTOCOL_VERSION}.`);
|
|
806
|
+
}
|
|
282
807
|
break;
|
|
808
|
+
}
|
|
283
809
|
case "TEXT_MESSAGE_START":
|
|
284
810
|
case "REASONING_START":
|
|
285
811
|
case "REASONING_MESSAGE_START":
|
|
@@ -344,17 +870,25 @@ function createAgUiRunner(options) {
|
|
|
344
870
|
if (meta?.tanstack?.state === "output-error") {
|
|
345
871
|
emit({ type: "tool-failed", toolCallId, error: content });
|
|
346
872
|
} else {
|
|
347
|
-
|
|
873
|
+
const parts = Array.isArray(event["content"]) ? { structuredResult: event["content"] } : {};
|
|
874
|
+
emit({ type: "tool-resolved", toolCallId, result: content, ...parts });
|
|
348
875
|
}
|
|
349
876
|
break;
|
|
350
877
|
}
|
|
351
878
|
case "RUN_ERROR": {
|
|
352
|
-
|
|
879
|
+
runError = new AparteError(
|
|
353
880
|
typeof event["message"] === "string" && event["message"] ? event["message"] : "The agent run failed.",
|
|
354
881
|
AparteErrorCode.PROVIDER_ERROR,
|
|
355
|
-
typeof event["code"] === "string" ? { code: event["code"] } :
|
|
882
|
+
{ agUi: "run-error", ...typeof event["code"] === "string" ? { code: event["code"] } : {} }
|
|
356
883
|
);
|
|
884
|
+
break;
|
|
357
885
|
}
|
|
886
|
+
case "STATE_SNAPSHOT":
|
|
887
|
+
states.set(threadId, event["snapshot"]);
|
|
888
|
+
break;
|
|
889
|
+
case "STATE_DELTA":
|
|
890
|
+
states.set(threadId, applyStateDelta(states.has(threadId) ? states.get(threadId) : {}, event["delta"], warn));
|
|
891
|
+
break;
|
|
358
892
|
case "RUN_FINISHED": {
|
|
359
893
|
for (const id of calls.keys()) startTool(id);
|
|
360
894
|
if (textOpen) {
|
|
@@ -373,23 +907,28 @@ function createAgUiRunner(options) {
|
|
|
373
907
|
}
|
|
374
908
|
}
|
|
375
909
|
if (opts.signal.aborted) return usage;
|
|
910
|
+
if (runError) {
|
|
911
|
+
for (const [toolCallId] of started) if (!answered.has(toolCallId)) emit({ type: "tool-aborted", toolCallId });
|
|
912
|
+
if (textOpen) emit({ type: "text-flush" });
|
|
913
|
+
throw runError;
|
|
914
|
+
}
|
|
376
915
|
if (!finished) {
|
|
377
916
|
throw new AparteError("The agent stream ended without RUN_FINISHED.", AparteErrorCode.PROVIDER_ERROR);
|
|
378
917
|
}
|
|
379
918
|
if (!endedOnFinish) {
|
|
380
919
|
throw new AparteError("The agent stream was cut: it ended in the middle of a step, after a RUN_FINISHED.", AparteErrorCode.PROVIDER_ERROR);
|
|
381
920
|
}
|
|
921
|
+
verify.end();
|
|
382
922
|
if (textOpen) emit({ type: "text-flush" });
|
|
383
923
|
const pending = [];
|
|
384
924
|
const interrupts = [];
|
|
925
|
+
const unanswerable = [];
|
|
385
926
|
if (finished.outcome?.type === "interrupt") {
|
|
386
927
|
for (const interrupt of finished.outcome.interrupts ?? []) {
|
|
387
928
|
const kind = interruptKind(interrupt);
|
|
388
929
|
if (!kind || !interrupt.toolCallId) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
AparteErrorCode.PROVIDER_ERROR
|
|
392
|
-
);
|
|
930
|
+
unanswerable.push(interrupt.id);
|
|
931
|
+
continue;
|
|
393
932
|
}
|
|
394
933
|
const call = callOf(interrupt);
|
|
395
934
|
if (kind === "approval") {
|
|
@@ -409,13 +948,24 @@ function createAgUiRunner(options) {
|
|
|
409
948
|
}
|
|
410
949
|
}
|
|
411
950
|
if (pending.length > 0) {
|
|
412
|
-
const
|
|
951
|
+
const cancelled = unanswerable.map((id) => ({ id, toolCallId: "", kind: "unanswerable" }));
|
|
952
|
+
const next = { v: 1, threadId, runId: finished.runId, interrupts: [...interrupts, ...cancelled] };
|
|
953
|
+
track.suspended = true;
|
|
413
954
|
emit({ type: "run-suspended", pending, ...usage ? { usage } : {}, ticket: JSON.stringify(next) });
|
|
414
955
|
} else {
|
|
956
|
+
if (unanswerable.length && stableThread) unanswered.set(threadId, unanswerable);
|
|
415
957
|
emit({ type: "run-done", ...usage ? { usage } : {} });
|
|
416
958
|
}
|
|
417
959
|
return usage;
|
|
418
960
|
};
|
|
961
|
+
return async (opts) => {
|
|
962
|
+
const track = { suspended: false };
|
|
963
|
+
try {
|
|
964
|
+
return await runOnce(opts, track);
|
|
965
|
+
} finally {
|
|
966
|
+
if (track.threadId && !stableThread && !track.suspended) states.delete(track.threadId);
|
|
967
|
+
}
|
|
968
|
+
};
|
|
419
969
|
}
|
|
420
970
|
export {
|
|
421
971
|
createAgUiRunner
|