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
@@ -0,0 +1,109 @@
1
+ // SARIF 2.1.0 output for code-scanning integrations (e.g. GitHub).
2
+ // Level mapping: error→error, warning→warning, info→note. When the input was a
3
+ // line-oriented file (JSONL/captured SSE fed line-per-event is not guaranteed,
4
+ // so only the caller knows), event N is reported at line N+1 via artifactUri.
5
+ import { RULES } from "../rules/catalog.js"
6
+ import type { Diagnostic, Report } from "../types.js"
7
+
8
+ export interface SarifOptions {
9
+ toolVersion: string
10
+ /** URI of the validated artifact; enables line-based locations. */
11
+ artifactUri?: string
12
+ }
13
+
14
+ export type SarifLevel = "error" | "warning" | "note"
15
+
16
+ export interface SarifResult {
17
+ ruleId: string
18
+ level: SarifLevel
19
+ message: { text: string }
20
+ locations: Array<{
21
+ physicalLocation: {
22
+ artifactLocation: { uri: string }
23
+ region: { startLine: number }
24
+ }
25
+ }>
26
+ }
27
+
28
+ export interface SarifLog {
29
+ $schema: string
30
+ version: "2.1.0"
31
+ runs: Array<{
32
+ tool: {
33
+ driver: {
34
+ name: string
35
+ version: string
36
+ informationUri: string
37
+ rules: Array<{
38
+ id: string
39
+ helpUri: string
40
+ shortDescription?: { text: string }
41
+ defaultConfiguration?: { level: SarifLevel }
42
+ }>
43
+ }
44
+ }
45
+ results: SarifResult[]
46
+ }>
47
+ }
48
+
49
+ const LEVEL: Record<Diagnostic["severity"], SarifLevel> = {
50
+ error: "error",
51
+ warning: "warning",
52
+ info: "note",
53
+ }
54
+
55
+ type SarifRule = {
56
+ id: string
57
+ helpUri: string
58
+ shortDescription?: { text: string }
59
+ defaultConfiguration?: { level: SarifLevel }
60
+ }
61
+
62
+ export function toSarif(report: Report, opts: SarifOptions): SarifLog {
63
+ const rules = new Map<string, SarifRule>()
64
+ for (const d of report.diagnostics) {
65
+ if (rules.has(d.rule)) continue
66
+ const entry: SarifRule = { id: d.rule, helpUri: d.specUrl }
67
+ const catalog = RULES.get(d.rule)
68
+ if (catalog !== undefined) {
69
+ entry.shortDescription = { text: catalog.title }
70
+ entry.defaultConfiguration = { level: LEVEL[catalog.severity] }
71
+ }
72
+ rules.set(d.rule, entry)
73
+ }
74
+
75
+ const results: SarifResult[] = report.diagnostics.map((d) => ({
76
+ ruleId: d.rule,
77
+ level: LEVEL[d.severity],
78
+ message: { text: d.message },
79
+ locations:
80
+ opts.artifactUri !== undefined && d.eventIndex >= 0
81
+ ? [
82
+ {
83
+ physicalLocation: {
84
+ artifactLocation: { uri: opts.artifactUri },
85
+ region: { startLine: d.eventIndex + 1 },
86
+ },
87
+ },
88
+ ]
89
+ : [],
90
+ }))
91
+
92
+ return {
93
+ $schema: "https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json",
94
+ version: "2.1.0",
95
+ runs: [
96
+ {
97
+ tool: {
98
+ driver: {
99
+ name: "ag-ui-validate",
100
+ version: opts.toolVersion,
101
+ informationUri: "https://github.com/langport-dev/ag-ui-validate",
102
+ rules: [...rules.values()],
103
+ },
104
+ },
105
+ results,
106
+ },
107
+ ],
108
+ }
109
+ }
@@ -0,0 +1,431 @@
1
+ {
2
+ "$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.",
3
+ "catalogVersion": "0.1.0",
4
+ "spec": "0.x",
5
+ "rules": [
6
+ {
7
+ "id": "AGUI001",
8
+ "severity": "error",
9
+ "title": "Run does not start with RUN_STARTED",
10
+ "messageTemplate": "First event of the run is {type}; expected RUN_STARTED",
11
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runstarted",
12
+ "specQuote": "The RunStarted event is the first event emitted when an agent begins processing a request.",
13
+ "since": "0.x",
14
+ "checkedIn": "core"
15
+ },
16
+ {
17
+ "id": "AGUI002",
18
+ "severity": "error",
19
+ "title": "Multiple RUN_STARTED in one run",
20
+ "messageTemplate": "Duplicate RUN_STARTED (runId '{runId}') before the active run terminated",
21
+ "specUrl": "https://docs.ag-ui.com/concepts/events#lifecycle-events",
22
+ "specQuote": "The RunStarted and either RunFinished or RunError events are mandatory, forming the boundaries of an agent run.",
23
+ "since": "0.x",
24
+ "checkedIn": "core"
25
+ },
26
+ {
27
+ "id": "AGUI003",
28
+ "severity": "error",
29
+ "title": "Run not terminated",
30
+ "messageTemplate": "Run '{runId}' ended without RUN_FINISHED or RUN_ERROR",
31
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runfinished",
32
+ "specQuote": "Every run terminates with either RunFinished or RunError.",
33
+ "since": "0.x",
34
+ "checkedIn": "core"
35
+ },
36
+ {
37
+ "id": "AGUI004",
38
+ "severity": "error",
39
+ "title": "Event after terminal event",
40
+ "messageTemplate": "{type} follows the run's terminal {terminalType}",
41
+ "specUrl": "https://docs.ag-ui.com/concepts/events#lifecycle-events",
42
+ "specQuote": "The RunStarted and either RunFinished or RunError events are mandatory, forming the boundaries of an agent run.",
43
+ "since": "0.x",
44
+ "checkedIn": "core"
45
+ },
46
+ {
47
+ "id": "AGUI005",
48
+ "severity": "error",
49
+ "title": "RUN_FINISHED and RUN_ERROR are mutually exclusive",
50
+ "messageTemplate": "{type} emitted after the run already terminated with {terminalType}",
51
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runfinished",
52
+ "specQuote": "Every run terminates with either RunFinished or RunError.",
53
+ "since": "0.x",
54
+ "checkedIn": "core"
55
+ },
56
+ {
57
+ "id": "AGUI006",
58
+ "severity": "error",
59
+ "title": "STEP_FINISHED without matching STEP_STARTED",
60
+ "messageTemplate": "STEP_FINISHED '{stepName}' has no open STEP_STARTED",
61
+ "specUrl": "https://docs.ag-ui.com/concepts/events#stepfinished",
62
+ "specQuote": "The stepName must match the corresponding StepStarted event.",
63
+ "since": "0.x",
64
+ "checkedIn": "core"
65
+ },
66
+ {
67
+ "id": "AGUI007",
68
+ "severity": "error",
69
+ "title": "Step unterminated at run end",
70
+ "messageTemplate": "STEP_STARTED '{stepName}' never finished",
71
+ "specUrl": "https://docs.ag-ui.com/concepts/events#stepstarted",
72
+ "specQuote": "The stepName must match the corresponding StepStarted event.",
73
+ "since": "0.x",
74
+ "checkedIn": "core"
75
+ },
76
+ {
77
+ "id": "AGUI008",
78
+ "severity": "warning",
79
+ "title": "Unstable threadId/runId across the run",
80
+ "messageTemplate": "RUN_FINISHED {field} '{actual}' does not match RUN_STARTED {field} '{expected}'",
81
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runstarted",
82
+ "specQuote": "It also provides crucial identifiers that can be used to associate subsequent events with this specific run.",
83
+ "since": "0.x",
84
+ "checkedIn": "core"
85
+ },
86
+ {
87
+ "id": "AGUI101",
88
+ "severity": "error",
89
+ "title": "TEXT_MESSAGE_CONTENT without start",
90
+ "messageTemplate": "TEXT_MESSAGE_CONTENT for messageId '{messageId}' with no open TEXT_MESSAGE_START",
91
+ "specUrl": "https://docs.ag-ui.com/concepts/events#text-message-events",
92
+ "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.",
93
+ "since": "0.x",
94
+ "feature": "agentic-chat",
95
+ "checkedIn": "core"
96
+ },
97
+ {
98
+ "id": "AGUI102",
99
+ "severity": "error",
100
+ "title": "TEXT_MESSAGE_END without start",
101
+ "messageTemplate": "TEXT_MESSAGE_END for messageId '{messageId}' with no open TEXT_MESSAGE_START",
102
+ "specUrl": "https://docs.ag-ui.com/concepts/events#textmessageend",
103
+ "specQuote": "messageId: Matches the ID from TextMessageStart",
104
+ "since": "0.x",
105
+ "feature": "agentic-chat",
106
+ "checkedIn": "core"
107
+ },
108
+ {
109
+ "id": "AGUI103",
110
+ "severity": "error",
111
+ "title": "Text message unterminated at run end",
112
+ "messageTemplate": "TEXT_MESSAGE_START messageId '{messageId}' never ended",
113
+ "specUrl": "https://docs.ag-ui.com/concepts/events#text-message-events",
114
+ "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.",
115
+ "since": "0.x",
116
+ "feature": "agentic-chat",
117
+ "checkedIn": "core"
118
+ },
119
+ {
120
+ "id": "AGUI104",
121
+ "severity": "error",
122
+ "title": "Duplicate messageId within a run",
123
+ "messageTemplate": "messageId '{messageId}' was already used by a completed message",
124
+ "specUrl": "https://docs.ag-ui.com/concepts/events#textmessagestart",
125
+ "specQuote": "It establishes a unique messageId that will be referenced by subsequent content chunks and the end event.",
126
+ "since": "0.x",
127
+ "feature": "agentic-chat",
128
+ "checkedIn": "core"
129
+ },
130
+ {
131
+ "id": "AGUI105",
132
+ "severity": "warning",
133
+ "title": "Empty content delta",
134
+ "messageTemplate": "TEXT_MESSAGE_CONTENT for messageId '{messageId}' has an empty delta",
135
+ "specUrl": "https://docs.ag-ui.com/concepts/events#textmessagecontent",
136
+ "specQuote": "delta: Text content chunk (non-empty)",
137
+ "since": "0.x",
138
+ "feature": "agentic-chat",
139
+ "specQuestion": "SQ-2",
140
+ "checkedIn": "core"
141
+ },
142
+ {
143
+ "id": "AGUI106",
144
+ "severity": "error",
145
+ "title": "Interleaved message streams sharing a messageId",
146
+ "messageTemplate": "TEXT_MESSAGE_START for messageId '{messageId}', which is already open",
147
+ "specUrl": "https://docs.ag-ui.com/concepts/events#implementation-considerations",
148
+ "specQuote": "Events with the same ID (e.g., messageId, toolCallId) belong to the same logical stream",
149
+ "since": "0.x",
150
+ "feature": "agentic-chat",
151
+ "checkedIn": "core"
152
+ },
153
+ {
154
+ "id": "AGUI201",
155
+ "severity": "error",
156
+ "title": "TOOL_CALL_ARGS without start",
157
+ "messageTemplate": "TOOL_CALL_ARGS for toolCallId '{toolCallId}' with no open TOOL_CALL_START",
158
+ "specUrl": "https://docs.ag-ui.com/concepts/events#tool-call-events",
159
+ "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.",
160
+ "since": "0.x",
161
+ "feature": "backend-tool-rendering",
162
+ "checkedIn": "core"
163
+ },
164
+ {
165
+ "id": "AGUI202",
166
+ "severity": "error",
167
+ "title": "TOOL_CALL_END without start",
168
+ "messageTemplate": "TOOL_CALL_END for toolCallId '{toolCallId}' with no open TOOL_CALL_START",
169
+ "specUrl": "https://docs.ag-ui.com/concepts/events#toolcallend",
170
+ "specQuote": "toolCallId: Matches the ID from ToolCallStart",
171
+ "since": "0.x",
172
+ "feature": "backend-tool-rendering",
173
+ "checkedIn": "core"
174
+ },
175
+ {
176
+ "id": "AGUI203",
177
+ "severity": "error",
178
+ "title": "Unterminated tool call",
179
+ "messageTemplate": "TOOL_CALL_START id '{toolCallId}' never terminated",
180
+ "specUrl": "https://docs.ag-ui.com/concepts/events#tool-call-events",
181
+ "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.",
182
+ "since": "0.x",
183
+ "feature": "backend-tool-rendering",
184
+ "checkedIn": "core"
185
+ },
186
+ {
187
+ "id": "AGUI204",
188
+ "severity": "error",
189
+ "title": "Tool call arguments are not valid JSON",
190
+ "messageTemplate": "Concatenated TOOL_CALL_ARGS for toolCallId '{toolCallId}' do not parse as JSON: {error}",
191
+ "specUrl": "https://docs.ag-ui.com/concepts/events#toolcallargs",
192
+ "specQuote": "Frontends should concatenate these deltas in the order received to construct the complete arguments object.",
193
+ "since": "0.x",
194
+ "feature": "backend-tool-rendering",
195
+ "checkedIn": "core"
196
+ },
197
+ {
198
+ "id": "AGUI205",
199
+ "severity": "error",
200
+ "title": "Duplicate toolCallId within a run",
201
+ "messageTemplate": "toolCallId '{toolCallId}' was already used by a completed tool call",
202
+ "specUrl": "https://docs.ag-ui.com/concepts/events#toolcallstart",
203
+ "specQuote": "toolCallId: Unique identifier for the tool call",
204
+ "since": "0.x",
205
+ "feature": "backend-tool-rendering",
206
+ "checkedIn": "core"
207
+ },
208
+ {
209
+ "id": "AGUI206",
210
+ "severity": "warning",
211
+ "title": "TOOL_CALL_RESULT before TOOL_CALL_END",
212
+ "messageTemplate": "TOOL_CALL_RESULT for toolCallId '{toolCallId}' arrived while the call is still open",
213
+ "specUrl": "https://docs.ag-ui.com/concepts/events#toolcallresult",
214
+ "specQuote": "This event is sent after the tool has been executed by the system and contains the actual output generated by the tool.",
215
+ "since": "0.x",
216
+ "feature": "backend-tool-rendering",
217
+ "checkedIn": "core"
218
+ },
219
+ {
220
+ "id": "AGUI207",
221
+ "severity": "error",
222
+ "title": "TOOL_CALL_RESULT references unknown toolCallId",
223
+ "messageTemplate": "TOOL_CALL_RESULT references toolCallId '{toolCallId}', which was never started",
224
+ "specUrl": "https://docs.ag-ui.com/concepts/events#toolcallresult",
225
+ "specQuote": "toolCallId: Matches the ID from the corresponding ToolCallStart event",
226
+ "since": "0.x",
227
+ "feature": "backend-tool-rendering",
228
+ "checkedIn": "core"
229
+ },
230
+ {
231
+ "id": "AGUI208",
232
+ "severity": "info",
233
+ "title": "parentMessageId references unknown message",
234
+ "messageTemplate": "TOOL_CALL_START parentMessageId '{parentMessageId}' matches no message observed in this stream",
235
+ "specUrl": "https://docs.ag-ui.com/concepts/events#toolcallstart",
236
+ "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.",
237
+ "since": "0.x",
238
+ "feature": "backend-tool-rendering",
239
+ "specQuestion": "SQ-11",
240
+ "checkedIn": "core"
241
+ },
242
+ {
243
+ "id": "AGUI301",
244
+ "severity": "info",
245
+ "title": "STATE_DELTA before any STATE_SNAPSHOT",
246
+ "messageTemplate": "STATE_DELTA precedes any STATE_SNAPSHOT; the base it applies to is not observable on this stream",
247
+ "specUrl": "https://docs.ag-ui.com/concepts/events#statesnapshot",
248
+ "specQuote": "This event is typically sent at the beginning of an interaction or when synchronization is needed.",
249
+ "since": "0.x",
250
+ "feature": "shared-state",
251
+ "specQuestion": "SQ-1",
252
+ "checkedIn": "core"
253
+ },
254
+ {
255
+ "id": "AGUI302",
256
+ "severity": "error",
257
+ "title": "STATE_DELTA failed to apply",
258
+ "messageTemplate": "STATE_DELTA failed to apply: {error}",
259
+ "specUrl": "https://docs.ag-ui.com/concepts/events#statedelta",
260
+ "specQuote": "Each delta represents specific changes to apply to the current state model.",
261
+ "since": "0.x",
262
+ "feature": "shared-state",
263
+ "checkedIn": "core"
264
+ },
265
+ {
266
+ "id": "AGUI303",
267
+ "severity": "error",
268
+ "title": "STATE_DELTA is not a valid RFC 6902 patch document",
269
+ "messageTemplate": "STATE_DELTA is not a valid RFC 6902 patch document: {error}",
270
+ "specUrl": "https://docs.ag-ui.com/concepts/events#statedelta",
271
+ "specQuote": "The StateDelta event contains incremental updates to the agent's state in the form of JSON Patch operations (as defined in RFC 6902).",
272
+ "since": "0.x",
273
+ "feature": "shared-state",
274
+ "checkedIn": "core"
275
+ },
276
+ {
277
+ "id": "AGUI304",
278
+ "severity": "info",
279
+ "title": "Mid-run STATE_SNAPSHOT discards accumulated deltas",
280
+ "messageTemplate": "STATE_SNAPSHOT replaces state previously built from {deltaCount} delta(s)",
281
+ "specUrl": "https://docs.ag-ui.com/concepts/events#statesnapshot",
282
+ "specQuote": "This event is typically sent at the beginning of an interaction or when synchronization is needed.",
283
+ "since": "0.x",
284
+ "feature": "shared-state",
285
+ "checkedIn": "core"
286
+ },
287
+ {
288
+ "id": "AGUI305",
289
+ "severity": "warning",
290
+ "title": "Shared state declared but never established",
291
+ "messageTemplate": "features include 'shared-state' but no STATE_SNAPSHOT was emitted",
292
+ "specUrl": "https://docs.ag-ui.com/concepts/events#state-management-events",
293
+ "specQuote": "These events are used to manage and synchronize the agent's state with the frontend.",
294
+ "since": "0.x",
295
+ "feature": "shared-state",
296
+ "requiresFeature": true,
297
+ "checkedIn": "core"
298
+ },
299
+ {
300
+ "id": "AGUI401",
301
+ "severity": "error",
302
+ "title": "REASONING_MESSAGE_CONTENT without start",
303
+ "messageTemplate": "REASONING_MESSAGE_CONTENT for messageId '{messageId}' with no open REASONING_MESSAGE_START",
304
+ "specUrl": "https://docs.ag-ui.com/concepts/events#reasoningmessagecontent",
305
+ "specQuote": "Multiple content events with the same messageId should be concatenated to form the complete visible reasoning.",
306
+ "since": "0.x",
307
+ "specQuestion": "SQ-4",
308
+ "checkedIn": "core"
309
+ },
310
+ {
311
+ "id": "AGUI402",
312
+ "severity": "warning",
313
+ "title": "Reasoning unterminated at run end",
314
+ "messageTemplate": "{startType} messageId '{messageId}' never ended",
315
+ "specUrl": "https://docs.ag-ui.com/concepts/events#reasoning-events",
316
+ "specQuote": "Reasoning events support LLM reasoning visibility and continuity, enabling chain-of-thought reasoning while maintaining privacy.",
317
+ "since": "0.x",
318
+ "specQuestion": "SQ-4",
319
+ "checkedIn": "core"
320
+ },
321
+ {
322
+ "id": "AGUI501",
323
+ "severity": "error",
324
+ "title": "Malformed SSE framing",
325
+ "messageTemplate": "Malformed SSE framing: {detail}",
326
+ "specUrl": "https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation",
327
+ "since": "0.x",
328
+ "checkedIn": "transport"
329
+ },
330
+ {
331
+ "id": "AGUI502",
332
+ "severity": "error",
333
+ "title": "Event payload is not valid JSON",
334
+ "messageTemplate": "Event payload is not valid JSON: {error}",
335
+ "specUrl": "https://docs.ag-ui.com/concepts/events#base-event-properties",
336
+ "specQuote": "All events share a common set of base properties.",
337
+ "since": "0.x",
338
+ "checkedIn": "core"
339
+ },
340
+ {
341
+ "id": "AGUI503",
342
+ "severity": "error",
343
+ "title": "Unknown event type",
344
+ "messageTemplate": "Unknown event type '{type}' (not in @ag-ui/core v{sdkVersion}, and not RAW or CUSTOM)",
345
+ "specUrl": "https://docs.ag-ui.com/concepts/events#event-types-overview",
346
+ "specQuote": "Events in the protocol are categorized by their purpose.",
347
+ "since": "0.x",
348
+ "checkedIn": "core"
349
+ },
350
+ {
351
+ "id": "AGUI504",
352
+ "severity": "error",
353
+ "title": "Event fails schema validation for its declared type",
354
+ "messageTemplate": "{type}: {detail}",
355
+ "specUrl": "https://docs.ag-ui.com/concepts/events#base-event-properties",
356
+ "specQuote": "All events share a common set of base properties.",
357
+ "since": "0.x",
358
+ "checkedIn": "core"
359
+ },
360
+ {
361
+ "id": "AGUI505",
362
+ "severity": "warning",
363
+ "title": "Unexpected Content-Type",
364
+ "messageTemplate": "Content-Type '{contentType}' is neither text/event-stream nor application/x-ndjson",
365
+ "specUrl": "https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model",
366
+ "since": "0.x",
367
+ "checkedIn": "transport"
368
+ },
369
+ {
370
+ "id": "AGUI506",
371
+ "severity": "info",
372
+ "title": "No keepalive frame within the configured window",
373
+ "messageTemplate": "No event or keepalive frame for {seconds}s",
374
+ "specUrl": "https://docs.ag-ui.com/concepts/architecture#standard-http-client",
375
+ "since": "0.x",
376
+ "specQuestion": "SQ-5",
377
+ "checkedIn": "transport"
378
+ },
379
+ {
380
+ "id": "AGUI507",
381
+ "severity": "info",
382
+ "title": "Response appears buffered rather than incrementally flushed",
383
+ "messageTemplate": "Response appears buffered: {detail}",
384
+ "specUrl": "https://docs.ag-ui.com/concepts/architecture#standard-http-client",
385
+ "since": "0.x",
386
+ "specQuestion": "SQ-5",
387
+ "checkedIn": "transport"
388
+ },
389
+ {
390
+ "id": "AGUI508",
391
+ "severity": "error",
392
+ "title": "Stream ended without a terminal event",
393
+ "messageTemplate": "Connection ended mid-run '{runId}' without RUN_FINISHED or RUN_ERROR",
394
+ "specUrl": "https://docs.ag-ui.com/concepts/events#runfinished",
395
+ "specQuote": "Every run terminates with either RunFinished or RunError.",
396
+ "since": "0.x",
397
+ "checkedIn": "transport"
398
+ },
399
+ {
400
+ "id": "AGUI901",
401
+ "severity": "info",
402
+ "title": "RAW event wraps a typed AG-UI event",
403
+ "messageTemplate": "RAW event wraps an event of type '{wrappedType}', which has a typed AG-UI equivalent",
404
+ "specUrl": "https://docs.ag-ui.com/concepts/events#raw",
405
+ "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.",
406
+ "since": "0.x",
407
+ "checkedIn": "core"
408
+ },
409
+ {
410
+ "id": "AGUI902",
411
+ "severity": "info",
412
+ "title": "Events carry no timestamps",
413
+ "messageTemplate": "None of the {eventCount} events carry the optional timestamp property",
414
+ "specUrl": "https://docs.ag-ui.com/concepts/events#base-event-properties",
415
+ "specQuote": "timestamp: Optional timestamp indicating when the event was created",
416
+ "since": "0.x",
417
+ "checkedIn": "core"
418
+ },
419
+ {
420
+ "id": "AGUI903",
421
+ "severity": "info",
422
+ "title": "CUSTOM event name is not namespaced",
423
+ "messageTemplate": "CUSTOM event name '{name}' has no namespace prefix (e.g. 'vendor.event')",
424
+ "specUrl": "https://docs.ag-ui.com/concepts/events#custom",
425
+ "specQuote": "Teams should document their custom events to ensure consistent implementation across frontends and agents.",
426
+ "since": "0.x",
427
+ "specQuestion": "SQ-14",
428
+ "checkedIn": "core"
429
+ }
430
+ ]
431
+ }
@@ -0,0 +1,84 @@
1
+ // Typed loader for the rule catalog. The catalog itself is data
2
+ // (catalog.json) so other implementations can share it; this module gives it
3
+ // types and validates its invariants once at load time.
4
+ //
5
+ // Note the asymmetry: the validator never throws on stream input, but a
6
+ // malformed catalog is a programming error in this package, so the loader
7
+ // throws loudly at import time.
8
+
9
+ import catalogJson from "./catalog.json"
10
+
11
+ export type Severity = "error" | "warning" | "info"
12
+ export type SeverityOrOff = Severity | "off"
13
+
14
+ const SEVERITIES: readonly string[] = ["error", "warning", "info"]
15
+ const LAYERS: readonly string[] = ["core", "transport"]
16
+
17
+ export interface RuleDefinition {
18
+ /** e.g. "AGUI203" */
19
+ id: string
20
+ severity: Severity
21
+ title: string
22
+ /** Human template with {placeholder} slots filled per diagnostic. */
23
+ messageTemplate: string
24
+ /** Governing spec section. Mandatory: rules that cannot cite one don't ship. */
25
+ specUrl: string
26
+ /** Exact sentence from the spec section, where one exists. */
27
+ specQuote?: string
28
+ since: string
29
+ /** Canonical AG-UI feature this rule relates to, if any. */
30
+ feature?: string
31
+ /** True when the rule only fires if opts.features declares `feature`. */
32
+ requiresFeature?: boolean
33
+ /** Cross-reference into docs/spec-questions.md for downgraded/ambiguous rules. */
34
+ specQuestion?: string
35
+ /** Where the rule is evaluated. Transport rules are skipped (and the skip
36
+ * reported) when validating recorded input with no transport in play. */
37
+ checkedIn: "core" | "transport"
38
+ }
39
+
40
+ export interface Catalog {
41
+ catalogVersion: string
42
+ spec: string
43
+ rules: readonly RuleDefinition[]
44
+ }
45
+
46
+ /** Validates catalog data and returns it typed. Throws on structural problems. */
47
+ export function validateCatalog(data: unknown): Catalog {
48
+ const problems: string[] = []
49
+ const cat = data as Catalog
50
+ if (typeof cat !== "object" || cat === null || !Array.isArray(cat.rules)) {
51
+ throw new Error("rule catalog: expected an object with a rules array")
52
+ }
53
+ const seen = new Set<string>()
54
+ for (const rule of cat.rules) {
55
+ const where = rule?.id ?? "<missing id>"
56
+ if (!/^AGUI\d{3}$/.test(rule.id ?? "")) problems.push(`${where}: id must match AGUI###`)
57
+ if (seen.has(rule.id)) problems.push(`${where}: duplicate id`)
58
+ seen.add(rule.id)
59
+ if (!SEVERITIES.includes(rule.severity)) problems.push(`${where}: bad severity '${rule.severity}'`)
60
+ if (!rule.title) problems.push(`${where}: missing title`)
61
+ if (!rule.messageTemplate) problems.push(`${where}: missing messageTemplate`)
62
+ if (!rule.specUrl?.startsWith("https://")) problems.push(`${where}: specUrl must be an https URL`)
63
+ if (!rule.since) problems.push(`${where}: missing since`)
64
+ if (!LAYERS.includes(rule.checkedIn)) problems.push(`${where}: bad checkedIn '${rule.checkedIn}'`)
65
+ if (rule.requiresFeature && !rule.feature) problems.push(`${where}: requiresFeature without feature`)
66
+ }
67
+ if (problems.length > 0) {
68
+ throw new Error(`rule catalog is invalid:\n ${problems.join("\n ")}`)
69
+ }
70
+ return cat
71
+ }
72
+
73
+ export const CATALOG: Catalog = validateCatalog(catalogJson)
74
+
75
+ export const RULES: ReadonlyMap<string, RuleDefinition> = new Map(
76
+ CATALOG.rules.map((r) => [r.id, r]),
77
+ )
78
+
79
+ /** Fills a rule's messageTemplate. Unknown placeholders are left intact. */
80
+ export function formatMessage(rule: RuleDefinition, params: Record<string, unknown>): string {
81
+ return rule.messageTemplate.replace(/\{(\w+)\}/g, (whole, key: string) =>
82
+ key in params ? String(params[key]) : whole,
83
+ )
84
+ }