@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/dist/index.js CHANGED
@@ -1,10 +1,23 @@
1
- import { contentToText, uuid, AparteError, AparteErrorCode } from "@aparte/core";
2
- function toAgUiMessages(messages) {
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, index) => {
5
- const id = `aparte-${index}`;
9
+ messages.forEach((m, index2) => {
10
+ const id = `aparte-${index2}`;
6
11
  const content = contentToText(m.content);
7
- if (m.role === "system" || m.role === "user") {
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
- return async (opts) => {
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 resume = ticket?.interrupts.length ? ticket.interrupts.map((interrupt) => resumeItem(interrupt, outcomes[interrupt.toolCallId])) : void 0;
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
- for await (const event of readAgUiEvents(response.body, response.headers.get("content-type"))) {
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
- emit({ type: "tool-resolved", toolCallId, result: content });
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
- throw new AparteError(
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"] } : void 0
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
- throw new AparteError(
390
- `The agent paused for something aparté cannot answer yet (interrupt reason "${interrupt.reason}").`,
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 next = { v: 1, threadId, runId: finished.runId, interrupts };
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