ag-ui-validate 0.1.0 → 0.3.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 (146) hide show
  1. package/README.md +77 -10
  2. package/dist/{catalog-Ci9dqc1a.cjs → catalog-AB5vuZ8j.cjs} +3 -3
  3. package/dist/catalog-AB5vuZ8j.cjs.map +1 -0
  4. package/dist/{catalog-BglXBNbL.js → catalog-DqTI4VLt.js} +3 -3
  5. package/dist/catalog-DqTI4VLt.js.map +1 -0
  6. package/dist/cli.js +107 -55
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.cjs +2 -2
  9. package/dist/index.js +2 -2
  10. package/dist/report.cjs +43 -1
  11. package/dist/report.cjs.map +1 -1
  12. package/dist/report.d.cts +7 -1
  13. package/dist/report.d.ts +7 -1
  14. package/dist/report.js +43 -2
  15. package/dist/report.js.map +1 -1
  16. package/dist/{src-HmI-kxef.cjs → src-D04i3Vso.cjs} +2 -2
  17. package/dist/src-D04i3Vso.cjs.map +1 -0
  18. package/dist/{src-rGZ2G4qA.js → src-Dcuqz2QC.js} +2 -2
  19. package/dist/src-Dcuqz2QC.js.map +1 -0
  20. package/dist/transport.cjs +1 -1
  21. package/dist/transport.cjs.map +1 -1
  22. package/dist/transport.js +1 -1
  23. package/dist/transport.js.map +1 -1
  24. package/dist/vitest.js +2 -2
  25. package/dist/vitest.js.map +1 -1
  26. package/{src → js/src}/cli-args.ts +11 -0
  27. package/{src → js/src}/cli.ts +8 -3
  28. package/{src → js/src}/report/index.ts +1 -1
  29. package/{src → js/src}/report/pretty.ts +43 -0
  30. package/{src → js/src}/rules/catalog.ts +1 -1
  31. package/{src → js/src}/transport/index.ts +1 -1
  32. package/package.json +14 -13
  33. package/{src/rules → spec}/catalog.json +1 -1
  34. package/spec/event-categories.json +56 -0
  35. package/spec/fixtures/README.md +91 -0
  36. package/spec/fixtures/invalid/AGUI001-first-event-not-run-started/expected.json +10 -0
  37. package/spec/fixtures/invalid/AGUI001-first-event-not-run-started/stream.jsonl +4 -0
  38. package/spec/fixtures/invalid/AGUI002-duplicate-run-started/expected.json +11 -0
  39. package/spec/fixtures/invalid/AGUI002-duplicate-run-started/stream.jsonl +3 -0
  40. package/spec/fixtures/invalid/AGUI003-run-never-terminated/expected.json +10 -0
  41. package/spec/fixtures/invalid/AGUI003-run-never-terminated/stream.jsonl +4 -0
  42. package/spec/fixtures/invalid/AGUI004-event-after-terminal/expected.json +11 -0
  43. package/spec/fixtures/invalid/AGUI004-event-after-terminal/stream.jsonl +3 -0
  44. package/spec/fixtures/invalid/AGUI005-finished-and-error/expected.json +11 -0
  45. package/spec/fixtures/invalid/AGUI005-finished-and-error/stream.jsonl +3 -0
  46. package/spec/fixtures/invalid/AGUI006-step-finished-unmatched/expected.json +11 -0
  47. package/spec/fixtures/invalid/AGUI006-step-finished-unmatched/stream.jsonl +3 -0
  48. package/spec/fixtures/invalid/AGUI007-step-unterminated/expected.json +10 -0
  49. package/spec/fixtures/invalid/AGUI007-step-unterminated/stream.jsonl +3 -0
  50. package/spec/fixtures/invalid/AGUI008-unstable-run-ids/expected.json +12 -0
  51. package/spec/fixtures/invalid/AGUI008-unstable-run-ids/stream.jsonl +2 -0
  52. package/spec/fixtures/invalid/AGUI101-content-without-start/expected.json +11 -0
  53. package/spec/fixtures/invalid/AGUI101-content-without-start/stream.jsonl +3 -0
  54. package/spec/fixtures/invalid/AGUI102-end-without-start/expected.json +11 -0
  55. package/spec/fixtures/invalid/AGUI102-end-without-start/stream.jsonl +3 -0
  56. package/spec/fixtures/invalid/AGUI103-message-unterminated/expected.json +10 -0
  57. package/spec/fixtures/invalid/AGUI103-message-unterminated/stream.jsonl +4 -0
  58. package/spec/fixtures/invalid/AGUI104-duplicate-message-id/expected.json +12 -0
  59. package/spec/fixtures/invalid/AGUI104-duplicate-message-id/stream.jsonl +6 -0
  60. package/spec/fixtures/invalid/AGUI105-empty-content-delta/expected.json +12 -0
  61. package/spec/fixtures/invalid/AGUI105-empty-content-delta/stream.jsonl +5 -0
  62. package/spec/fixtures/invalid/AGUI106-interleaved-same-message-id/expected.json +12 -0
  63. package/spec/fixtures/invalid/AGUI106-interleaved-same-message-id/stream.jsonl +5 -0
  64. package/spec/fixtures/invalid/AGUI201-args-without-start/expected.json +11 -0
  65. package/spec/fixtures/invalid/AGUI201-args-without-start/stream.jsonl +3 -0
  66. package/spec/fixtures/invalid/AGUI202-end-without-start/expected.json +11 -0
  67. package/spec/fixtures/invalid/AGUI202-end-without-start/stream.jsonl +3 -0
  68. package/spec/fixtures/invalid/AGUI203-unterminated-tool-call/expected.json +10 -0
  69. package/spec/fixtures/invalid/AGUI203-unterminated-tool-call/stream.jsonl +4 -0
  70. package/spec/fixtures/invalid/AGUI204-args-not-json/expected.json +10 -0
  71. package/spec/fixtures/invalid/AGUI204-args-not-json/stream.jsonl +6 -0
  72. package/spec/fixtures/invalid/AGUI205-duplicate-tool-call-id/expected.json +12 -0
  73. package/spec/fixtures/invalid/AGUI205-duplicate-tool-call-id/stream.jsonl +5 -0
  74. package/spec/fixtures/invalid/AGUI206-result-before-end/expected.json +12 -0
  75. package/spec/fixtures/invalid/AGUI206-result-before-end/stream.jsonl +5 -0
  76. package/spec/fixtures/invalid/AGUI207-result-unknown-id/expected.json +11 -0
  77. package/spec/fixtures/invalid/AGUI207-result-unknown-id/stream.jsonl +3 -0
  78. package/spec/fixtures/invalid/AGUI208-unknown-parent-message-id/expected.json +11 -0
  79. package/spec/fixtures/invalid/AGUI208-unknown-parent-message-id/stream.jsonl +4 -0
  80. package/spec/fixtures/invalid/AGUI301-delta-before-snapshot/expected.json +10 -0
  81. package/spec/fixtures/invalid/AGUI301-delta-before-snapshot/stream.jsonl +3 -0
  82. package/spec/fixtures/invalid/AGUI302-delta-failed-to-apply/expected.json +11 -0
  83. package/spec/fixtures/invalid/AGUI302-delta-failed-to-apply/stream.jsonl +4 -0
  84. package/spec/fixtures/invalid/AGUI303-invalid-patch-document/expected.json +11 -0
  85. package/spec/fixtures/invalid/AGUI303-invalid-patch-document/stream.jsonl +4 -0
  86. package/spec/fixtures/invalid/AGUI304-midrun-snapshot-discards-deltas/expected.json +10 -0
  87. package/spec/fixtures/invalid/AGUI304-midrun-snapshot-discards-deltas/stream.jsonl +5 -0
  88. package/spec/fixtures/invalid/AGUI305-shared-state-never-established/expected.json +9 -0
  89. package/spec/fixtures/invalid/AGUI305-shared-state-never-established/options.json +5 -0
  90. package/spec/fixtures/invalid/AGUI305-shared-state-never-established/stream.jsonl +5 -0
  91. package/spec/fixtures/invalid/AGUI401-reasoning-content-without-start/expected.json +11 -0
  92. package/spec/fixtures/invalid/AGUI401-reasoning-content-without-start/stream.jsonl +3 -0
  93. package/spec/fixtures/invalid/AGUI402-reasoning-unterminated/expected.json +10 -0
  94. package/spec/fixtures/invalid/AGUI402-reasoning-unterminated/stream.jsonl +3 -0
  95. package/spec/fixtures/invalid/AGUI501-missing-data-prefix/expected.json +9 -0
  96. package/spec/fixtures/invalid/AGUI501-missing-data-prefix/scenario.json +17 -0
  97. package/spec/fixtures/invalid/AGUI502-payload-not-json/expected.json +10 -0
  98. package/spec/fixtures/invalid/AGUI502-payload-not-json/stream.jsonl +3 -0
  99. package/spec/fixtures/invalid/AGUI503-unknown-event-type/expected.json +10 -0
  100. package/spec/fixtures/invalid/AGUI503-unknown-event-type/stream.jsonl +3 -0
  101. package/spec/fixtures/invalid/AGUI504-schema-violation/expected.json +11 -0
  102. package/spec/fixtures/invalid/AGUI504-schema-violation/stream.jsonl +4 -0
  103. package/spec/fixtures/invalid/AGUI505-unexpected-content-type/expected.json +9 -0
  104. package/spec/fixtures/invalid/AGUI505-unexpected-content-type/scenario.json +25 -0
  105. package/spec/fixtures/invalid/AGUI506-keepalive-gap/expected.json +9 -0
  106. package/spec/fixtures/invalid/AGUI506-keepalive-gap/scenario.json +25 -0
  107. package/spec/fixtures/invalid/AGUI507-buffered-response/expected.json +9 -0
  108. package/spec/fixtures/invalid/AGUI507-buffered-response/scenario.json +9 -0
  109. package/spec/fixtures/invalid/AGUI508-connection-dropped/expected.json +17 -0
  110. package/spec/fixtures/invalid/AGUI508-connection-dropped/scenario.json +10 -0
  111. package/spec/fixtures/invalid/AGUI901-raw-wraps-typed-event/expected.json +11 -0
  112. package/spec/fixtures/invalid/AGUI901-raw-wraps-typed-event/stream.jsonl +3 -0
  113. package/spec/fixtures/invalid/AGUI902-no-timestamps/expected.json +9 -0
  114. package/spec/fixtures/invalid/AGUI902-no-timestamps/stream.jsonl +2 -0
  115. package/spec/fixtures/invalid/AGUI903-custom-name-not-namespaced/expected.json +11 -0
  116. package/spec/fixtures/invalid/AGUI903-custom-name-not-namespaced/stream.jsonl +3 -0
  117. package/spec/fixtures/valid/agentic-chat.jsonl +14 -0
  118. package/spec/fixtures/valid/agentic-generative-ui.jsonl +13 -0
  119. package/spec/fixtures/valid/backend-tool-rendering.jsonl +14 -0
  120. package/spec/fixtures/valid/human-in-the-loop.jsonl +13 -0
  121. package/spec/fixtures/valid/predictive-state-updates.expected.json +11 -0
  122. package/spec/fixtures/valid/predictive-state-updates.jsonl +11 -0
  123. package/spec/fixtures/valid/shared-state.jsonl +8 -0
  124. package/spec/fixtures/valid/tool-based-generative-ui.jsonl +5 -0
  125. package/dist/catalog-BglXBNbL.js.map +0 -1
  126. package/dist/catalog-Ci9dqc1a.cjs.map +0 -1
  127. package/dist/src-HmI-kxef.cjs.map +0 -1
  128. package/dist/src-rGZ2G4qA.js.map +0 -1
  129. /package/{src → js/src}/index.ts +0 -0
  130. /package/{src → js/src}/protocol/event-table.ts +0 -0
  131. /package/{src → js/src}/protocol/jsonpatch.ts +0 -0
  132. /package/{src → js/src}/report/json.ts +0 -0
  133. /package/{src → js/src}/report/junit.ts +0 -0
  134. /package/{src → js/src}/report/sarif.ts +0 -0
  135. /package/{src → js/src}/rules/checks/context.ts +0 -0
  136. /package/{src → js/src}/rules/checks/lifecycle.ts +0 -0
  137. /package/{src → js/src}/rules/checks/reasoning.ts +0 -0
  138. /package/{src → js/src}/rules/checks/state.ts +0 -0
  139. /package/{src → js/src}/rules/checks/text.ts +0 -0
  140. /package/{src → js/src}/rules/checks/toolcalls.ts +0 -0
  141. /package/{src → js/src}/rules/checks/transport.ts +0 -0
  142. /package/{src → js/src}/transport/ndjson.ts +0 -0
  143. /package/{src → js/src}/transport/sse.ts +0 -0
  144. /package/{src → js/src}/types.ts +0 -0
  145. /package/{src → js/src}/vitest/index.ts +0 -0
  146. /package/{src → js/src}/vitest/matcher.ts +0 -0
@@ -20,6 +20,8 @@ export interface CliConfig {
20
20
  sarifFile?: string
21
21
  junitFile?: string
22
22
  jsonFile?: string
23
+ /** Pretty output only: one line per rule with a count, not one per finding. */
24
+ group: boolean
23
25
  help: boolean
24
26
  version: boolean
25
27
  }
@@ -40,6 +42,7 @@ Output (default: human-readable):
40
42
  --json-file <path> additionally write the JSON report to a file
41
43
  --sarif-file <path> additionally write a SARIF log to a file
42
44
  --junit-file <path> additionally write JUnit XML to a file
45
+ --group one line per rule with a count (large streams)
43
46
  --no-color disable ANSI colors
44
47
 
45
48
  Rules:
@@ -67,6 +70,7 @@ const FLAGS: Record<string, Flag> = {
67
70
  "--help": { takesValue: false, apply: (c) => ((c.help = true), null) },
68
71
  "--version": { takesValue: false, apply: (c) => ((c.version = true), null) },
69
72
  "--no-color": { takesValue: false, apply: (c) => ((c.color = false), null) },
73
+ "--group": { takesValue: false, apply: (c) => ((c.group = true), null) },
70
74
  "--json": { takesValue: false, apply: (c) => setFormat(c, "json") },
71
75
  "--sarif": { takesValue: false, apply: (c) => setFormat(c, "sarif") },
72
76
  "--junit": { takesValue: false, apply: (c) => setFormat(c, "junit") },
@@ -150,6 +154,7 @@ export function parseCliArgs(argv: string[]): ParseResult {
150
154
  color: null,
151
155
  headers: {},
152
156
  severityOverrides: {},
157
+ group: false,
153
158
  help: false,
154
159
  version: false,
155
160
  }
@@ -182,6 +187,12 @@ export function parseCliArgs(argv: string[]): ParseResult {
182
187
  if (error !== null) return { ok: false, error }
183
188
  }
184
189
 
190
+ if (config.group && config.format !== "pretty") {
191
+ return {
192
+ ok: false,
193
+ error: "--group only applies to the default pretty output, not --json/--sarif/--junit",
194
+ }
195
+ }
185
196
  if (targets.length > 1) {
186
197
  return { ok: false, error: `expected exactly one target, got ${targets.length}` }
187
198
  }
@@ -7,7 +7,7 @@ import { createReadStream, readFileSync, writeFileSync } from "node:fs"
7
7
  import process from "node:process"
8
8
  import { decideExitCode, parseCliArgs, USAGE } from "./cli-args.js"
9
9
  import type { CliConfig } from "./cli-args.js"
10
- import { formatDiagnosticLine, formatReportSummary, toJsonReport, toJUnit, toSarif } from "./report/index.js"
10
+ import { formatDiagnosticLine, formatGroupedDiagnostics, formatReportSummary, toJsonReport, toJUnit, toSarif } from "./report/index.js"
11
11
  import { TransportError, validateBody, validateEndpoint } from "./transport/index.js"
12
12
  import type { TransportOptions, TransportResult } from "./transport/index.js"
13
13
  import type { Diagnostic, ValidatorOptions } from "./types.js"
@@ -62,8 +62,9 @@ async function main(): Promise<number> {
62
62
 
63
63
  // Pretty mode streams findings as they are detected; machine formats emit
64
64
  // one document at the end, so nothing else may touch stdout before it.
65
+ // --group defers everything to one collapsed block after the run.
65
66
  let printed = 0
66
- const onDiagnostic = pretty
67
+ const onDiagnostic = pretty && !config.group
67
68
  ? (d: Diagnostic): void => {
68
69
  printed += 1
69
70
  process.stdout.write(`${formatDiagnosticLine(d, { color })}\n`)
@@ -116,7 +117,11 @@ async function main(): Promise<number> {
116
117
  }
117
118
 
118
119
  if (pretty) {
119
- if (printed > 0) process.stdout.write("\n")
120
+ if (config.group && report.diagnostics.length > 0) {
121
+ process.stdout.write(`${formatGroupedDiagnostics(report.diagnostics, { color })}\n\n`)
122
+ } else if (printed > 0) {
123
+ process.stdout.write("\n")
124
+ }
120
125
  process.stdout.write(`${formatReportSummary(report, { color })}\n`)
121
126
  } else if (config.format === "json") {
122
127
  const doc = toJsonReport(report, { tool: { name: TOOL_NAME, version }, target: targetLabel })
@@ -1,6 +1,6 @@
1
1
  // ag-ui-validate/report: pure reporters over a core Report. No I/O — callers
2
2
  // decide where the strings go.
3
- export { formatDiagnosticLine, formatReportSummary } from "./pretty.js"
3
+ export { formatDiagnosticLine, formatGroupedDiagnostics, formatReportSummary } from "./pretty.js"
4
4
  export type { PrettyOptions } from "./pretty.js"
5
5
  export { toJsonReport } from "./json.js"
6
6
  export type { JsonReportDocument, JsonReportOptions } from "./json.js"
@@ -1,5 +1,6 @@
1
1
  // Human-readable output. Pure string formatting — the CLI decides where it
2
2
  // goes and whether a TTY wants color.
3
+ import { RULES } from "../rules/catalog.js"
3
4
  import type { Diagnostic, Report, Severity } from "../types.js"
4
5
 
5
6
  export interface PrettyOptions {
@@ -25,6 +26,48 @@ function count(n: number, noun: string): string {
25
26
  return `${n} ${noun}${n === 1 ? "" : "s"}`
26
27
  }
27
28
 
29
+ const SEVERITY_RANK: Record<Severity, number> = { error: 0, warning: 1, info: 2 }
30
+ const SAMPLE_INDEXES = 3
31
+
32
+ /**
33
+ * One line per rule instead of one per occurrence — for large streams where
34
+ * the same violation repeats. Totals stay honest in the summary; this only
35
+ * changes what is listed.
36
+ */
37
+ export function formatGroupedDiagnostics(diagnostics: Diagnostic[], opts: PrettyOptions): string {
38
+ const groups = new Map<string, Diagnostic[]>()
39
+ for (const d of diagnostics) {
40
+ const list = groups.get(d.rule)
41
+ if (list === undefined) groups.set(d.rule, [d])
42
+ else list.push(d)
43
+ }
44
+
45
+ const sorted = [...groups.values()].sort((a, b) => {
46
+ const bySeverity = SEVERITY_RANK[a[0]!.severity] - SEVERITY_RANK[b[0]!.severity]
47
+ return bySeverity !== 0 ? bySeverity : a[0]!.rule.localeCompare(b[0]!.rule)
48
+ })
49
+
50
+ const lines: string[] = []
51
+ for (const group of sorted) {
52
+ const first = group[0]!
53
+ const title = RULES.get(first.rule)?.title ?? first.message
54
+ const indexes = group.map((d) => d.eventIndex).filter((i) => i >= 0)
55
+ let where: string
56
+ if (indexes.length === 0) {
57
+ where = "stream-level"
58
+ } else {
59
+ const sample = indexes.slice(0, SAMPLE_INDEXES).join(", ")
60
+ const rest = indexes.length - SAMPLE_INDEXES
61
+ where = `events ${sample}${rest > 0 ? ` (+${rest} more)` : ""}`
62
+ }
63
+ const head = paint(SGR[first.severity], `${SYMBOL[first.severity]} ${first.rule}`, opts.color)
64
+ const meta = paint("2", `${first.severity.padEnd(7)} ×${group.length}`, opts.color)
65
+ const cite = paint("2", ` ↳ ${first.specUrl}`, opts.color)
66
+ lines.push(`${head} ${meta} ${title} — ${where}`, cite)
67
+ }
68
+ return lines.join("\n")
69
+ }
70
+
28
71
  export function formatReportSummary(report: Report, opts: PrettyOptions): string {
29
72
  const { errors, warnings, info } = report.summary
30
73
  const lines: string[] = []
@@ -6,7 +6,7 @@
6
6
  // malformed catalog is a programming error in this package, so the loader
7
7
  // throws loudly at import time.
8
8
 
9
- import catalogJson from "./catalog.json"
9
+ import catalogJson from "../../../spec/catalog.json"
10
10
 
11
11
  export type Severity = "error" | "warning" | "info"
12
12
  export type SeverityOrOff = Severity | "off"
@@ -1,7 +1,7 @@
1
1
  // ag-ui-validate/transport: SSE + NDJSON clients over the pure core.
2
2
  // This is the I/O layer — fetch, streams, and clocks live here, never in the
3
3
  // core. Uses only web-platform APIs (fetch, TextDecoder, AbortController) so
4
- // it stays isomorphic: Node 20+, browsers, Deno, Workers.
4
+ // it stays isomorphic: Node 22+, browsers, Deno, Workers.
5
5
 
6
6
  import { createValidator } from "../index.js"
7
7
  import type { Diagnostic, Report, ValidationLayer, ValidatorOptions } from "../index.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ag-ui-validate",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Conformance validator for the AG-UI protocol. Feed it an event stream, get every violation with a rule ID, a severity, and a link to the governing spec section.",
5
5
  "keywords": [
6
6
  "ag-ui",
@@ -23,10 +23,10 @@
23
23
  "type": "module",
24
24
  "sideEffects": [
25
25
  "./dist/vitest.js",
26
- "./src/vitest/index.ts"
26
+ "./js/src/vitest/index.ts"
27
27
  ],
28
28
  "engines": {
29
- "node": ">=20"
29
+ "node": ">=22"
30
30
  },
31
31
  "bin": {
32
32
  "ag-ui-validate": "./dist/cli.js"
@@ -88,20 +88,21 @@
88
88
  },
89
89
  "files": [
90
90
  "dist",
91
- "src"
91
+ "js/src",
92
+ "spec"
92
93
  ],
93
94
  "scripts": {
94
- "build": "tsdown",
95
- "typecheck": "tsc --noEmit -p tsconfig.src.json && tsc --noEmit",
96
- "test": "vitest run",
97
- "test:watch": "vitest",
98
- "fixtures:build": "npm run build && node scripts/build-fixtures.mjs",
99
- "protocol:derive": "node scripts/generate-event-table.mjs",
95
+ "build": "tsdown --config js/tsdown.config.mjs",
96
+ "typecheck": "tsc --noEmit -p js/tsconfig.src.json && tsc --noEmit -p js/tsconfig.json",
97
+ "test": "vitest run --config js/vitest.config.ts",
98
+ "test:watch": "vitest --config js/vitest.config.ts",
99
+ "fixtures:build": "npm run build && node js/scripts/build-fixtures.mjs",
100
+ "protocol:derive": "node js/scripts/generate-event-table.mjs",
100
101
  "docs:generate": "node scripts/generate-rule-docs.mjs",
101
102
  "docs:check": "node scripts/generate-rule-docs.mjs --check",
102
- "demo": "node scripts/demo.mjs",
103
- "fuzz": "node scripts/fuzz.mjs",
104
- "e2e": "node scripts/e2e-live-server.mjs",
103
+ "demo": "node js/scripts/demo.mjs",
104
+ "fuzz": "node js/scripts/fuzz.mjs",
105
+ "e2e": "node js/scripts/e2e-live-server.mjs",
105
106
  "links:check": "node scripts/check-spec-links.mjs",
106
107
  "changeset": "changeset",
107
108
  "version": "changeset version",
@@ -341,7 +341,7 @@
341
341
  "id": "AGUI503",
342
342
  "severity": "error",
343
343
  "title": "Unknown event type",
344
- "messageTemplate": "Unknown event type '{type}' (not in @ag-ui/core v{sdkVersion}, and not RAW or CUSTOM)",
344
+ "messageTemplate": "Unknown event type '{type}' (not in the installed AG-UI SDK v{sdkVersion}, and not RAW or CUSTOM)",
345
345
  "specUrl": "https://docs.ag-ui.com/concepts/events#event-types-overview",
346
346
  "specQuote": "Events in the protocol are categorized by their purpose.",
347
347
  "since": "0.x",
@@ -0,0 +1,56 @@
1
+ {
2
+ "$comment": "Wire-type -> category/deprecation data for the canonical event table. Hand-maintained (not derivable from the SDK schemas themselves); shared by every language's event-table derivation tool so this classification is written once. Categories are display/grouping metadata only, not conformance rules — those live in catalog.json.",
3
+ "categories": [
4
+ "lifecycle",
5
+ "text",
6
+ "toolcall",
7
+ "state",
8
+ "activity",
9
+ "reasoning",
10
+ "thinking",
11
+ "special"
12
+ ],
13
+ "eventCategory": {
14
+ "RUN_STARTED": "lifecycle",
15
+ "RUN_FINISHED": "lifecycle",
16
+ "RUN_ERROR": "lifecycle",
17
+ "STEP_STARTED": "lifecycle",
18
+ "STEP_FINISHED": "lifecycle",
19
+ "TEXT_MESSAGE_START": "text",
20
+ "TEXT_MESSAGE_CONTENT": "text",
21
+ "TEXT_MESSAGE_END": "text",
22
+ "TEXT_MESSAGE_CHUNK": "text",
23
+ "TOOL_CALL_START": "toolcall",
24
+ "TOOL_CALL_ARGS": "toolcall",
25
+ "TOOL_CALL_END": "toolcall",
26
+ "TOOL_CALL_CHUNK": "toolcall",
27
+ "TOOL_CALL_RESULT": "toolcall",
28
+ "STATE_SNAPSHOT": "state",
29
+ "STATE_DELTA": "state",
30
+ "MESSAGES_SNAPSHOT": "state",
31
+ "ACTIVITY_SNAPSHOT": "activity",
32
+ "ACTIVITY_DELTA": "activity",
33
+ "REASONING_START": "reasoning",
34
+ "REASONING_MESSAGE_START": "reasoning",
35
+ "REASONING_MESSAGE_CONTENT": "reasoning",
36
+ "REASONING_MESSAGE_END": "reasoning",
37
+ "REASONING_MESSAGE_CHUNK": "reasoning",
38
+ "REASONING_END": "reasoning",
39
+ "REASONING_ENCRYPTED_VALUE": "reasoning",
40
+ "THINKING_START": "thinking",
41
+ "THINKING_END": "thinking",
42
+ "THINKING_TEXT_MESSAGE_START": "thinking",
43
+ "THINKING_TEXT_MESSAGE_CONTENT": "thinking",
44
+ "THINKING_TEXT_MESSAGE_END": "thinking",
45
+ "RAW": "special",
46
+ "CUSTOM": "special"
47
+ },
48
+ "$deprecatedComment": "@deprecated markers in @ag-ui/core's type definitions, 'Will be removed in 1.0.0'. Maps a deprecated wire type to its replacement.",
49
+ "deprecated": {
50
+ "THINKING_START": "REASONING_START",
51
+ "THINKING_END": "REASONING_END",
52
+ "THINKING_TEXT_MESSAGE_START": "REASONING_MESSAGE_START",
53
+ "THINKING_TEXT_MESSAGE_CONTENT": "REASONING_MESSAGE_CONTENT",
54
+ "THINKING_TEXT_MESSAGE_END": "REASONING_MESSAGE_END"
55
+ }
56
+ }
@@ -0,0 +1,91 @@
1
+ # AG-UI conformance fixture corpus
2
+
3
+ A language-neutral corpus for testing AG-UI protocol validators. Any
4
+ implementation — TypeScript, Python, Go — that consumes the shared rule
5
+ catalog ([`catalog.json`](../catalog.json)) can be tested
6
+ against these files. The corpus, not per-rule test code, is the suite: adding
7
+ a rule to the catalog means adding a fixture directory here.
8
+
9
+ ## Layout
10
+
11
+ ```
12
+ fixtures/
13
+ ├── valid/ one stream per canonical AG-UI feature
14
+ │ ├── agentic-chat.jsonl
15
+ │ ├── agentic-chat.expected.json (optional; assumed [] when absent)
16
+ │ └── …
17
+ └── invalid/ one directory per rule
18
+ ├── AGUI203-unterminated-tool-call/
19
+ │ ├── stream.jsonl the event stream
20
+ │ ├── expected.json the exact expected Diagnostic[]
21
+ │ └── options.json (optional) validator options for this run
22
+ └── AGUI506-keepalive-gap/ transport rules use a scenario instead
23
+ ├── scenario.json an HTTP response body as timed byte chunks
24
+ └── expected.json
25
+ ```
26
+
27
+ ## Running the corpus against an implementation
28
+
29
+ For every event-stream fixture (`stream.jsonl`):
30
+
31
+ 1. Read the `.jsonl` file and split it into lines; drop empty lines.
32
+ 2. Feed each remaining line to the validator **as a raw string** — do not
33
+ pre-parse. Malformed lines are intentional (they exercise AGUI502); a
34
+ conforming validator treats them as diagnostics, never exceptions.
35
+ 3. If `options.json` exists, construct the validator with those options
36
+ (e.g. `{"features": ["shared-state"]}` for AGUI305).
37
+ 4. After the last line, finalize (end-of-stream checks).
38
+ 5. Deep-compare the accumulated diagnostics with `expected.json`. Order
39
+ matters: diagnostics are emitted in stream order, and end-of-stream
40
+ diagnostics use `eventIndex: -1`.
41
+
42
+ For every transport fixture (`scenario.json`) — these exercise the rules that
43
+ need a live connection (AGUI501, AGUI505–AGUI508):
44
+
45
+ 1. The scenario describes an HTTP response: `contentType` and `chunks`, each
46
+ chunk being `{ "gapMs": n, "text": "…" }` — the raw bytes as they arrived,
47
+ with `gapMs` of simulated silence before the chunk.
48
+ 2. Feed the chunk bytes through your transport layer (SSE or NDJSON per the
49
+ content type, sniffing when unknown) with a simulated clock that advances
50
+ by each `gapMs`; the keepalive window is the default 30 000 ms.
51
+ 3. `"abnormalEof": true` means the connection errored after the last chunk
52
+ (as opposed to closing cleanly) — the AGUI508 condition.
53
+ 4. Finalize and deep-compare diagnostics with `expected.json` as above.
54
+
55
+ Every `invalid/` fixture must fire its directory's namesake rule; every
56
+ `valid/` fixture must produce exactly its `*.expected.json` (usually nothing —
57
+ they are the false-positive guards). `valid/predictive-state-updates` expects
58
+ one `info` finding: the Dojo's real `PredictState` custom event name is
59
+ un-namespaced (AGUI903, see `docs/spec-questions.md` SQ-14).
60
+
61
+ ## Diagnostic shape
62
+
63
+ ```json
64
+ {
65
+ "rule": "AGUI203",
66
+ "severity": "error",
67
+ "message": "TOOL_CALL_START id 'call_7' never terminated",
68
+ "eventIndex": 3,
69
+ "eventType": "RUN_FINISHED",
70
+ "pointer": "/toolCallId",
71
+ "relatedEventIndex": 1,
72
+ "specUrl": "https://docs.ag-ui.com/concepts/events#tool-call-events"
73
+ }
74
+ ```
75
+
76
+ `eventType`, `pointer`, and `relatedEventIndex` are optional and omitted when
77
+ absent. Messages come from the catalog's `messageTemplate`s, so independent
78
+ implementations produce identical output.
79
+
80
+ ## Maintenance
81
+
82
+ Fixtures are generated by [`scripts/build-fixtures.mjs`](../../js/scripts/build-fixtures.mjs),
83
+ which declares each stream's *intended* rule hits independently of the
84
+ validator and refuses to write anything on mismatch — regeneration can never
85
+ silently bake an implementation bug into `expected.json`.
86
+
87
+ ## Not yet covered
88
+
89
+ - **`valid/real-world/`** — captures from real frameworks (LangGraph, CrewAI,
90
+ Pydantic AI, Agno, Langflow `stream_protocol=agui`). Contributions welcome;
91
+ sanitise credentials before submitting.
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "rule": "AGUI001",
4
+ "severity": "error",
5
+ "message": "First event of the run is TEXT_MESSAGE_START; expected RUN_STARTED",
6
+ "eventIndex": 0,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runstarted",
8
+ "eventType": "TEXT_MESSAGE_START"
9
+ }
10
+ ]
@@ -0,0 +1,4 @@
1
+ {"type":"TEXT_MESSAGE_START","messageId":"msg_001","role":"assistant","timestamp":1755300007900}
2
+ {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_001","delta":"Hello without a run.","timestamp":1755300008000}
3
+ {"type":"TEXT_MESSAGE_END","messageId":"msg_001","timestamp":1755300008100}
4
+ {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001","timestamp":1755300008200}
@@ -0,0 +1,11 @@
1
+ [
2
+ {
3
+ "rule": "AGUI002",
4
+ "severity": "error",
5
+ "message": "Duplicate RUN_STARTED (runId 'run_002') before the active run terminated",
6
+ "eventIndex": 1,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#lifecycle-events",
8
+ "eventType": "RUN_STARTED",
9
+ "relatedEventIndex": 0
10
+ }
11
+ ]
@@ -0,0 +1,3 @@
1
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001","timestamp":1755300008300}
2
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_002","timestamp":1755300008400}
3
+ {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001","outcome":{"type":"success"},"timestamp":1755300008500}
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "rule": "AGUI003",
4
+ "severity": "error",
5
+ "message": "Run 'run_001' ended without RUN_FINISHED or RUN_ERROR",
6
+ "eventIndex": -1,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runfinished",
8
+ "relatedEventIndex": 0
9
+ }
10
+ ]
@@ -0,0 +1,4 @@
1
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001","timestamp":1755300008600}
2
+ {"type":"TEXT_MESSAGE_START","messageId":"msg_001","role":"assistant","timestamp":1755300008700}
3
+ {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_001","delta":"This run just stops.","timestamp":1755300008800}
4
+ {"type":"TEXT_MESSAGE_END","messageId":"msg_001","timestamp":1755300008900}
@@ -0,0 +1,11 @@
1
+ [
2
+ {
3
+ "rule": "AGUI004",
4
+ "severity": "error",
5
+ "message": "TEXT_MESSAGE_START follows the run's terminal RUN_FINISHED",
6
+ "eventIndex": 2,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#lifecycle-events",
8
+ "eventType": "TEXT_MESSAGE_START",
9
+ "relatedEventIndex": 1
10
+ }
11
+ ]
@@ -0,0 +1,3 @@
1
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001","timestamp":1755300009000}
2
+ {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001","outcome":{"type":"success"},"timestamp":1755300009100}
3
+ {"type":"TEXT_MESSAGE_START","messageId":"msg_001","role":"assistant","timestamp":1755300009200}
@@ -0,0 +1,11 @@
1
+ [
2
+ {
3
+ "rule": "AGUI005",
4
+ "severity": "error",
5
+ "message": "RUN_ERROR emitted after the run already terminated with RUN_FINISHED",
6
+ "eventIndex": 2,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runfinished",
8
+ "eventType": "RUN_ERROR",
9
+ "relatedEventIndex": 1
10
+ }
11
+ ]
@@ -0,0 +1,3 @@
1
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001","timestamp":1755300009300}
2
+ {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001","outcome":{"type":"success"},"timestamp":1755300009400}
3
+ {"type":"RUN_ERROR","message":"boom","timestamp":1755300009500}
@@ -0,0 +1,11 @@
1
+ [
2
+ {
3
+ "rule": "AGUI006",
4
+ "severity": "error",
5
+ "message": "STEP_FINISHED 'plan' has no open STEP_STARTED",
6
+ "eventIndex": 1,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#stepfinished",
8
+ "eventType": "STEP_FINISHED",
9
+ "pointer": "/stepName"
10
+ }
11
+ ]
@@ -0,0 +1,3 @@
1
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001","timestamp":1755300009600}
2
+ {"type":"STEP_FINISHED","stepName":"plan","timestamp":1755300009700}
3
+ {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001","outcome":{"type":"success"},"timestamp":1755300009800}
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "rule": "AGUI007",
4
+ "severity": "error",
5
+ "message": "STEP_STARTED 'plan' never finished",
6
+ "eventIndex": 2,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#stepstarted",
8
+ "relatedEventIndex": 1
9
+ }
10
+ ]
@@ -0,0 +1,3 @@
1
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001","timestamp":1755300009900}
2
+ {"type":"STEP_STARTED","stepName":"plan","timestamp":1755300010000}
3
+ {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001","outcome":{"type":"success"},"timestamp":1755300010100}
@@ -0,0 +1,12 @@
1
+ [
2
+ {
3
+ "rule": "AGUI008",
4
+ "severity": "warning",
5
+ "message": "RUN_FINISHED runId 'run_999' does not match RUN_STARTED runId 'run_001'",
6
+ "eventIndex": 1,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runstarted",
8
+ "eventType": "RUN_FINISHED",
9
+ "pointer": "/runId",
10
+ "relatedEventIndex": 0
11
+ }
12
+ ]
@@ -0,0 +1,2 @@
1
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001","timestamp":1755300010200}
2
+ {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_999","outcome":{"type":"success"},"timestamp":1755300010300}
@@ -0,0 +1,11 @@
1
+ [
2
+ {
3
+ "rule": "AGUI101",
4
+ "severity": "error",
5
+ "message": "TEXT_MESSAGE_CONTENT for messageId 'msg_ghost' with no open TEXT_MESSAGE_START",
6
+ "eventIndex": 1,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#text-message-events",
8
+ "eventType": "TEXT_MESSAGE_CONTENT",
9
+ "pointer": "/messageId"
10
+ }
11
+ ]
@@ -0,0 +1,3 @@
1
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001","timestamp":1755300010400}
2
+ {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_ghost","delta":"orphan","timestamp":1755300010500}
3
+ {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001","outcome":{"type":"success"},"timestamp":1755300010600}
@@ -0,0 +1,11 @@
1
+ [
2
+ {
3
+ "rule": "AGUI102",
4
+ "severity": "error",
5
+ "message": "TEXT_MESSAGE_END for messageId 'msg_ghost' with no open TEXT_MESSAGE_START",
6
+ "eventIndex": 1,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#textmessageend",
8
+ "eventType": "TEXT_MESSAGE_END",
9
+ "pointer": "/messageId"
10
+ }
11
+ ]
@@ -0,0 +1,3 @@
1
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001","timestamp":1755300010700}
2
+ {"type":"TEXT_MESSAGE_END","messageId":"msg_ghost","timestamp":1755300010800}
3
+ {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001","outcome":{"type":"success"},"timestamp":1755300010900}
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "rule": "AGUI103",
4
+ "severity": "error",
5
+ "message": "TEXT_MESSAGE_START messageId 'msg_001' never ended",
6
+ "eventIndex": 3,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#text-message-events",
8
+ "relatedEventIndex": 1
9
+ }
10
+ ]
@@ -0,0 +1,4 @@
1
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001","timestamp":1755300011000}
2
+ {"type":"TEXT_MESSAGE_START","messageId":"msg_001","role":"assistant","timestamp":1755300011100}
3
+ {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_001","delta":"never ends","timestamp":1755300011200}
4
+ {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001","outcome":{"type":"success"},"timestamp":1755300011300}
@@ -0,0 +1,12 @@
1
+ [
2
+ {
3
+ "rule": "AGUI104",
4
+ "severity": "error",
5
+ "message": "messageId 'msg_001' was already used by a completed message",
6
+ "eventIndex": 4,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#textmessagestart",
8
+ "eventType": "TEXT_MESSAGE_START",
9
+ "pointer": "/messageId",
10
+ "relatedEventIndex": 3
11
+ }
12
+ ]
@@ -0,0 +1,6 @@
1
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001","timestamp":1755300011400}
2
+ {"type":"TEXT_MESSAGE_START","messageId":"msg_001","role":"assistant","timestamp":1755300011500}
3
+ {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_001","delta":"first use","timestamp":1755300011600}
4
+ {"type":"TEXT_MESSAGE_END","messageId":"msg_001","timestamp":1755300011700}
5
+ {"type":"TEXT_MESSAGE_START","messageId":"msg_001","role":"assistant","timestamp":1755300011800}
6
+ {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001","outcome":{"type":"success"},"timestamp":1755300011900}
@@ -0,0 +1,12 @@
1
+ [
2
+ {
3
+ "rule": "AGUI105",
4
+ "severity": "warning",
5
+ "message": "TEXT_MESSAGE_CONTENT for messageId 'msg_001' has an empty delta",
6
+ "eventIndex": 2,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#textmessagecontent",
8
+ "eventType": "TEXT_MESSAGE_CONTENT",
9
+ "pointer": "/delta",
10
+ "relatedEventIndex": 1
11
+ }
12
+ ]
@@ -0,0 +1,5 @@
1
+ {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001","timestamp":1755300012000}
2
+ {"type":"TEXT_MESSAGE_START","messageId":"msg_001","role":"assistant","timestamp":1755300012100}
3
+ {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_001","delta":"","timestamp":1755300012200}
4
+ {"type":"TEXT_MESSAGE_END","messageId":"msg_001","timestamp":1755300012300}
5
+ {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001","outcome":{"type":"success"},"timestamp":1755300012400}
@@ -0,0 +1,12 @@
1
+ [
2
+ {
3
+ "rule": "AGUI106",
4
+ "severity": "error",
5
+ "message": "TEXT_MESSAGE_START for messageId 'msg_001', which is already open",
6
+ "eventIndex": 2,
7
+ "specUrl": "https://docs.ag-ui.com/concepts/events#implementation-considerations",
8
+ "eventType": "TEXT_MESSAGE_START",
9
+ "pointer": "/messageId",
10
+ "relatedEventIndex": 1
11
+ }
12
+ ]