ag-ui-validate 0.1.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/dist/catalog-BglXBNbL.js +472 -0
  4. package/dist/catalog-BglXBNbL.js.map +1 -0
  5. package/dist/catalog-Ci9dqc1a.cjs +495 -0
  6. package/dist/catalog-Ci9dqc1a.cjs.map +1 -0
  7. package/dist/cli.js +2783 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/index-Hmqj3r_r.d.cts +52 -0
  10. package/dist/index-oNG1kOp9.d.ts +52 -0
  11. package/dist/index.cjs +14 -0
  12. package/dist/index.d.cts +3 -0
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.js +3 -0
  15. package/dist/report.cjs +139 -0
  16. package/dist/report.cjs.map +1 -0
  17. package/dist/report.d.cts +85 -0
  18. package/dist/report.d.ts +85 -0
  19. package/dist/report.js +134 -0
  20. package/dist/report.js.map +1 -0
  21. package/dist/src-HmI-kxef.cjs +1596 -0
  22. package/dist/src-HmI-kxef.cjs.map +1 -0
  23. package/dist/src-rGZ2G4qA.js +1555 -0
  24. package/dist/src-rGZ2G4qA.js.map +1 -0
  25. package/dist/transport.cjs +329 -0
  26. package/dist/transport.cjs.map +1 -0
  27. package/dist/transport.d.cts +89 -0
  28. package/dist/transport.d.ts +89 -0
  29. package/dist/transport.js +323 -0
  30. package/dist/transport.js.map +1 -0
  31. package/dist/types-oH_QTnn2.d.cts +148 -0
  32. package/dist/types-oH_QTnn2.d.ts +148 -0
  33. package/dist/vitest.d.ts +28 -0
  34. package/dist/vitest.js +2089 -0
  35. package/dist/vitest.js.map +1 -0
  36. package/package.json +127 -0
  37. package/src/cli-args.ts +202 -0
  38. package/src/cli.ts +147 -0
  39. package/src/index.ts +465 -0
  40. package/src/protocol/event-table.ts +316 -0
  41. package/src/protocol/jsonpatch.ts +220 -0
  42. package/src/report/index.ts +10 -0
  43. package/src/report/json.ts +20 -0
  44. package/src/report/junit.ts +56 -0
  45. package/src/report/pretty.ts +59 -0
  46. package/src/report/sarif.ts +109 -0
  47. package/src/rules/catalog.json +431 -0
  48. package/src/rules/catalog.ts +84 -0
  49. package/src/rules/checks/context.ts +117 -0
  50. package/src/rules/checks/lifecycle.ts +59 -0
  51. package/src/rules/checks/reasoning.ts +97 -0
  52. package/src/rules/checks/state.ts +72 -0
  53. package/src/rules/checks/text.ts +109 -0
  54. package/src/rules/checks/toolcalls.ts +167 -0
  55. package/src/rules/checks/transport.ts +17 -0
  56. package/src/transport/index.ts +331 -0
  57. package/src/transport/ndjson.ts +25 -0
  58. package/src/transport/sse.ts +126 -0
  59. package/src/types.ts +136 -0
  60. package/src/vitest/index.ts +19 -0
  61. package/src/vitest/matcher.ts +77 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2783 @@
1
+ #!/usr/bin/env node
2
+ import { createReadStream, readFileSync, writeFileSync } from "node:fs";
3
+ import process from "node:process";
4
+ //#region src/cli-args.ts
5
+ const USAGE = `ag-ui-validate — conformance validator for the AG-UI protocol
6
+
7
+ Usage:
8
+ ag-ui-validate <url> connect to a live endpoint and validate the stream
9
+ ag-ui-validate <file> validate a recorded stream (SSE capture or NDJSON/JSONL)
10
+ ag-ui-validate - read a recorded stream from stdin
11
+
12
+ Output (default: human-readable):
13
+ --json machine-readable report on stdout
14
+ --sarif SARIF 2.1.0 log on stdout (code scanning)
15
+ --junit JUnit XML on stdout (CI test reports)
16
+ --json-file <path> additionally write the JSON report to a file
17
+ --sarif-file <path> additionally write a SARIF log to a file
18
+ --junit-file <path> additionally write JUnit XML to a file
19
+ --no-color disable ANSI colors
20
+
21
+ Rules:
22
+ --rule AGUI###=<severity> override a rule's severity (error|warning|info|off)
23
+ --off AGUI### shorthand for --rule AGUI###=off
24
+ --features <a,b,...> declare exercised features (enables e.g. AGUI305)
25
+ --max-warnings <n> exit 1 when warnings exceed n
26
+
27
+ Endpoint options:
28
+ --header "Name: value" extra request header (repeatable)
29
+ --timeout <seconds> abort the request after this long
30
+
31
+ Exit codes: 0 clean, 1 findings at error level (or warnings over --max-warnings), 2 tool failure.
32
+ `;
33
+ const SEVERITIES$1 = /* @__PURE__ */ new Set([
34
+ "error",
35
+ "warning",
36
+ "info",
37
+ "off"
38
+ ]);
39
+ const RULE_ID = /^AGUI\d{3}$/;
40
+ const FLAGS = {
41
+ "--help": {
42
+ takesValue: false,
43
+ apply: (c) => (c.help = true, null)
44
+ },
45
+ "--version": {
46
+ takesValue: false,
47
+ apply: (c) => (c.version = true, null)
48
+ },
49
+ "--no-color": {
50
+ takesValue: false,
51
+ apply: (c) => (c.color = false, null)
52
+ },
53
+ "--json": {
54
+ takesValue: false,
55
+ apply: (c) => setFormat(c, "json")
56
+ },
57
+ "--sarif": {
58
+ takesValue: false,
59
+ apply: (c) => setFormat(c, "sarif")
60
+ },
61
+ "--junit": {
62
+ takesValue: false,
63
+ apply: (c) => setFormat(c, "junit")
64
+ },
65
+ "--off": {
66
+ takesValue: true,
67
+ apply: (c, v) => {
68
+ if (!RULE_ID.test(v)) return `--off expects a rule ID like AGUI203, got '${v}'`;
69
+ c.severityOverrides[v] = "off";
70
+ return null;
71
+ }
72
+ },
73
+ "--rule": {
74
+ takesValue: true,
75
+ apply: (c, v) => {
76
+ const eq = v.indexOf("=");
77
+ if (eq === -1) return `--rule expects ID=severity (e.g. AGUI105=warning), got '${v}'`;
78
+ const id = v.slice(0, eq);
79
+ const severity = v.slice(eq + 1);
80
+ if (!RULE_ID.test(id)) return `--rule expects a rule ID like AGUI105, got '${id}'`;
81
+ if (!SEVERITIES$1.has(severity)) return `'${severity}' is not a severity; use error, warning, info, or off`;
82
+ c.severityOverrides[id] = severity;
83
+ return null;
84
+ }
85
+ },
86
+ "--max-warnings": {
87
+ takesValue: true,
88
+ apply: (c, v) => {
89
+ const n = Number(v);
90
+ if (!Number.isInteger(n) || n < 0) return `--max-warnings expects a non-negative integer, got '${v}'`;
91
+ c.maxWarnings = n;
92
+ return null;
93
+ }
94
+ },
95
+ "--timeout": {
96
+ takesValue: true,
97
+ apply: (c, v) => {
98
+ const n = Number(v);
99
+ if (!Number.isFinite(n) || n <= 0) return `--timeout expects a positive number of seconds, got '${v}'`;
100
+ c.timeoutMs = Math.round(n * 1e3);
101
+ return null;
102
+ }
103
+ },
104
+ "--header": {
105
+ takesValue: true,
106
+ apply: (c, v) => {
107
+ const colon = v.indexOf(":");
108
+ if (colon <= 0) return `--header expects "Name: value", got '${v}'`;
109
+ c.headers[v.slice(0, colon).trim().toLowerCase()] = v.slice(colon + 1).trim();
110
+ return null;
111
+ }
112
+ },
113
+ "--sarif-file": {
114
+ takesValue: true,
115
+ apply: (c, v) => (c.sarifFile = v, null)
116
+ },
117
+ "--junit-file": {
118
+ takesValue: true,
119
+ apply: (c, v) => (c.junitFile = v, null)
120
+ },
121
+ "--json-file": {
122
+ takesValue: true,
123
+ apply: (c, v) => (c.jsonFile = v, null)
124
+ },
125
+ "--features": {
126
+ takesValue: true,
127
+ apply: (c, v) => {
128
+ c.features = v.split(",").map((f) => f.trim()).filter((f) => f !== "");
129
+ return null;
130
+ }
131
+ }
132
+ };
133
+ function setFormat(c, format) {
134
+ if (c.format !== "pretty" && c.format !== format) return `pick at most one of --json, --sarif, --junit`;
135
+ c.format = format;
136
+ return null;
137
+ }
138
+ function parseCliArgs(argv) {
139
+ const config = {
140
+ target: "",
141
+ format: "pretty",
142
+ color: null,
143
+ headers: {},
144
+ severityOverrides: {},
145
+ help: false,
146
+ version: false
147
+ };
148
+ const targets = [];
149
+ for (let i = 0; i < argv.length; i += 1) {
150
+ const arg = argv[i];
151
+ if (arg === "-" || !arg.startsWith("-")) {
152
+ targets.push(arg);
153
+ continue;
154
+ }
155
+ let name = arg;
156
+ let inlineValue = null;
157
+ const eq = arg.indexOf("=");
158
+ if (eq !== -1) {
159
+ name = arg.slice(0, eq);
160
+ inlineValue = arg.slice(eq + 1);
161
+ }
162
+ const flag = FLAGS[name];
163
+ if (flag === void 0) return {
164
+ ok: false,
165
+ error: `unknown flag '${name}'`
166
+ };
167
+ let value = "";
168
+ if (flag.takesValue) {
169
+ if (inlineValue !== null) value = inlineValue;
170
+ else if (i + 1 < argv.length) value = argv[i += 1];
171
+ else return {
172
+ ok: false,
173
+ error: `${name} requires a value`
174
+ };
175
+ } else if (inlineValue !== null) return {
176
+ ok: false,
177
+ error: `${name} does not take a value`
178
+ };
179
+ const error = flag.apply(config, value);
180
+ if (error !== null) return {
181
+ ok: false,
182
+ error
183
+ };
184
+ }
185
+ if (targets.length > 1) return {
186
+ ok: false,
187
+ error: `expected exactly one target, got ${targets.length}`
188
+ };
189
+ if (targets.length === 1) config.target = targets[0];
190
+ else if (!config.help && !config.version) return {
191
+ ok: false,
192
+ error: "missing target: a URL, a file path, or - for stdin"
193
+ };
194
+ return {
195
+ ok: true,
196
+ config
197
+ };
198
+ }
199
+ function decideExitCode(summary, maxWarnings) {
200
+ if (summary.errors > 0) return 1;
201
+ if (maxWarnings !== void 0 && summary.warnings > maxWarnings) return 1;
202
+ return 0;
203
+ }
204
+ //#endregion
205
+ //#region src/report/pretty.ts
206
+ const SYMBOL = {
207
+ error: "✖",
208
+ warning: "⚠",
209
+ info: "ℹ"
210
+ };
211
+ const SGR = {
212
+ error: "31",
213
+ warning: "33",
214
+ info: "36"
215
+ };
216
+ function paint(code, s, on) {
217
+ return on ? `\x1b[${code}m${s}\x1b[0m` : s;
218
+ }
219
+ function formatDiagnosticLine(d, opts) {
220
+ const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : "—";
221
+ const head = paint(SGR[d.severity], `${SYMBOL[d.severity]} ${d.rule}`, opts.color);
222
+ const meta = paint("2", `${d.severity.padEnd(7)} ${where.padEnd(10)}`, opts.color);
223
+ const cite = paint("2", ` ↳ ${d.specUrl}`, opts.color);
224
+ return `${head} ${meta} ${d.message}\n${cite}`;
225
+ }
226
+ function count(n, noun) {
227
+ return `${n} ${noun}${n === 1 ? "" : "s"}`;
228
+ }
229
+ function formatReportSummary(report, opts) {
230
+ const { errors, warnings, info } = report.summary;
231
+ const lines = [];
232
+ if (errors + warnings + info === 0) lines.push(paint("32", `✔ no conformance violations across ${count(report.eventCount, "event")}`, opts.color));
233
+ else lines.push(`${count(errors, "error")}, ${count(warnings, "warning")}, ${info} info across ${count(report.eventCount, "event")}`);
234
+ const features = Object.entries(report.features);
235
+ const exercised = features.filter(([, s]) => s === "exercised").map(([f]) => f);
236
+ const suffix = exercised.length > 0 ? `: ${exercised.join(", ")}` : "";
237
+ lines.push(`${exercised.length} of ${features.length} AG-UI features exercised${suffix}`);
238
+ if (report.skipped.length > 0) {
239
+ lines.push(`${count(report.skipped.length, "rule")} not evaluated:`);
240
+ for (const s of report.skipped) lines.push(paint("2", ` – ${s.rule}: ${s.reason}`, opts.color));
241
+ }
242
+ for (const e of report.internalErrors) lines.push(paint("31", `! internal validator error: ${e}`, opts.color));
243
+ return lines.join("\n");
244
+ }
245
+ //#endregion
246
+ //#region src/report/json.ts
247
+ function toJsonReport(report, opts) {
248
+ const doc = {
249
+ tool: opts.tool,
250
+ ...report
251
+ };
252
+ if (opts.target !== void 0) doc.target = opts.target;
253
+ return doc;
254
+ }
255
+ //#endregion
256
+ //#region src/rules/catalog.json
257
+ var catalog_default = {
258
+ $comment: "AG-UI conformance rule catalog. Data, not code: a Python/Go implementation shares these rules. Severities follow the working agreement that behaviour the spec does not clearly govern is at most 'info' — entries with 'specQuestion' were downgraded accordingly; see docs/spec-questions.md.",
259
+ catalogVersion: "0.1.0",
260
+ spec: "0.x",
261
+ rules: [
262
+ {
263
+ "id": "AGUI001",
264
+ "severity": "error",
265
+ "title": "Run does not start with RUN_STARTED",
266
+ "messageTemplate": "First event of the run is {type}; expected RUN_STARTED",
267
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runstarted",
268
+ "specQuote": "The RunStarted event is the first event emitted when an agent begins processing a request.",
269
+ "since": "0.x",
270
+ "checkedIn": "core"
271
+ },
272
+ {
273
+ "id": "AGUI002",
274
+ "severity": "error",
275
+ "title": "Multiple RUN_STARTED in one run",
276
+ "messageTemplate": "Duplicate RUN_STARTED (runId '{runId}') before the active run terminated",
277
+ "specUrl": "https://docs.ag-ui.com/concepts/events#lifecycle-events",
278
+ "specQuote": "The RunStarted and either RunFinished or RunError events are mandatory, forming the boundaries of an agent run.",
279
+ "since": "0.x",
280
+ "checkedIn": "core"
281
+ },
282
+ {
283
+ "id": "AGUI003",
284
+ "severity": "error",
285
+ "title": "Run not terminated",
286
+ "messageTemplate": "Run '{runId}' ended without RUN_FINISHED or RUN_ERROR",
287
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runfinished",
288
+ "specQuote": "Every run terminates with either RunFinished or RunError.",
289
+ "since": "0.x",
290
+ "checkedIn": "core"
291
+ },
292
+ {
293
+ "id": "AGUI004",
294
+ "severity": "error",
295
+ "title": "Event after terminal event",
296
+ "messageTemplate": "{type} follows the run's terminal {terminalType}",
297
+ "specUrl": "https://docs.ag-ui.com/concepts/events#lifecycle-events",
298
+ "specQuote": "The RunStarted and either RunFinished or RunError events are mandatory, forming the boundaries of an agent run.",
299
+ "since": "0.x",
300
+ "checkedIn": "core"
301
+ },
302
+ {
303
+ "id": "AGUI005",
304
+ "severity": "error",
305
+ "title": "RUN_FINISHED and RUN_ERROR are mutually exclusive",
306
+ "messageTemplate": "{type} emitted after the run already terminated with {terminalType}",
307
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runfinished",
308
+ "specQuote": "Every run terminates with either RunFinished or RunError.",
309
+ "since": "0.x",
310
+ "checkedIn": "core"
311
+ },
312
+ {
313
+ "id": "AGUI006",
314
+ "severity": "error",
315
+ "title": "STEP_FINISHED without matching STEP_STARTED",
316
+ "messageTemplate": "STEP_FINISHED '{stepName}' has no open STEP_STARTED",
317
+ "specUrl": "https://docs.ag-ui.com/concepts/events#stepfinished",
318
+ "specQuote": "The stepName must match the corresponding StepStarted event.",
319
+ "since": "0.x",
320
+ "checkedIn": "core"
321
+ },
322
+ {
323
+ "id": "AGUI007",
324
+ "severity": "error",
325
+ "title": "Step unterminated at run end",
326
+ "messageTemplate": "STEP_STARTED '{stepName}' never finished",
327
+ "specUrl": "https://docs.ag-ui.com/concepts/events#stepstarted",
328
+ "specQuote": "The stepName must match the corresponding StepStarted event.",
329
+ "since": "0.x",
330
+ "checkedIn": "core"
331
+ },
332
+ {
333
+ "id": "AGUI008",
334
+ "severity": "warning",
335
+ "title": "Unstable threadId/runId across the run",
336
+ "messageTemplate": "RUN_FINISHED {field} '{actual}' does not match RUN_STARTED {field} '{expected}'",
337
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runstarted",
338
+ "specQuote": "It also provides crucial identifiers that can be used to associate subsequent events with this specific run.",
339
+ "since": "0.x",
340
+ "checkedIn": "core"
341
+ },
342
+ {
343
+ "id": "AGUI101",
344
+ "severity": "error",
345
+ "title": "TEXT_MESSAGE_CONTENT without start",
346
+ "messageTemplate": "TEXT_MESSAGE_CONTENT for messageId '{messageId}' with no open TEXT_MESSAGE_START",
347
+ "specUrl": "https://docs.ag-ui.com/concepts/events#text-message-events",
348
+ "specQuote": "A message begins with a TextMessageStart event, followed by one or more TextMessageContent events that deliver chunks of text as they become available, and concludes with a TextMessageEnd event.",
349
+ "since": "0.x",
350
+ "feature": "agentic-chat",
351
+ "checkedIn": "core"
352
+ },
353
+ {
354
+ "id": "AGUI102",
355
+ "severity": "error",
356
+ "title": "TEXT_MESSAGE_END without start",
357
+ "messageTemplate": "TEXT_MESSAGE_END for messageId '{messageId}' with no open TEXT_MESSAGE_START",
358
+ "specUrl": "https://docs.ag-ui.com/concepts/events#textmessageend",
359
+ "specQuote": "messageId: Matches the ID from TextMessageStart",
360
+ "since": "0.x",
361
+ "feature": "agentic-chat",
362
+ "checkedIn": "core"
363
+ },
364
+ {
365
+ "id": "AGUI103",
366
+ "severity": "error",
367
+ "title": "Text message unterminated at run end",
368
+ "messageTemplate": "TEXT_MESSAGE_START messageId '{messageId}' never ended",
369
+ "specUrl": "https://docs.ag-ui.com/concepts/events#text-message-events",
370
+ "specQuote": "A message begins with a TextMessageStart event, followed by one or more TextMessageContent events that deliver chunks of text as they become available, and concludes with a TextMessageEnd event.",
371
+ "since": "0.x",
372
+ "feature": "agentic-chat",
373
+ "checkedIn": "core"
374
+ },
375
+ {
376
+ "id": "AGUI104",
377
+ "severity": "error",
378
+ "title": "Duplicate messageId within a run",
379
+ "messageTemplate": "messageId '{messageId}' was already used by a completed message",
380
+ "specUrl": "https://docs.ag-ui.com/concepts/events#textmessagestart",
381
+ "specQuote": "It establishes a unique messageId that will be referenced by subsequent content chunks and the end event.",
382
+ "since": "0.x",
383
+ "feature": "agentic-chat",
384
+ "checkedIn": "core"
385
+ },
386
+ {
387
+ "id": "AGUI105",
388
+ "severity": "warning",
389
+ "title": "Empty content delta",
390
+ "messageTemplate": "TEXT_MESSAGE_CONTENT for messageId '{messageId}' has an empty delta",
391
+ "specUrl": "https://docs.ag-ui.com/concepts/events#textmessagecontent",
392
+ "specQuote": "delta: Text content chunk (non-empty)",
393
+ "since": "0.x",
394
+ "feature": "agentic-chat",
395
+ "specQuestion": "SQ-2",
396
+ "checkedIn": "core"
397
+ },
398
+ {
399
+ "id": "AGUI106",
400
+ "severity": "error",
401
+ "title": "Interleaved message streams sharing a messageId",
402
+ "messageTemplate": "TEXT_MESSAGE_START for messageId '{messageId}', which is already open",
403
+ "specUrl": "https://docs.ag-ui.com/concepts/events#implementation-considerations",
404
+ "specQuote": "Events with the same ID (e.g., messageId, toolCallId) belong to the same logical stream",
405
+ "since": "0.x",
406
+ "feature": "agentic-chat",
407
+ "checkedIn": "core"
408
+ },
409
+ {
410
+ "id": "AGUI201",
411
+ "severity": "error",
412
+ "title": "TOOL_CALL_ARGS without start",
413
+ "messageTemplate": "TOOL_CALL_ARGS for toolCallId '{toolCallId}' with no open TOOL_CALL_START",
414
+ "specUrl": "https://docs.ag-ui.com/concepts/events#tool-call-events",
415
+ "specQuote": "When an agent needs to use a tool, it emits a ToolCallStart event, followed by one or more ToolCallArgs events that stream the arguments being passed to the tool, and concludes with a ToolCallEnd event.",
416
+ "since": "0.x",
417
+ "feature": "backend-tool-rendering",
418
+ "checkedIn": "core"
419
+ },
420
+ {
421
+ "id": "AGUI202",
422
+ "severity": "error",
423
+ "title": "TOOL_CALL_END without start",
424
+ "messageTemplate": "TOOL_CALL_END for toolCallId '{toolCallId}' with no open TOOL_CALL_START",
425
+ "specUrl": "https://docs.ag-ui.com/concepts/events#toolcallend",
426
+ "specQuote": "toolCallId: Matches the ID from ToolCallStart",
427
+ "since": "0.x",
428
+ "feature": "backend-tool-rendering",
429
+ "checkedIn": "core"
430
+ },
431
+ {
432
+ "id": "AGUI203",
433
+ "severity": "error",
434
+ "title": "Unterminated tool call",
435
+ "messageTemplate": "TOOL_CALL_START id '{toolCallId}' never terminated",
436
+ "specUrl": "https://docs.ag-ui.com/concepts/events#tool-call-events",
437
+ "specQuote": "When an agent needs to use a tool, it emits a ToolCallStart event, followed by one or more ToolCallArgs events that stream the arguments being passed to the tool, and concludes with a ToolCallEnd event.",
438
+ "since": "0.x",
439
+ "feature": "backend-tool-rendering",
440
+ "checkedIn": "core"
441
+ },
442
+ {
443
+ "id": "AGUI204",
444
+ "severity": "error",
445
+ "title": "Tool call arguments are not valid JSON",
446
+ "messageTemplate": "Concatenated TOOL_CALL_ARGS for toolCallId '{toolCallId}' do not parse as JSON: {error}",
447
+ "specUrl": "https://docs.ag-ui.com/concepts/events#toolcallargs",
448
+ "specQuote": "Frontends should concatenate these deltas in the order received to construct the complete arguments object.",
449
+ "since": "0.x",
450
+ "feature": "backend-tool-rendering",
451
+ "checkedIn": "core"
452
+ },
453
+ {
454
+ "id": "AGUI205",
455
+ "severity": "error",
456
+ "title": "Duplicate toolCallId within a run",
457
+ "messageTemplate": "toolCallId '{toolCallId}' was already used by a completed tool call",
458
+ "specUrl": "https://docs.ag-ui.com/concepts/events#toolcallstart",
459
+ "specQuote": "toolCallId: Unique identifier for the tool call",
460
+ "since": "0.x",
461
+ "feature": "backend-tool-rendering",
462
+ "checkedIn": "core"
463
+ },
464
+ {
465
+ "id": "AGUI206",
466
+ "severity": "warning",
467
+ "title": "TOOL_CALL_RESULT before TOOL_CALL_END",
468
+ "messageTemplate": "TOOL_CALL_RESULT for toolCallId '{toolCallId}' arrived while the call is still open",
469
+ "specUrl": "https://docs.ag-ui.com/concepts/events#toolcallresult",
470
+ "specQuote": "This event is sent after the tool has been executed by the system and contains the actual output generated by the tool.",
471
+ "since": "0.x",
472
+ "feature": "backend-tool-rendering",
473
+ "checkedIn": "core"
474
+ },
475
+ {
476
+ "id": "AGUI207",
477
+ "severity": "error",
478
+ "title": "TOOL_CALL_RESULT references unknown toolCallId",
479
+ "messageTemplate": "TOOL_CALL_RESULT references toolCallId '{toolCallId}', which was never started",
480
+ "specUrl": "https://docs.ag-ui.com/concepts/events#toolcallresult",
481
+ "specQuote": "toolCallId: Matches the ID from the corresponding ToolCallStart event",
482
+ "since": "0.x",
483
+ "feature": "backend-tool-rendering",
484
+ "checkedIn": "core"
485
+ },
486
+ {
487
+ "id": "AGUI208",
488
+ "severity": "info",
489
+ "title": "parentMessageId references unknown message",
490
+ "messageTemplate": "TOOL_CALL_START parentMessageId '{parentMessageId}' matches no message observed in this stream",
491
+ "specUrl": "https://docs.ag-ui.com/concepts/events#toolcallstart",
492
+ "specQuote": "The optional parentMessageId allows linking the tool call to a specific message in the conversation, providing context for why the tool is being used.",
493
+ "since": "0.x",
494
+ "feature": "backend-tool-rendering",
495
+ "specQuestion": "SQ-11",
496
+ "checkedIn": "core"
497
+ },
498
+ {
499
+ "id": "AGUI301",
500
+ "severity": "info",
501
+ "title": "STATE_DELTA before any STATE_SNAPSHOT",
502
+ "messageTemplate": "STATE_DELTA precedes any STATE_SNAPSHOT; the base it applies to is not observable on this stream",
503
+ "specUrl": "https://docs.ag-ui.com/concepts/events#statesnapshot",
504
+ "specQuote": "This event is typically sent at the beginning of an interaction or when synchronization is needed.",
505
+ "since": "0.x",
506
+ "feature": "shared-state",
507
+ "specQuestion": "SQ-1",
508
+ "checkedIn": "core"
509
+ },
510
+ {
511
+ "id": "AGUI302",
512
+ "severity": "error",
513
+ "title": "STATE_DELTA failed to apply",
514
+ "messageTemplate": "STATE_DELTA failed to apply: {error}",
515
+ "specUrl": "https://docs.ag-ui.com/concepts/events#statedelta",
516
+ "specQuote": "Each delta represents specific changes to apply to the current state model.",
517
+ "since": "0.x",
518
+ "feature": "shared-state",
519
+ "checkedIn": "core"
520
+ },
521
+ {
522
+ "id": "AGUI303",
523
+ "severity": "error",
524
+ "title": "STATE_DELTA is not a valid RFC 6902 patch document",
525
+ "messageTemplate": "STATE_DELTA is not a valid RFC 6902 patch document: {error}",
526
+ "specUrl": "https://docs.ag-ui.com/concepts/events#statedelta",
527
+ "specQuote": "The StateDelta event contains incremental updates to the agent's state in the form of JSON Patch operations (as defined in RFC 6902).",
528
+ "since": "0.x",
529
+ "feature": "shared-state",
530
+ "checkedIn": "core"
531
+ },
532
+ {
533
+ "id": "AGUI304",
534
+ "severity": "info",
535
+ "title": "Mid-run STATE_SNAPSHOT discards accumulated deltas",
536
+ "messageTemplate": "STATE_SNAPSHOT replaces state previously built from {deltaCount} delta(s)",
537
+ "specUrl": "https://docs.ag-ui.com/concepts/events#statesnapshot",
538
+ "specQuote": "This event is typically sent at the beginning of an interaction or when synchronization is needed.",
539
+ "since": "0.x",
540
+ "feature": "shared-state",
541
+ "checkedIn": "core"
542
+ },
543
+ {
544
+ "id": "AGUI305",
545
+ "severity": "warning",
546
+ "title": "Shared state declared but never established",
547
+ "messageTemplate": "features include 'shared-state' but no STATE_SNAPSHOT was emitted",
548
+ "specUrl": "https://docs.ag-ui.com/concepts/events#state-management-events",
549
+ "specQuote": "These events are used to manage and synchronize the agent's state with the frontend.",
550
+ "since": "0.x",
551
+ "feature": "shared-state",
552
+ "requiresFeature": true,
553
+ "checkedIn": "core"
554
+ },
555
+ {
556
+ "id": "AGUI401",
557
+ "severity": "error",
558
+ "title": "REASONING_MESSAGE_CONTENT without start",
559
+ "messageTemplate": "REASONING_MESSAGE_CONTENT for messageId '{messageId}' with no open REASONING_MESSAGE_START",
560
+ "specUrl": "https://docs.ag-ui.com/concepts/events#reasoningmessagecontent",
561
+ "specQuote": "Multiple content events with the same messageId should be concatenated to form the complete visible reasoning.",
562
+ "since": "0.x",
563
+ "specQuestion": "SQ-4",
564
+ "checkedIn": "core"
565
+ },
566
+ {
567
+ "id": "AGUI402",
568
+ "severity": "warning",
569
+ "title": "Reasoning unterminated at run end",
570
+ "messageTemplate": "{startType} messageId '{messageId}' never ended",
571
+ "specUrl": "https://docs.ag-ui.com/concepts/events#reasoning-events",
572
+ "specQuote": "Reasoning events support LLM reasoning visibility and continuity, enabling chain-of-thought reasoning while maintaining privacy.",
573
+ "since": "0.x",
574
+ "specQuestion": "SQ-4",
575
+ "checkedIn": "core"
576
+ },
577
+ {
578
+ "id": "AGUI501",
579
+ "severity": "error",
580
+ "title": "Malformed SSE framing",
581
+ "messageTemplate": "Malformed SSE framing: {detail}",
582
+ "specUrl": "https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation",
583
+ "since": "0.x",
584
+ "checkedIn": "transport"
585
+ },
586
+ {
587
+ "id": "AGUI502",
588
+ "severity": "error",
589
+ "title": "Event payload is not valid JSON",
590
+ "messageTemplate": "Event payload is not valid JSON: {error}",
591
+ "specUrl": "https://docs.ag-ui.com/concepts/events#base-event-properties",
592
+ "specQuote": "All events share a common set of base properties.",
593
+ "since": "0.x",
594
+ "checkedIn": "core"
595
+ },
596
+ {
597
+ "id": "AGUI503",
598
+ "severity": "error",
599
+ "title": "Unknown event type",
600
+ "messageTemplate": "Unknown event type '{type}' (not in @ag-ui/core v{sdkVersion}, and not RAW or CUSTOM)",
601
+ "specUrl": "https://docs.ag-ui.com/concepts/events#event-types-overview",
602
+ "specQuote": "Events in the protocol are categorized by their purpose.",
603
+ "since": "0.x",
604
+ "checkedIn": "core"
605
+ },
606
+ {
607
+ "id": "AGUI504",
608
+ "severity": "error",
609
+ "title": "Event fails schema validation for its declared type",
610
+ "messageTemplate": "{type}: {detail}",
611
+ "specUrl": "https://docs.ag-ui.com/concepts/events#base-event-properties",
612
+ "specQuote": "All events share a common set of base properties.",
613
+ "since": "0.x",
614
+ "checkedIn": "core"
615
+ },
616
+ {
617
+ "id": "AGUI505",
618
+ "severity": "warning",
619
+ "title": "Unexpected Content-Type",
620
+ "messageTemplate": "Content-Type '{contentType}' is neither text/event-stream nor application/x-ndjson",
621
+ "specUrl": "https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model",
622
+ "since": "0.x",
623
+ "checkedIn": "transport"
624
+ },
625
+ {
626
+ "id": "AGUI506",
627
+ "severity": "info",
628
+ "title": "No keepalive frame within the configured window",
629
+ "messageTemplate": "No event or keepalive frame for {seconds}s",
630
+ "specUrl": "https://docs.ag-ui.com/concepts/architecture#standard-http-client",
631
+ "since": "0.x",
632
+ "specQuestion": "SQ-5",
633
+ "checkedIn": "transport"
634
+ },
635
+ {
636
+ "id": "AGUI507",
637
+ "severity": "info",
638
+ "title": "Response appears buffered rather than incrementally flushed",
639
+ "messageTemplate": "Response appears buffered: {detail}",
640
+ "specUrl": "https://docs.ag-ui.com/concepts/architecture#standard-http-client",
641
+ "since": "0.x",
642
+ "specQuestion": "SQ-5",
643
+ "checkedIn": "transport"
644
+ },
645
+ {
646
+ "id": "AGUI508",
647
+ "severity": "error",
648
+ "title": "Stream ended without a terminal event",
649
+ "messageTemplate": "Connection ended mid-run '{runId}' without RUN_FINISHED or RUN_ERROR",
650
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runfinished",
651
+ "specQuote": "Every run terminates with either RunFinished or RunError.",
652
+ "since": "0.x",
653
+ "checkedIn": "transport"
654
+ },
655
+ {
656
+ "id": "AGUI901",
657
+ "severity": "info",
658
+ "title": "RAW event wraps a typed AG-UI event",
659
+ "messageTemplate": "RAW event wraps an event of type '{wrappedType}', which has a typed AG-UI equivalent",
660
+ "specUrl": "https://docs.ag-ui.com/concepts/events#raw",
661
+ "specQuote": "The Raw event acts as a container for events originating from external systems or sources that don't natively follow the Agent UI Protocol.",
662
+ "since": "0.x",
663
+ "checkedIn": "core"
664
+ },
665
+ {
666
+ "id": "AGUI902",
667
+ "severity": "info",
668
+ "title": "Events carry no timestamps",
669
+ "messageTemplate": "None of the {eventCount} events carry the optional timestamp property",
670
+ "specUrl": "https://docs.ag-ui.com/concepts/events#base-event-properties",
671
+ "specQuote": "timestamp: Optional timestamp indicating when the event was created",
672
+ "since": "0.x",
673
+ "checkedIn": "core"
674
+ },
675
+ {
676
+ "id": "AGUI903",
677
+ "severity": "info",
678
+ "title": "CUSTOM event name is not namespaced",
679
+ "messageTemplate": "CUSTOM event name '{name}' has no namespace prefix (e.g. 'vendor.event')",
680
+ "specUrl": "https://docs.ag-ui.com/concepts/events#custom",
681
+ "specQuote": "Teams should document their custom events to ensure consistent implementation across frontends and agents.",
682
+ "since": "0.x",
683
+ "specQuestion": "SQ-14",
684
+ "checkedIn": "core"
685
+ }
686
+ ]
687
+ };
688
+ //#endregion
689
+ //#region src/rules/catalog.ts
690
+ const SEVERITIES = [
691
+ "error",
692
+ "warning",
693
+ "info"
694
+ ];
695
+ const LAYERS = ["core", "transport"];
696
+ /** Validates catalog data and returns it typed. Throws on structural problems. */
697
+ function validateCatalog(data) {
698
+ const problems = [];
699
+ const cat = data;
700
+ if (typeof cat !== "object" || cat === null || !Array.isArray(cat.rules)) throw new Error("rule catalog: expected an object with a rules array");
701
+ const seen = /* @__PURE__ */ new Set();
702
+ for (const rule of cat.rules) {
703
+ const where = rule?.id ?? "<missing id>";
704
+ if (!/^AGUI\d{3}$/.test(rule.id ?? "")) problems.push(`${where}: id must match AGUI###`);
705
+ if (seen.has(rule.id)) problems.push(`${where}: duplicate id`);
706
+ seen.add(rule.id);
707
+ if (!SEVERITIES.includes(rule.severity)) problems.push(`${where}: bad severity '${rule.severity}'`);
708
+ if (!rule.title) problems.push(`${where}: missing title`);
709
+ if (!rule.messageTemplate) problems.push(`${where}: missing messageTemplate`);
710
+ if (!rule.specUrl?.startsWith("https://")) problems.push(`${where}: specUrl must be an https URL`);
711
+ if (!rule.since) problems.push(`${where}: missing since`);
712
+ if (!LAYERS.includes(rule.checkedIn)) problems.push(`${where}: bad checkedIn '${rule.checkedIn}'`);
713
+ if (rule.requiresFeature && !rule.feature) problems.push(`${where}: requiresFeature without feature`);
714
+ }
715
+ if (problems.length > 0) throw new Error(`rule catalog is invalid:\n ${problems.join("\n ")}`);
716
+ return cat;
717
+ }
718
+ const CATALOG = validateCatalog(catalog_default);
719
+ const RULES = new Map(CATALOG.rules.map((r) => [r.id, r]));
720
+ /** Fills a rule's messageTemplate. Unknown placeholders are left intact. */
721
+ function formatMessage(rule, params) {
722
+ return rule.messageTemplate.replace(/\{(\w+)\}/g, (whole, key) => key in params ? String(params[key]) : whole);
723
+ }
724
+ //#endregion
725
+ //#region src/report/sarif.ts
726
+ const LEVEL = {
727
+ error: "error",
728
+ warning: "warning",
729
+ info: "note"
730
+ };
731
+ function toSarif(report, opts) {
732
+ const rules = /* @__PURE__ */ new Map();
733
+ for (const d of report.diagnostics) {
734
+ if (rules.has(d.rule)) continue;
735
+ const entry = {
736
+ id: d.rule,
737
+ helpUri: d.specUrl
738
+ };
739
+ const catalog = RULES.get(d.rule);
740
+ if (catalog !== void 0) {
741
+ entry.shortDescription = { text: catalog.title };
742
+ entry.defaultConfiguration = { level: LEVEL[catalog.severity] };
743
+ }
744
+ rules.set(d.rule, entry);
745
+ }
746
+ const results = report.diagnostics.map((d) => ({
747
+ ruleId: d.rule,
748
+ level: LEVEL[d.severity],
749
+ message: { text: d.message },
750
+ locations: opts.artifactUri !== void 0 && d.eventIndex >= 0 ? [{ physicalLocation: {
751
+ artifactLocation: { uri: opts.artifactUri },
752
+ region: { startLine: d.eventIndex + 1 }
753
+ } }] : []
754
+ }));
755
+ return {
756
+ $schema: "https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json",
757
+ version: "2.1.0",
758
+ runs: [{
759
+ tool: { driver: {
760
+ name: "ag-ui-validate",
761
+ version: opts.toolVersion,
762
+ informationUri: "https://github.com/langport-dev/ag-ui-validate",
763
+ rules: [...rules.values()]
764
+ } },
765
+ results
766
+ }]
767
+ };
768
+ }
769
+ //#endregion
770
+ //#region src/report/junit.ts
771
+ function esc(s) {
772
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
773
+ }
774
+ function toJUnit(report, opts) {
775
+ const lines = ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>"];
776
+ const suite = esc(opts.name);
777
+ if (report.diagnostics.length === 0) {
778
+ lines.push("<testsuites name=\"ag-ui-validate\" tests=\"1\" failures=\"0\" errors=\"0\" skipped=\"0\">");
779
+ lines.push(` <testsuite name="${suite}" tests="1" failures="0" errors="0" skipped="0">`);
780
+ lines.push(` <testcase name="AG-UI conformance: no violations across ${report.eventCount} events" classname="${suite}"/>`);
781
+ lines.push(" </testsuite>");
782
+ lines.push("</testsuites>");
783
+ return `${lines.join("\n")}\n`;
784
+ }
785
+ const failures = report.diagnostics.filter((d) => d.severity === "error").length;
786
+ const skipped = report.diagnostics.length - failures;
787
+ const counts = `tests="${report.diagnostics.length}" failures="${failures}" errors="0" skipped="${skipped}"`;
788
+ lines.push(`<testsuites name="ag-ui-validate" ${counts}>`);
789
+ lines.push(` <testsuite name="${suite}" ${counts}>`);
790
+ for (const d of report.diagnostics) {
791
+ const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : "stream";
792
+ const name = esc(`${d.rule} (${where})`);
793
+ lines.push(` <testcase name="${name}" classname="${suite}">`);
794
+ const body = `${esc(d.message)}\n${esc(d.specUrl)}`;
795
+ if (d.severity === "error") lines.push(` <failure message="${esc(d.message)}">${body}</failure>`);
796
+ else lines.push(` <skipped message="${esc(`${d.severity}: ${d.message}`)}">${body}</skipped>`);
797
+ lines.push(" </testcase>");
798
+ }
799
+ lines.push(" </testsuite>");
800
+ lines.push("</testsuites>");
801
+ return `${lines.join("\n")}\n`;
802
+ }
803
+ //#endregion
804
+ //#region src/protocol/event-table.ts
805
+ /** Version of @ag-ui/core this table was derived from. */
806
+ const SDK_VERSION = "0.0.58";
807
+ /** Canonical wire event types and their schemas, derived from @ag-ui/core. */
808
+ const EVENT_TABLE = {
809
+ "TEXT_MESSAGE_START": {
810
+ category: "text",
811
+ specUrl: "https://docs.ag-ui.com/concepts/events#textmessagestart",
812
+ fields: {
813
+ "messageId": {
814
+ kind: "string",
815
+ required: true
816
+ },
817
+ "role": {
818
+ kind: "string",
819
+ required: false,
820
+ enum: [
821
+ "developer",
822
+ "system",
823
+ "assistant",
824
+ "user"
825
+ ]
826
+ },
827
+ "name": {
828
+ kind: "string",
829
+ required: false
830
+ }
831
+ }
832
+ },
833
+ "TEXT_MESSAGE_CONTENT": {
834
+ category: "text",
835
+ specUrl: "https://docs.ag-ui.com/concepts/events#textmessagecontent",
836
+ fields: {
837
+ "messageId": {
838
+ kind: "string",
839
+ required: true
840
+ },
841
+ "delta": {
842
+ kind: "string",
843
+ required: true
844
+ }
845
+ }
846
+ },
847
+ "TEXT_MESSAGE_END": {
848
+ category: "text",
849
+ specUrl: "https://docs.ag-ui.com/concepts/events#textmessageend",
850
+ fields: { "messageId": {
851
+ kind: "string",
852
+ required: true
853
+ } }
854
+ },
855
+ "TEXT_MESSAGE_CHUNK": {
856
+ category: "text",
857
+ specUrl: "https://docs.ag-ui.com/concepts/events#textmessagechunk",
858
+ fields: {
859
+ "messageId": {
860
+ kind: "string",
861
+ required: false
862
+ },
863
+ "role": {
864
+ kind: "string",
865
+ required: false,
866
+ enum: [
867
+ "developer",
868
+ "system",
869
+ "assistant",
870
+ "user"
871
+ ]
872
+ },
873
+ "delta": {
874
+ kind: "string",
875
+ required: false
876
+ },
877
+ "name": {
878
+ kind: "string",
879
+ required: false
880
+ }
881
+ }
882
+ },
883
+ "TOOL_CALL_START": {
884
+ category: "toolcall",
885
+ specUrl: "https://docs.ag-ui.com/concepts/events#toolcallstart",
886
+ fields: {
887
+ "toolCallId": {
888
+ kind: "string",
889
+ required: true
890
+ },
891
+ "toolCallName": {
892
+ kind: "string",
893
+ required: true
894
+ },
895
+ "parentMessageId": {
896
+ kind: "string",
897
+ required: false
898
+ }
899
+ }
900
+ },
901
+ "TOOL_CALL_ARGS": {
902
+ category: "toolcall",
903
+ specUrl: "https://docs.ag-ui.com/concepts/events#toolcallargs",
904
+ fields: {
905
+ "toolCallId": {
906
+ kind: "string",
907
+ required: true
908
+ },
909
+ "delta": {
910
+ kind: "string",
911
+ required: true
912
+ }
913
+ }
914
+ },
915
+ "TOOL_CALL_END": {
916
+ category: "toolcall",
917
+ specUrl: "https://docs.ag-ui.com/concepts/events#toolcallend",
918
+ fields: { "toolCallId": {
919
+ kind: "string",
920
+ required: true
921
+ } }
922
+ },
923
+ "TOOL_CALL_CHUNK": {
924
+ category: "toolcall",
925
+ specUrl: "https://docs.ag-ui.com/concepts/events#toolcallchunk",
926
+ fields: {
927
+ "toolCallId": {
928
+ kind: "string",
929
+ required: false
930
+ },
931
+ "toolCallName": {
932
+ kind: "string",
933
+ required: false
934
+ },
935
+ "parentMessageId": {
936
+ kind: "string",
937
+ required: false
938
+ },
939
+ "delta": {
940
+ kind: "string",
941
+ required: false
942
+ }
943
+ }
944
+ },
945
+ "TOOL_CALL_RESULT": {
946
+ category: "toolcall",
947
+ specUrl: "https://docs.ag-ui.com/concepts/events#toolcallresult",
948
+ fields: {
949
+ "messageId": {
950
+ kind: "string",
951
+ required: true
952
+ },
953
+ "toolCallId": {
954
+ kind: "string",
955
+ required: true
956
+ },
957
+ "content": {
958
+ kind: "string",
959
+ required: true
960
+ },
961
+ "role": {
962
+ kind: "string",
963
+ required: false,
964
+ enum: ["tool"]
965
+ }
966
+ }
967
+ },
968
+ "THINKING_START": {
969
+ category: "thinking",
970
+ deprecated: "REASONING_START",
971
+ specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
972
+ fields: { "title": {
973
+ kind: "string",
974
+ required: false
975
+ } }
976
+ },
977
+ "THINKING_END": {
978
+ category: "thinking",
979
+ deprecated: "REASONING_END",
980
+ specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
981
+ fields: {}
982
+ },
983
+ "THINKING_TEXT_MESSAGE_START": {
984
+ category: "thinking",
985
+ deprecated: "REASONING_MESSAGE_START",
986
+ specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
987
+ fields: {}
988
+ },
989
+ "THINKING_TEXT_MESSAGE_CONTENT": {
990
+ category: "thinking",
991
+ deprecated: "REASONING_MESSAGE_CONTENT",
992
+ specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
993
+ fields: { "delta": {
994
+ kind: "string",
995
+ required: true
996
+ } }
997
+ },
998
+ "THINKING_TEXT_MESSAGE_END": {
999
+ category: "thinking",
1000
+ deprecated: "REASONING_MESSAGE_END",
1001
+ specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
1002
+ fields: {}
1003
+ },
1004
+ "STATE_SNAPSHOT": {
1005
+ category: "state",
1006
+ specUrl: "https://docs.ag-ui.com/concepts/events#statesnapshot",
1007
+ fields: { "snapshot": {
1008
+ kind: "any",
1009
+ required: true
1010
+ } }
1011
+ },
1012
+ "STATE_DELTA": {
1013
+ category: "state",
1014
+ specUrl: "https://docs.ag-ui.com/concepts/events#statedelta",
1015
+ fields: { "delta": {
1016
+ kind: "array",
1017
+ required: true
1018
+ } }
1019
+ },
1020
+ "MESSAGES_SNAPSHOT": {
1021
+ category: "state",
1022
+ specUrl: "https://docs.ag-ui.com/concepts/events#messagessnapshot",
1023
+ fields: { "messages": {
1024
+ kind: "array",
1025
+ required: true
1026
+ } }
1027
+ },
1028
+ "ACTIVITY_SNAPSHOT": {
1029
+ category: "activity",
1030
+ specUrl: "https://docs.ag-ui.com/concepts/events#activitysnapshot",
1031
+ fields: {
1032
+ "messageId": {
1033
+ kind: "string",
1034
+ required: true
1035
+ },
1036
+ "activityType": {
1037
+ kind: "string",
1038
+ required: true
1039
+ },
1040
+ "content": {
1041
+ kind: "object",
1042
+ required: true
1043
+ },
1044
+ "replace": {
1045
+ kind: "boolean",
1046
+ required: false
1047
+ }
1048
+ }
1049
+ },
1050
+ "ACTIVITY_DELTA": {
1051
+ category: "activity",
1052
+ specUrl: "https://docs.ag-ui.com/concepts/events#activitydelta",
1053
+ fields: {
1054
+ "messageId": {
1055
+ kind: "string",
1056
+ required: true
1057
+ },
1058
+ "activityType": {
1059
+ kind: "string",
1060
+ required: true
1061
+ },
1062
+ "patch": {
1063
+ kind: "array",
1064
+ required: true
1065
+ }
1066
+ }
1067
+ },
1068
+ "RAW": {
1069
+ category: "special",
1070
+ specUrl: "https://docs.ag-ui.com/concepts/events#raw",
1071
+ fields: {
1072
+ "event": {
1073
+ kind: "any",
1074
+ required: true
1075
+ },
1076
+ "source": {
1077
+ kind: "string",
1078
+ required: false
1079
+ }
1080
+ }
1081
+ },
1082
+ "CUSTOM": {
1083
+ category: "special",
1084
+ specUrl: "https://docs.ag-ui.com/concepts/events#custom",
1085
+ fields: {
1086
+ "name": {
1087
+ kind: "string",
1088
+ required: true
1089
+ },
1090
+ "value": {
1091
+ kind: "any",
1092
+ required: true
1093
+ }
1094
+ }
1095
+ },
1096
+ "RUN_STARTED": {
1097
+ category: "lifecycle",
1098
+ specUrl: "https://docs.ag-ui.com/concepts/events#runstarted",
1099
+ fields: {
1100
+ "threadId": {
1101
+ kind: "string",
1102
+ required: true
1103
+ },
1104
+ "runId": {
1105
+ kind: "string",
1106
+ required: true
1107
+ },
1108
+ "parentRunId": {
1109
+ kind: "string",
1110
+ required: false
1111
+ },
1112
+ "input": {
1113
+ kind: "object",
1114
+ required: false
1115
+ }
1116
+ }
1117
+ },
1118
+ "RUN_FINISHED": {
1119
+ category: "lifecycle",
1120
+ specUrl: "https://docs.ag-ui.com/concepts/events#runfinished",
1121
+ fields: {
1122
+ "threadId": {
1123
+ kind: "string",
1124
+ required: true
1125
+ },
1126
+ "runId": {
1127
+ kind: "string",
1128
+ required: true
1129
+ },
1130
+ "result": {
1131
+ kind: "any",
1132
+ required: false
1133
+ },
1134
+ "outcome": {
1135
+ kind: "object",
1136
+ required: false
1137
+ },
1138
+ "usage": {
1139
+ kind: "array",
1140
+ required: false
1141
+ }
1142
+ }
1143
+ },
1144
+ "RUN_ERROR": {
1145
+ category: "lifecycle",
1146
+ specUrl: "https://docs.ag-ui.com/concepts/events#runerror",
1147
+ fields: {
1148
+ "message": {
1149
+ kind: "string",
1150
+ required: true
1151
+ },
1152
+ "code": {
1153
+ kind: "string",
1154
+ required: false
1155
+ },
1156
+ "usage": {
1157
+ kind: "array",
1158
+ required: false
1159
+ }
1160
+ }
1161
+ },
1162
+ "STEP_STARTED": {
1163
+ category: "lifecycle",
1164
+ specUrl: "https://docs.ag-ui.com/concepts/events#stepstarted",
1165
+ fields: { "stepName": {
1166
+ kind: "string",
1167
+ required: true
1168
+ } }
1169
+ },
1170
+ "STEP_FINISHED": {
1171
+ category: "lifecycle",
1172
+ specUrl: "https://docs.ag-ui.com/concepts/events#stepfinished",
1173
+ fields: { "stepName": {
1174
+ kind: "string",
1175
+ required: true
1176
+ } }
1177
+ },
1178
+ "REASONING_START": {
1179
+ category: "reasoning",
1180
+ specUrl: "https://docs.ag-ui.com/concepts/events#reasoningstart",
1181
+ fields: { "messageId": {
1182
+ kind: "string",
1183
+ required: true
1184
+ } }
1185
+ },
1186
+ "REASONING_MESSAGE_START": {
1187
+ category: "reasoning",
1188
+ specUrl: "https://docs.ag-ui.com/concepts/events#reasoningmessagestart",
1189
+ fields: {
1190
+ "messageId": {
1191
+ kind: "string",
1192
+ required: true
1193
+ },
1194
+ "role": {
1195
+ kind: "string",
1196
+ required: true,
1197
+ enum: ["reasoning"]
1198
+ }
1199
+ }
1200
+ },
1201
+ "REASONING_MESSAGE_CONTENT": {
1202
+ category: "reasoning",
1203
+ specUrl: "https://docs.ag-ui.com/concepts/events#reasoningmessagecontent",
1204
+ fields: {
1205
+ "messageId": {
1206
+ kind: "string",
1207
+ required: true
1208
+ },
1209
+ "delta": {
1210
+ kind: "string",
1211
+ required: true
1212
+ }
1213
+ }
1214
+ },
1215
+ "REASONING_MESSAGE_END": {
1216
+ category: "reasoning",
1217
+ specUrl: "https://docs.ag-ui.com/concepts/events#reasoningmessageend",
1218
+ fields: { "messageId": {
1219
+ kind: "string",
1220
+ required: true
1221
+ } }
1222
+ },
1223
+ "REASONING_MESSAGE_CHUNK": {
1224
+ category: "reasoning",
1225
+ specUrl: "https://docs.ag-ui.com/concepts/events#reasoningmessagechunk",
1226
+ fields: {
1227
+ "messageId": {
1228
+ kind: "string",
1229
+ required: false
1230
+ },
1231
+ "delta": {
1232
+ kind: "string",
1233
+ required: false
1234
+ }
1235
+ }
1236
+ },
1237
+ "REASONING_END": {
1238
+ category: "reasoning",
1239
+ specUrl: "https://docs.ag-ui.com/concepts/events#reasoningend",
1240
+ fields: { "messageId": {
1241
+ kind: "string",
1242
+ required: true
1243
+ } }
1244
+ },
1245
+ "REASONING_ENCRYPTED_VALUE": {
1246
+ category: "reasoning",
1247
+ specUrl: "https://docs.ag-ui.com/concepts/events#reasoningencryptedvalue",
1248
+ fields: {
1249
+ "subtype": {
1250
+ kind: "string",
1251
+ required: true,
1252
+ enum: ["tool-call", "message"]
1253
+ },
1254
+ "entityId": {
1255
+ kind: "string",
1256
+ required: true
1257
+ },
1258
+ "encryptedValue": {
1259
+ kind: "string",
1260
+ required: true
1261
+ }
1262
+ }
1263
+ }
1264
+ };
1265
+ /** All canonical wire `type` values, in @ag-ui/core enum order. */
1266
+ const EVENT_TYPES = Object.keys(EVENT_TABLE);
1267
+ /**
1268
+ * Wire types documented as drafts (https://docs.ag-ui.com/drafts/overview) but
1269
+ * not yet in @ag-ui/core. Not errors: reported at info severity.
1270
+ */
1271
+ const DRAFT_EVENT_TYPES = ["META"];
1272
+ //#endregion
1273
+ //#region src/rules/checks/context.ts
1274
+ function newRunState(init) {
1275
+ return {
1276
+ ...init,
1277
+ terminal: null,
1278
+ openMessages: /* @__PURE__ */ new Map(),
1279
+ closedMessages: /* @__PURE__ */ new Map(),
1280
+ knownMessageIds: /* @__PURE__ */ new Set(),
1281
+ openToolCalls: /* @__PURE__ */ new Map(),
1282
+ closedToolCalls: /* @__PURE__ */ new Map(),
1283
+ knownToolCallIds: /* @__PURE__ */ new Set(),
1284
+ openSteps: /* @__PURE__ */ new Map(),
1285
+ openReasoningBlocks: /* @__PURE__ */ new Map(),
1286
+ openReasoningMessages: /* @__PURE__ */ new Map(),
1287
+ state: {
1288
+ known: false,
1289
+ value: void 0,
1290
+ deltasSinceSnapshot: 0,
1291
+ snapshotSeen: false,
1292
+ agui301Fired: false
1293
+ },
1294
+ textChunk: null,
1295
+ toolChunk: null,
1296
+ reasoningChunk: null
1297
+ };
1298
+ }
1299
+ /** Reads a field only if it is a string (schema problems already reported). */
1300
+ function str(event, field) {
1301
+ const v = event[field];
1302
+ return typeof v === "string" ? v : void 0;
1303
+ }
1304
+ //#endregion
1305
+ //#region src/rules/checks/lifecycle.ts
1306
+ function handleStepEvent(api) {
1307
+ const { type, event, run, emit, index } = api;
1308
+ const stepName = str(event, "stepName");
1309
+ if (stepName === void 0) return;
1310
+ if (type === "STEP_STARTED") {
1311
+ const open = run.openSteps.get(stepName);
1312
+ if (open !== void 0) open.count += 1;
1313
+ else run.openSteps.set(stepName, {
1314
+ count: 1,
1315
+ firstIndex: index
1316
+ });
1317
+ return;
1318
+ }
1319
+ if (type === "STEP_FINISHED") {
1320
+ const open = run.openSteps.get(stepName);
1321
+ if (open === void 0) {
1322
+ emit("AGUI006", { stepName }, { pointer: "/stepName" });
1323
+ return;
1324
+ }
1325
+ open.count -= 1;
1326
+ if (open.count === 0) run.openSteps.delete(stepName);
1327
+ }
1328
+ }
1329
+ /** AGUI008 — RUN_FINISHED must carry the ids RUN_STARTED established. */
1330
+ function checkRunIdStability(api) {
1331
+ const { event, run, emit } = api;
1332
+ if (run.implicit) return;
1333
+ for (const field of ["threadId", "runId"]) {
1334
+ const actual = str(api.event, field);
1335
+ const expected = field === "threadId" ? run.threadId : run.runId;
1336
+ if (actual !== void 0 && expected !== null && actual !== expected) emit("AGUI008", {
1337
+ field,
1338
+ actual,
1339
+ expected
1340
+ }, {
1341
+ pointer: `/${field}`,
1342
+ relatedEventIndex: run.startIndex
1343
+ });
1344
+ }
1345
+ }
1346
+ /** AGUI007 — open steps when the run reaches a clean end. */
1347
+ function endOfRunSteps(run, emit, atIndex) {
1348
+ for (const [stepName, open] of run.openSteps) emit("AGUI007", { stepName }, {
1349
+ eventIndex: atIndex,
1350
+ relatedEventIndex: open.firstIndex
1351
+ });
1352
+ }
1353
+ //#endregion
1354
+ //#region src/rules/checks/reasoning.ts
1355
+ function handleReasoningEvent(api) {
1356
+ const { type, event, run, emit, index } = api;
1357
+ switch (type) {
1358
+ case "REASONING_START": {
1359
+ const id = str(event, "messageId");
1360
+ if (id !== void 0) run.openReasoningBlocks.set(id, index);
1361
+ return;
1362
+ }
1363
+ case "REASONING_END": {
1364
+ const id = str(event, "messageId");
1365
+ if (id !== void 0) run.openReasoningBlocks.delete(id);
1366
+ return;
1367
+ }
1368
+ case "REASONING_MESSAGE_START": {
1369
+ const id = str(event, "messageId");
1370
+ if (id !== void 0) run.openReasoningMessages.set(id, index);
1371
+ return;
1372
+ }
1373
+ case "REASONING_MESSAGE_CONTENT": {
1374
+ const id = str(event, "messageId");
1375
+ if (id === void 0) return;
1376
+ const openChunk = run.reasoningChunk !== null && run.reasoningChunk.messageId === id;
1377
+ if (!run.openReasoningMessages.has(id) && !openChunk) emit("AGUI401", { messageId: id }, { pointer: "/messageId" });
1378
+ return;
1379
+ }
1380
+ case "REASONING_MESSAGE_END": {
1381
+ const id = str(event, "messageId");
1382
+ if (id !== void 0) run.openReasoningMessages.delete(id);
1383
+ return;
1384
+ }
1385
+ case "REASONING_MESSAGE_CHUNK": {
1386
+ const id = str(event, "messageId");
1387
+ if (id === void 0) {
1388
+ if (run.reasoningChunk === null) {
1389
+ emit("AGUI504", {
1390
+ type,
1391
+ detail: "first REASONING_MESSAGE_CHUNK must include messageId"
1392
+ }, { pointer: "/messageId" });
1393
+ return;
1394
+ }
1395
+ if (event.delta === "") run.reasoningChunk = null;
1396
+ return;
1397
+ }
1398
+ if (run.reasoningChunk !== null && run.reasoningChunk.messageId !== id) run.reasoningChunk = null;
1399
+ if (event.delta === "") {
1400
+ run.reasoningChunk = null;
1401
+ return;
1402
+ }
1403
+ if (run.reasoningChunk === null) run.reasoningChunk = {
1404
+ messageId: id,
1405
+ startIndex: index
1406
+ };
1407
+ return;
1408
+ }
1409
+ }
1410
+ }
1411
+ /** Chunked reasoning also closes on any non-reasoning event (documented). */
1412
+ function closeReasoningChunk(run) {
1413
+ run.reasoningChunk = null;
1414
+ }
1415
+ /** AGUI402 — unterminated reasoning at a clean run end. */
1416
+ function endOfRunReasoning(run, emit, atIndex) {
1417
+ for (const [id, startIndex] of run.openReasoningBlocks) emit("AGUI402", {
1418
+ startType: "REASONING_START",
1419
+ messageId: id
1420
+ }, {
1421
+ eventIndex: atIndex,
1422
+ relatedEventIndex: startIndex
1423
+ });
1424
+ for (const [id, startIndex] of run.openReasoningMessages) emit("AGUI402", {
1425
+ startType: "REASONING_MESSAGE_START",
1426
+ messageId: id
1427
+ }, {
1428
+ eventIndex: atIndex,
1429
+ relatedEventIndex: startIndex
1430
+ });
1431
+ }
1432
+ //#endregion
1433
+ //#region src/protocol/jsonpatch.ts
1434
+ const OPS = [
1435
+ "add",
1436
+ "remove",
1437
+ "replace",
1438
+ "move",
1439
+ "copy",
1440
+ "test"
1441
+ ];
1442
+ /** RFC 6901: "" → whole document; "/a/b" → ["a", "b"]. Returns null when invalid. */
1443
+ function parsePointer(pointer) {
1444
+ if (typeof pointer !== "string") return null;
1445
+ if (pointer === "") return [];
1446
+ if (!pointer.startsWith("/")) return null;
1447
+ return pointer.slice(1).split("/").map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"));
1448
+ }
1449
+ /** Structural validation of a patch document, without applying it. */
1450
+ function validatePatchShape(patch) {
1451
+ if (!Array.isArray(patch)) return {
1452
+ error: "patch document must be an array of operations",
1453
+ opIndex: -1,
1454
+ pointer: ""
1455
+ };
1456
+ for (let i = 0; i < patch.length; i++) {
1457
+ const op = patch[i];
1458
+ if (typeof op !== "object" || op === null || Array.isArray(op)) return {
1459
+ error: `operation ${i} is not an object`,
1460
+ opIndex: i,
1461
+ pointer: `/${i}`
1462
+ };
1463
+ const o = op;
1464
+ if (!OPS.includes(o.op)) return {
1465
+ error: `operation ${i} has invalid op '${String(o.op)}'`,
1466
+ opIndex: i,
1467
+ pointer: `/${i}/op`
1468
+ };
1469
+ if (parsePointer(o.path) === null) return {
1470
+ error: `operation ${i} (${String(o.op)}) has invalid path`,
1471
+ opIndex: i,
1472
+ pointer: `/${i}/path`
1473
+ };
1474
+ if ((o.op === "add" || o.op === "replace" || o.op === "test") && !("value" in o)) return {
1475
+ error: `operation ${i} (${String(o.op)}) is missing value`,
1476
+ opIndex: i,
1477
+ pointer: `/${i}`
1478
+ };
1479
+ if ((o.op === "move" || o.op === "copy") && parsePointer(o.from) === null) return {
1480
+ error: `operation ${i} (${String(o.op)}) is missing or has invalid from`,
1481
+ opIndex: i,
1482
+ pointer: `/${i}`
1483
+ };
1484
+ }
1485
+ return null;
1486
+ }
1487
+ function clone(value) {
1488
+ if (Array.isArray(value)) return value.map(clone);
1489
+ if (typeof value === "object" && value !== null) {
1490
+ const out = {};
1491
+ for (const [k, v] of Object.entries(value)) out[k] = clone(v);
1492
+ return out;
1493
+ }
1494
+ return value;
1495
+ }
1496
+ function deepEqual(a, b) {
1497
+ if (a === b) return true;
1498
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
1499
+ if (typeof a === "object" && a !== null && typeof b === "object" && b !== null && !Array.isArray(a) && !Array.isArray(b)) {
1500
+ const ka = Object.keys(a);
1501
+ const kb = Object.keys(b);
1502
+ return ka.length === kb.length && ka.every((k) => deepEqual(a[k], b[k]));
1503
+ }
1504
+ return false;
1505
+ }
1506
+ function arrayIndex(token, length, allowAppend) {
1507
+ if (allowAppend && token === "-") return length;
1508
+ if (!/^(0|[1-9]\d*)$/.test(token)) return null;
1509
+ const i = Number(token);
1510
+ return i > length ? null : i;
1511
+ }
1512
+ function locate(doc, tokens, forAdd) {
1513
+ if (tokens.length === 0) return {
1514
+ parent: null,
1515
+ key: "",
1516
+ exists: true,
1517
+ value: doc
1518
+ };
1519
+ let current = doc;
1520
+ for (let i = 0; i < tokens.length - 1; i++) {
1521
+ const token = tokens[i];
1522
+ if (Array.isArray(current)) {
1523
+ const idx = arrayIndex(token, current.length, false);
1524
+ if (idx === null || idx >= current.length) return `path segment '/${token}' does not exist`;
1525
+ current = current[idx];
1526
+ } else if (typeof current === "object" && current !== null) {
1527
+ const obj = current;
1528
+ if (!(token in obj)) return `path segment '/${token}' does not exist`;
1529
+ current = obj[token];
1530
+ } else return `path segment '/${token}' is not an object or array`;
1531
+ }
1532
+ const last = tokens[tokens.length - 1];
1533
+ if (Array.isArray(current)) {
1534
+ const idx = arrayIndex(last, current.length, forAdd);
1535
+ if (idx === null) return `'${last}' is not a valid index for an array of length ${current.length}`;
1536
+ return {
1537
+ parent: current,
1538
+ key: idx,
1539
+ exists: idx < current.length,
1540
+ value: current[idx]
1541
+ };
1542
+ }
1543
+ if (typeof current === "object" && current !== null) {
1544
+ const obj = current;
1545
+ return {
1546
+ parent: obj,
1547
+ key: last,
1548
+ exists: last in obj,
1549
+ value: obj[last]
1550
+ };
1551
+ }
1552
+ return `target of '/${last}' is not an object or array`;
1553
+ }
1554
+ /** Applies an RFC 6902 patch. Validates shape first; never throws. */
1555
+ function applyPatch(doc, patch) {
1556
+ const shape = validatePatchShape(patch);
1557
+ if (shape !== null) return {
1558
+ ok: false,
1559
+ error: shape.error,
1560
+ opIndex: shape.opIndex
1561
+ };
1562
+ let result = clone(doc);
1563
+ const ops = patch;
1564
+ const getAt = (pointer) => {
1565
+ const loc = locate(result, parsePointer(pointer), false);
1566
+ if (typeof loc === "string") return {
1567
+ ok: false,
1568
+ error: `${pointer}: ${loc}`
1569
+ };
1570
+ if (!loc.exists) return {
1571
+ ok: false,
1572
+ error: `${pointer} does not exist`
1573
+ };
1574
+ return {
1575
+ ok: true,
1576
+ value: loc.value
1577
+ };
1578
+ };
1579
+ const setAt = (pointer, value, mustExist) => {
1580
+ const tokens = parsePointer(pointer);
1581
+ if (tokens.length === 0) {
1582
+ result = value;
1583
+ return null;
1584
+ }
1585
+ const loc = locate(result, tokens, !mustExist);
1586
+ if (typeof loc === "string") return `${pointer}: ${loc}`;
1587
+ if (mustExist && !loc.exists) return `${pointer} does not exist`;
1588
+ if (Array.isArray(loc.parent)) {
1589
+ if (mustExist) loc.parent[loc.key] = value;
1590
+ else loc.parent.splice(loc.key, 0, value);
1591
+ } else loc.parent[loc.key] = value;
1592
+ return null;
1593
+ };
1594
+ const removeAt = (pointer) => {
1595
+ const tokens = parsePointer(pointer);
1596
+ if (tokens.length === 0) return {
1597
+ ok: false,
1598
+ error: "cannot remove the whole document"
1599
+ };
1600
+ const loc = locate(result, tokens, false);
1601
+ if (typeof loc === "string") return {
1602
+ ok: false,
1603
+ error: `${pointer}: ${loc}`
1604
+ };
1605
+ if (!loc.exists) return {
1606
+ ok: false,
1607
+ error: `${pointer} does not exist`
1608
+ };
1609
+ if (Array.isArray(loc.parent)) loc.parent.splice(loc.key, 1);
1610
+ else delete loc.parent[loc.key];
1611
+ return {
1612
+ ok: true,
1613
+ removed: loc.value
1614
+ };
1615
+ };
1616
+ for (let i = 0; i < ops.length; i++) {
1617
+ const op = ops[i];
1618
+ const path = op.path;
1619
+ let error = null;
1620
+ switch (op.op) {
1621
+ case "add":
1622
+ error = setAt(path, clone(op.value), false);
1623
+ break;
1624
+ case "replace":
1625
+ error = setAt(path, clone(op.value), true);
1626
+ break;
1627
+ case "remove": {
1628
+ const r = removeAt(path);
1629
+ if (!r.ok) error = r.error;
1630
+ break;
1631
+ }
1632
+ case "move": {
1633
+ const from = op.from;
1634
+ if (path.startsWith(`${from}/`)) {
1635
+ error = `cannot move ${from} into its own child ${path}`;
1636
+ break;
1637
+ }
1638
+ const r = removeAt(from);
1639
+ if (!r.ok) {
1640
+ error = r.error;
1641
+ break;
1642
+ }
1643
+ error = setAt(path, r.removed, false);
1644
+ break;
1645
+ }
1646
+ case "copy": {
1647
+ const r = getAt(op.from);
1648
+ if (!r.ok) {
1649
+ error = r.error;
1650
+ break;
1651
+ }
1652
+ error = setAt(path, clone(r.value), false);
1653
+ break;
1654
+ }
1655
+ case "test": {
1656
+ const r = getAt(path);
1657
+ if (!r.ok) error = r.error;
1658
+ else if (!deepEqual(r.value, op.value)) error = `test failed at ${path}`;
1659
+ break;
1660
+ }
1661
+ }
1662
+ if (error !== null) return {
1663
+ ok: false,
1664
+ error,
1665
+ opIndex: i
1666
+ };
1667
+ }
1668
+ return {
1669
+ ok: true,
1670
+ result
1671
+ };
1672
+ }
1673
+ //#endregion
1674
+ //#region src/rules/checks/state.ts
1675
+ function handleStateEvent(api) {
1676
+ const { type, event, run, stream, emit } = api;
1677
+ switch (type) {
1678
+ case "STATE_SNAPSHOT":
1679
+ api.feature("shared-state");
1680
+ if (run.state.deltasSinceSnapshot > 0) emit("AGUI304", { deltaCount: run.state.deltasSinceSnapshot }, {});
1681
+ run.state.known = true;
1682
+ run.state.value = event.snapshot;
1683
+ run.state.snapshotSeen = true;
1684
+ run.state.deltasSinceSnapshot = 0;
1685
+ stream.anySnapshot = true;
1686
+ return;
1687
+ case "STATE_DELTA": {
1688
+ api.feature("shared-state");
1689
+ const delta = event.delta;
1690
+ if (!Array.isArray(delta)) return;
1691
+ const shape = validatePatchShape(delta);
1692
+ if (shape !== null) {
1693
+ emit("AGUI303", { error: shape.error }, { pointer: `/delta${shape.pointer}` });
1694
+ return;
1695
+ }
1696
+ if (!run.state.snapshotSeen && !run.state.agui301Fired) {
1697
+ emit("AGUI301", {}, {});
1698
+ run.state.agui301Fired = true;
1699
+ }
1700
+ if (run.state.known) {
1701
+ const applied = applyPatch(run.state.value, delta);
1702
+ if (applied.ok) run.state.value = applied.result;
1703
+ else emit("AGUI302", { error: applied.error }, { pointer: `/delta/${applied.opIndex}` });
1704
+ }
1705
+ run.state.deltasSinceSnapshot += 1;
1706
+ return;
1707
+ }
1708
+ case "MESSAGES_SNAPSHOT": {
1709
+ const messages = event.messages;
1710
+ if (!Array.isArray(messages)) return;
1711
+ for (const message of messages) {
1712
+ if (typeof message !== "object" || message === null) continue;
1713
+ const m = message;
1714
+ if (typeof m.id === "string") run.knownMessageIds.add(m.id);
1715
+ if (Array.isArray(m.toolCalls)) {
1716
+ for (const call of m.toolCalls) if (typeof call === "object" && call !== null) {
1717
+ const id = call.id;
1718
+ if (typeof id === "string") run.knownToolCallIds.add(id);
1719
+ }
1720
+ }
1721
+ }
1722
+ return;
1723
+ }
1724
+ }
1725
+ }
1726
+ //#endregion
1727
+ //#region src/rules/checks/text.ts
1728
+ function handleTextEvent(api) {
1729
+ const { type, event, run, emit, index } = api;
1730
+ api.feature("agentic-chat");
1731
+ switch (type) {
1732
+ case "TEXT_MESSAGE_START": {
1733
+ const id = str(event, "messageId");
1734
+ if (id === void 0) return;
1735
+ if (run.openMessages.has(id)) {
1736
+ emit("AGUI106", { messageId: id }, {
1737
+ pointer: "/messageId",
1738
+ relatedEventIndex: run.openMessages.get(id).startIndex
1739
+ });
1740
+ return;
1741
+ }
1742
+ if (run.closedMessages.has(id)) {
1743
+ emit("AGUI104", { messageId: id }, {
1744
+ pointer: "/messageId",
1745
+ relatedEventIndex: run.closedMessages.get(id)
1746
+ });
1747
+ return;
1748
+ }
1749
+ run.openMessages.set(id, { startIndex: index });
1750
+ run.knownMessageIds.add(id);
1751
+ return;
1752
+ }
1753
+ case "TEXT_MESSAGE_CONTENT": {
1754
+ const id = str(event, "messageId");
1755
+ if (id === void 0) return;
1756
+ const open = run.openMessages.get(id);
1757
+ if (open === void 0) {
1758
+ const closedAt = run.closedMessages.get(id);
1759
+ emit("AGUI101", { messageId: id }, {
1760
+ pointer: "/messageId",
1761
+ ...closedAt !== void 0 ? { relatedEventIndex: closedAt } : {}
1762
+ });
1763
+ return;
1764
+ }
1765
+ if (event.delta === "") emit("AGUI105", { messageId: id }, {
1766
+ pointer: "/delta",
1767
+ relatedEventIndex: open.startIndex
1768
+ });
1769
+ return;
1770
+ }
1771
+ case "TEXT_MESSAGE_END": {
1772
+ const id = str(event, "messageId");
1773
+ if (id === void 0) return;
1774
+ if (!run.openMessages.has(id)) {
1775
+ emit("AGUI102", { messageId: id }, { pointer: "/messageId" });
1776
+ return;
1777
+ }
1778
+ run.openMessages.delete(id);
1779
+ run.closedMessages.set(id, index);
1780
+ return;
1781
+ }
1782
+ case "TEXT_MESSAGE_CHUNK": {
1783
+ const id = str(event, "messageId");
1784
+ if (id === void 0) {
1785
+ if (run.textChunk === null) emit("AGUI504", {
1786
+ type,
1787
+ detail: "first TEXT_MESSAGE_CHUNK for a message must include messageId"
1788
+ }, { pointer: "/messageId" });
1789
+ return;
1790
+ }
1791
+ if (run.openMessages.has(id)) return;
1792
+ if (run.textChunk !== null && run.textChunk.messageId === id) return;
1793
+ closeTextChunk(run, index);
1794
+ if (run.closedMessages.has(id)) {
1795
+ emit("AGUI104", { messageId: id }, {
1796
+ pointer: "/messageId",
1797
+ relatedEventIndex: run.closedMessages.get(id)
1798
+ });
1799
+ return;
1800
+ }
1801
+ run.textChunk = {
1802
+ messageId: id,
1803
+ startIndex: index
1804
+ };
1805
+ run.knownMessageIds.add(id);
1806
+ return;
1807
+ }
1808
+ }
1809
+ }
1810
+ /** Chunk streams close implicitly on the next non-chunk event. */
1811
+ function closeTextChunk(run, atIndex) {
1812
+ if (run.textChunk === null) return;
1813
+ run.closedMessages.set(run.textChunk.messageId, atIndex);
1814
+ run.textChunk = null;
1815
+ }
1816
+ /** AGUI103 — open messages when the run reaches a clean end. */
1817
+ function endOfRunText(run, emit, atIndex) {
1818
+ for (const [id, open] of run.openMessages) emit("AGUI103", { messageId: id }, {
1819
+ eventIndex: atIndex,
1820
+ relatedEventIndex: open.startIndex
1821
+ });
1822
+ }
1823
+ //#endregion
1824
+ //#region src/rules/checks/toolcalls.ts
1825
+ function handleToolCallEvent(api) {
1826
+ const { type, event, run, emit, index } = api;
1827
+ api.feature("backend-tool-rendering");
1828
+ switch (type) {
1829
+ case "TOOL_CALL_START": {
1830
+ const id = str(event, "toolCallId");
1831
+ if (id === void 0) return;
1832
+ if (run.openToolCalls.has(id) || run.closedToolCalls.has(id)) {
1833
+ const related = run.openToolCalls.get(id)?.startIndex ?? run.closedToolCalls.get(id);
1834
+ emit("AGUI205", { toolCallId: id }, {
1835
+ pointer: "/toolCallId",
1836
+ relatedEventIndex: related
1837
+ });
1838
+ return;
1839
+ }
1840
+ run.openToolCalls.set(id, {
1841
+ startIndex: index,
1842
+ args: "",
1843
+ sawArgs: false
1844
+ });
1845
+ const parent = str(event, "parentMessageId");
1846
+ if (parent !== void 0 && !run.knownMessageIds.has(parent)) emit("AGUI208", { parentMessageId: parent }, { pointer: "/parentMessageId" });
1847
+ return;
1848
+ }
1849
+ case "TOOL_CALL_ARGS": {
1850
+ const id = str(event, "toolCallId");
1851
+ if (id === void 0) return;
1852
+ const open = run.openToolCalls.get(id);
1853
+ if (open === void 0) {
1854
+ emit("AGUI201", { toolCallId: id }, { pointer: "/toolCallId" });
1855
+ return;
1856
+ }
1857
+ const delta = str(event, "delta");
1858
+ if (delta !== void 0) {
1859
+ open.args += delta;
1860
+ open.sawArgs = true;
1861
+ }
1862
+ return;
1863
+ }
1864
+ case "TOOL_CALL_END": {
1865
+ const id = str(event, "toolCallId");
1866
+ if (id === void 0) return;
1867
+ const open = run.openToolCalls.get(id);
1868
+ if (open === void 0) {
1869
+ emit("AGUI202", { toolCallId: id }, { pointer: "/toolCallId" });
1870
+ return;
1871
+ }
1872
+ run.openToolCalls.delete(id);
1873
+ run.closedToolCalls.set(id, index);
1874
+ checkArgsJson(id, open, emit, index);
1875
+ return;
1876
+ }
1877
+ case "TOOL_CALL_RESULT": {
1878
+ const id = str(event, "toolCallId");
1879
+ if (id !== void 0) {
1880
+ const open = run.openToolCalls.get(id);
1881
+ if (open !== void 0) emit("AGUI206", { toolCallId: id }, {
1882
+ pointer: "/toolCallId",
1883
+ relatedEventIndex: open.startIndex
1884
+ });
1885
+ else if (!run.closedToolCalls.has(id) && !run.knownToolCallIds.has(id)) emit("AGUI207", { toolCallId: id }, { pointer: "/toolCallId" });
1886
+ }
1887
+ const messageId = str(event, "messageId");
1888
+ if (messageId !== void 0) run.knownMessageIds.add(messageId);
1889
+ return;
1890
+ }
1891
+ case "TOOL_CALL_CHUNK": {
1892
+ const id = str(event, "toolCallId");
1893
+ const delta = str(event, "delta");
1894
+ if (id === void 0) {
1895
+ if (run.toolChunk === null) {
1896
+ emit("AGUI504", {
1897
+ type,
1898
+ detail: "first TOOL_CALL_CHUNK for a tool call must include toolCallId and toolCallName"
1899
+ }, { pointer: "/toolCallId" });
1900
+ return;
1901
+ }
1902
+ if (delta !== void 0) {
1903
+ run.toolChunk.args += delta;
1904
+ run.toolChunk.sawArgs = true;
1905
+ }
1906
+ return;
1907
+ }
1908
+ const openExplicit = run.openToolCalls.get(id);
1909
+ if (openExplicit !== void 0) {
1910
+ if (delta !== void 0) {
1911
+ openExplicit.args += delta;
1912
+ openExplicit.sawArgs = true;
1913
+ }
1914
+ return;
1915
+ }
1916
+ if (run.toolChunk !== null && run.toolChunk.toolCallId === id) {
1917
+ if (delta !== void 0) {
1918
+ run.toolChunk.args += delta;
1919
+ run.toolChunk.sawArgs = true;
1920
+ }
1921
+ return;
1922
+ }
1923
+ closeToolChunk(run, emit, index);
1924
+ if (run.closedToolCalls.has(id)) {
1925
+ emit("AGUI205", { toolCallId: id }, {
1926
+ pointer: "/toolCallId",
1927
+ relatedEventIndex: run.closedToolCalls.get(id)
1928
+ });
1929
+ return;
1930
+ }
1931
+ if (str(event, "toolCallName") === void 0) emit("AGUI504", {
1932
+ type,
1933
+ detail: "first TOOL_CALL_CHUNK for a tool call must include toolCallName"
1934
+ }, { pointer: "/toolCallName" });
1935
+ run.toolChunk = {
1936
+ toolCallId: id,
1937
+ startIndex: index,
1938
+ args: delta ?? "",
1939
+ sawArgs: delta !== void 0 && delta.length > 0
1940
+ };
1941
+ return;
1942
+ }
1943
+ }
1944
+ }
1945
+ function checkArgsJson(id, call, emit, atIndex) {
1946
+ if (!call.sawArgs || call.args.length === 0) return;
1947
+ try {
1948
+ JSON.parse(call.args);
1949
+ } catch (e) {
1950
+ emit("AGUI204", {
1951
+ toolCallId: id,
1952
+ error: e instanceof Error ? e.message : String(e)
1953
+ }, {
1954
+ eventIndex: atIndex,
1955
+ relatedEventIndex: call.startIndex
1956
+ });
1957
+ }
1958
+ }
1959
+ /** Chunk streams close implicitly on the next non-chunk event. */
1960
+ function closeToolChunk(run, emit, atIndex) {
1961
+ if (run.toolChunk === null) return;
1962
+ const { toolCallId, startIndex, args, sawArgs } = run.toolChunk;
1963
+ run.toolChunk = null;
1964
+ run.closedToolCalls.set(toolCallId, atIndex);
1965
+ checkArgsJson(toolCallId, {
1966
+ startIndex,
1967
+ args,
1968
+ sawArgs
1969
+ }, emit, atIndex);
1970
+ }
1971
+ /** AGUI203 — open tool calls when the run reaches a clean end. */
1972
+ function endOfRunToolCalls(run, emit, atIndex) {
1973
+ for (const [id, open] of run.openToolCalls) emit("AGUI203", { toolCallId: id }, {
1974
+ eventIndex: atIndex,
1975
+ relatedEventIndex: open.startIndex
1976
+ });
1977
+ }
1978
+ //#endregion
1979
+ //#region src/rules/checks/transport.ts
1980
+ const TRANSPORT_SKIP_REASON = "transport-layer rule; only checkable against a live connection (validated by ag-ui-validate/transport)";
1981
+ const TRANSPORT_RULE_IDS = CATALOG.rules.filter((r) => r.checkedIn === "transport").map((r) => r.id);
1982
+ //#endregion
1983
+ //#region src/types.ts
1984
+ const CANONICAL_FEATURES = [
1985
+ "agentic-chat",
1986
+ "backend-tool-rendering",
1987
+ "human-in-the-loop",
1988
+ "agentic-generative-ui",
1989
+ "tool-based-generative-ui",
1990
+ "shared-state",
1991
+ "predictive-state-updates"
1992
+ ];
1993
+ //#endregion
1994
+ //#region src/index.ts
1995
+ const DRAFTS_META_URL = "https://docs.ag-ui.com/drafts/meta-events";
1996
+ const NOT_INFERABLE = ["agentic-generative-ui", "tool-based-generative-ui"];
1997
+ /** "runStarted" → "RUN_STARTED": case-insensitive match against wire types. */
1998
+ const CANONICAL_BY_SQUASHED = new Map(EVENT_TYPES.map((t) => [t.replace(/_/g, "").toLowerCase(), t]));
1999
+ function describeValue(v) {
2000
+ if (v === null) return "null";
2001
+ if (Array.isArray(v)) return "array";
2002
+ return typeof v;
2003
+ }
2004
+ function createValidator(opts = {}) {
2005
+ const overrides = opts.severityOverrides ?? {};
2006
+ const declaredFeatures = new Set(opts.features ?? []);
2007
+ const layers = /* @__PURE__ */ new Set(["core", ...opts.layers ?? []]);
2008
+ const diagnostics = [];
2009
+ const internalErrors = [];
2010
+ const explicitSkips = /* @__PURE__ */ new Map();
2011
+ const stream = {
2012
+ eventCount: 0,
2013
+ sawTimestamp: false,
2014
+ anySnapshot: false,
2015
+ agui001Fired: false,
2016
+ features: /* @__PURE__ */ new Set()
2017
+ };
2018
+ let run = null;
2019
+ let finalized = false;
2020
+ function mkEmit(batch, current) {
2021
+ return (ruleId, params, extra = {}) => {
2022
+ const rule = RULES.get(ruleId);
2023
+ if (rule === void 0) {
2024
+ internalErrors.push(`emit() for unknown rule ${ruleId}`);
2025
+ return;
2026
+ }
2027
+ const override = overrides[ruleId];
2028
+ if (override === "off") return;
2029
+ const severity = override ?? extra.severity ?? rule.severity;
2030
+ const aboutCurrentEvent = extra.eventIndex === void 0 && current !== null;
2031
+ const diag = {
2032
+ rule: ruleId,
2033
+ severity,
2034
+ message: formatMessage(rule, params) + (extra.messageSuffix ?? ""),
2035
+ eventIndex: extra.eventIndex ?? current?.index ?? -1,
2036
+ specUrl: extra.specUrl ?? rule.specUrl
2037
+ };
2038
+ if (aboutCurrentEvent) diag.eventType = current.type;
2039
+ if (extra.pointer !== void 0) diag.pointer = extra.pointer;
2040
+ if (extra.relatedEventIndex !== void 0) diag.relatedEventIndex = extra.relatedEventIndex;
2041
+ diagnostics.push(diag);
2042
+ batch.push(diag);
2043
+ };
2044
+ }
2045
+ function validateSchema(type, spec, ev, emit) {
2046
+ for (const [field, fs] of Object.entries(spec.fields)) {
2047
+ const v = ev[field];
2048
+ if (v === void 0) {
2049
+ if (fs.required) emit("AGUI504", {
2050
+ type,
2051
+ detail: `missing required field '${field}' (${fs.kind})`
2052
+ }, { pointer: `/${field}` });
2053
+ continue;
2054
+ }
2055
+ let kindOk = true;
2056
+ switch (fs.kind) {
2057
+ case "string":
2058
+ case "number":
2059
+ case "boolean":
2060
+ kindOk = typeof v === fs.kind;
2061
+ break;
2062
+ case "array":
2063
+ kindOk = Array.isArray(v);
2064
+ break;
2065
+ case "object": kindOk = typeof v === "object" && v !== null && !Array.isArray(v);
2066
+ }
2067
+ if (!kindOk) {
2068
+ emit("AGUI504", {
2069
+ type,
2070
+ detail: `field '${field}' must be ${fs.kind === "array" ? "an" : "a"} ${fs.kind}, got ${describeValue(v)}`
2071
+ }, { pointer: `/${field}` });
2072
+ continue;
2073
+ }
2074
+ if (fs.enum !== void 0 && typeof v === "string" && !fs.enum.includes(v)) emit("AGUI504", {
2075
+ type,
2076
+ detail: `field '${field}' must be one of ${fs.enum.join("|")}, got '${v}'`
2077
+ }, { pointer: `/${field}` });
2078
+ }
2079
+ }
2080
+ /** Chunk streams close implicitly on any event of a different type. */
2081
+ function closeChunks(r, emit, atIndex, except) {
2082
+ if (except !== "TEXT_MESSAGE_CHUNK") closeTextChunk(r, atIndex);
2083
+ if (except !== "TOOL_CALL_CHUNK") closeToolChunk(r, emit, atIndex);
2084
+ if (except !== "REASONING_MESSAGE_CHUNK") closeReasoningChunk(r);
2085
+ }
2086
+ /** Unterminated-at-run-end rules. Only on a *clean* end (RUN_FINISHED or a
2087
+ * stream that just stops): after RUN_ERROR, open streams are expected
2088
+ * debris of the failure, and flagging them would manufacture noise. */
2089
+ function endOfRunChecks(r, emit, atIndex) {
2090
+ endOfRunText(r, emit, atIndex);
2091
+ endOfRunToolCalls(r, emit, atIndex);
2092
+ endOfRunSteps(r, emit, atIndex);
2093
+ endOfRunReasoning(r, emit, atIndex);
2094
+ }
2095
+ /** Opens the implicit run scope for streams that never announced one. */
2096
+ function ensureRun(index, type, emit) {
2097
+ if (run === null) {
2098
+ if (!stream.agui001Fired) {
2099
+ emit("AGUI001", { type }, {});
2100
+ stream.agui001Fired = true;
2101
+ }
2102
+ run = newRunState({
2103
+ runId: null,
2104
+ threadId: null,
2105
+ startIndex: index,
2106
+ implicit: true
2107
+ });
2108
+ }
2109
+ return run;
2110
+ }
2111
+ function processEvent(input, batch) {
2112
+ const index = stream.eventCount;
2113
+ stream.eventCount += 1;
2114
+ let parsed = input;
2115
+ if (typeof input === "string") try {
2116
+ parsed = JSON.parse(input);
2117
+ } catch (e) {
2118
+ mkEmit(batch, {
2119
+ index,
2120
+ type: ""
2121
+ })("AGUI502", { error: e instanceof Error ? e.message : String(e) });
2122
+ return;
2123
+ }
2124
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2125
+ mkEmit(batch, {
2126
+ index,
2127
+ type: ""
2128
+ })("AGUI502", { error: `payload is ${describeValue(parsed)}, expected a JSON object` });
2129
+ return;
2130
+ }
2131
+ const ev = parsed;
2132
+ if (typeof ev.type !== "string") {
2133
+ mkEmit(batch, {
2134
+ index,
2135
+ type: ""
2136
+ })("AGUI504", {
2137
+ type: "(untyped)",
2138
+ detail: "event has no string 'type' property"
2139
+ }, { pointer: "/type" });
2140
+ return;
2141
+ }
2142
+ const type = ev.type;
2143
+ const emit = mkEmit(batch, {
2144
+ index,
2145
+ type
2146
+ });
2147
+ if ("timestamp" in ev && ev.timestamp !== void 0) {
2148
+ if (typeof ev.timestamp === "number") stream.sawTimestamp = true;
2149
+ else emit("AGUI504", {
2150
+ type,
2151
+ detail: `field 'timestamp' must be a number, got ${describeValue(ev.timestamp)}`
2152
+ }, { pointer: "/timestamp" });
2153
+ }
2154
+ const spec = EVENT_TABLE[type];
2155
+ if (spec === void 0) {
2156
+ if (DRAFT_EVENT_TYPES.includes(type)) emit("AGUI503", {
2157
+ type,
2158
+ sdkVersion: SDK_VERSION
2159
+ }, {
2160
+ severity: "info",
2161
+ specUrl: DRAFTS_META_URL,
2162
+ messageSuffix: " — documented draft event type, not yet in @ag-ui/core"
2163
+ });
2164
+ else {
2165
+ const canonical = CANONICAL_BY_SQUASHED.get(type.replace(/[_\-\s]/g, "").toLowerCase());
2166
+ emit("AGUI503", {
2167
+ type,
2168
+ sdkVersion: SDK_VERSION
2169
+ }, { ...canonical !== void 0 ? { messageSuffix: ` — did you mean '${canonical}'? AG-UI wire types use SCREAMING_SNAKE_CASE` } : {} });
2170
+ }
2171
+ return;
2172
+ }
2173
+ validateSchema(type, spec, ev, emit);
2174
+ if (type === "RUN_STARTED") {
2175
+ if (run === null) run = newRunState({
2176
+ runId: str(ev, "runId") ?? null,
2177
+ threadId: str(ev, "threadId") ?? null,
2178
+ startIndex: index,
2179
+ implicit: false
2180
+ });
2181
+ else if (run.terminal !== null) run = newRunState({
2182
+ runId: str(ev, "runId") ?? null,
2183
+ threadId: str(ev, "threadId") ?? null,
2184
+ startIndex: index,
2185
+ implicit: false
2186
+ });
2187
+ else if (run.implicit) {
2188
+ closeChunks(run, emit, index);
2189
+ run.implicit = false;
2190
+ run.runId = str(ev, "runId") ?? null;
2191
+ run.threadId = str(ev, "threadId") ?? null;
2192
+ } else {
2193
+ closeChunks(run, emit, index);
2194
+ emit("AGUI002", { runId: str(ev, "runId") ?? "(missing)" }, { relatedEventIndex: run.startIndex });
2195
+ }
2196
+ return;
2197
+ }
2198
+ const r = ensureRun(index, type, emit);
2199
+ if (r.terminal !== null) {
2200
+ const terminalType = r.terminal.type;
2201
+ if ((type === "RUN_FINISHED" || type === "RUN_ERROR") && type !== terminalType) emit("AGUI005", {
2202
+ type,
2203
+ terminalType
2204
+ }, { relatedEventIndex: r.terminal.index });
2205
+ else emit("AGUI004", {
2206
+ type,
2207
+ terminalType
2208
+ }, { relatedEventIndex: r.terminal.index });
2209
+ return;
2210
+ }
2211
+ closeChunks(r, emit, index, type);
2212
+ if (type === "RUN_FINISHED" || type === "RUN_ERROR") {
2213
+ if (type === "RUN_FINISHED") {
2214
+ checkRunIdStability(api(index, type, ev, r, emit));
2215
+ endOfRunChecks(r, emit, index);
2216
+ const outcome = ev.outcome;
2217
+ if (typeof outcome === "object" && outcome !== null && outcome.type === "interrupt") stream.features.add("human-in-the-loop");
2218
+ }
2219
+ r.terminal = {
2220
+ type,
2221
+ index
2222
+ };
2223
+ return;
2224
+ }
2225
+ const a = api(index, type, ev, r, emit);
2226
+ switch (spec.category) {
2227
+ case "lifecycle":
2228
+ handleStepEvent(a);
2229
+ break;
2230
+ case "text":
2231
+ handleTextEvent(a);
2232
+ break;
2233
+ case "toolcall":
2234
+ handleToolCallEvent(a);
2235
+ break;
2236
+ case "state":
2237
+ handleStateEvent(a);
2238
+ break;
2239
+ case "reasoning":
2240
+ handleReasoningEvent(a);
2241
+ break;
2242
+ case "thinking": break;
2243
+ case "activity": break;
2244
+ case "special": if (type === "RAW") {
2245
+ const wrapped = ev.event;
2246
+ if (typeof wrapped === "object" && wrapped !== null) {
2247
+ const wrappedType = wrapped.type;
2248
+ if (typeof wrappedType === "string" && EVENT_TABLE[wrappedType] !== void 0) emit("AGUI901", { wrappedType }, { pointer: "/event/type" });
2249
+ }
2250
+ } else if (type === "CUSTOM") {
2251
+ const name = str(ev, "name");
2252
+ if (name !== void 0) {
2253
+ if (name === "PredictState") stream.features.add("predictive-state-updates");
2254
+ if (!/[.:/]/.test(name)) emit("AGUI903", { name }, { pointer: "/name" });
2255
+ }
2256
+ }
2257
+ }
2258
+ }
2259
+ function api(index, type, event, r, emit) {
2260
+ return {
2261
+ index,
2262
+ type,
2263
+ event,
2264
+ run: r,
2265
+ stream,
2266
+ emit,
2267
+ feature: (f) => stream.features.add(f)
2268
+ };
2269
+ }
2270
+ return {
2271
+ feed(event) {
2272
+ const batch = [];
2273
+ try {
2274
+ processEvent(event, batch);
2275
+ } catch (e) {
2276
+ internalErrors.push(`feed(event ${stream.eventCount - 1}): ${e instanceof Error ? e.stack ?? e.message : String(e)}`);
2277
+ }
2278
+ return batch;
2279
+ },
2280
+ finalize() {
2281
+ if (finalized) return [];
2282
+ finalized = true;
2283
+ const batch = [];
2284
+ const emit = mkEmit(batch, null);
2285
+ try {
2286
+ if (run !== null && run.terminal === null) {
2287
+ closeChunks(run, emit, -1);
2288
+ emit("AGUI003", { runId: run.runId ?? "(unknown)" }, {
2289
+ eventIndex: -1,
2290
+ relatedEventIndex: run.startIndex
2291
+ });
2292
+ endOfRunChecks(run, emit, -1);
2293
+ }
2294
+ if (stream.eventCount > 0 && !stream.sawTimestamp) emit("AGUI902", { eventCount: stream.eventCount }, { eventIndex: -1 });
2295
+ if (declaredFeatures.has("shared-state") && !stream.anySnapshot) emit("AGUI305", {}, { eventIndex: -1 });
2296
+ } catch (e) {
2297
+ internalErrors.push(`finalize(): ${e instanceof Error ? e.stack ?? e.message : String(e)}`);
2298
+ }
2299
+ return batch;
2300
+ },
2301
+ emitExternal(rule, params = {}, extra = {}) {
2302
+ const batch = [];
2303
+ try {
2304
+ mkEmit(batch, null)(rule, params, extra);
2305
+ } catch (e) {
2306
+ internalErrors.push(`emitExternal(${rule}): ${e instanceof Error ? e.message : String(e)}`);
2307
+ }
2308
+ return batch[0] ?? null;
2309
+ },
2310
+ markSkipped(rule, reason) {
2311
+ explicitSkips.set(String(rule), String(reason));
2312
+ },
2313
+ report() {
2314
+ const summary = {
2315
+ errors: 0,
2316
+ warnings: 0,
2317
+ info: 0
2318
+ };
2319
+ for (const d of diagnostics) if (d.severity === "error") summary.errors += 1;
2320
+ else if (d.severity === "warning") summary.warnings += 1;
2321
+ else summary.info += 1;
2322
+ const features = {};
2323
+ for (const f of CANONICAL_FEATURES) features[f] = NOT_INFERABLE.includes(f) ? "not-inferable" : stream.features.has(f) ? "exercised" : "not-exercised";
2324
+ let skipped = layers.has("transport") ? [] : TRANSPORT_RULE_IDS.filter((id) => overrides[id] !== "off").map((rule) => ({
2325
+ rule,
2326
+ reason: TRANSPORT_SKIP_REASON
2327
+ }));
2328
+ if (!declaredFeatures.has("shared-state") && overrides.AGUI305 !== "off") skipped.push({
2329
+ rule: "AGUI305",
2330
+ reason: "only evaluated when the 'shared-state' feature is declared via options.features"
2331
+ });
2332
+ for (const [rule, severity] of Object.entries(overrides)) if (severity === "off" && RULES.has(rule)) skipped.push({
2333
+ rule,
2334
+ reason: "disabled by severityOverrides"
2335
+ });
2336
+ if (explicitSkips.size > 0) {
2337
+ skipped = skipped.filter((s) => !explicitSkips.has(s.rule));
2338
+ for (const [rule, reason] of explicitSkips) skipped.push({
2339
+ rule,
2340
+ reason
2341
+ });
2342
+ }
2343
+ return {
2344
+ diagnostics: [...diagnostics],
2345
+ summary,
2346
+ features,
2347
+ skipped,
2348
+ eventCount: stream.eventCount,
2349
+ internalErrors: [...internalErrors]
2350
+ };
2351
+ }
2352
+ };
2353
+ }
2354
+ //#endregion
2355
+ //#region src/transport/ndjson.ts
2356
+ async function* ndjsonLines(source) {
2357
+ const decoder = new TextDecoder("utf-8");
2358
+ let buffer = "";
2359
+ function clean(line) {
2360
+ const trimmed = line.endsWith("\r") ? line.slice(0, -1) : line;
2361
+ return trimmed.trim() === "" ? null : trimmed;
2362
+ }
2363
+ for await (const chunk of source) {
2364
+ buffer += decoder.decode(chunk, { stream: true });
2365
+ let i;
2366
+ while ((i = buffer.indexOf("\n")) !== -1) {
2367
+ const line = clean(buffer.slice(0, i));
2368
+ buffer = buffer.slice(i + 1);
2369
+ if (line !== null) yield line;
2370
+ }
2371
+ }
2372
+ buffer += decoder.decode();
2373
+ const last = clean(buffer);
2374
+ if (last !== null) yield last;
2375
+ }
2376
+ //#endregion
2377
+ //#region src/transport/sse.ts
2378
+ const truncate = (s, n) => s.length > n ? `${s.slice(0, n)}…` : s;
2379
+ async function* sseItems(source) {
2380
+ const decoder = new TextDecoder("utf-8");
2381
+ let buffer = "";
2382
+ let sawFirstChars = false;
2383
+ let dataLines = [];
2384
+ let eventName = "";
2385
+ let lastId;
2386
+ function handleLine(line) {
2387
+ if (line === "") {
2388
+ const data = dataLines.join("\n");
2389
+ const hadData = dataLines.length > 0;
2390
+ dataLines = [];
2391
+ const name = eventName;
2392
+ eventName = "";
2393
+ if (!hadData || data === "") return null;
2394
+ const item = {
2395
+ kind: "event",
2396
+ data
2397
+ };
2398
+ if (name !== "") item.event = name;
2399
+ if (lastId !== void 0) item.id = lastId;
2400
+ return item;
2401
+ }
2402
+ if (line.startsWith(":")) return {
2403
+ kind: "comment",
2404
+ text: line.slice(1)
2405
+ };
2406
+ const colon = line.indexOf(":");
2407
+ const field = colon === -1 ? line : line.slice(0, colon);
2408
+ let value = colon === -1 ? "" : line.slice(colon + 1);
2409
+ if (value.startsWith(" ")) value = value.slice(1);
2410
+ switch (field) {
2411
+ case "data":
2412
+ dataLines.push(value);
2413
+ return null;
2414
+ case "event":
2415
+ eventName = value;
2416
+ return null;
2417
+ case "id":
2418
+ if (!value.includes("\0")) lastId = value;
2419
+ return null;
2420
+ case "retry": return null;
2421
+ default: {
2422
+ const trimmed = line.trimStart();
2423
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return {
2424
+ kind: "problem",
2425
+ code: "json-line-without-data-prefix",
2426
+ detail: `line '${truncate(trimmed, 60)}' looks like a JSON payload but lacks the 'data:' field prefix, so SSE clients silently drop it`
2427
+ };
2428
+ return null;
2429
+ }
2430
+ }
2431
+ }
2432
+ function* drainLines(eof) {
2433
+ for (;;) {
2434
+ const nl = buffer.indexOf("\n");
2435
+ const cr = buffer.indexOf("\r");
2436
+ let end;
2437
+ let next;
2438
+ if (cr !== -1 && (nl === -1 || cr < nl)) {
2439
+ if (cr === buffer.length - 1 && !eof) return;
2440
+ end = cr;
2441
+ next = buffer[cr + 1] === "\n" ? cr + 2 : cr + 1;
2442
+ } else if (nl !== -1) {
2443
+ end = nl;
2444
+ next = nl + 1;
2445
+ } else return;
2446
+ const line = buffer.slice(0, end);
2447
+ buffer = buffer.slice(next);
2448
+ const item = handleLine(line);
2449
+ if (item !== null) yield item;
2450
+ }
2451
+ }
2452
+ for await (const chunk of source) {
2453
+ buffer += decoder.decode(chunk, { stream: true });
2454
+ if (!sawFirstChars && buffer.length > 0) {
2455
+ if (buffer.startsWith("")) buffer = buffer.slice(1);
2456
+ sawFirstChars = true;
2457
+ }
2458
+ yield* drainLines(false);
2459
+ }
2460
+ buffer += decoder.decode();
2461
+ yield* drainLines(true);
2462
+ if (buffer !== "") yield {
2463
+ kind: "problem",
2464
+ code: "truncated-frame",
2465
+ detail: `stream ended mid-line: '${truncate(buffer, 60)}' (no trailing newline; the frame was never dispatched)`
2466
+ };
2467
+ else if (dataLines.length > 0) yield {
2468
+ kind: "problem",
2469
+ code: "truncated-frame",
2470
+ detail: "stream ended with a pending frame that was never terminated by a blank line"
2471
+ };
2472
+ }
2473
+ //#endregion
2474
+ //#region src/transport/index.ts
2475
+ const DEFAULT_KEEPALIVE_MS = 3e4;
2476
+ /** Operational failure (unreachable endpoint, non-2xx, no body) — distinct
2477
+ * from conformance findings, which are diagnostics. */
2478
+ var TransportError = class extends Error {
2479
+ status;
2480
+ constructor(message, status) {
2481
+ super(message);
2482
+ this.name = "TransportError";
2483
+ if (status !== void 0) this.status = status;
2484
+ }
2485
+ };
2486
+ function randomId(prefix) {
2487
+ const c = globalThis.crypto;
2488
+ return `${prefix}_${c !== void 0 && "randomUUID" in c ? c.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 10)}`;
2489
+ }
2490
+ /** The minimal valid RunAgentInput POSTed when none is supplied. */
2491
+ function defaultRunAgentInput() {
2492
+ return {
2493
+ threadId: randomId("thread"),
2494
+ runId: randomId("run"),
2495
+ state: {},
2496
+ messages: [{
2497
+ id: randomId("msg"),
2498
+ role: "user",
2499
+ content: "Hello! Please respond briefly."
2500
+ }],
2501
+ tools: [],
2502
+ context: [],
2503
+ forwardedProps: {}
2504
+ };
2505
+ }
2506
+ async function* iterateBody(body) {
2507
+ if (Symbol.asyncIterator in body) {
2508
+ yield* body;
2509
+ return;
2510
+ }
2511
+ const reader = body.getReader();
2512
+ try {
2513
+ for (;;) {
2514
+ const { done, value } = await reader.read();
2515
+ if (done) return;
2516
+ if (value !== void 0) yield value;
2517
+ }
2518
+ } finally {
2519
+ reader.releaseLock();
2520
+ }
2521
+ }
2522
+ /** Buffers up to the first line to guess SSE vs NDJSON, then replays. */
2523
+ async function sniffFormat(source) {
2524
+ const held = [];
2525
+ const decoder = new TextDecoder();
2526
+ let text = "";
2527
+ while (!text.includes("\n") && text.length < 4096) {
2528
+ const { done, value } = await source.next();
2529
+ if (done) break;
2530
+ held.push(value);
2531
+ text += decoder.decode(value, { stream: true });
2532
+ }
2533
+ const firstLine = (text.split("\n")[0] ?? "").trim();
2534
+ const format = firstLine.startsWith("{") || firstLine.startsWith("[") ? "ndjson" : "sse";
2535
+ async function* replay() {
2536
+ yield* held;
2537
+ for (;;) {
2538
+ const { done, value } = await source.next();
2539
+ if (done) return;
2540
+ yield value;
2541
+ }
2542
+ }
2543
+ return {
2544
+ format,
2545
+ replay: replay()
2546
+ };
2547
+ }
2548
+ async function validateBody(body, contentType, opts = {}) {
2549
+ const userLayers = opts.validator?.layers ?? [];
2550
+ const layers = [.../* @__PURE__ */ new Set([
2551
+ ...userLayers,
2552
+ "core",
2553
+ "transport"
2554
+ ])];
2555
+ const v = createValidator({
2556
+ ...opts.validator ?? {},
2557
+ layers
2558
+ });
2559
+ const now = opts.now ?? Date.now;
2560
+ const keepaliveWindow = opts.keepaliveWindowMs ?? DEFAULT_KEEPALIVE_MS;
2561
+ const recorded = opts.recorded === true;
2562
+ const emitTransport = (rule, params, extra) => {
2563
+ const d = v.emitExternal(rule, params, extra);
2564
+ if (d !== null) opts.onDiagnostic?.(d);
2565
+ return d;
2566
+ };
2567
+ let chunkCount = 0;
2568
+ let maxGapMs = 0;
2569
+ let lastArrival = null;
2570
+ async function* tapped() {
2571
+ for await (const chunk of iterateBody(body)) {
2572
+ const t = now();
2573
+ if (lastArrival !== null) maxGapMs = Math.max(maxGapMs, t - lastArrival);
2574
+ lastArrival = t;
2575
+ chunkCount += 1;
2576
+ yield chunk;
2577
+ }
2578
+ }
2579
+ const mime = contentType === null ? null : (contentType.split(";")[0] ?? "").trim().toLowerCase();
2580
+ if (contentType === null) v.markSkipped("AGUI505", "no Content-Type header is available for this input");
2581
+ else if (mime !== "text/event-stream" && mime !== "application/x-ndjson") emitTransport("AGUI505", { contentType: mime === "" ? "(none)" : mime });
2582
+ if (recorded) {
2583
+ v.markSkipped("AGUI506", "keepalive timing is not meaningful for recorded input");
2584
+ v.markSkipped("AGUI507", "chunk arrival timing is not meaningful for recorded input");
2585
+ v.markSkipped("AGUI508", "abnormal disconnects cannot be distinguished from end-of-capture in recorded input");
2586
+ }
2587
+ let format;
2588
+ let stream = tapped();
2589
+ if (mime === "text/event-stream") format = "sse";
2590
+ else if (mime === "application/x-ndjson") format = "ndjson";
2591
+ else ({format, replay: stream} = await sniffFormat(stream));
2592
+ if (format === "ndjson") v.markSkipped("AGUI501", "the stream is NDJSON; there is no SSE framing to check");
2593
+ let eventCount = 0;
2594
+ let runOpen = false;
2595
+ let openRunId = null;
2596
+ const feedRaw = (raw) => {
2597
+ eventCount += 1;
2598
+ const diags = v.feed(raw);
2599
+ try {
2600
+ const parsed = JSON.parse(raw);
2601
+ if (parsed?.type === "RUN_STARTED") {
2602
+ runOpen = true;
2603
+ openRunId = typeof parsed.runId === "string" ? parsed.runId : null;
2604
+ } else if (parsed?.type === "RUN_FINISHED" || parsed?.type === "RUN_ERROR") runOpen = false;
2605
+ } catch {}
2606
+ opts.onEvent?.(raw, diags);
2607
+ if (opts.onDiagnostic !== void 0) for (const d of diags) opts.onDiagnostic(d);
2608
+ };
2609
+ let transportError;
2610
+ try {
2611
+ if (format === "sse") {
2612
+ for await (const item of sseItems(stream)) if (item.kind === "event") feedRaw(item.data);
2613
+ else if (item.kind === "problem") emitTransport("AGUI501", { detail: item.detail });
2614
+ } else for await (const line of ndjsonLines(stream)) feedRaw(line);
2615
+ } catch (e) {
2616
+ if (recorded) throw e instanceof TransportError ? e : new TransportError(`failed to read recorded input: ${e instanceof Error ? e.message : String(e)}`);
2617
+ transportError = e instanceof Error ? e.message : String(e);
2618
+ if (runOpen) emitTransport("AGUI508", { runId: openRunId ?? "(unknown)" });
2619
+ }
2620
+ if (!recorded) {
2621
+ if (lastArrival !== null) maxGapMs = Math.max(maxGapMs, now() - lastArrival);
2622
+ if (maxGapMs > keepaliveWindow) emitTransport("AGUI506", { seconds: Math.round(maxGapMs / 1e3) });
2623
+ if (chunkCount === 1 && eventCount >= 3) emitTransport("AGUI507", { detail: `entire body (${eventCount} events) arrived in a single chunk` });
2624
+ }
2625
+ const finalDiags = v.finalize();
2626
+ if (opts.onDiagnostic !== void 0) for (const d of finalDiags) opts.onDiagnostic(d);
2627
+ const result = {
2628
+ report: v.report(),
2629
+ status: null,
2630
+ contentType,
2631
+ eventCount
2632
+ };
2633
+ if (transportError !== void 0) result.transportError = transportError;
2634
+ return result;
2635
+ }
2636
+ async function validateEndpoint(url, opts = {}) {
2637
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
2638
+ if (fetchImpl === void 0) throw new TransportError("no fetch implementation available; pass fetchImpl");
2639
+ const method = opts.method ?? "POST";
2640
+ const headers = {
2641
+ accept: "text/event-stream, application/x-ndjson",
2642
+ ...method === "POST" ? { "content-type": "application/json" } : {},
2643
+ ...opts.headers ?? {}
2644
+ };
2645
+ const controller = new AbortController();
2646
+ const timers = [];
2647
+ if (opts.timeoutMs !== void 0) timers.push(setTimeout(() => controller.abort(new TransportError(`timed out after ${opts.timeoutMs}ms`)), opts.timeoutMs));
2648
+ opts.signal?.addEventListener("abort", () => controller.abort(opts.signal?.reason), { once: true });
2649
+ try {
2650
+ let res;
2651
+ try {
2652
+ const init = {
2653
+ method,
2654
+ headers,
2655
+ signal: controller.signal
2656
+ };
2657
+ if (method === "POST") init.body = JSON.stringify(opts.input ?? defaultRunAgentInput());
2658
+ res = await fetchImpl(url, init);
2659
+ } catch (e) {
2660
+ throw e instanceof TransportError ? e : new TransportError(`request failed: ${e instanceof Error ? e.message : String(e)}`);
2661
+ }
2662
+ if (res.status < 200 || res.status >= 300) throw new TransportError(`endpoint responded with HTTP ${res.status}`, res.status);
2663
+ if (res.body === null) throw new TransportError("response has no body", res.status);
2664
+ const contentType = res.headers.get("content-type") ?? "";
2665
+ return {
2666
+ ...await validateBody(res.body, contentType, opts),
2667
+ status: res.status
2668
+ };
2669
+ } finally {
2670
+ for (const timer of timers) clearTimeout(timer);
2671
+ }
2672
+ }
2673
+ //#endregion
2674
+ //#region src/cli.ts
2675
+ const TOOL_NAME = "ag-ui-validate";
2676
+ function toolVersion() {
2677
+ try {
2678
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version ?? "0.0.0";
2679
+ } catch {
2680
+ return "0.0.0";
2681
+ }
2682
+ }
2683
+ function transportOptions(config, onDiagnostic) {
2684
+ const validator = {};
2685
+ if (config.features !== void 0) validator.features = config.features;
2686
+ if (Object.keys(config.severityOverrides).length > 0) validator.severityOverrides = config.severityOverrides;
2687
+ const opts = { validator };
2688
+ if (onDiagnostic !== void 0) opts.onDiagnostic = onDiagnostic;
2689
+ return opts;
2690
+ }
2691
+ async function main() {
2692
+ const parsed = parseCliArgs(process.argv.slice(2));
2693
+ if (!parsed.ok) {
2694
+ process.stderr.write(`error: ${parsed.error}\n\n${USAGE}`);
2695
+ return 2;
2696
+ }
2697
+ const config = parsed.config;
2698
+ if (config.help) {
2699
+ process.stdout.write(USAGE);
2700
+ return 0;
2701
+ }
2702
+ const version = toolVersion();
2703
+ if (config.version) {
2704
+ process.stdout.write(`${TOOL_NAME} ${version}\n`);
2705
+ return 0;
2706
+ }
2707
+ const color = config.color ?? (process.stdout.isTTY === true && process.env.NO_COLOR === void 0);
2708
+ const pretty = config.format === "pretty";
2709
+ let printed = 0;
2710
+ const onDiagnostic = pretty ? (d) => {
2711
+ printed += 1;
2712
+ process.stdout.write(`${formatDiagnosticLine(d, { color })}\n`);
2713
+ } : void 0;
2714
+ const isUrl = /^https?:\/\//i.test(config.target);
2715
+ let result;
2716
+ let targetLabel;
2717
+ if (isUrl) {
2718
+ targetLabel = config.target;
2719
+ const opts = transportOptions(config, onDiagnostic);
2720
+ if (Object.keys(config.headers).length > 0) opts.headers = config.headers;
2721
+ if (config.timeoutMs !== void 0) opts.timeoutMs = config.timeoutMs;
2722
+ result = await validateEndpoint(config.target, opts);
2723
+ } else if (config.target === "-") {
2724
+ targetLabel = "stdin";
2725
+ if (process.stdin.isTTY === true) {
2726
+ process.stderr.write("error: stdin is a terminal — pipe a recording in, or pass a file path\n");
2727
+ return 2;
2728
+ }
2729
+ result = await validateBody(process.stdin, null, {
2730
+ ...transportOptions(config, onDiagnostic),
2731
+ recorded: true
2732
+ });
2733
+ } else {
2734
+ targetLabel = config.target;
2735
+ result = await validateBody(createReadStream(config.target), null, {
2736
+ ...transportOptions(config, onDiagnostic),
2737
+ recorded: true
2738
+ });
2739
+ }
2740
+ const report = result.report;
2741
+ const sarifOptions = !isUrl && config.target !== "-" && /\.(jsonl|ndjson)$/i.test(config.target) ? {
2742
+ toolVersion: version,
2743
+ artifactUri: config.target
2744
+ } : { toolVersion: version };
2745
+ if (config.jsonFile !== void 0) {
2746
+ const doc = toJsonReport(report, {
2747
+ tool: {
2748
+ name: TOOL_NAME,
2749
+ version
2750
+ },
2751
+ target: targetLabel
2752
+ });
2753
+ writeFileSync(config.jsonFile, `${JSON.stringify(doc, null, 2)}\n`);
2754
+ }
2755
+ if (config.sarifFile !== void 0) writeFileSync(config.sarifFile, `${JSON.stringify(toSarif(report, sarifOptions), null, 2)}\n`);
2756
+ if (config.junitFile !== void 0) writeFileSync(config.junitFile, toJUnit(report, { name: targetLabel }));
2757
+ if (pretty) {
2758
+ if (printed > 0) process.stdout.write("\n");
2759
+ process.stdout.write(`${formatReportSummary(report, { color })}\n`);
2760
+ } else if (config.format === "json") {
2761
+ const doc = toJsonReport(report, {
2762
+ tool: {
2763
+ name: TOOL_NAME,
2764
+ version
2765
+ },
2766
+ target: targetLabel
2767
+ });
2768
+ process.stdout.write(`${JSON.stringify(doc, null, 2)}\n`);
2769
+ } else if (config.format === "sarif") process.stdout.write(`${JSON.stringify(toSarif(report, sarifOptions), null, 2)}\n`);
2770
+ else process.stdout.write(toJUnit(report, { name: targetLabel }));
2771
+ return decideExitCode(report.summary, config.maxWarnings);
2772
+ }
2773
+ main().then((code) => {
2774
+ process.exitCode = code;
2775
+ }, (e) => {
2776
+ const message = e instanceof TransportError ? e.message : e instanceof Error ? e.stack ?? e.message : String(e);
2777
+ process.stderr.write(`error: ${message}\n`);
2778
+ process.exitCode = 2;
2779
+ });
2780
+ //#endregion
2781
+ export {};
2782
+
2783
+ //# sourceMappingURL=cli.js.map