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.
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/catalog-BglXBNbL.js +472 -0
- package/dist/catalog-BglXBNbL.js.map +1 -0
- package/dist/catalog-Ci9dqc1a.cjs +495 -0
- package/dist/catalog-Ci9dqc1a.cjs.map +1 -0
- package/dist/cli.js +2783 -0
- package/dist/cli.js.map +1 -0
- package/dist/index-Hmqj3r_r.d.cts +52 -0
- package/dist/index-oNG1kOp9.d.ts +52 -0
- package/dist/index.cjs +14 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/report.cjs +139 -0
- package/dist/report.cjs.map +1 -0
- package/dist/report.d.cts +85 -0
- package/dist/report.d.ts +85 -0
- package/dist/report.js +134 -0
- package/dist/report.js.map +1 -0
- package/dist/src-HmI-kxef.cjs +1596 -0
- package/dist/src-HmI-kxef.cjs.map +1 -0
- package/dist/src-rGZ2G4qA.js +1555 -0
- package/dist/src-rGZ2G4qA.js.map +1 -0
- package/dist/transport.cjs +329 -0
- package/dist/transport.cjs.map +1 -0
- package/dist/transport.d.cts +89 -0
- package/dist/transport.d.ts +89 -0
- package/dist/transport.js +323 -0
- package/dist/transport.js.map +1 -0
- package/dist/types-oH_QTnn2.d.cts +148 -0
- package/dist/types-oH_QTnn2.d.ts +148 -0
- package/dist/vitest.d.ts +28 -0
- package/dist/vitest.js +2089 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +127 -0
- package/src/cli-args.ts +202 -0
- package/src/cli.ts +147 -0
- package/src/index.ts +465 -0
- package/src/protocol/event-table.ts +316 -0
- package/src/protocol/jsonpatch.ts +220 -0
- package/src/report/index.ts +10 -0
- package/src/report/json.ts +20 -0
- package/src/report/junit.ts +56 -0
- package/src/report/pretty.ts +59 -0
- package/src/report/sarif.ts +109 -0
- package/src/rules/catalog.json +431 -0
- package/src/rules/catalog.ts +84 -0
- package/src/rules/checks/context.ts +117 -0
- package/src/rules/checks/lifecycle.ts +59 -0
- package/src/rules/checks/reasoning.ts +97 -0
- package/src/rules/checks/state.ts +72 -0
- package/src/rules/checks/text.ts +109 -0
- package/src/rules/checks/toolcalls.ts +167 -0
- package/src/rules/checks/transport.ts +17 -0
- package/src/transport/index.ts +331 -0
- package/src/transport/ndjson.ts +25 -0
- package/src/transport/sse.ts +126 -0
- package/src/types.ts +136 -0
- package/src/vitest/index.ts +19 -0
- package/src/vitest/matcher.ts +77 -0
package/dist/vitest.js
ADDED
|
@@ -0,0 +1,2089 @@
|
|
|
1
|
+
import { expect } from "vitest";
|
|
2
|
+
//#region src/cli-args.ts
|
|
3
|
+
function decideExitCode(summary, maxWarnings) {
|
|
4
|
+
if (summary.errors > 0) return 1;
|
|
5
|
+
if (maxWarnings !== void 0 && summary.warnings > maxWarnings) return 1;
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/protocol/event-table.ts
|
|
10
|
+
/** Version of @ag-ui/core this table was derived from. */
|
|
11
|
+
const SDK_VERSION = "0.0.58";
|
|
12
|
+
/** Canonical wire event types and their schemas, derived from @ag-ui/core. */
|
|
13
|
+
const EVENT_TABLE = {
|
|
14
|
+
"TEXT_MESSAGE_START": {
|
|
15
|
+
category: "text",
|
|
16
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#textmessagestart",
|
|
17
|
+
fields: {
|
|
18
|
+
"messageId": {
|
|
19
|
+
kind: "string",
|
|
20
|
+
required: true
|
|
21
|
+
},
|
|
22
|
+
"role": {
|
|
23
|
+
kind: "string",
|
|
24
|
+
required: false,
|
|
25
|
+
enum: [
|
|
26
|
+
"developer",
|
|
27
|
+
"system",
|
|
28
|
+
"assistant",
|
|
29
|
+
"user"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"name": {
|
|
33
|
+
kind: "string",
|
|
34
|
+
required: false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"TEXT_MESSAGE_CONTENT": {
|
|
39
|
+
category: "text",
|
|
40
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#textmessagecontent",
|
|
41
|
+
fields: {
|
|
42
|
+
"messageId": {
|
|
43
|
+
kind: "string",
|
|
44
|
+
required: true
|
|
45
|
+
},
|
|
46
|
+
"delta": {
|
|
47
|
+
kind: "string",
|
|
48
|
+
required: true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"TEXT_MESSAGE_END": {
|
|
53
|
+
category: "text",
|
|
54
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#textmessageend",
|
|
55
|
+
fields: { "messageId": {
|
|
56
|
+
kind: "string",
|
|
57
|
+
required: true
|
|
58
|
+
} }
|
|
59
|
+
},
|
|
60
|
+
"TEXT_MESSAGE_CHUNK": {
|
|
61
|
+
category: "text",
|
|
62
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#textmessagechunk",
|
|
63
|
+
fields: {
|
|
64
|
+
"messageId": {
|
|
65
|
+
kind: "string",
|
|
66
|
+
required: false
|
|
67
|
+
},
|
|
68
|
+
"role": {
|
|
69
|
+
kind: "string",
|
|
70
|
+
required: false,
|
|
71
|
+
enum: [
|
|
72
|
+
"developer",
|
|
73
|
+
"system",
|
|
74
|
+
"assistant",
|
|
75
|
+
"user"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"delta": {
|
|
79
|
+
kind: "string",
|
|
80
|
+
required: false
|
|
81
|
+
},
|
|
82
|
+
"name": {
|
|
83
|
+
kind: "string",
|
|
84
|
+
required: false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"TOOL_CALL_START": {
|
|
89
|
+
category: "toolcall",
|
|
90
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#toolcallstart",
|
|
91
|
+
fields: {
|
|
92
|
+
"toolCallId": {
|
|
93
|
+
kind: "string",
|
|
94
|
+
required: true
|
|
95
|
+
},
|
|
96
|
+
"toolCallName": {
|
|
97
|
+
kind: "string",
|
|
98
|
+
required: true
|
|
99
|
+
},
|
|
100
|
+
"parentMessageId": {
|
|
101
|
+
kind: "string",
|
|
102
|
+
required: false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"TOOL_CALL_ARGS": {
|
|
107
|
+
category: "toolcall",
|
|
108
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#toolcallargs",
|
|
109
|
+
fields: {
|
|
110
|
+
"toolCallId": {
|
|
111
|
+
kind: "string",
|
|
112
|
+
required: true
|
|
113
|
+
},
|
|
114
|
+
"delta": {
|
|
115
|
+
kind: "string",
|
|
116
|
+
required: true
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"TOOL_CALL_END": {
|
|
121
|
+
category: "toolcall",
|
|
122
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#toolcallend",
|
|
123
|
+
fields: { "toolCallId": {
|
|
124
|
+
kind: "string",
|
|
125
|
+
required: true
|
|
126
|
+
} }
|
|
127
|
+
},
|
|
128
|
+
"TOOL_CALL_CHUNK": {
|
|
129
|
+
category: "toolcall",
|
|
130
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#toolcallchunk",
|
|
131
|
+
fields: {
|
|
132
|
+
"toolCallId": {
|
|
133
|
+
kind: "string",
|
|
134
|
+
required: false
|
|
135
|
+
},
|
|
136
|
+
"toolCallName": {
|
|
137
|
+
kind: "string",
|
|
138
|
+
required: false
|
|
139
|
+
},
|
|
140
|
+
"parentMessageId": {
|
|
141
|
+
kind: "string",
|
|
142
|
+
required: false
|
|
143
|
+
},
|
|
144
|
+
"delta": {
|
|
145
|
+
kind: "string",
|
|
146
|
+
required: false
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"TOOL_CALL_RESULT": {
|
|
151
|
+
category: "toolcall",
|
|
152
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#toolcallresult",
|
|
153
|
+
fields: {
|
|
154
|
+
"messageId": {
|
|
155
|
+
kind: "string",
|
|
156
|
+
required: true
|
|
157
|
+
},
|
|
158
|
+
"toolCallId": {
|
|
159
|
+
kind: "string",
|
|
160
|
+
required: true
|
|
161
|
+
},
|
|
162
|
+
"content": {
|
|
163
|
+
kind: "string",
|
|
164
|
+
required: true
|
|
165
|
+
},
|
|
166
|
+
"role": {
|
|
167
|
+
kind: "string",
|
|
168
|
+
required: false,
|
|
169
|
+
enum: ["tool"]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"THINKING_START": {
|
|
174
|
+
category: "thinking",
|
|
175
|
+
deprecated: "REASONING_START",
|
|
176
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
|
|
177
|
+
fields: { "title": {
|
|
178
|
+
kind: "string",
|
|
179
|
+
required: false
|
|
180
|
+
} }
|
|
181
|
+
},
|
|
182
|
+
"THINKING_END": {
|
|
183
|
+
category: "thinking",
|
|
184
|
+
deprecated: "REASONING_END",
|
|
185
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
|
|
186
|
+
fields: {}
|
|
187
|
+
},
|
|
188
|
+
"THINKING_TEXT_MESSAGE_START": {
|
|
189
|
+
category: "thinking",
|
|
190
|
+
deprecated: "REASONING_MESSAGE_START",
|
|
191
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
|
|
192
|
+
fields: {}
|
|
193
|
+
},
|
|
194
|
+
"THINKING_TEXT_MESSAGE_CONTENT": {
|
|
195
|
+
category: "thinking",
|
|
196
|
+
deprecated: "REASONING_MESSAGE_CONTENT",
|
|
197
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
|
|
198
|
+
fields: { "delta": {
|
|
199
|
+
kind: "string",
|
|
200
|
+
required: true
|
|
201
|
+
} }
|
|
202
|
+
},
|
|
203
|
+
"THINKING_TEXT_MESSAGE_END": {
|
|
204
|
+
category: "thinking",
|
|
205
|
+
deprecated: "REASONING_MESSAGE_END",
|
|
206
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
|
|
207
|
+
fields: {}
|
|
208
|
+
},
|
|
209
|
+
"STATE_SNAPSHOT": {
|
|
210
|
+
category: "state",
|
|
211
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#statesnapshot",
|
|
212
|
+
fields: { "snapshot": {
|
|
213
|
+
kind: "any",
|
|
214
|
+
required: true
|
|
215
|
+
} }
|
|
216
|
+
},
|
|
217
|
+
"STATE_DELTA": {
|
|
218
|
+
category: "state",
|
|
219
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#statedelta",
|
|
220
|
+
fields: { "delta": {
|
|
221
|
+
kind: "array",
|
|
222
|
+
required: true
|
|
223
|
+
} }
|
|
224
|
+
},
|
|
225
|
+
"MESSAGES_SNAPSHOT": {
|
|
226
|
+
category: "state",
|
|
227
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#messagessnapshot",
|
|
228
|
+
fields: { "messages": {
|
|
229
|
+
kind: "array",
|
|
230
|
+
required: true
|
|
231
|
+
} }
|
|
232
|
+
},
|
|
233
|
+
"ACTIVITY_SNAPSHOT": {
|
|
234
|
+
category: "activity",
|
|
235
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#activitysnapshot",
|
|
236
|
+
fields: {
|
|
237
|
+
"messageId": {
|
|
238
|
+
kind: "string",
|
|
239
|
+
required: true
|
|
240
|
+
},
|
|
241
|
+
"activityType": {
|
|
242
|
+
kind: "string",
|
|
243
|
+
required: true
|
|
244
|
+
},
|
|
245
|
+
"content": {
|
|
246
|
+
kind: "object",
|
|
247
|
+
required: true
|
|
248
|
+
},
|
|
249
|
+
"replace": {
|
|
250
|
+
kind: "boolean",
|
|
251
|
+
required: false
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
"ACTIVITY_DELTA": {
|
|
256
|
+
category: "activity",
|
|
257
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#activitydelta",
|
|
258
|
+
fields: {
|
|
259
|
+
"messageId": {
|
|
260
|
+
kind: "string",
|
|
261
|
+
required: true
|
|
262
|
+
},
|
|
263
|
+
"activityType": {
|
|
264
|
+
kind: "string",
|
|
265
|
+
required: true
|
|
266
|
+
},
|
|
267
|
+
"patch": {
|
|
268
|
+
kind: "array",
|
|
269
|
+
required: true
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
"RAW": {
|
|
274
|
+
category: "special",
|
|
275
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#raw",
|
|
276
|
+
fields: {
|
|
277
|
+
"event": {
|
|
278
|
+
kind: "any",
|
|
279
|
+
required: true
|
|
280
|
+
},
|
|
281
|
+
"source": {
|
|
282
|
+
kind: "string",
|
|
283
|
+
required: false
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
"CUSTOM": {
|
|
288
|
+
category: "special",
|
|
289
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#custom",
|
|
290
|
+
fields: {
|
|
291
|
+
"name": {
|
|
292
|
+
kind: "string",
|
|
293
|
+
required: true
|
|
294
|
+
},
|
|
295
|
+
"value": {
|
|
296
|
+
kind: "any",
|
|
297
|
+
required: true
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
"RUN_STARTED": {
|
|
302
|
+
category: "lifecycle",
|
|
303
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#runstarted",
|
|
304
|
+
fields: {
|
|
305
|
+
"threadId": {
|
|
306
|
+
kind: "string",
|
|
307
|
+
required: true
|
|
308
|
+
},
|
|
309
|
+
"runId": {
|
|
310
|
+
kind: "string",
|
|
311
|
+
required: true
|
|
312
|
+
},
|
|
313
|
+
"parentRunId": {
|
|
314
|
+
kind: "string",
|
|
315
|
+
required: false
|
|
316
|
+
},
|
|
317
|
+
"input": {
|
|
318
|
+
kind: "object",
|
|
319
|
+
required: false
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
"RUN_FINISHED": {
|
|
324
|
+
category: "lifecycle",
|
|
325
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#runfinished",
|
|
326
|
+
fields: {
|
|
327
|
+
"threadId": {
|
|
328
|
+
kind: "string",
|
|
329
|
+
required: true
|
|
330
|
+
},
|
|
331
|
+
"runId": {
|
|
332
|
+
kind: "string",
|
|
333
|
+
required: true
|
|
334
|
+
},
|
|
335
|
+
"result": {
|
|
336
|
+
kind: "any",
|
|
337
|
+
required: false
|
|
338
|
+
},
|
|
339
|
+
"outcome": {
|
|
340
|
+
kind: "object",
|
|
341
|
+
required: false
|
|
342
|
+
},
|
|
343
|
+
"usage": {
|
|
344
|
+
kind: "array",
|
|
345
|
+
required: false
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
"RUN_ERROR": {
|
|
350
|
+
category: "lifecycle",
|
|
351
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#runerror",
|
|
352
|
+
fields: {
|
|
353
|
+
"message": {
|
|
354
|
+
kind: "string",
|
|
355
|
+
required: true
|
|
356
|
+
},
|
|
357
|
+
"code": {
|
|
358
|
+
kind: "string",
|
|
359
|
+
required: false
|
|
360
|
+
},
|
|
361
|
+
"usage": {
|
|
362
|
+
kind: "array",
|
|
363
|
+
required: false
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
"STEP_STARTED": {
|
|
368
|
+
category: "lifecycle",
|
|
369
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#stepstarted",
|
|
370
|
+
fields: { "stepName": {
|
|
371
|
+
kind: "string",
|
|
372
|
+
required: true
|
|
373
|
+
} }
|
|
374
|
+
},
|
|
375
|
+
"STEP_FINISHED": {
|
|
376
|
+
category: "lifecycle",
|
|
377
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#stepfinished",
|
|
378
|
+
fields: { "stepName": {
|
|
379
|
+
kind: "string",
|
|
380
|
+
required: true
|
|
381
|
+
} }
|
|
382
|
+
},
|
|
383
|
+
"REASONING_START": {
|
|
384
|
+
category: "reasoning",
|
|
385
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningstart",
|
|
386
|
+
fields: { "messageId": {
|
|
387
|
+
kind: "string",
|
|
388
|
+
required: true
|
|
389
|
+
} }
|
|
390
|
+
},
|
|
391
|
+
"REASONING_MESSAGE_START": {
|
|
392
|
+
category: "reasoning",
|
|
393
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningmessagestart",
|
|
394
|
+
fields: {
|
|
395
|
+
"messageId": {
|
|
396
|
+
kind: "string",
|
|
397
|
+
required: true
|
|
398
|
+
},
|
|
399
|
+
"role": {
|
|
400
|
+
kind: "string",
|
|
401
|
+
required: true,
|
|
402
|
+
enum: ["reasoning"]
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
"REASONING_MESSAGE_CONTENT": {
|
|
407
|
+
category: "reasoning",
|
|
408
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningmessagecontent",
|
|
409
|
+
fields: {
|
|
410
|
+
"messageId": {
|
|
411
|
+
kind: "string",
|
|
412
|
+
required: true
|
|
413
|
+
},
|
|
414
|
+
"delta": {
|
|
415
|
+
kind: "string",
|
|
416
|
+
required: true
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
"REASONING_MESSAGE_END": {
|
|
421
|
+
category: "reasoning",
|
|
422
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningmessageend",
|
|
423
|
+
fields: { "messageId": {
|
|
424
|
+
kind: "string",
|
|
425
|
+
required: true
|
|
426
|
+
} }
|
|
427
|
+
},
|
|
428
|
+
"REASONING_MESSAGE_CHUNK": {
|
|
429
|
+
category: "reasoning",
|
|
430
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningmessagechunk",
|
|
431
|
+
fields: {
|
|
432
|
+
"messageId": {
|
|
433
|
+
kind: "string",
|
|
434
|
+
required: false
|
|
435
|
+
},
|
|
436
|
+
"delta": {
|
|
437
|
+
kind: "string",
|
|
438
|
+
required: false
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
"REASONING_END": {
|
|
443
|
+
category: "reasoning",
|
|
444
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningend",
|
|
445
|
+
fields: { "messageId": {
|
|
446
|
+
kind: "string",
|
|
447
|
+
required: true
|
|
448
|
+
} }
|
|
449
|
+
},
|
|
450
|
+
"REASONING_ENCRYPTED_VALUE": {
|
|
451
|
+
category: "reasoning",
|
|
452
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningencryptedvalue",
|
|
453
|
+
fields: {
|
|
454
|
+
"subtype": {
|
|
455
|
+
kind: "string",
|
|
456
|
+
required: true,
|
|
457
|
+
enum: ["tool-call", "message"]
|
|
458
|
+
},
|
|
459
|
+
"entityId": {
|
|
460
|
+
kind: "string",
|
|
461
|
+
required: true
|
|
462
|
+
},
|
|
463
|
+
"encryptedValue": {
|
|
464
|
+
kind: "string",
|
|
465
|
+
required: true
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
/** All canonical wire `type` values, in @ag-ui/core enum order. */
|
|
471
|
+
const EVENT_TYPES = Object.keys(EVENT_TABLE);
|
|
472
|
+
/**
|
|
473
|
+
* Wire types documented as drafts (https://docs.ag-ui.com/drafts/overview) but
|
|
474
|
+
* not yet in @ag-ui/core. Not errors: reported at info severity.
|
|
475
|
+
*/
|
|
476
|
+
const DRAFT_EVENT_TYPES = ["META"];
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region src/rules/catalog.json
|
|
479
|
+
var catalog_default = {
|
|
480
|
+
$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.",
|
|
481
|
+
catalogVersion: "0.1.0",
|
|
482
|
+
spec: "0.x",
|
|
483
|
+
rules: [
|
|
484
|
+
{
|
|
485
|
+
"id": "AGUI001",
|
|
486
|
+
"severity": "error",
|
|
487
|
+
"title": "Run does not start with RUN_STARTED",
|
|
488
|
+
"messageTemplate": "First event of the run is {type}; expected RUN_STARTED",
|
|
489
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#runstarted",
|
|
490
|
+
"specQuote": "The RunStarted event is the first event emitted when an agent begins processing a request.",
|
|
491
|
+
"since": "0.x",
|
|
492
|
+
"checkedIn": "core"
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
"id": "AGUI002",
|
|
496
|
+
"severity": "error",
|
|
497
|
+
"title": "Multiple RUN_STARTED in one run",
|
|
498
|
+
"messageTemplate": "Duplicate RUN_STARTED (runId '{runId}') before the active run terminated",
|
|
499
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#lifecycle-events",
|
|
500
|
+
"specQuote": "The RunStarted and either RunFinished or RunError events are mandatory, forming the boundaries of an agent run.",
|
|
501
|
+
"since": "0.x",
|
|
502
|
+
"checkedIn": "core"
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
"id": "AGUI003",
|
|
506
|
+
"severity": "error",
|
|
507
|
+
"title": "Run not terminated",
|
|
508
|
+
"messageTemplate": "Run '{runId}' ended without RUN_FINISHED or RUN_ERROR",
|
|
509
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#runfinished",
|
|
510
|
+
"specQuote": "Every run terminates with either RunFinished or RunError.",
|
|
511
|
+
"since": "0.x",
|
|
512
|
+
"checkedIn": "core"
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
"id": "AGUI004",
|
|
516
|
+
"severity": "error",
|
|
517
|
+
"title": "Event after terminal event",
|
|
518
|
+
"messageTemplate": "{type} follows the run's terminal {terminalType}",
|
|
519
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#lifecycle-events",
|
|
520
|
+
"specQuote": "The RunStarted and either RunFinished or RunError events are mandatory, forming the boundaries of an agent run.",
|
|
521
|
+
"since": "0.x",
|
|
522
|
+
"checkedIn": "core"
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"id": "AGUI005",
|
|
526
|
+
"severity": "error",
|
|
527
|
+
"title": "RUN_FINISHED and RUN_ERROR are mutually exclusive",
|
|
528
|
+
"messageTemplate": "{type} emitted after the run already terminated with {terminalType}",
|
|
529
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#runfinished",
|
|
530
|
+
"specQuote": "Every run terminates with either RunFinished or RunError.",
|
|
531
|
+
"since": "0.x",
|
|
532
|
+
"checkedIn": "core"
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
"id": "AGUI006",
|
|
536
|
+
"severity": "error",
|
|
537
|
+
"title": "STEP_FINISHED without matching STEP_STARTED",
|
|
538
|
+
"messageTemplate": "STEP_FINISHED '{stepName}' has no open STEP_STARTED",
|
|
539
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#stepfinished",
|
|
540
|
+
"specQuote": "The stepName must match the corresponding StepStarted event.",
|
|
541
|
+
"since": "0.x",
|
|
542
|
+
"checkedIn": "core"
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
"id": "AGUI007",
|
|
546
|
+
"severity": "error",
|
|
547
|
+
"title": "Step unterminated at run end",
|
|
548
|
+
"messageTemplate": "STEP_STARTED '{stepName}' never finished",
|
|
549
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#stepstarted",
|
|
550
|
+
"specQuote": "The stepName must match the corresponding StepStarted event.",
|
|
551
|
+
"since": "0.x",
|
|
552
|
+
"checkedIn": "core"
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
"id": "AGUI008",
|
|
556
|
+
"severity": "warning",
|
|
557
|
+
"title": "Unstable threadId/runId across the run",
|
|
558
|
+
"messageTemplate": "RUN_FINISHED {field} '{actual}' does not match RUN_STARTED {field} '{expected}'",
|
|
559
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#runstarted",
|
|
560
|
+
"specQuote": "It also provides crucial identifiers that can be used to associate subsequent events with this specific run.",
|
|
561
|
+
"since": "0.x",
|
|
562
|
+
"checkedIn": "core"
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
"id": "AGUI101",
|
|
566
|
+
"severity": "error",
|
|
567
|
+
"title": "TEXT_MESSAGE_CONTENT without start",
|
|
568
|
+
"messageTemplate": "TEXT_MESSAGE_CONTENT for messageId '{messageId}' with no open TEXT_MESSAGE_START",
|
|
569
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#text-message-events",
|
|
570
|
+
"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.",
|
|
571
|
+
"since": "0.x",
|
|
572
|
+
"feature": "agentic-chat",
|
|
573
|
+
"checkedIn": "core"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
"id": "AGUI102",
|
|
577
|
+
"severity": "error",
|
|
578
|
+
"title": "TEXT_MESSAGE_END without start",
|
|
579
|
+
"messageTemplate": "TEXT_MESSAGE_END for messageId '{messageId}' with no open TEXT_MESSAGE_START",
|
|
580
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#textmessageend",
|
|
581
|
+
"specQuote": "messageId: Matches the ID from TextMessageStart",
|
|
582
|
+
"since": "0.x",
|
|
583
|
+
"feature": "agentic-chat",
|
|
584
|
+
"checkedIn": "core"
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
"id": "AGUI103",
|
|
588
|
+
"severity": "error",
|
|
589
|
+
"title": "Text message unterminated at run end",
|
|
590
|
+
"messageTemplate": "TEXT_MESSAGE_START messageId '{messageId}' never ended",
|
|
591
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#text-message-events",
|
|
592
|
+
"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.",
|
|
593
|
+
"since": "0.x",
|
|
594
|
+
"feature": "agentic-chat",
|
|
595
|
+
"checkedIn": "core"
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
"id": "AGUI104",
|
|
599
|
+
"severity": "error",
|
|
600
|
+
"title": "Duplicate messageId within a run",
|
|
601
|
+
"messageTemplate": "messageId '{messageId}' was already used by a completed message",
|
|
602
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#textmessagestart",
|
|
603
|
+
"specQuote": "It establishes a unique messageId that will be referenced by subsequent content chunks and the end event.",
|
|
604
|
+
"since": "0.x",
|
|
605
|
+
"feature": "agentic-chat",
|
|
606
|
+
"checkedIn": "core"
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
"id": "AGUI105",
|
|
610
|
+
"severity": "warning",
|
|
611
|
+
"title": "Empty content delta",
|
|
612
|
+
"messageTemplate": "TEXT_MESSAGE_CONTENT for messageId '{messageId}' has an empty delta",
|
|
613
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#textmessagecontent",
|
|
614
|
+
"specQuote": "delta: Text content chunk (non-empty)",
|
|
615
|
+
"since": "0.x",
|
|
616
|
+
"feature": "agentic-chat",
|
|
617
|
+
"specQuestion": "SQ-2",
|
|
618
|
+
"checkedIn": "core"
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
"id": "AGUI106",
|
|
622
|
+
"severity": "error",
|
|
623
|
+
"title": "Interleaved message streams sharing a messageId",
|
|
624
|
+
"messageTemplate": "TEXT_MESSAGE_START for messageId '{messageId}', which is already open",
|
|
625
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#implementation-considerations",
|
|
626
|
+
"specQuote": "Events with the same ID (e.g., messageId, toolCallId) belong to the same logical stream",
|
|
627
|
+
"since": "0.x",
|
|
628
|
+
"feature": "agentic-chat",
|
|
629
|
+
"checkedIn": "core"
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
"id": "AGUI201",
|
|
633
|
+
"severity": "error",
|
|
634
|
+
"title": "TOOL_CALL_ARGS without start",
|
|
635
|
+
"messageTemplate": "TOOL_CALL_ARGS for toolCallId '{toolCallId}' with no open TOOL_CALL_START",
|
|
636
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#tool-call-events",
|
|
637
|
+
"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.",
|
|
638
|
+
"since": "0.x",
|
|
639
|
+
"feature": "backend-tool-rendering",
|
|
640
|
+
"checkedIn": "core"
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
"id": "AGUI202",
|
|
644
|
+
"severity": "error",
|
|
645
|
+
"title": "TOOL_CALL_END without start",
|
|
646
|
+
"messageTemplate": "TOOL_CALL_END for toolCallId '{toolCallId}' with no open TOOL_CALL_START",
|
|
647
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#toolcallend",
|
|
648
|
+
"specQuote": "toolCallId: Matches the ID from ToolCallStart",
|
|
649
|
+
"since": "0.x",
|
|
650
|
+
"feature": "backend-tool-rendering",
|
|
651
|
+
"checkedIn": "core"
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
"id": "AGUI203",
|
|
655
|
+
"severity": "error",
|
|
656
|
+
"title": "Unterminated tool call",
|
|
657
|
+
"messageTemplate": "TOOL_CALL_START id '{toolCallId}' never terminated",
|
|
658
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#tool-call-events",
|
|
659
|
+
"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.",
|
|
660
|
+
"since": "0.x",
|
|
661
|
+
"feature": "backend-tool-rendering",
|
|
662
|
+
"checkedIn": "core"
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
"id": "AGUI204",
|
|
666
|
+
"severity": "error",
|
|
667
|
+
"title": "Tool call arguments are not valid JSON",
|
|
668
|
+
"messageTemplate": "Concatenated TOOL_CALL_ARGS for toolCallId '{toolCallId}' do not parse as JSON: {error}",
|
|
669
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#toolcallargs",
|
|
670
|
+
"specQuote": "Frontends should concatenate these deltas in the order received to construct the complete arguments object.",
|
|
671
|
+
"since": "0.x",
|
|
672
|
+
"feature": "backend-tool-rendering",
|
|
673
|
+
"checkedIn": "core"
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
"id": "AGUI205",
|
|
677
|
+
"severity": "error",
|
|
678
|
+
"title": "Duplicate toolCallId within a run",
|
|
679
|
+
"messageTemplate": "toolCallId '{toolCallId}' was already used by a completed tool call",
|
|
680
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#toolcallstart",
|
|
681
|
+
"specQuote": "toolCallId: Unique identifier for the tool call",
|
|
682
|
+
"since": "0.x",
|
|
683
|
+
"feature": "backend-tool-rendering",
|
|
684
|
+
"checkedIn": "core"
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
"id": "AGUI206",
|
|
688
|
+
"severity": "warning",
|
|
689
|
+
"title": "TOOL_CALL_RESULT before TOOL_CALL_END",
|
|
690
|
+
"messageTemplate": "TOOL_CALL_RESULT for toolCallId '{toolCallId}' arrived while the call is still open",
|
|
691
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#toolcallresult",
|
|
692
|
+
"specQuote": "This event is sent after the tool has been executed by the system and contains the actual output generated by the tool.",
|
|
693
|
+
"since": "0.x",
|
|
694
|
+
"feature": "backend-tool-rendering",
|
|
695
|
+
"checkedIn": "core"
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
"id": "AGUI207",
|
|
699
|
+
"severity": "error",
|
|
700
|
+
"title": "TOOL_CALL_RESULT references unknown toolCallId",
|
|
701
|
+
"messageTemplate": "TOOL_CALL_RESULT references toolCallId '{toolCallId}', which was never started",
|
|
702
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#toolcallresult",
|
|
703
|
+
"specQuote": "toolCallId: Matches the ID from the corresponding ToolCallStart event",
|
|
704
|
+
"since": "0.x",
|
|
705
|
+
"feature": "backend-tool-rendering",
|
|
706
|
+
"checkedIn": "core"
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
"id": "AGUI208",
|
|
710
|
+
"severity": "info",
|
|
711
|
+
"title": "parentMessageId references unknown message",
|
|
712
|
+
"messageTemplate": "TOOL_CALL_START parentMessageId '{parentMessageId}' matches no message observed in this stream",
|
|
713
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#toolcallstart",
|
|
714
|
+
"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.",
|
|
715
|
+
"since": "0.x",
|
|
716
|
+
"feature": "backend-tool-rendering",
|
|
717
|
+
"specQuestion": "SQ-11",
|
|
718
|
+
"checkedIn": "core"
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
"id": "AGUI301",
|
|
722
|
+
"severity": "info",
|
|
723
|
+
"title": "STATE_DELTA before any STATE_SNAPSHOT",
|
|
724
|
+
"messageTemplate": "STATE_DELTA precedes any STATE_SNAPSHOT; the base it applies to is not observable on this stream",
|
|
725
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#statesnapshot",
|
|
726
|
+
"specQuote": "This event is typically sent at the beginning of an interaction or when synchronization is needed.",
|
|
727
|
+
"since": "0.x",
|
|
728
|
+
"feature": "shared-state",
|
|
729
|
+
"specQuestion": "SQ-1",
|
|
730
|
+
"checkedIn": "core"
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
"id": "AGUI302",
|
|
734
|
+
"severity": "error",
|
|
735
|
+
"title": "STATE_DELTA failed to apply",
|
|
736
|
+
"messageTemplate": "STATE_DELTA failed to apply: {error}",
|
|
737
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#statedelta",
|
|
738
|
+
"specQuote": "Each delta represents specific changes to apply to the current state model.",
|
|
739
|
+
"since": "0.x",
|
|
740
|
+
"feature": "shared-state",
|
|
741
|
+
"checkedIn": "core"
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
"id": "AGUI303",
|
|
745
|
+
"severity": "error",
|
|
746
|
+
"title": "STATE_DELTA is not a valid RFC 6902 patch document",
|
|
747
|
+
"messageTemplate": "STATE_DELTA is not a valid RFC 6902 patch document: {error}",
|
|
748
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#statedelta",
|
|
749
|
+
"specQuote": "The StateDelta event contains incremental updates to the agent's state in the form of JSON Patch operations (as defined in RFC 6902).",
|
|
750
|
+
"since": "0.x",
|
|
751
|
+
"feature": "shared-state",
|
|
752
|
+
"checkedIn": "core"
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
"id": "AGUI304",
|
|
756
|
+
"severity": "info",
|
|
757
|
+
"title": "Mid-run STATE_SNAPSHOT discards accumulated deltas",
|
|
758
|
+
"messageTemplate": "STATE_SNAPSHOT replaces state previously built from {deltaCount} delta(s)",
|
|
759
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#statesnapshot",
|
|
760
|
+
"specQuote": "This event is typically sent at the beginning of an interaction or when synchronization is needed.",
|
|
761
|
+
"since": "0.x",
|
|
762
|
+
"feature": "shared-state",
|
|
763
|
+
"checkedIn": "core"
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
"id": "AGUI305",
|
|
767
|
+
"severity": "warning",
|
|
768
|
+
"title": "Shared state declared but never established",
|
|
769
|
+
"messageTemplate": "features include 'shared-state' but no STATE_SNAPSHOT was emitted",
|
|
770
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#state-management-events",
|
|
771
|
+
"specQuote": "These events are used to manage and synchronize the agent's state with the frontend.",
|
|
772
|
+
"since": "0.x",
|
|
773
|
+
"feature": "shared-state",
|
|
774
|
+
"requiresFeature": true,
|
|
775
|
+
"checkedIn": "core"
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
"id": "AGUI401",
|
|
779
|
+
"severity": "error",
|
|
780
|
+
"title": "REASONING_MESSAGE_CONTENT without start",
|
|
781
|
+
"messageTemplate": "REASONING_MESSAGE_CONTENT for messageId '{messageId}' with no open REASONING_MESSAGE_START",
|
|
782
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#reasoningmessagecontent",
|
|
783
|
+
"specQuote": "Multiple content events with the same messageId should be concatenated to form the complete visible reasoning.",
|
|
784
|
+
"since": "0.x",
|
|
785
|
+
"specQuestion": "SQ-4",
|
|
786
|
+
"checkedIn": "core"
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
"id": "AGUI402",
|
|
790
|
+
"severity": "warning",
|
|
791
|
+
"title": "Reasoning unterminated at run end",
|
|
792
|
+
"messageTemplate": "{startType} messageId '{messageId}' never ended",
|
|
793
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#reasoning-events",
|
|
794
|
+
"specQuote": "Reasoning events support LLM reasoning visibility and continuity, enabling chain-of-thought reasoning while maintaining privacy.",
|
|
795
|
+
"since": "0.x",
|
|
796
|
+
"specQuestion": "SQ-4",
|
|
797
|
+
"checkedIn": "core"
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
"id": "AGUI501",
|
|
801
|
+
"severity": "error",
|
|
802
|
+
"title": "Malformed SSE framing",
|
|
803
|
+
"messageTemplate": "Malformed SSE framing: {detail}",
|
|
804
|
+
"specUrl": "https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation",
|
|
805
|
+
"since": "0.x",
|
|
806
|
+
"checkedIn": "transport"
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
"id": "AGUI502",
|
|
810
|
+
"severity": "error",
|
|
811
|
+
"title": "Event payload is not valid JSON",
|
|
812
|
+
"messageTemplate": "Event payload is not valid JSON: {error}",
|
|
813
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#base-event-properties",
|
|
814
|
+
"specQuote": "All events share a common set of base properties.",
|
|
815
|
+
"since": "0.x",
|
|
816
|
+
"checkedIn": "core"
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
"id": "AGUI503",
|
|
820
|
+
"severity": "error",
|
|
821
|
+
"title": "Unknown event type",
|
|
822
|
+
"messageTemplate": "Unknown event type '{type}' (not in @ag-ui/core v{sdkVersion}, and not RAW or CUSTOM)",
|
|
823
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#event-types-overview",
|
|
824
|
+
"specQuote": "Events in the protocol are categorized by their purpose.",
|
|
825
|
+
"since": "0.x",
|
|
826
|
+
"checkedIn": "core"
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
"id": "AGUI504",
|
|
830
|
+
"severity": "error",
|
|
831
|
+
"title": "Event fails schema validation for its declared type",
|
|
832
|
+
"messageTemplate": "{type}: {detail}",
|
|
833
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#base-event-properties",
|
|
834
|
+
"specQuote": "All events share a common set of base properties.",
|
|
835
|
+
"since": "0.x",
|
|
836
|
+
"checkedIn": "core"
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
"id": "AGUI505",
|
|
840
|
+
"severity": "warning",
|
|
841
|
+
"title": "Unexpected Content-Type",
|
|
842
|
+
"messageTemplate": "Content-Type '{contentType}' is neither text/event-stream nor application/x-ndjson",
|
|
843
|
+
"specUrl": "https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model",
|
|
844
|
+
"since": "0.x",
|
|
845
|
+
"checkedIn": "transport"
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
"id": "AGUI506",
|
|
849
|
+
"severity": "info",
|
|
850
|
+
"title": "No keepalive frame within the configured window",
|
|
851
|
+
"messageTemplate": "No event or keepalive frame for {seconds}s",
|
|
852
|
+
"specUrl": "https://docs.ag-ui.com/concepts/architecture#standard-http-client",
|
|
853
|
+
"since": "0.x",
|
|
854
|
+
"specQuestion": "SQ-5",
|
|
855
|
+
"checkedIn": "transport"
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
"id": "AGUI507",
|
|
859
|
+
"severity": "info",
|
|
860
|
+
"title": "Response appears buffered rather than incrementally flushed",
|
|
861
|
+
"messageTemplate": "Response appears buffered: {detail}",
|
|
862
|
+
"specUrl": "https://docs.ag-ui.com/concepts/architecture#standard-http-client",
|
|
863
|
+
"since": "0.x",
|
|
864
|
+
"specQuestion": "SQ-5",
|
|
865
|
+
"checkedIn": "transport"
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
"id": "AGUI508",
|
|
869
|
+
"severity": "error",
|
|
870
|
+
"title": "Stream ended without a terminal event",
|
|
871
|
+
"messageTemplate": "Connection ended mid-run '{runId}' without RUN_FINISHED or RUN_ERROR",
|
|
872
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#runfinished",
|
|
873
|
+
"specQuote": "Every run terminates with either RunFinished or RunError.",
|
|
874
|
+
"since": "0.x",
|
|
875
|
+
"checkedIn": "transport"
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
"id": "AGUI901",
|
|
879
|
+
"severity": "info",
|
|
880
|
+
"title": "RAW event wraps a typed AG-UI event",
|
|
881
|
+
"messageTemplate": "RAW event wraps an event of type '{wrappedType}', which has a typed AG-UI equivalent",
|
|
882
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#raw",
|
|
883
|
+
"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.",
|
|
884
|
+
"since": "0.x",
|
|
885
|
+
"checkedIn": "core"
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
"id": "AGUI902",
|
|
889
|
+
"severity": "info",
|
|
890
|
+
"title": "Events carry no timestamps",
|
|
891
|
+
"messageTemplate": "None of the {eventCount} events carry the optional timestamp property",
|
|
892
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#base-event-properties",
|
|
893
|
+
"specQuote": "timestamp: Optional timestamp indicating when the event was created",
|
|
894
|
+
"since": "0.x",
|
|
895
|
+
"checkedIn": "core"
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
"id": "AGUI903",
|
|
899
|
+
"severity": "info",
|
|
900
|
+
"title": "CUSTOM event name is not namespaced",
|
|
901
|
+
"messageTemplate": "CUSTOM event name '{name}' has no namespace prefix (e.g. 'vendor.event')",
|
|
902
|
+
"specUrl": "https://docs.ag-ui.com/concepts/events#custom",
|
|
903
|
+
"specQuote": "Teams should document their custom events to ensure consistent implementation across frontends and agents.",
|
|
904
|
+
"since": "0.x",
|
|
905
|
+
"specQuestion": "SQ-14",
|
|
906
|
+
"checkedIn": "core"
|
|
907
|
+
}
|
|
908
|
+
]
|
|
909
|
+
};
|
|
910
|
+
//#endregion
|
|
911
|
+
//#region src/rules/catalog.ts
|
|
912
|
+
const SEVERITIES = [
|
|
913
|
+
"error",
|
|
914
|
+
"warning",
|
|
915
|
+
"info"
|
|
916
|
+
];
|
|
917
|
+
const LAYERS = ["core", "transport"];
|
|
918
|
+
/** Validates catalog data and returns it typed. Throws on structural problems. */
|
|
919
|
+
function validateCatalog(data) {
|
|
920
|
+
const problems = [];
|
|
921
|
+
const cat = data;
|
|
922
|
+
if (typeof cat !== "object" || cat === null || !Array.isArray(cat.rules)) throw new Error("rule catalog: expected an object with a rules array");
|
|
923
|
+
const seen = /* @__PURE__ */ new Set();
|
|
924
|
+
for (const rule of cat.rules) {
|
|
925
|
+
const where = rule?.id ?? "<missing id>";
|
|
926
|
+
if (!/^AGUI\d{3}$/.test(rule.id ?? "")) problems.push(`${where}: id must match AGUI###`);
|
|
927
|
+
if (seen.has(rule.id)) problems.push(`${where}: duplicate id`);
|
|
928
|
+
seen.add(rule.id);
|
|
929
|
+
if (!SEVERITIES.includes(rule.severity)) problems.push(`${where}: bad severity '${rule.severity}'`);
|
|
930
|
+
if (!rule.title) problems.push(`${where}: missing title`);
|
|
931
|
+
if (!rule.messageTemplate) problems.push(`${where}: missing messageTemplate`);
|
|
932
|
+
if (!rule.specUrl?.startsWith("https://")) problems.push(`${where}: specUrl must be an https URL`);
|
|
933
|
+
if (!rule.since) problems.push(`${where}: missing since`);
|
|
934
|
+
if (!LAYERS.includes(rule.checkedIn)) problems.push(`${where}: bad checkedIn '${rule.checkedIn}'`);
|
|
935
|
+
if (rule.requiresFeature && !rule.feature) problems.push(`${where}: requiresFeature without feature`);
|
|
936
|
+
}
|
|
937
|
+
if (problems.length > 0) throw new Error(`rule catalog is invalid:\n ${problems.join("\n ")}`);
|
|
938
|
+
return cat;
|
|
939
|
+
}
|
|
940
|
+
const CATALOG = validateCatalog(catalog_default);
|
|
941
|
+
const RULES = new Map(CATALOG.rules.map((r) => [r.id, r]));
|
|
942
|
+
/** Fills a rule's messageTemplate. Unknown placeholders are left intact. */
|
|
943
|
+
function formatMessage(rule, params) {
|
|
944
|
+
return rule.messageTemplate.replace(/\{(\w+)\}/g, (whole, key) => key in params ? String(params[key]) : whole);
|
|
945
|
+
}
|
|
946
|
+
//#endregion
|
|
947
|
+
//#region src/rules/checks/context.ts
|
|
948
|
+
function newRunState(init) {
|
|
949
|
+
return {
|
|
950
|
+
...init,
|
|
951
|
+
terminal: null,
|
|
952
|
+
openMessages: /* @__PURE__ */ new Map(),
|
|
953
|
+
closedMessages: /* @__PURE__ */ new Map(),
|
|
954
|
+
knownMessageIds: /* @__PURE__ */ new Set(),
|
|
955
|
+
openToolCalls: /* @__PURE__ */ new Map(),
|
|
956
|
+
closedToolCalls: /* @__PURE__ */ new Map(),
|
|
957
|
+
knownToolCallIds: /* @__PURE__ */ new Set(),
|
|
958
|
+
openSteps: /* @__PURE__ */ new Map(),
|
|
959
|
+
openReasoningBlocks: /* @__PURE__ */ new Map(),
|
|
960
|
+
openReasoningMessages: /* @__PURE__ */ new Map(),
|
|
961
|
+
state: {
|
|
962
|
+
known: false,
|
|
963
|
+
value: void 0,
|
|
964
|
+
deltasSinceSnapshot: 0,
|
|
965
|
+
snapshotSeen: false,
|
|
966
|
+
agui301Fired: false
|
|
967
|
+
},
|
|
968
|
+
textChunk: null,
|
|
969
|
+
toolChunk: null,
|
|
970
|
+
reasoningChunk: null
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
/** Reads a field only if it is a string (schema problems already reported). */
|
|
974
|
+
function str(event, field) {
|
|
975
|
+
const v = event[field];
|
|
976
|
+
return typeof v === "string" ? v : void 0;
|
|
977
|
+
}
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region src/rules/checks/lifecycle.ts
|
|
980
|
+
function handleStepEvent(api) {
|
|
981
|
+
const { type, event, run, emit, index } = api;
|
|
982
|
+
const stepName = str(event, "stepName");
|
|
983
|
+
if (stepName === void 0) return;
|
|
984
|
+
if (type === "STEP_STARTED") {
|
|
985
|
+
const open = run.openSteps.get(stepName);
|
|
986
|
+
if (open !== void 0) open.count += 1;
|
|
987
|
+
else run.openSteps.set(stepName, {
|
|
988
|
+
count: 1,
|
|
989
|
+
firstIndex: index
|
|
990
|
+
});
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (type === "STEP_FINISHED") {
|
|
994
|
+
const open = run.openSteps.get(stepName);
|
|
995
|
+
if (open === void 0) {
|
|
996
|
+
emit("AGUI006", { stepName }, { pointer: "/stepName" });
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
open.count -= 1;
|
|
1000
|
+
if (open.count === 0) run.openSteps.delete(stepName);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/** AGUI008 — RUN_FINISHED must carry the ids RUN_STARTED established. */
|
|
1004
|
+
function checkRunIdStability(api) {
|
|
1005
|
+
const { event, run, emit } = api;
|
|
1006
|
+
if (run.implicit) return;
|
|
1007
|
+
for (const field of ["threadId", "runId"]) {
|
|
1008
|
+
const actual = str(api.event, field);
|
|
1009
|
+
const expected = field === "threadId" ? run.threadId : run.runId;
|
|
1010
|
+
if (actual !== void 0 && expected !== null && actual !== expected) emit("AGUI008", {
|
|
1011
|
+
field,
|
|
1012
|
+
actual,
|
|
1013
|
+
expected
|
|
1014
|
+
}, {
|
|
1015
|
+
pointer: `/${field}`,
|
|
1016
|
+
relatedEventIndex: run.startIndex
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
/** AGUI007 — open steps when the run reaches a clean end. */
|
|
1021
|
+
function endOfRunSteps(run, emit, atIndex) {
|
|
1022
|
+
for (const [stepName, open] of run.openSteps) emit("AGUI007", { stepName }, {
|
|
1023
|
+
eventIndex: atIndex,
|
|
1024
|
+
relatedEventIndex: open.firstIndex
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
//#endregion
|
|
1028
|
+
//#region src/rules/checks/reasoning.ts
|
|
1029
|
+
function handleReasoningEvent(api) {
|
|
1030
|
+
const { type, event, run, emit, index } = api;
|
|
1031
|
+
switch (type) {
|
|
1032
|
+
case "REASONING_START": {
|
|
1033
|
+
const id = str(event, "messageId");
|
|
1034
|
+
if (id !== void 0) run.openReasoningBlocks.set(id, index);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
case "REASONING_END": {
|
|
1038
|
+
const id = str(event, "messageId");
|
|
1039
|
+
if (id !== void 0) run.openReasoningBlocks.delete(id);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
case "REASONING_MESSAGE_START": {
|
|
1043
|
+
const id = str(event, "messageId");
|
|
1044
|
+
if (id !== void 0) run.openReasoningMessages.set(id, index);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
case "REASONING_MESSAGE_CONTENT": {
|
|
1048
|
+
const id = str(event, "messageId");
|
|
1049
|
+
if (id === void 0) return;
|
|
1050
|
+
const openChunk = run.reasoningChunk !== null && run.reasoningChunk.messageId === id;
|
|
1051
|
+
if (!run.openReasoningMessages.has(id) && !openChunk) emit("AGUI401", { messageId: id }, { pointer: "/messageId" });
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
case "REASONING_MESSAGE_END": {
|
|
1055
|
+
const id = str(event, "messageId");
|
|
1056
|
+
if (id !== void 0) run.openReasoningMessages.delete(id);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
case "REASONING_MESSAGE_CHUNK": {
|
|
1060
|
+
const id = str(event, "messageId");
|
|
1061
|
+
if (id === void 0) {
|
|
1062
|
+
if (run.reasoningChunk === null) {
|
|
1063
|
+
emit("AGUI504", {
|
|
1064
|
+
type,
|
|
1065
|
+
detail: "first REASONING_MESSAGE_CHUNK must include messageId"
|
|
1066
|
+
}, { pointer: "/messageId" });
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (event.delta === "") run.reasoningChunk = null;
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (run.reasoningChunk !== null && run.reasoningChunk.messageId !== id) run.reasoningChunk = null;
|
|
1073
|
+
if (event.delta === "") {
|
|
1074
|
+
run.reasoningChunk = null;
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (run.reasoningChunk === null) run.reasoningChunk = {
|
|
1078
|
+
messageId: id,
|
|
1079
|
+
startIndex: index
|
|
1080
|
+
};
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
/** Chunked reasoning also closes on any non-reasoning event (documented). */
|
|
1086
|
+
function closeReasoningChunk(run) {
|
|
1087
|
+
run.reasoningChunk = null;
|
|
1088
|
+
}
|
|
1089
|
+
/** AGUI402 — unterminated reasoning at a clean run end. */
|
|
1090
|
+
function endOfRunReasoning(run, emit, atIndex) {
|
|
1091
|
+
for (const [id, startIndex] of run.openReasoningBlocks) emit("AGUI402", {
|
|
1092
|
+
startType: "REASONING_START",
|
|
1093
|
+
messageId: id
|
|
1094
|
+
}, {
|
|
1095
|
+
eventIndex: atIndex,
|
|
1096
|
+
relatedEventIndex: startIndex
|
|
1097
|
+
});
|
|
1098
|
+
for (const [id, startIndex] of run.openReasoningMessages) emit("AGUI402", {
|
|
1099
|
+
startType: "REASONING_MESSAGE_START",
|
|
1100
|
+
messageId: id
|
|
1101
|
+
}, {
|
|
1102
|
+
eventIndex: atIndex,
|
|
1103
|
+
relatedEventIndex: startIndex
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
//#endregion
|
|
1107
|
+
//#region src/protocol/jsonpatch.ts
|
|
1108
|
+
const OPS = [
|
|
1109
|
+
"add",
|
|
1110
|
+
"remove",
|
|
1111
|
+
"replace",
|
|
1112
|
+
"move",
|
|
1113
|
+
"copy",
|
|
1114
|
+
"test"
|
|
1115
|
+
];
|
|
1116
|
+
/** RFC 6901: "" → whole document; "/a/b" → ["a", "b"]. Returns null when invalid. */
|
|
1117
|
+
function parsePointer(pointer) {
|
|
1118
|
+
if (typeof pointer !== "string") return null;
|
|
1119
|
+
if (pointer === "") return [];
|
|
1120
|
+
if (!pointer.startsWith("/")) return null;
|
|
1121
|
+
return pointer.slice(1).split("/").map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
1122
|
+
}
|
|
1123
|
+
/** Structural validation of a patch document, without applying it. */
|
|
1124
|
+
function validatePatchShape(patch) {
|
|
1125
|
+
if (!Array.isArray(patch)) return {
|
|
1126
|
+
error: "patch document must be an array of operations",
|
|
1127
|
+
opIndex: -1,
|
|
1128
|
+
pointer: ""
|
|
1129
|
+
};
|
|
1130
|
+
for (let i = 0; i < patch.length; i++) {
|
|
1131
|
+
const op = patch[i];
|
|
1132
|
+
if (typeof op !== "object" || op === null || Array.isArray(op)) return {
|
|
1133
|
+
error: `operation ${i} is not an object`,
|
|
1134
|
+
opIndex: i,
|
|
1135
|
+
pointer: `/${i}`
|
|
1136
|
+
};
|
|
1137
|
+
const o = op;
|
|
1138
|
+
if (!OPS.includes(o.op)) return {
|
|
1139
|
+
error: `operation ${i} has invalid op '${String(o.op)}'`,
|
|
1140
|
+
opIndex: i,
|
|
1141
|
+
pointer: `/${i}/op`
|
|
1142
|
+
};
|
|
1143
|
+
if (parsePointer(o.path) === null) return {
|
|
1144
|
+
error: `operation ${i} (${String(o.op)}) has invalid path`,
|
|
1145
|
+
opIndex: i,
|
|
1146
|
+
pointer: `/${i}/path`
|
|
1147
|
+
};
|
|
1148
|
+
if ((o.op === "add" || o.op === "replace" || o.op === "test") && !("value" in o)) return {
|
|
1149
|
+
error: `operation ${i} (${String(o.op)}) is missing value`,
|
|
1150
|
+
opIndex: i,
|
|
1151
|
+
pointer: `/${i}`
|
|
1152
|
+
};
|
|
1153
|
+
if ((o.op === "move" || o.op === "copy") && parsePointer(o.from) === null) return {
|
|
1154
|
+
error: `operation ${i} (${String(o.op)}) is missing or has invalid from`,
|
|
1155
|
+
opIndex: i,
|
|
1156
|
+
pointer: `/${i}`
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
function clone(value) {
|
|
1162
|
+
if (Array.isArray(value)) return value.map(clone);
|
|
1163
|
+
if (typeof value === "object" && value !== null) {
|
|
1164
|
+
const out = {};
|
|
1165
|
+
for (const [k, v] of Object.entries(value)) out[k] = clone(v);
|
|
1166
|
+
return out;
|
|
1167
|
+
}
|
|
1168
|
+
return value;
|
|
1169
|
+
}
|
|
1170
|
+
function deepEqual(a, b) {
|
|
1171
|
+
if (a === b) return true;
|
|
1172
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
|
|
1173
|
+
if (typeof a === "object" && a !== null && typeof b === "object" && b !== null && !Array.isArray(a) && !Array.isArray(b)) {
|
|
1174
|
+
const ka = Object.keys(a);
|
|
1175
|
+
const kb = Object.keys(b);
|
|
1176
|
+
return ka.length === kb.length && ka.every((k) => deepEqual(a[k], b[k]));
|
|
1177
|
+
}
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
function arrayIndex(token, length, allowAppend) {
|
|
1181
|
+
if (allowAppend && token === "-") return length;
|
|
1182
|
+
if (!/^(0|[1-9]\d*)$/.test(token)) return null;
|
|
1183
|
+
const i = Number(token);
|
|
1184
|
+
return i > length ? null : i;
|
|
1185
|
+
}
|
|
1186
|
+
function locate(doc, tokens, forAdd) {
|
|
1187
|
+
if (tokens.length === 0) return {
|
|
1188
|
+
parent: null,
|
|
1189
|
+
key: "",
|
|
1190
|
+
exists: true,
|
|
1191
|
+
value: doc
|
|
1192
|
+
};
|
|
1193
|
+
let current = doc;
|
|
1194
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
1195
|
+
const token = tokens[i];
|
|
1196
|
+
if (Array.isArray(current)) {
|
|
1197
|
+
const idx = arrayIndex(token, current.length, false);
|
|
1198
|
+
if (idx === null || idx >= current.length) return `path segment '/${token}' does not exist`;
|
|
1199
|
+
current = current[idx];
|
|
1200
|
+
} else if (typeof current === "object" && current !== null) {
|
|
1201
|
+
const obj = current;
|
|
1202
|
+
if (!(token in obj)) return `path segment '/${token}' does not exist`;
|
|
1203
|
+
current = obj[token];
|
|
1204
|
+
} else return `path segment '/${token}' is not an object or array`;
|
|
1205
|
+
}
|
|
1206
|
+
const last = tokens[tokens.length - 1];
|
|
1207
|
+
if (Array.isArray(current)) {
|
|
1208
|
+
const idx = arrayIndex(last, current.length, forAdd);
|
|
1209
|
+
if (idx === null) return `'${last}' is not a valid index for an array of length ${current.length}`;
|
|
1210
|
+
return {
|
|
1211
|
+
parent: current,
|
|
1212
|
+
key: idx,
|
|
1213
|
+
exists: idx < current.length,
|
|
1214
|
+
value: current[idx]
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
if (typeof current === "object" && current !== null) {
|
|
1218
|
+
const obj = current;
|
|
1219
|
+
return {
|
|
1220
|
+
parent: obj,
|
|
1221
|
+
key: last,
|
|
1222
|
+
exists: last in obj,
|
|
1223
|
+
value: obj[last]
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
return `target of '/${last}' is not an object or array`;
|
|
1227
|
+
}
|
|
1228
|
+
/** Applies an RFC 6902 patch. Validates shape first; never throws. */
|
|
1229
|
+
function applyPatch(doc, patch) {
|
|
1230
|
+
const shape = validatePatchShape(patch);
|
|
1231
|
+
if (shape !== null) return {
|
|
1232
|
+
ok: false,
|
|
1233
|
+
error: shape.error,
|
|
1234
|
+
opIndex: shape.opIndex
|
|
1235
|
+
};
|
|
1236
|
+
let result = clone(doc);
|
|
1237
|
+
const ops = patch;
|
|
1238
|
+
const getAt = (pointer) => {
|
|
1239
|
+
const loc = locate(result, parsePointer(pointer), false);
|
|
1240
|
+
if (typeof loc === "string") return {
|
|
1241
|
+
ok: false,
|
|
1242
|
+
error: `${pointer}: ${loc}`
|
|
1243
|
+
};
|
|
1244
|
+
if (!loc.exists) return {
|
|
1245
|
+
ok: false,
|
|
1246
|
+
error: `${pointer} does not exist`
|
|
1247
|
+
};
|
|
1248
|
+
return {
|
|
1249
|
+
ok: true,
|
|
1250
|
+
value: loc.value
|
|
1251
|
+
};
|
|
1252
|
+
};
|
|
1253
|
+
const setAt = (pointer, value, mustExist) => {
|
|
1254
|
+
const tokens = parsePointer(pointer);
|
|
1255
|
+
if (tokens.length === 0) {
|
|
1256
|
+
result = value;
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
const loc = locate(result, tokens, !mustExist);
|
|
1260
|
+
if (typeof loc === "string") return `${pointer}: ${loc}`;
|
|
1261
|
+
if (mustExist && !loc.exists) return `${pointer} does not exist`;
|
|
1262
|
+
if (Array.isArray(loc.parent)) {
|
|
1263
|
+
if (mustExist) loc.parent[loc.key] = value;
|
|
1264
|
+
else loc.parent.splice(loc.key, 0, value);
|
|
1265
|
+
} else loc.parent[loc.key] = value;
|
|
1266
|
+
return null;
|
|
1267
|
+
};
|
|
1268
|
+
const removeAt = (pointer) => {
|
|
1269
|
+
const tokens = parsePointer(pointer);
|
|
1270
|
+
if (tokens.length === 0) return {
|
|
1271
|
+
ok: false,
|
|
1272
|
+
error: "cannot remove the whole document"
|
|
1273
|
+
};
|
|
1274
|
+
const loc = locate(result, tokens, false);
|
|
1275
|
+
if (typeof loc === "string") return {
|
|
1276
|
+
ok: false,
|
|
1277
|
+
error: `${pointer}: ${loc}`
|
|
1278
|
+
};
|
|
1279
|
+
if (!loc.exists) return {
|
|
1280
|
+
ok: false,
|
|
1281
|
+
error: `${pointer} does not exist`
|
|
1282
|
+
};
|
|
1283
|
+
if (Array.isArray(loc.parent)) loc.parent.splice(loc.key, 1);
|
|
1284
|
+
else delete loc.parent[loc.key];
|
|
1285
|
+
return {
|
|
1286
|
+
ok: true,
|
|
1287
|
+
removed: loc.value
|
|
1288
|
+
};
|
|
1289
|
+
};
|
|
1290
|
+
for (let i = 0; i < ops.length; i++) {
|
|
1291
|
+
const op = ops[i];
|
|
1292
|
+
const path = op.path;
|
|
1293
|
+
let error = null;
|
|
1294
|
+
switch (op.op) {
|
|
1295
|
+
case "add":
|
|
1296
|
+
error = setAt(path, clone(op.value), false);
|
|
1297
|
+
break;
|
|
1298
|
+
case "replace":
|
|
1299
|
+
error = setAt(path, clone(op.value), true);
|
|
1300
|
+
break;
|
|
1301
|
+
case "remove": {
|
|
1302
|
+
const r = removeAt(path);
|
|
1303
|
+
if (!r.ok) error = r.error;
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
case "move": {
|
|
1307
|
+
const from = op.from;
|
|
1308
|
+
if (path.startsWith(`${from}/`)) {
|
|
1309
|
+
error = `cannot move ${from} into its own child ${path}`;
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
const r = removeAt(from);
|
|
1313
|
+
if (!r.ok) {
|
|
1314
|
+
error = r.error;
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
error = setAt(path, r.removed, false);
|
|
1318
|
+
break;
|
|
1319
|
+
}
|
|
1320
|
+
case "copy": {
|
|
1321
|
+
const r = getAt(op.from);
|
|
1322
|
+
if (!r.ok) {
|
|
1323
|
+
error = r.error;
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
error = setAt(path, clone(r.value), false);
|
|
1327
|
+
break;
|
|
1328
|
+
}
|
|
1329
|
+
case "test": {
|
|
1330
|
+
const r = getAt(path);
|
|
1331
|
+
if (!r.ok) error = r.error;
|
|
1332
|
+
else if (!deepEqual(r.value, op.value)) error = `test failed at ${path}`;
|
|
1333
|
+
break;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (error !== null) return {
|
|
1337
|
+
ok: false,
|
|
1338
|
+
error,
|
|
1339
|
+
opIndex: i
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
return {
|
|
1343
|
+
ok: true,
|
|
1344
|
+
result
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
//#endregion
|
|
1348
|
+
//#region src/rules/checks/state.ts
|
|
1349
|
+
function handleStateEvent(api) {
|
|
1350
|
+
const { type, event, run, stream, emit } = api;
|
|
1351
|
+
switch (type) {
|
|
1352
|
+
case "STATE_SNAPSHOT":
|
|
1353
|
+
api.feature("shared-state");
|
|
1354
|
+
if (run.state.deltasSinceSnapshot > 0) emit("AGUI304", { deltaCount: run.state.deltasSinceSnapshot }, {});
|
|
1355
|
+
run.state.known = true;
|
|
1356
|
+
run.state.value = event.snapshot;
|
|
1357
|
+
run.state.snapshotSeen = true;
|
|
1358
|
+
run.state.deltasSinceSnapshot = 0;
|
|
1359
|
+
stream.anySnapshot = true;
|
|
1360
|
+
return;
|
|
1361
|
+
case "STATE_DELTA": {
|
|
1362
|
+
api.feature("shared-state");
|
|
1363
|
+
const delta = event.delta;
|
|
1364
|
+
if (!Array.isArray(delta)) return;
|
|
1365
|
+
const shape = validatePatchShape(delta);
|
|
1366
|
+
if (shape !== null) {
|
|
1367
|
+
emit("AGUI303", { error: shape.error }, { pointer: `/delta${shape.pointer}` });
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (!run.state.snapshotSeen && !run.state.agui301Fired) {
|
|
1371
|
+
emit("AGUI301", {}, {});
|
|
1372
|
+
run.state.agui301Fired = true;
|
|
1373
|
+
}
|
|
1374
|
+
if (run.state.known) {
|
|
1375
|
+
const applied = applyPatch(run.state.value, delta);
|
|
1376
|
+
if (applied.ok) run.state.value = applied.result;
|
|
1377
|
+
else emit("AGUI302", { error: applied.error }, { pointer: `/delta/${applied.opIndex}` });
|
|
1378
|
+
}
|
|
1379
|
+
run.state.deltasSinceSnapshot += 1;
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
case "MESSAGES_SNAPSHOT": {
|
|
1383
|
+
const messages = event.messages;
|
|
1384
|
+
if (!Array.isArray(messages)) return;
|
|
1385
|
+
for (const message of messages) {
|
|
1386
|
+
if (typeof message !== "object" || message === null) continue;
|
|
1387
|
+
const m = message;
|
|
1388
|
+
if (typeof m.id === "string") run.knownMessageIds.add(m.id);
|
|
1389
|
+
if (Array.isArray(m.toolCalls)) {
|
|
1390
|
+
for (const call of m.toolCalls) if (typeof call === "object" && call !== null) {
|
|
1391
|
+
const id = call.id;
|
|
1392
|
+
if (typeof id === "string") run.knownToolCallIds.add(id);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
//#endregion
|
|
1401
|
+
//#region src/rules/checks/text.ts
|
|
1402
|
+
function handleTextEvent(api) {
|
|
1403
|
+
const { type, event, run, emit, index } = api;
|
|
1404
|
+
api.feature("agentic-chat");
|
|
1405
|
+
switch (type) {
|
|
1406
|
+
case "TEXT_MESSAGE_START": {
|
|
1407
|
+
const id = str(event, "messageId");
|
|
1408
|
+
if (id === void 0) return;
|
|
1409
|
+
if (run.openMessages.has(id)) {
|
|
1410
|
+
emit("AGUI106", { messageId: id }, {
|
|
1411
|
+
pointer: "/messageId",
|
|
1412
|
+
relatedEventIndex: run.openMessages.get(id).startIndex
|
|
1413
|
+
});
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
if (run.closedMessages.has(id)) {
|
|
1417
|
+
emit("AGUI104", { messageId: id }, {
|
|
1418
|
+
pointer: "/messageId",
|
|
1419
|
+
relatedEventIndex: run.closedMessages.get(id)
|
|
1420
|
+
});
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
run.openMessages.set(id, { startIndex: index });
|
|
1424
|
+
run.knownMessageIds.add(id);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
case "TEXT_MESSAGE_CONTENT": {
|
|
1428
|
+
const id = str(event, "messageId");
|
|
1429
|
+
if (id === void 0) return;
|
|
1430
|
+
const open = run.openMessages.get(id);
|
|
1431
|
+
if (open === void 0) {
|
|
1432
|
+
const closedAt = run.closedMessages.get(id);
|
|
1433
|
+
emit("AGUI101", { messageId: id }, {
|
|
1434
|
+
pointer: "/messageId",
|
|
1435
|
+
...closedAt !== void 0 ? { relatedEventIndex: closedAt } : {}
|
|
1436
|
+
});
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (event.delta === "") emit("AGUI105", { messageId: id }, {
|
|
1440
|
+
pointer: "/delta",
|
|
1441
|
+
relatedEventIndex: open.startIndex
|
|
1442
|
+
});
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
case "TEXT_MESSAGE_END": {
|
|
1446
|
+
const id = str(event, "messageId");
|
|
1447
|
+
if (id === void 0) return;
|
|
1448
|
+
if (!run.openMessages.has(id)) {
|
|
1449
|
+
emit("AGUI102", { messageId: id }, { pointer: "/messageId" });
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
run.openMessages.delete(id);
|
|
1453
|
+
run.closedMessages.set(id, index);
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
case "TEXT_MESSAGE_CHUNK": {
|
|
1457
|
+
const id = str(event, "messageId");
|
|
1458
|
+
if (id === void 0) {
|
|
1459
|
+
if (run.textChunk === null) emit("AGUI504", {
|
|
1460
|
+
type,
|
|
1461
|
+
detail: "first TEXT_MESSAGE_CHUNK for a message must include messageId"
|
|
1462
|
+
}, { pointer: "/messageId" });
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
if (run.openMessages.has(id)) return;
|
|
1466
|
+
if (run.textChunk !== null && run.textChunk.messageId === id) return;
|
|
1467
|
+
closeTextChunk(run, index);
|
|
1468
|
+
if (run.closedMessages.has(id)) {
|
|
1469
|
+
emit("AGUI104", { messageId: id }, {
|
|
1470
|
+
pointer: "/messageId",
|
|
1471
|
+
relatedEventIndex: run.closedMessages.get(id)
|
|
1472
|
+
});
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
run.textChunk = {
|
|
1476
|
+
messageId: id,
|
|
1477
|
+
startIndex: index
|
|
1478
|
+
};
|
|
1479
|
+
run.knownMessageIds.add(id);
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
/** Chunk streams close implicitly on the next non-chunk event. */
|
|
1485
|
+
function closeTextChunk(run, atIndex) {
|
|
1486
|
+
if (run.textChunk === null) return;
|
|
1487
|
+
run.closedMessages.set(run.textChunk.messageId, atIndex);
|
|
1488
|
+
run.textChunk = null;
|
|
1489
|
+
}
|
|
1490
|
+
/** AGUI103 — open messages when the run reaches a clean end. */
|
|
1491
|
+
function endOfRunText(run, emit, atIndex) {
|
|
1492
|
+
for (const [id, open] of run.openMessages) emit("AGUI103", { messageId: id }, {
|
|
1493
|
+
eventIndex: atIndex,
|
|
1494
|
+
relatedEventIndex: open.startIndex
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
//#endregion
|
|
1498
|
+
//#region src/rules/checks/toolcalls.ts
|
|
1499
|
+
function handleToolCallEvent(api) {
|
|
1500
|
+
const { type, event, run, emit, index } = api;
|
|
1501
|
+
api.feature("backend-tool-rendering");
|
|
1502
|
+
switch (type) {
|
|
1503
|
+
case "TOOL_CALL_START": {
|
|
1504
|
+
const id = str(event, "toolCallId");
|
|
1505
|
+
if (id === void 0) return;
|
|
1506
|
+
if (run.openToolCalls.has(id) || run.closedToolCalls.has(id)) {
|
|
1507
|
+
const related = run.openToolCalls.get(id)?.startIndex ?? run.closedToolCalls.get(id);
|
|
1508
|
+
emit("AGUI205", { toolCallId: id }, {
|
|
1509
|
+
pointer: "/toolCallId",
|
|
1510
|
+
relatedEventIndex: related
|
|
1511
|
+
});
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
run.openToolCalls.set(id, {
|
|
1515
|
+
startIndex: index,
|
|
1516
|
+
args: "",
|
|
1517
|
+
sawArgs: false
|
|
1518
|
+
});
|
|
1519
|
+
const parent = str(event, "parentMessageId");
|
|
1520
|
+
if (parent !== void 0 && !run.knownMessageIds.has(parent)) emit("AGUI208", { parentMessageId: parent }, { pointer: "/parentMessageId" });
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
case "TOOL_CALL_ARGS": {
|
|
1524
|
+
const id = str(event, "toolCallId");
|
|
1525
|
+
if (id === void 0) return;
|
|
1526
|
+
const open = run.openToolCalls.get(id);
|
|
1527
|
+
if (open === void 0) {
|
|
1528
|
+
emit("AGUI201", { toolCallId: id }, { pointer: "/toolCallId" });
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
const delta = str(event, "delta");
|
|
1532
|
+
if (delta !== void 0) {
|
|
1533
|
+
open.args += delta;
|
|
1534
|
+
open.sawArgs = true;
|
|
1535
|
+
}
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
case "TOOL_CALL_END": {
|
|
1539
|
+
const id = str(event, "toolCallId");
|
|
1540
|
+
if (id === void 0) return;
|
|
1541
|
+
const open = run.openToolCalls.get(id);
|
|
1542
|
+
if (open === void 0) {
|
|
1543
|
+
emit("AGUI202", { toolCallId: id }, { pointer: "/toolCallId" });
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
run.openToolCalls.delete(id);
|
|
1547
|
+
run.closedToolCalls.set(id, index);
|
|
1548
|
+
checkArgsJson(id, open, emit, index);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
case "TOOL_CALL_RESULT": {
|
|
1552
|
+
const id = str(event, "toolCallId");
|
|
1553
|
+
if (id !== void 0) {
|
|
1554
|
+
const open = run.openToolCalls.get(id);
|
|
1555
|
+
if (open !== void 0) emit("AGUI206", { toolCallId: id }, {
|
|
1556
|
+
pointer: "/toolCallId",
|
|
1557
|
+
relatedEventIndex: open.startIndex
|
|
1558
|
+
});
|
|
1559
|
+
else if (!run.closedToolCalls.has(id) && !run.knownToolCallIds.has(id)) emit("AGUI207", { toolCallId: id }, { pointer: "/toolCallId" });
|
|
1560
|
+
}
|
|
1561
|
+
const messageId = str(event, "messageId");
|
|
1562
|
+
if (messageId !== void 0) run.knownMessageIds.add(messageId);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
case "TOOL_CALL_CHUNK": {
|
|
1566
|
+
const id = str(event, "toolCallId");
|
|
1567
|
+
const delta = str(event, "delta");
|
|
1568
|
+
if (id === void 0) {
|
|
1569
|
+
if (run.toolChunk === null) {
|
|
1570
|
+
emit("AGUI504", {
|
|
1571
|
+
type,
|
|
1572
|
+
detail: "first TOOL_CALL_CHUNK for a tool call must include toolCallId and toolCallName"
|
|
1573
|
+
}, { pointer: "/toolCallId" });
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (delta !== void 0) {
|
|
1577
|
+
run.toolChunk.args += delta;
|
|
1578
|
+
run.toolChunk.sawArgs = true;
|
|
1579
|
+
}
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
const openExplicit = run.openToolCalls.get(id);
|
|
1583
|
+
if (openExplicit !== void 0) {
|
|
1584
|
+
if (delta !== void 0) {
|
|
1585
|
+
openExplicit.args += delta;
|
|
1586
|
+
openExplicit.sawArgs = true;
|
|
1587
|
+
}
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
if (run.toolChunk !== null && run.toolChunk.toolCallId === id) {
|
|
1591
|
+
if (delta !== void 0) {
|
|
1592
|
+
run.toolChunk.args += delta;
|
|
1593
|
+
run.toolChunk.sawArgs = true;
|
|
1594
|
+
}
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
closeToolChunk(run, emit, index);
|
|
1598
|
+
if (run.closedToolCalls.has(id)) {
|
|
1599
|
+
emit("AGUI205", { toolCallId: id }, {
|
|
1600
|
+
pointer: "/toolCallId",
|
|
1601
|
+
relatedEventIndex: run.closedToolCalls.get(id)
|
|
1602
|
+
});
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (str(event, "toolCallName") === void 0) emit("AGUI504", {
|
|
1606
|
+
type,
|
|
1607
|
+
detail: "first TOOL_CALL_CHUNK for a tool call must include toolCallName"
|
|
1608
|
+
}, { pointer: "/toolCallName" });
|
|
1609
|
+
run.toolChunk = {
|
|
1610
|
+
toolCallId: id,
|
|
1611
|
+
startIndex: index,
|
|
1612
|
+
args: delta ?? "",
|
|
1613
|
+
sawArgs: delta !== void 0 && delta.length > 0
|
|
1614
|
+
};
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
function checkArgsJson(id, call, emit, atIndex) {
|
|
1620
|
+
if (!call.sawArgs || call.args.length === 0) return;
|
|
1621
|
+
try {
|
|
1622
|
+
JSON.parse(call.args);
|
|
1623
|
+
} catch (e) {
|
|
1624
|
+
emit("AGUI204", {
|
|
1625
|
+
toolCallId: id,
|
|
1626
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1627
|
+
}, {
|
|
1628
|
+
eventIndex: atIndex,
|
|
1629
|
+
relatedEventIndex: call.startIndex
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
/** Chunk streams close implicitly on the next non-chunk event. */
|
|
1634
|
+
function closeToolChunk(run, emit, atIndex) {
|
|
1635
|
+
if (run.toolChunk === null) return;
|
|
1636
|
+
const { toolCallId, startIndex, args, sawArgs } = run.toolChunk;
|
|
1637
|
+
run.toolChunk = null;
|
|
1638
|
+
run.closedToolCalls.set(toolCallId, atIndex);
|
|
1639
|
+
checkArgsJson(toolCallId, {
|
|
1640
|
+
startIndex,
|
|
1641
|
+
args,
|
|
1642
|
+
sawArgs
|
|
1643
|
+
}, emit, atIndex);
|
|
1644
|
+
}
|
|
1645
|
+
/** AGUI203 — open tool calls when the run reaches a clean end. */
|
|
1646
|
+
function endOfRunToolCalls(run, emit, atIndex) {
|
|
1647
|
+
for (const [id, open] of run.openToolCalls) emit("AGUI203", { toolCallId: id }, {
|
|
1648
|
+
eventIndex: atIndex,
|
|
1649
|
+
relatedEventIndex: open.startIndex
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
//#endregion
|
|
1653
|
+
//#region src/rules/checks/transport.ts
|
|
1654
|
+
const TRANSPORT_SKIP_REASON = "transport-layer rule; only checkable against a live connection (validated by ag-ui-validate/transport)";
|
|
1655
|
+
const TRANSPORT_RULE_IDS = CATALOG.rules.filter((r) => r.checkedIn === "transport").map((r) => r.id);
|
|
1656
|
+
//#endregion
|
|
1657
|
+
//#region src/types.ts
|
|
1658
|
+
const CANONICAL_FEATURES = [
|
|
1659
|
+
"agentic-chat",
|
|
1660
|
+
"backend-tool-rendering",
|
|
1661
|
+
"human-in-the-loop",
|
|
1662
|
+
"agentic-generative-ui",
|
|
1663
|
+
"tool-based-generative-ui",
|
|
1664
|
+
"shared-state",
|
|
1665
|
+
"predictive-state-updates"
|
|
1666
|
+
];
|
|
1667
|
+
//#endregion
|
|
1668
|
+
//#region src/index.ts
|
|
1669
|
+
const DRAFTS_META_URL = "https://docs.ag-ui.com/drafts/meta-events";
|
|
1670
|
+
const NOT_INFERABLE = ["agentic-generative-ui", "tool-based-generative-ui"];
|
|
1671
|
+
/** "runStarted" → "RUN_STARTED": case-insensitive match against wire types. */
|
|
1672
|
+
const CANONICAL_BY_SQUASHED = new Map(EVENT_TYPES.map((t) => [t.replace(/_/g, "").toLowerCase(), t]));
|
|
1673
|
+
function describeValue(v) {
|
|
1674
|
+
if (v === null) return "null";
|
|
1675
|
+
if (Array.isArray(v)) return "array";
|
|
1676
|
+
return typeof v;
|
|
1677
|
+
}
|
|
1678
|
+
function createValidator(opts = {}) {
|
|
1679
|
+
const overrides = opts.severityOverrides ?? {};
|
|
1680
|
+
const declaredFeatures = new Set(opts.features ?? []);
|
|
1681
|
+
const layers = /* @__PURE__ */ new Set(["core", ...opts.layers ?? []]);
|
|
1682
|
+
const diagnostics = [];
|
|
1683
|
+
const internalErrors = [];
|
|
1684
|
+
const explicitSkips = /* @__PURE__ */ new Map();
|
|
1685
|
+
const stream = {
|
|
1686
|
+
eventCount: 0,
|
|
1687
|
+
sawTimestamp: false,
|
|
1688
|
+
anySnapshot: false,
|
|
1689
|
+
agui001Fired: false,
|
|
1690
|
+
features: /* @__PURE__ */ new Set()
|
|
1691
|
+
};
|
|
1692
|
+
let run = null;
|
|
1693
|
+
let finalized = false;
|
|
1694
|
+
function mkEmit(batch, current) {
|
|
1695
|
+
return (ruleId, params, extra = {}) => {
|
|
1696
|
+
const rule = RULES.get(ruleId);
|
|
1697
|
+
if (rule === void 0) {
|
|
1698
|
+
internalErrors.push(`emit() for unknown rule ${ruleId}`);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const override = overrides[ruleId];
|
|
1702
|
+
if (override === "off") return;
|
|
1703
|
+
const severity = override ?? extra.severity ?? rule.severity;
|
|
1704
|
+
const aboutCurrentEvent = extra.eventIndex === void 0 && current !== null;
|
|
1705
|
+
const diag = {
|
|
1706
|
+
rule: ruleId,
|
|
1707
|
+
severity,
|
|
1708
|
+
message: formatMessage(rule, params) + (extra.messageSuffix ?? ""),
|
|
1709
|
+
eventIndex: extra.eventIndex ?? current?.index ?? -1,
|
|
1710
|
+
specUrl: extra.specUrl ?? rule.specUrl
|
|
1711
|
+
};
|
|
1712
|
+
if (aboutCurrentEvent) diag.eventType = current.type;
|
|
1713
|
+
if (extra.pointer !== void 0) diag.pointer = extra.pointer;
|
|
1714
|
+
if (extra.relatedEventIndex !== void 0) diag.relatedEventIndex = extra.relatedEventIndex;
|
|
1715
|
+
diagnostics.push(diag);
|
|
1716
|
+
batch.push(diag);
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function validateSchema(type, spec, ev, emit) {
|
|
1720
|
+
for (const [field, fs] of Object.entries(spec.fields)) {
|
|
1721
|
+
const v = ev[field];
|
|
1722
|
+
if (v === void 0) {
|
|
1723
|
+
if (fs.required) emit("AGUI504", {
|
|
1724
|
+
type,
|
|
1725
|
+
detail: `missing required field '${field}' (${fs.kind})`
|
|
1726
|
+
}, { pointer: `/${field}` });
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
let kindOk = true;
|
|
1730
|
+
switch (fs.kind) {
|
|
1731
|
+
case "string":
|
|
1732
|
+
case "number":
|
|
1733
|
+
case "boolean":
|
|
1734
|
+
kindOk = typeof v === fs.kind;
|
|
1735
|
+
break;
|
|
1736
|
+
case "array":
|
|
1737
|
+
kindOk = Array.isArray(v);
|
|
1738
|
+
break;
|
|
1739
|
+
case "object": kindOk = typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1740
|
+
}
|
|
1741
|
+
if (!kindOk) {
|
|
1742
|
+
emit("AGUI504", {
|
|
1743
|
+
type,
|
|
1744
|
+
detail: `field '${field}' must be ${fs.kind === "array" ? "an" : "a"} ${fs.kind}, got ${describeValue(v)}`
|
|
1745
|
+
}, { pointer: `/${field}` });
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
if (fs.enum !== void 0 && typeof v === "string" && !fs.enum.includes(v)) emit("AGUI504", {
|
|
1749
|
+
type,
|
|
1750
|
+
detail: `field '${field}' must be one of ${fs.enum.join("|")}, got '${v}'`
|
|
1751
|
+
}, { pointer: `/${field}` });
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
/** Chunk streams close implicitly on any event of a different type. */
|
|
1755
|
+
function closeChunks(r, emit, atIndex, except) {
|
|
1756
|
+
if (except !== "TEXT_MESSAGE_CHUNK") closeTextChunk(r, atIndex);
|
|
1757
|
+
if (except !== "TOOL_CALL_CHUNK") closeToolChunk(r, emit, atIndex);
|
|
1758
|
+
if (except !== "REASONING_MESSAGE_CHUNK") closeReasoningChunk(r);
|
|
1759
|
+
}
|
|
1760
|
+
/** Unterminated-at-run-end rules. Only on a *clean* end (RUN_FINISHED or a
|
|
1761
|
+
* stream that just stops): after RUN_ERROR, open streams are expected
|
|
1762
|
+
* debris of the failure, and flagging them would manufacture noise. */
|
|
1763
|
+
function endOfRunChecks(r, emit, atIndex) {
|
|
1764
|
+
endOfRunText(r, emit, atIndex);
|
|
1765
|
+
endOfRunToolCalls(r, emit, atIndex);
|
|
1766
|
+
endOfRunSteps(r, emit, atIndex);
|
|
1767
|
+
endOfRunReasoning(r, emit, atIndex);
|
|
1768
|
+
}
|
|
1769
|
+
/** Opens the implicit run scope for streams that never announced one. */
|
|
1770
|
+
function ensureRun(index, type, emit) {
|
|
1771
|
+
if (run === null) {
|
|
1772
|
+
if (!stream.agui001Fired) {
|
|
1773
|
+
emit("AGUI001", { type }, {});
|
|
1774
|
+
stream.agui001Fired = true;
|
|
1775
|
+
}
|
|
1776
|
+
run = newRunState({
|
|
1777
|
+
runId: null,
|
|
1778
|
+
threadId: null,
|
|
1779
|
+
startIndex: index,
|
|
1780
|
+
implicit: true
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
return run;
|
|
1784
|
+
}
|
|
1785
|
+
function processEvent(input, batch) {
|
|
1786
|
+
const index = stream.eventCount;
|
|
1787
|
+
stream.eventCount += 1;
|
|
1788
|
+
let parsed = input;
|
|
1789
|
+
if (typeof input === "string") try {
|
|
1790
|
+
parsed = JSON.parse(input);
|
|
1791
|
+
} catch (e) {
|
|
1792
|
+
mkEmit(batch, {
|
|
1793
|
+
index,
|
|
1794
|
+
type: ""
|
|
1795
|
+
})("AGUI502", { error: e instanceof Error ? e.message : String(e) });
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1799
|
+
mkEmit(batch, {
|
|
1800
|
+
index,
|
|
1801
|
+
type: ""
|
|
1802
|
+
})("AGUI502", { error: `payload is ${describeValue(parsed)}, expected a JSON object` });
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
const ev = parsed;
|
|
1806
|
+
if (typeof ev.type !== "string") {
|
|
1807
|
+
mkEmit(batch, {
|
|
1808
|
+
index,
|
|
1809
|
+
type: ""
|
|
1810
|
+
})("AGUI504", {
|
|
1811
|
+
type: "(untyped)",
|
|
1812
|
+
detail: "event has no string 'type' property"
|
|
1813
|
+
}, { pointer: "/type" });
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
const type = ev.type;
|
|
1817
|
+
const emit = mkEmit(batch, {
|
|
1818
|
+
index,
|
|
1819
|
+
type
|
|
1820
|
+
});
|
|
1821
|
+
if ("timestamp" in ev && ev.timestamp !== void 0) {
|
|
1822
|
+
if (typeof ev.timestamp === "number") stream.sawTimestamp = true;
|
|
1823
|
+
else emit("AGUI504", {
|
|
1824
|
+
type,
|
|
1825
|
+
detail: `field 'timestamp' must be a number, got ${describeValue(ev.timestamp)}`
|
|
1826
|
+
}, { pointer: "/timestamp" });
|
|
1827
|
+
}
|
|
1828
|
+
const spec = EVENT_TABLE[type];
|
|
1829
|
+
if (spec === void 0) {
|
|
1830
|
+
if (DRAFT_EVENT_TYPES.includes(type)) emit("AGUI503", {
|
|
1831
|
+
type,
|
|
1832
|
+
sdkVersion: SDK_VERSION
|
|
1833
|
+
}, {
|
|
1834
|
+
severity: "info",
|
|
1835
|
+
specUrl: DRAFTS_META_URL,
|
|
1836
|
+
messageSuffix: " — documented draft event type, not yet in @ag-ui/core"
|
|
1837
|
+
});
|
|
1838
|
+
else {
|
|
1839
|
+
const canonical = CANONICAL_BY_SQUASHED.get(type.replace(/[_\-\s]/g, "").toLowerCase());
|
|
1840
|
+
emit("AGUI503", {
|
|
1841
|
+
type,
|
|
1842
|
+
sdkVersion: SDK_VERSION
|
|
1843
|
+
}, { ...canonical !== void 0 ? { messageSuffix: ` — did you mean '${canonical}'? AG-UI wire types use SCREAMING_SNAKE_CASE` } : {} });
|
|
1844
|
+
}
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
validateSchema(type, spec, ev, emit);
|
|
1848
|
+
if (type === "RUN_STARTED") {
|
|
1849
|
+
if (run === null) run = newRunState({
|
|
1850
|
+
runId: str(ev, "runId") ?? null,
|
|
1851
|
+
threadId: str(ev, "threadId") ?? null,
|
|
1852
|
+
startIndex: index,
|
|
1853
|
+
implicit: false
|
|
1854
|
+
});
|
|
1855
|
+
else if (run.terminal !== null) run = newRunState({
|
|
1856
|
+
runId: str(ev, "runId") ?? null,
|
|
1857
|
+
threadId: str(ev, "threadId") ?? null,
|
|
1858
|
+
startIndex: index,
|
|
1859
|
+
implicit: false
|
|
1860
|
+
});
|
|
1861
|
+
else if (run.implicit) {
|
|
1862
|
+
closeChunks(run, emit, index);
|
|
1863
|
+
run.implicit = false;
|
|
1864
|
+
run.runId = str(ev, "runId") ?? null;
|
|
1865
|
+
run.threadId = str(ev, "threadId") ?? null;
|
|
1866
|
+
} else {
|
|
1867
|
+
closeChunks(run, emit, index);
|
|
1868
|
+
emit("AGUI002", { runId: str(ev, "runId") ?? "(missing)" }, { relatedEventIndex: run.startIndex });
|
|
1869
|
+
}
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
const r = ensureRun(index, type, emit);
|
|
1873
|
+
if (r.terminal !== null) {
|
|
1874
|
+
const terminalType = r.terminal.type;
|
|
1875
|
+
if ((type === "RUN_FINISHED" || type === "RUN_ERROR") && type !== terminalType) emit("AGUI005", {
|
|
1876
|
+
type,
|
|
1877
|
+
terminalType
|
|
1878
|
+
}, { relatedEventIndex: r.terminal.index });
|
|
1879
|
+
else emit("AGUI004", {
|
|
1880
|
+
type,
|
|
1881
|
+
terminalType
|
|
1882
|
+
}, { relatedEventIndex: r.terminal.index });
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
closeChunks(r, emit, index, type);
|
|
1886
|
+
if (type === "RUN_FINISHED" || type === "RUN_ERROR") {
|
|
1887
|
+
if (type === "RUN_FINISHED") {
|
|
1888
|
+
checkRunIdStability(api(index, type, ev, r, emit));
|
|
1889
|
+
endOfRunChecks(r, emit, index);
|
|
1890
|
+
const outcome = ev.outcome;
|
|
1891
|
+
if (typeof outcome === "object" && outcome !== null && outcome.type === "interrupt") stream.features.add("human-in-the-loop");
|
|
1892
|
+
}
|
|
1893
|
+
r.terminal = {
|
|
1894
|
+
type,
|
|
1895
|
+
index
|
|
1896
|
+
};
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
const a = api(index, type, ev, r, emit);
|
|
1900
|
+
switch (spec.category) {
|
|
1901
|
+
case "lifecycle":
|
|
1902
|
+
handleStepEvent(a);
|
|
1903
|
+
break;
|
|
1904
|
+
case "text":
|
|
1905
|
+
handleTextEvent(a);
|
|
1906
|
+
break;
|
|
1907
|
+
case "toolcall":
|
|
1908
|
+
handleToolCallEvent(a);
|
|
1909
|
+
break;
|
|
1910
|
+
case "state":
|
|
1911
|
+
handleStateEvent(a);
|
|
1912
|
+
break;
|
|
1913
|
+
case "reasoning":
|
|
1914
|
+
handleReasoningEvent(a);
|
|
1915
|
+
break;
|
|
1916
|
+
case "thinking": break;
|
|
1917
|
+
case "activity": break;
|
|
1918
|
+
case "special": if (type === "RAW") {
|
|
1919
|
+
const wrapped = ev.event;
|
|
1920
|
+
if (typeof wrapped === "object" && wrapped !== null) {
|
|
1921
|
+
const wrappedType = wrapped.type;
|
|
1922
|
+
if (typeof wrappedType === "string" && EVENT_TABLE[wrappedType] !== void 0) emit("AGUI901", { wrappedType }, { pointer: "/event/type" });
|
|
1923
|
+
}
|
|
1924
|
+
} else if (type === "CUSTOM") {
|
|
1925
|
+
const name = str(ev, "name");
|
|
1926
|
+
if (name !== void 0) {
|
|
1927
|
+
if (name === "PredictState") stream.features.add("predictive-state-updates");
|
|
1928
|
+
if (!/[.:/]/.test(name)) emit("AGUI903", { name }, { pointer: "/name" });
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
function api(index, type, event, r, emit) {
|
|
1934
|
+
return {
|
|
1935
|
+
index,
|
|
1936
|
+
type,
|
|
1937
|
+
event,
|
|
1938
|
+
run: r,
|
|
1939
|
+
stream,
|
|
1940
|
+
emit,
|
|
1941
|
+
feature: (f) => stream.features.add(f)
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
return {
|
|
1945
|
+
feed(event) {
|
|
1946
|
+
const batch = [];
|
|
1947
|
+
try {
|
|
1948
|
+
processEvent(event, batch);
|
|
1949
|
+
} catch (e) {
|
|
1950
|
+
internalErrors.push(`feed(event ${stream.eventCount - 1}): ${e instanceof Error ? e.stack ?? e.message : String(e)}`);
|
|
1951
|
+
}
|
|
1952
|
+
return batch;
|
|
1953
|
+
},
|
|
1954
|
+
finalize() {
|
|
1955
|
+
if (finalized) return [];
|
|
1956
|
+
finalized = true;
|
|
1957
|
+
const batch = [];
|
|
1958
|
+
const emit = mkEmit(batch, null);
|
|
1959
|
+
try {
|
|
1960
|
+
if (run !== null && run.terminal === null) {
|
|
1961
|
+
closeChunks(run, emit, -1);
|
|
1962
|
+
emit("AGUI003", { runId: run.runId ?? "(unknown)" }, {
|
|
1963
|
+
eventIndex: -1,
|
|
1964
|
+
relatedEventIndex: run.startIndex
|
|
1965
|
+
});
|
|
1966
|
+
endOfRunChecks(run, emit, -1);
|
|
1967
|
+
}
|
|
1968
|
+
if (stream.eventCount > 0 && !stream.sawTimestamp) emit("AGUI902", { eventCount: stream.eventCount }, { eventIndex: -1 });
|
|
1969
|
+
if (declaredFeatures.has("shared-state") && !stream.anySnapshot) emit("AGUI305", {}, { eventIndex: -1 });
|
|
1970
|
+
} catch (e) {
|
|
1971
|
+
internalErrors.push(`finalize(): ${e instanceof Error ? e.stack ?? e.message : String(e)}`);
|
|
1972
|
+
}
|
|
1973
|
+
return batch;
|
|
1974
|
+
},
|
|
1975
|
+
emitExternal(rule, params = {}, extra = {}) {
|
|
1976
|
+
const batch = [];
|
|
1977
|
+
try {
|
|
1978
|
+
mkEmit(batch, null)(rule, params, extra);
|
|
1979
|
+
} catch (e) {
|
|
1980
|
+
internalErrors.push(`emitExternal(${rule}): ${e instanceof Error ? e.message : String(e)}`);
|
|
1981
|
+
}
|
|
1982
|
+
return batch[0] ?? null;
|
|
1983
|
+
},
|
|
1984
|
+
markSkipped(rule, reason) {
|
|
1985
|
+
explicitSkips.set(String(rule), String(reason));
|
|
1986
|
+
},
|
|
1987
|
+
report() {
|
|
1988
|
+
const summary = {
|
|
1989
|
+
errors: 0,
|
|
1990
|
+
warnings: 0,
|
|
1991
|
+
info: 0
|
|
1992
|
+
};
|
|
1993
|
+
for (const d of diagnostics) if (d.severity === "error") summary.errors += 1;
|
|
1994
|
+
else if (d.severity === "warning") summary.warnings += 1;
|
|
1995
|
+
else summary.info += 1;
|
|
1996
|
+
const features = {};
|
|
1997
|
+
for (const f of CANONICAL_FEATURES) features[f] = NOT_INFERABLE.includes(f) ? "not-inferable" : stream.features.has(f) ? "exercised" : "not-exercised";
|
|
1998
|
+
let skipped = layers.has("transport") ? [] : TRANSPORT_RULE_IDS.filter((id) => overrides[id] !== "off").map((rule) => ({
|
|
1999
|
+
rule,
|
|
2000
|
+
reason: TRANSPORT_SKIP_REASON
|
|
2001
|
+
}));
|
|
2002
|
+
if (!declaredFeatures.has("shared-state") && overrides.AGUI305 !== "off") skipped.push({
|
|
2003
|
+
rule: "AGUI305",
|
|
2004
|
+
reason: "only evaluated when the 'shared-state' feature is declared via options.features"
|
|
2005
|
+
});
|
|
2006
|
+
for (const [rule, severity] of Object.entries(overrides)) if (severity === "off" && RULES.has(rule)) skipped.push({
|
|
2007
|
+
rule,
|
|
2008
|
+
reason: "disabled by severityOverrides"
|
|
2009
|
+
});
|
|
2010
|
+
if (explicitSkips.size > 0) {
|
|
2011
|
+
skipped = skipped.filter((s) => !explicitSkips.has(s.rule));
|
|
2012
|
+
for (const [rule, reason] of explicitSkips) skipped.push({
|
|
2013
|
+
rule,
|
|
2014
|
+
reason
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
return {
|
|
2018
|
+
diagnostics: [...diagnostics],
|
|
2019
|
+
summary,
|
|
2020
|
+
features,
|
|
2021
|
+
skipped,
|
|
2022
|
+
eventCount: stream.eventCount,
|
|
2023
|
+
internalErrors: [...internalErrors]
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
//#endregion
|
|
2029
|
+
//#region src/report/pretty.ts
|
|
2030
|
+
const SYMBOL = {
|
|
2031
|
+
error: "✖",
|
|
2032
|
+
warning: "⚠",
|
|
2033
|
+
info: "ℹ"
|
|
2034
|
+
};
|
|
2035
|
+
const SGR = {
|
|
2036
|
+
error: "31",
|
|
2037
|
+
warning: "33",
|
|
2038
|
+
info: "36"
|
|
2039
|
+
};
|
|
2040
|
+
function paint(code, s, on) {
|
|
2041
|
+
return on ? `\x1b[${code}m${s}\x1b[0m` : s;
|
|
2042
|
+
}
|
|
2043
|
+
function formatDiagnosticLine(d, opts) {
|
|
2044
|
+
const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : "—";
|
|
2045
|
+
const head = paint(SGR[d.severity], `${SYMBOL[d.severity]} ${d.rule}`, opts.color);
|
|
2046
|
+
const meta = paint("2", `${d.severity.padEnd(7)} ${where.padEnd(10)}`, opts.color);
|
|
2047
|
+
const cite = paint("2", ` ↳ ${d.specUrl}`, opts.color);
|
|
2048
|
+
return `${head} ${meta} ${d.message}\n${cite}`;
|
|
2049
|
+
}
|
|
2050
|
+
//#endregion
|
|
2051
|
+
//#region src/vitest/matcher.ts
|
|
2052
|
+
function toBeValidAGUI(received, options = {}) {
|
|
2053
|
+
let events;
|
|
2054
|
+
if (typeof received === "string") events = received.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
2055
|
+
else if (Array.isArray(received)) events = received;
|
|
2056
|
+
else throw new TypeError(`toBeValidAGUI expects an array of events (objects or JSON strings) or a JSONL string; received ${received === null ? "null" : typeof received}. Wrap a single event in an array.`);
|
|
2057
|
+
const validatorOptions = {};
|
|
2058
|
+
if (options.features !== void 0) validatorOptions.features = options.features;
|
|
2059
|
+
if (options.severityOverrides !== void 0) validatorOptions.severityOverrides = options.severityOverrides;
|
|
2060
|
+
const v = createValidator(validatorOptions);
|
|
2061
|
+
for (const e of events) v.feed(e);
|
|
2062
|
+
v.finalize();
|
|
2063
|
+
const report = v.report();
|
|
2064
|
+
const { errors, warnings, info } = report.summary;
|
|
2065
|
+
const pass = decideExitCode(report.summary, options.maxWarnings) === 0;
|
|
2066
|
+
return {
|
|
2067
|
+
pass,
|
|
2068
|
+
message: () => {
|
|
2069
|
+
if (pass) return `expected the stream to violate AG-UI conformance, but it is valid: ${report.eventCount} events, ${errors} errors, ${warnings} warnings, ${info} info`;
|
|
2070
|
+
const lines = [];
|
|
2071
|
+
if (errors > 0) lines.push(`expected a valid AG-UI stream, but found ${errors} error-severity finding${errors === 1 ? "" : "s"}:`);
|
|
2072
|
+
else lines.push(`expected a valid AG-UI stream, but ${warnings} warning${warnings === 1 ? "" : "s"} exceed maxWarnings: ${options.maxWarnings}:`);
|
|
2073
|
+
for (const d of report.diagnostics) {
|
|
2074
|
+
if (d.severity === "info") continue;
|
|
2075
|
+
lines.push(formatDiagnosticLine(d, { color: false }));
|
|
2076
|
+
}
|
|
2077
|
+
return lines.join("\n");
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
//#endregion
|
|
2082
|
+
//#region src/vitest/index.ts
|
|
2083
|
+
expect.extend({ toBeValidAGUI(received, options) {
|
|
2084
|
+
return toBeValidAGUI(received, options);
|
|
2085
|
+
} });
|
|
2086
|
+
//#endregion
|
|
2087
|
+
export { toBeValidAGUI };
|
|
2088
|
+
|
|
2089
|
+
//# sourceMappingURL=vitest.js.map
|