-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgraph-schema.json
More file actions
669 lines (669 loc) · 28.8 KB
/
Copy pathgraph-schema.json
File metadata and controls
669 lines (669 loc) · 28.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
{
"$defs": {
"AgentNode": {
"additionalProperties": false,
"description": "An `agent` node: a full agent loop referenced by content hash.",
"properties": {
"agent_hash": {
"description": "Content hash of the agent definition (model, prompt, tools, budget) this\nnode runs, in `sha256:<64 lowercase hex>` form. A hash, never an\nembedded definition: that keeps this crate independent of the\nagent-definition schema and lets the same definition be shared across\nnodes and runs by identity.",
"type": "string"
},
"id": {
"description": "The node's stable id, unique within the document.",
"type": "string"
},
"input_schema": {
"description": "Optional JSON Schema for the payload this node consumes. Additive: absent\non the wire when unset."
},
"name": {
"description": "Optional short display label for this node. See the module docs' \"The\noptional node display name\" section for the bound and the deliberate\nhash-inclusion contrast with the agent `name` field. Additive: absent\non the wire when unset.",
"type": [
"string",
"null"
]
},
"output_schema": {
"description": "Optional JSON Schema for the payload this node produces."
}
},
"required": [
"id",
"agent_hash"
],
"type": "object"
},
"BranchCase": {
"additionalProperties": false,
"description": "One case of a `BranchNode`: a name and the condition that selects it.\n\nThe realized routing (which downstream node a fired case flows to) is\ncarried by an `Edge` whose `label` matches the case `name`, so topology\nstays entirely in the edge list and a branch has real outbound edges.",
"properties": {
"name": {
"description": "The case name. An edge labeled with this name realizes the route.",
"type": "string"
},
"when": {
"$ref": "#/$defs/BranchCondition",
"description": "The condition that selects this case. Data only, never evaluated here."
}
},
"required": [
"name",
"when"
],
"type": "object"
},
"BranchCondition": {
"description": "How a `BranchCase` is selected. Modeled as data; not evaluated in this\ncrate.\n\nAdjacently tagged (`{\"kind\": \"...\", \"value\": ...}`) so it stays additive:\na future condition kind is a new variant, which does not change how an\nexisting document encodes.",
"oneOf": [
{
"additionalProperties": false,
"description": "A constrained boolean expression over the routed value, recorded as an\nopaque string. NOT parsed or evaluated in this crate.",
"properties": {
"kind": {
"const": "expression",
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "The case is chosen by a model decision at run time, recorded as an event.\nCarries no author-time data.",
"properties": {
"kind": {
"const": "model_decision",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
}
]
},
"BranchNode": {
"additionalProperties": false,
"description": "A `branch` node: routes on a typed output.",
"properties": {
"agent_hash": {
"description": "Content hash of the agent that decides a `BranchCondition::ModelDecision`\ncase, in `sha256:<64 lowercase hex>` form. Present only on a branch that\ncarries a model-decision case: the engine drives this agent with the\nrouted value and maps its reply to a case name. Additive: absent on the\nwire when unset, so a purely expression-driven branch (and every document\nwritten before this field existed) serializes byte for byte as before.\n`crate::validate` reports a model-decision case with no agent here as a\nnode-precise error.",
"type": [
"string",
"null"
]
},
"cases": {
"description": "The cases, each a named condition. An expression condition is evaluated\nagainst the routed value; a model-decision condition is resolved by the\nnode's `agent_hash` agent. The first matching case in\nauthor order wins, and the engine records the choice as a\n`crate::document`-external `BranchTaken` event.",
"items": {
"$ref": "#/$defs/BranchCase"
},
"type": "array"
},
"id": {
"description": "The node's stable id, unique within the document.",
"type": "string"
},
"name": {
"description": "Optional short display label for this node. See the module docs' \"The\noptional node display name\" section for the bound and the deliberate\nhash-inclusion contrast with the agent `name` field. Additive: absent\non the wire when unset.",
"type": [
"string",
"null"
]
},
"on": {
"description": "Optional opaque reference to the typed value the branch routes on.\nRecorded as DATA; not resolved in this crate.",
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"cases"
],
"type": "object"
},
"DelayNode": {
"additionalProperties": false,
"description": "A `delay` node: a durable wait that parks the run, then continues the walk.\n\n# Why the wait is a DURATION and not an instant\n\nA graph document is authored once, content-addressed, and then run any\nnumber of times: the hash IS the identity, and the same document backs\n`salvor graph run`, `POST /v1/graph-runs`, and every fork of every run that\nreferenced it. An absolute wake instant baked into the document would make\nit a single-use artifact: correct on the first run and already in the past\non the second, where every delay would fall through instantly and the\ndocument would silently mean something else than it did the day it was\nwritten.\n\nA duration says the thing that stays true across runs (\"hold this for an\nhour\"), and it is what every other author-time number in this format\nalready is: a `map`'s `concurrency`, a `fold`'s `max_iterations`. Nothing in\na graph document is a value belonging to one particular run, and this field\nkeeps it that way. The instant is resolved at EXECUTION, by\n`salvor_runtime::RunCtx::sleep_for`, which observes the clock into the log\nfirst and derives `wake_at` from that recorded reading, so the wake instant\nis recorded once and replays identically forever.\n\nThere is deliberately no second, absolute spelling. Two ways to say a wait\nwould need a mutual-exclusion rule in the validator and would leave authors\nchoosing between one form that composes and one that expires; a format with\none answer is the smaller thing to keep true.",
"properties": {
"id": {
"description": "The node's stable id, unique within the document.",
"type": "string"
},
"name": {
"description": "Optional short display label for this node. See the module docs' \"The\noptional node display name\" section for the bound and the deliberate\nhash-inclusion contrast with the agent `name` field. Additive: absent\non the wire when unset.",
"type": [
"string",
"null"
]
},
"seconds": {
"description": "How long the run waits, in whole seconds, measured from the clock\nreading the engine records on entering the node. Must be at least 1;\n`crate::validate` reports a zero wait by node id.",
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"id",
"seconds"
],
"type": "object"
},
"Edge": {
"additionalProperties": false,
"description": "A directed edge: a typed payload flows from one node to another.\n\nEdges are the single source of graph topology. Referential integrity, the\nacyclic check, and the entry/terminal summary all read the edge list. No\nports are modeled here; a `port` pair is a documented additive\nfollow-up.",
"properties": {
"from": {
"description": "The source node id.",
"type": "string"
},
"label": {
"description": "Optional label. When the source is a `BranchNode`, this names the\n`BranchCase` this edge realizes. Data only.",
"type": [
"string",
"null"
]
},
"to": {
"description": "The destination node id.",
"type": "string"
}
},
"required": [
"from",
"to"
],
"type": "object"
},
"FoldBody": {
"description": "The body a `FoldNode` runs each pass. Adjacently tagged, mirroring\n`MapBody`, so adding a third form later stays additive. A `node` body names\nan existing node by id (checked for existence during validation); a\n`subgraph` body embeds a whole `Graph` and is deferred exactly as the map's\nsubgraph body is.",
"oneOf": [
{
"additionalProperties": false,
"description": "Run each pass through an existing node in this document, by id.",
"properties": {
"kind": {
"const": "node",
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "Run each pass through an embedded sub-graph. Boxed because a `Graph`\ncontains nodes, one of which may itself be a `fold`, so the type is\nrecursive.",
"properties": {
"kind": {
"const": "subgraph",
"type": "string"
},
"value": {
"$ref": "#"
}
},
"required": [
"kind",
"value"
],
"type": "object"
}
]
},
"FoldJoin": {
"description": "How a `FoldNode` folds its passes into the single value it produces.\n\nAdjacently tagged (`{\"kind\": \"...\", \"value\": ...}` for the variant that\ncarries data, `{\"kind\": \"...\"}` for the unit variants) so a future join rule\nis a new variant that does not change how an existing document encodes,\nexactly like `BranchCondition`.\n\nThe variants are grounded in what the AARG loop actually needs. `best_by` is\nthe argmax winner the loop's \"best draft wins, never the last pass\" rule\nrequires; `last` and `all` are the two obvious simpler folds a different\nconsumer might want.",
"oneOf": [
{
"additionalProperties": false,
"description": "Produce the pass whose value MAXIMIZES the given reference (a path into\nthe accumulated value, `score` or `review.overall_score`). This is the\nargmax the AARG loop needs: the best draft wins, never the last. The\nreference is parsed at submit like a `crate::expr` path, so a malformed\none is a node-precise error.",
"properties": {
"kind": {
"const": "best_by",
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "Produce the value of the last pass the loop ran.",
"properties": {
"kind": {
"const": "last",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "Produce every pass's value as a list, in pass order.",
"properties": {
"kind": {
"const": "all",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
}
]
},
"FoldNode": {
"additionalProperties": false,
"description": "A `fold` node: bounded iteration that accumulates across passes.\n\nModels an adversarial refine loop as one node: a `body` is run up to\n`max_iterations` times, each pass folding into an accumulated value, and the\nloop stops when `stop_when` holds over that value (or the bound is reached).\nThe `join` rule then selects the value the node produces. Every field is\nauthor-time data; this crate never runs the loop.\n\nGrounded in the AARG tailor loop the graph wiring models: bounded revisions\n(`max_iterations`), a stop predicate over the accumulated score\n(`stop_when`, an expression in the same language a branch case uses), and an\nargmax winner (`join` = `FoldJoin::BestBy` over the score). See the\ncrate-level docs and the graph wiring plan.",
"properties": {
"accumulator_schema": {
"description": "Optional JSON Schema for the accumulated value the loop carries and\nproduces. Data only, like an `AgentNode`'s `output_schema`: recorded\nfor authoring and tooling, never wired into the edge type-compatibility\ncheck (a fold's produced-value semantics are not implemented with\nits execution). Additive: absent on the wire when unset."
},
"body": {
"$ref": "#/$defs/FoldBody",
"description": "What each pass runs: a node already in this document, or an embedded\nsub-graph. Not implemented exactly as `MapBody`'s subgraph form is not:\nthe shape is legal, but no engine runs it yet."
},
"id": {
"description": "The node's stable id, unique within the document.",
"type": "string"
},
"join": {
"$ref": "#/$defs/FoldJoin",
"description": "How the passes are folded into the value the node produces."
},
"max_iterations": {
"description": "The iteration bound: the most passes the loop may run. Must be at least\n1; `crate::validate` reports a zero bound by node id.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"name": {
"description": "Optional short display label for this node. See the module docs' \"The\noptional node display name\" section for the bound and the deliberate\nhash-inclusion contrast with the agent `name` field. Additive: absent\non the wire when unset.",
"type": [
"string",
"null"
]
},
"on_bound": {
"anyOf": [
{
"$ref": "#/$defs/OnBound"
},
{
"type": "null"
}
],
"description": "What a reached `max_iterations` bound means, when `stop_when` never\nheld. Absent is `OnBound::Join`: the bound joins the best pass, which is\nwhat every fold written before this field existed does, so an absent\nfield is both the default and byte-identical to those documents on the\nwire. Additive: absent on the wire when unset."
},
"stop_when": {
"description": "A boolean expression over the accumulated value that stops the loop when\nit holds. Written in the `crate::expr` condition language, the same one\na `BranchCondition::Expression` uses, and validated at submit so a\nmalformed predicate is a node-precise error, never a run-time failure.",
"type": "string"
}
},
"required": [
"id",
"body",
"max_iterations",
"stop_when",
"join"
],
"type": "object"
},
"GateNode": {
"additionalProperties": false,
"description": "A `gate` node: human approval that suspends the run.",
"properties": {
"approval_schema": {
"description": "JSON Schema the human approval input must satisfy, mirroring the recorded\n`Suspended` event's `input_schema`. Required: a gate with no declared\napproval shape is meaningless."
},
"id": {
"description": "The node's stable id, unique within the document.",
"type": "string"
},
"name": {
"description": "Optional short display label for this node. See the module docs' \"The\noptional node display name\" section for the bound and the deliberate\nhash-inclusion contrast with the agent `name` field. Additive: absent\non the wire when unset.",
"type": [
"string",
"null"
]
},
"prompt": {
"description": "Optional human-readable prompt shown in the approval inbox.",
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"approval_schema"
],
"type": "object"
},
"MapBody": {
"description": "The body a `MapNode` maps each element through.\n\nAdjacently tagged, so adding a third form later is additive. A `node` body\nnames an existing node by id (checked for existence during validation); a\n`subgraph` body embeds a whole `Graph`.",
"oneOf": [
{
"additionalProperties": false,
"description": "Map each element through an existing node in this document, by id.",
"properties": {
"kind": {
"const": "node",
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "Map each element through an embedded sub-graph. Boxed because a `Graph`\ncontains nodes, one of which may be a `map`, so the type is recursive.",
"properties": {
"kind": {
"const": "subgraph",
"type": "string"
},
"value": {
"$ref": "#"
}
},
"required": [
"kind",
"value"
],
"type": "object"
}
]
},
"MapNode": {
"additionalProperties": false,
"description": "A `map` node: fan-out a sub-run per element of a typed list, with a\nconcurrency cap.",
"properties": {
"body": {
"$ref": "#/$defs/MapBody",
"description": "What each element is mapped through: a node already in this document, or\nan embedded sub-graph."
},
"concurrency": {
"description": "The maximum number of sub-runs in flight at once. Must be at least 1;\n`crate::validate` reports a non-positive cap by node id.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"id": {
"description": "The node's stable id, unique within the document.",
"type": "string"
},
"name": {
"description": "Optional short display label for this node. See the module docs' \"The\noptional node display name\" section for the bound and the deliberate\nhash-inclusion contrast with the agent `name` field. Additive: absent\non the wire when unset.",
"type": [
"string",
"null"
]
},
"output_schema": {
"description": "Optional JSON Schema for the joined list this node produces."
},
"over": {
"description": "Opaque reference to the typed list this node fans out over. Data only,\nnot resolved in this crate.",
"type": "string"
}
},
"required": [
"id",
"over",
"concurrency",
"body"
],
"type": "object"
},
"Node": {
"description": "One node in a graph: exactly one of the seven kinds the runtime knows how to\nexecute.\n\nAdjacently tagged like the event enum: each node serializes as `{\"kind\":\n\"...\", \"payload\": {...}}`. The tag (`kind`) and content (`payload`) live in\nseparate keys, which never collides with a payload field and does not force\npayloads to be JSON objects. `deny_unknown_fields` on the enum rejects any\nkey other than `kind` and `payload`.\n\nThe stable node id lives inside each payload (every payload struct carries\nan `id`), reachable generically through `Node::id`. Keeping the id in the\npayload is what lets the outer shape stay exactly the two-key adjacent\ntagging the event log uses, with no third common field to special-case.",
"oneOf": [
{
"additionalProperties": false,
"description": "A full agent loop (model, prompt, tools, budget). The whole v0.1 product\nbecomes one node kind. It references its agent definition BY CONTENT\nHASH, never an embedded definition, so this crate stays a leaf that does\nnot depend on the agent-definition schema.",
"properties": {
"kind": {
"const": "agent",
"type": "string"
},
"payload": {
"$ref": "#/$defs/AgentNode"
}
},
"required": [
"kind",
"payload"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "A single direct tool invocation, no model in the loop.",
"properties": {
"kind": {
"const": "tool",
"type": "string"
},
"payload": {
"$ref": "#/$defs/ToolNode"
}
},
"required": [
"kind",
"payload"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "Human approval: suspends the graph run and renders in the approval\ninbox.",
"properties": {
"kind": {
"const": "gate",
"type": "string"
},
"payload": {
"$ref": "#/$defs/GateNode"
}
},
"required": [
"kind",
"payload"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "Routes on a typed output. The cases (conditions and their names) are\nrecorded as DATA here; this crate never evaluates them.",
"properties": {
"kind": {
"const": "branch",
"type": "string"
},
"payload": {
"$ref": "#/$defs/BranchNode"
}
},
"required": [
"kind",
"payload"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "Fan-out: spawn a sub-run per element of a typed list, join on\ncompletion, with a concurrency cap.",
"properties": {
"kind": {
"const": "map",
"type": "string"
},
"payload": {
"$ref": "#/$defs/MapNode"
}
},
"required": [
"kind",
"payload"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "Bounded iteration: run a body repeatedly, accumulating across passes,\nuntil a stop predicate holds or an iteration bound is reached, then join\nthe passes into one value. Models an adversarial refine loop (draft,\nscore, review, revise) as one node. Execution is not implemented: the\nfold exists in the format, the validator, the projection, and the\ncanvas; the engine records a typed refusal for it.",
"properties": {
"kind": {
"const": "fold",
"type": "string"
},
"payload": {
"$ref": "#/$defs/FoldNode"
}
},
"required": [
"kind",
"payload"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "A durable wait: parks the run on a timer, then continues the walk. It\ntransforms nothing, so its output is its input verbatim.",
"properties": {
"kind": {
"const": "delay",
"type": "string"
},
"payload": {
"$ref": "#/$defs/DelayNode"
}
},
"required": [
"kind",
"payload"
],
"type": "object"
}
]
},
"OnBound": {
"description": "What a `FoldNode` reaching its iteration bound means, when `stop_when` never\nheld.\n\nA bare lowercase string on the wire (`\"join\"`, `\"fail\"`), not the adjacently\ntagged shape `FoldJoin` uses: the two variants carry no data and none is\nforeseen, so a tag object would be ceremony around a word. Adding a third\nvariant later stays additive all the same, because an older document simply\nomits the field.\n\nAbsent means [`OnBound::Join`], which is what every fold written before this\nfield existed does, so the default is the behavior already shipped rather\nthan a new one chosen now. Authors reach for [`OnBound::Fail`] when the stop\npredicate is a REQUIREMENT rather than an early exit: a loop that must reach\na score before its value is worth anything is better off saying so than\nhanding a caller the best of several passes that all fell short. Data only;\nthis crate never runs the loop, and the engine reads this field in a later\nslice.",
"oneOf": [
{
"const": "join",
"description": "Reaching the bound joins the passes exactly as `stop_when` holding\nwould: the `join` rule picks the value the node produces. Today's\nbehavior, and what an absent field means.",
"type": "string"
},
{
"const": "fail",
"description": "Reaching the bound without `stop_when` holding is an error: the loop\nconverged on nothing, and the node produces no value.",
"type": "string"
}
]
},
"ToolNode": {
"additionalProperties": false,
"description": "A `tool` node: one direct tool invocation.",
"properties": {
"id": {
"description": "The node's stable id, unique within the document.",
"type": "string"
},
"input": {
"additionalProperties": {
"type": "string"
},
"description": "The input mapping: tool input field name to an opaque source reference.\nRecorded as DATA; this crate does not resolve or evaluate the references.\nAdditive: omitted on the wire when empty.",
"type": "object"
},
"input_schema": {
"description": "Optional JSON Schema for the payload this node consumes."
},
"name": {
"description": "Optional short display label for this node. See the module docs' \"The\noptional node display name\" section for the bound and the deliberate\nhash-inclusion contrast with the agent `name` field. Additive: absent\non the wire when unset.",
"type": [
"string",
"null"
]
},
"output_schema": {
"description": "Optional JSON Schema for the payload this node produces."
},
"tool": {
"description": "The tool's name, as registered with the runtime.",
"type": "string"
}
},
"required": [
"id",
"tool"
],
"type": "object"
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"description": "A graph document: the control document authored once, submitted, hashed into\na run, and then frozen. It carries the schema version, the set of nodes\n(each with a stable string id), and the edges that connect them.\n\nSerializing a `Graph` always includes `schema_version`, so the wire form is\nself-describing. Unknown top-level keys are rejected.",
"properties": {
"edges": {
"default": [],
"description": "The directed edges connecting nodes. A document with a single node and\nno edges is legal, so this defaults to empty.",
"items": {
"$ref": "#/$defs/Edge"
},
"type": "array"
},
"nodes": {
"description": "The nodes, each identified by a stable string id unique within the\ndocument.",
"items": {
"$ref": "#/$defs/Node"
},
"type": "array"
},
"schema_version": {
"description": "The document schema version. Always `SCHEMA_VERSION` for documents\nthis build writes; an older value may appear when reading a recorded\ndocument.",
"format": "uint32",
"minimum": 0,
"type": "integer"
}
},
"required": [
"schema_version",
"nodes"
],
"title": "Graph",
"type": "object"
}