lorebase-client 0.2.3__py3-none-any.whl

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.
@@ -0,0 +1,1662 @@
1
+ """Typed request and response models of the Keeper API.
2
+
3
+ They mirror the routes in `packages/keeper` and the OpenAPI document in `docs/openapi.json`
4
+ (a contract test compares the two). Response models ignore members they do not know, so a
5
+ newer server does not break an older client. Values of Records and Links are in canonical
6
+ form: integers and decimals are JSON strings, timestamps RFC 3339.
7
+
8
+ Request models are validated when built; the clients send them without unset members.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from datetime import datetime
14
+ from typing import Any, Literal, Protocol, Self
15
+
16
+ from pydantic import BaseModel, ConfigDict, Field, JsonValue, model_validator
17
+
18
+ DOMAIN_ID_PATTERN = r"^[a-z][a-z0-9_-]*$"
19
+
20
+ SchemaSection = Literal["kinds", "relations", "traits", "operations", "derivations", "sources"]
21
+ SearchMode = Literal["lexical", "semantic", "hybrid"]
22
+ BranchMode = Literal["copy", "overlay"]
23
+ MaterializationStatus = Literal["pending", "running", "done", "failed"]
24
+ MaterializationPhase = Literal[
25
+ "records",
26
+ "links",
27
+ "history",
28
+ "clear",
29
+ "tree_history",
30
+ "tree_rows",
31
+ "switch",
32
+ "prune",
33
+ "derived",
34
+ "paths",
35
+ "search",
36
+ "done",
37
+ ]
38
+ ServedFrom = Literal["materialized", "tree"]
39
+ ChangeKind = Literal["created", "updated", "deleted"]
40
+ LinkDirection = Literal["forward", "inverse"]
41
+ ProposalStatus = Literal["open", "merged", "closed"]
42
+ ProposalOrigin = Literal["manual", "source_sync", "extraction", "operation"]
43
+ ReviewVerdict = Literal["approve", "request_changes", "comment"]
44
+ ConflictStatus = Literal["open", "resolved"]
45
+ CheckStatus = Literal["passed", "failed", "pending"]
46
+ ContextFormat = Literal["json", "markdown"]
47
+ ItemDecisionValue = Literal["pending", "accepted", "rejected"]
48
+ ItemType = Literal["record", "value", "link"]
49
+ SuggestionType = Literal["kind", "attribute", "relation"]
50
+
51
+
52
+ class Model(BaseModel):
53
+ """Base of response models: immutable, unknown members ignored."""
54
+
55
+ model_config = ConfigDict(extra="ignore", populate_by_name=True, frozen=True)
56
+
57
+
58
+ class RequestModel(BaseModel):
59
+ """Base of request models: unknown members are rejected before anything is sent."""
60
+
61
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
62
+
63
+ def payload(self) -> dict[str, Any]:
64
+ """JSON body: set members only, by alias."""
65
+ return self.model_dump(mode="json", exclude_none=True, by_alias=True)
66
+
67
+
68
+ # Pages ------------------------------------------------------------------------------------
69
+
70
+
71
+ class Paged[T](Protocol):
72
+ """Anything with `items` and `next_cursor`: what the pagination helpers walk."""
73
+
74
+ @property
75
+ def items(self) -> list[T]: ...
76
+
77
+ @property
78
+ def next_cursor(self) -> str | None: ...
79
+
80
+
81
+ class Page[T](Model):
82
+ """One page of a list endpoint; `next_cursor` is null on the last page."""
83
+
84
+ items: list[T]
85
+ next_cursor: str | None = None
86
+
87
+
88
+ # Identity ---------------------------------------------------------------------------------
89
+
90
+
91
+ class QuotaUse(Model):
92
+ """Use of one daily quota; without a limit, `limit` and `remaining` are None."""
93
+
94
+ limit: int | None = None
95
+ used: int
96
+ remaining: int | None = None
97
+
98
+
99
+ class QuotaReport(Model):
100
+ """Daily quotas of an API key and what it has used since the last reset."""
101
+
102
+ period: str = "day"
103
+ resets_at: datetime
104
+ requests: QuotaUse
105
+ context_tokens: QuotaUse
106
+
107
+
108
+ class WhoAmI(Model):
109
+ """The authenticated caller."""
110
+
111
+ id: str
112
+ type: str
113
+ display_name: str
114
+ method: str
115
+ subjects: list[str]
116
+ key_scopes: list[str]
117
+ installation_admin: bool
118
+ quota: QuotaReport | None = None
119
+
120
+
121
+ # Domains ----------------------------------------------------------------------------------
122
+
123
+
124
+ class DomainCreate(RequestModel):
125
+ """A new Domain."""
126
+
127
+ id: str = Field(pattern=DOMAIN_ID_PATTERN, max_length=63)
128
+ display_name: str = Field(default="", max_length=200)
129
+ description: str = Field(default="", max_length=4000)
130
+ default_branch: str | None = Field(default=None, min_length=1, max_length=200)
131
+ settings: dict[str, JsonValue] = Field(default_factory=dict)
132
+
133
+
134
+ class DomainUpdate(RequestModel):
135
+ """Display metadata; omitted members stay as they are."""
136
+
137
+ display_name: str | None = Field(default=None, min_length=1, max_length=200)
138
+ description: str | None = Field(default=None, max_length=4000)
139
+
140
+
141
+ class Domain(Model):
142
+ """A Domain and the head Revision of its default branch."""
143
+
144
+ id: str
145
+ display_name: str
146
+ description: str | None = None
147
+ default_branch: str
148
+ settings: dict[str, Any] = Field(default_factory=dict)
149
+ created_at: datetime
150
+ created_by: str
151
+ head: str | None = Field(default=None, description="Head of the default branch (get only).")
152
+
153
+
154
+ # Branches and tags ------------------------------------------------------------------------
155
+
156
+
157
+ class BranchCreate(RequestModel):
158
+ """A new branch, started at a Revision or at the head of a branch or tag."""
159
+
160
+ name: str = Field(min_length=1)
161
+ source: str | None = None
162
+ revision: str | None = None
163
+ mode: BranchMode | None = None
164
+ """`copy` (own rows) or `overlay` (only its changes); default: the server's setting."""
165
+
166
+
167
+ class BranchUpdate(RequestModel):
168
+ """Branch flags."""
169
+
170
+ protected: bool
171
+
172
+
173
+ class MaterializationProgress(Model):
174
+ """How far the background copy of a branch got."""
175
+
176
+ phase: MaterializationPhase
177
+ rows_done: int
178
+ rows_total: int | None
179
+ error: str | None
180
+ updated_at: datetime
181
+ finished_at: datetime | None
182
+
183
+
184
+ class Branch(Model):
185
+ """A branch."""
186
+
187
+ name: str
188
+ head: str
189
+ protected: bool
190
+ default: bool
191
+ materialization: str
192
+ version: int
193
+ created_at: datetime
194
+ created_by: str
195
+ materialization_status: MaterializationStatus | None = None
196
+ materialization_progress: MaterializationProgress | None = None
197
+
198
+
199
+ class BranchDetail(Branch):
200
+ """A branch with its distance to the default branch."""
201
+
202
+ ahead: int
203
+ behind: int
204
+
205
+
206
+ class TagCreate(RequestModel):
207
+ """A new tag at a Revision or at the head of a branch."""
208
+
209
+ name: str = Field(min_length=1)
210
+ source: str | None = None
211
+ revision: str | None = None
212
+
213
+
214
+ class Tag(Model):
215
+ """An immutable tag."""
216
+
217
+ name: str
218
+ revision: str
219
+ created_at: datetime
220
+ created_by: str
221
+
222
+
223
+ # Schema -----------------------------------------------------------------------------------
224
+
225
+
226
+ class ElementRemoval(RequestModel):
227
+ """A schema element to remove."""
228
+
229
+ section: SchemaSection
230
+ name: str = Field(min_length=1)
231
+
232
+
233
+ class _ChangeSet(RequestModel):
234
+ yaml: str | None = Field(default=None, description="Full schema document.")
235
+ patch: dict[str, JsonValue] | None = Field(default=None, description="Partial document.")
236
+ remove: list[ElementRemoval] = Field(default_factory=list)
237
+
238
+ @model_validator(mode="after")
239
+ def _one_form(self) -> Self:
240
+ if (self.yaml is not None) == (self.patch is not None or bool(self.remove)):
241
+ raise ValueError("give either `yaml`, or `patch` and/or `remove`")
242
+ return self
243
+
244
+
245
+ class SchemaChangeRequest(_ChangeSet):
246
+ """A schema change set for one branch."""
247
+
248
+ message: str | None = Field(default=None, max_length=2000)
249
+ expected_head: str | None = None
250
+ dry_run: bool = False
251
+ cascade: bool = False
252
+
253
+
254
+ class SchemaValidateRequest(_ChangeSet):
255
+ """A change set to validate against a branch or Revision."""
256
+
257
+ branch: str | None = None
258
+ at: str | None = None
259
+
260
+
261
+ class EmbeddingModel(Model):
262
+ """The model a `query_vector` must be made with."""
263
+
264
+ model: str
265
+ dims: int
266
+
267
+
268
+ class SchemaSearch(Model):
269
+ """How the Domain can be searched; `embedding` is set when semantic search is available."""
270
+
271
+ modes: list[Literal["lexical", "semantic", "hybrid"]] = ["lexical"]
272
+ embedding: EmbeddingModel | None = None
273
+
274
+
275
+ class SchemaDocument(Model):
276
+ """The schema of a Revision: elements keyed by id, with api names."""
277
+
278
+ domain: str
279
+ branch: str | None = None
280
+ revision: str
281
+ schema_: dict[str, Any] = Field(alias="schema")
282
+ search: SchemaSearch = Field(default_factory=SchemaSearch)
283
+
284
+
285
+ class ElementChange(Model):
286
+ """One schema element written or removed."""
287
+
288
+ op: Literal["create", "update", "delete"]
289
+ section: str
290
+ id: str
291
+ api_name: str
292
+
293
+
294
+ class SchemaChangeResult(Model):
295
+ """Outcome of a schema change set."""
296
+
297
+ status: Literal["committed", "dry_run", "no_op"]
298
+ branch: str
299
+ head: str
300
+ revision: str | None = None
301
+ changes: list[ElementChange]
302
+ change_count: int
303
+ data_changes: int
304
+ issues: list[dict[str, Any]]
305
+ issue_count: int
306
+
307
+
308
+ class RebaseTagsRequest(RequestModel):
309
+ """A tag rebase of one branch: take the default branch's sensitivity tags."""
310
+
311
+ message: str | None = Field(default=None, max_length=2000)
312
+ expected_head: str | None = None
313
+ dry_run: bool = False
314
+ rows_only: bool = False
315
+
316
+
317
+ class TagChange(Model):
318
+ """Sensitivity tags a tag rebase adds to one Attribute."""
319
+
320
+ section: Literal["kinds", "relations"]
321
+ owner: str
322
+ attribute: str
323
+ tags: list[str]
324
+
325
+
326
+ class RebaseTagsResult(Model):
327
+ """Outcome of a tag rebase; `job` is the queued row rewrite of a `rows_only` rebase."""
328
+
329
+ status: Literal["committed", "dry_run", "no_op", "queued"]
330
+ branch: str
331
+ head: str
332
+ revision: str | None = None
333
+ tags: list[TagChange] = Field(default_factory=list)
334
+ changes: list[ElementChange] = Field(default_factory=list)
335
+ change_count: int = 0
336
+ issues: list[dict[str, Any]] = Field(default_factory=list)
337
+ issue_count: int = 0
338
+ job: int | None = None
339
+
340
+
341
+ class SchemaValidation(Model):
342
+ """Static validation of a change set."""
343
+
344
+ valid: bool
345
+ issues: list[dict[str, Any]]
346
+ issue_count: int
347
+ changes: list[ElementChange]
348
+
349
+
350
+ # Operations -------------------------------------------------------------------------------
351
+
352
+
353
+ class ExecuteRequest(RequestModel):
354
+ """An execution of an Operation."""
355
+
356
+ parameters: dict[str, JsonValue] = Field(default_factory=dict)
357
+ branch: str | None = None
358
+ dry_run: bool = False
359
+ idempotency_key: str | None = None
360
+ expected_head: str | None = None
361
+ message: str | None = Field(default=None, max_length=2000)
362
+ task: str | None = Field(default=None, min_length=1, max_length=200)
363
+ intent: str | None = Field(default=None, max_length=4000)
364
+ auto_propose: bool | None = None
365
+ auto_merge: bool | None = None
366
+
367
+
368
+ class Execution(Model):
369
+ """Result of an Operation execution."""
370
+
371
+ status: Literal["committed", "dry_run"]
372
+ operation: str
373
+ branch: str
374
+ invocation_id: str
375
+ parameters_hash: str
376
+ head: str
377
+ revision: str | None = None
378
+ revisions: list[str]
379
+ edits: list[dict[str, Any]]
380
+ edit_count: int
381
+ edits_truncated: bool
382
+ violations: list[dict[str, Any]]
383
+ violation_count: int
384
+ warnings: list[dict[str, Any]]
385
+ review_required: bool
386
+ replayed: bool
387
+
388
+
389
+ class ProposalRef(Model):
390
+ """The Proposal carrying the edits of an agent write."""
391
+
392
+ id: str
393
+ title: str
394
+ status: str
395
+ source_ref: str
396
+ target_ref: str
397
+ url: str
398
+ created: bool
399
+
400
+
401
+ class AgentApproval(Model):
402
+ required: int
403
+ approvals: int
404
+ satisfied: bool
405
+
406
+
407
+ class AgentAttempt(Model):
408
+ """Conflicts between the task branch and the target."""
409
+
410
+ id: str
411
+ result: str | None = None
412
+ open_conflicts: int
413
+ resolved_conflicts: int
414
+ counts: dict[str, dict[str, int]]
415
+
416
+
417
+ class AgentMerge(Model):
418
+ requested: bool
419
+ allowed: bool
420
+ performed: bool
421
+ blockers: list[str]
422
+
423
+
424
+ class NextHint(Model):
425
+ """A next step: what to do, and the call that does it when there is one."""
426
+
427
+ action: str
428
+ detail: str
429
+ method: str | None = None
430
+ href: str | None = None
431
+ permission: str | None = None
432
+
433
+
434
+ class AgentWrite(Model):
435
+ """An Operation that needed review, run on a task branch and proposed (HTTP 202)."""
436
+
437
+ status: Literal["proposed", "merged", "conflicts"]
438
+ operation: str
439
+ branch: str
440
+ target: str
441
+ invocation_id: str
442
+ parameters_hash: str
443
+ revision: str | None = None
444
+ revisions: list[str]
445
+ edit_count: int
446
+ replayed: bool
447
+ proposal: ProposalRef
448
+ approval: AgentApproval
449
+ attempt: AgentAttempt | None = None
450
+ merge: AgentMerge
451
+ merged_revision: str | None = None
452
+ branch_created: bool
453
+ branch_deleted: bool
454
+ warnings: list[dict[str, Any]]
455
+ next: list[NextHint]
456
+
457
+
458
+ # Reads ------------------------------------------------------------------------------------
459
+
460
+
461
+ class LinkCount(Model):
462
+ relation: str
463
+ direction: LinkDirection
464
+ count: int
465
+
466
+
467
+ class Lineage(Model):
468
+ provenance_id: str
469
+ origin_type: str | None = None
470
+ actor: str | None = None
471
+ at: datetime | None = None
472
+
473
+
474
+ class RecordItem(Model):
475
+ """A Record with values keyed by Attribute api name."""
476
+
477
+ kind: str
478
+ key: str
479
+ title: str | None = None
480
+ values: dict[str, JsonValue] = Field(default_factory=dict)
481
+ updated_revision: str | None = None
482
+ updated_at: datetime | None = None
483
+ links: list[LinkCount] | None = None
484
+ lineage: dict[str, Lineage] | None = None
485
+ included: dict[str, list[RecordItem]] | None = None
486
+
487
+
488
+ class RecordRef(Model):
489
+ kind: str
490
+ key: str
491
+ title: str | None = None
492
+
493
+
494
+ class Served(Model):
495
+ """Where a response was read."""
496
+
497
+ revision: str
498
+ served_from: ServedFrom
499
+
500
+
501
+ class RecordResponse(Served):
502
+ record: RecordItem
503
+
504
+
505
+ class RecordPage(Served):
506
+ items: list[RecordItem]
507
+ next_cursor: str | None = None
508
+
509
+
510
+ class AggregateCount(Model):
511
+ kind: str
512
+ value: JsonValue = None
513
+ count: int
514
+
515
+
516
+ class Aggregate(Model):
517
+ counts: list[AggregateCount]
518
+ truncated: bool = False
519
+
520
+
521
+ class QueryResult(RecordPage):
522
+ aggregate: Aggregate | None = None
523
+
524
+
525
+ class LinkItem(Model):
526
+ """A Link seen from one side."""
527
+
528
+ relation: str
529
+ direction: LinkDirection
530
+ from_key: str
531
+ to_key: str
532
+ values: dict[str, JsonValue] = Field(default_factory=dict)
533
+ target: RecordRef | None = None
534
+
535
+
536
+ class LinkPage(Served):
537
+ items: list[LinkItem]
538
+ next_cursor: str | None = None
539
+
540
+
541
+ class Edge(Model):
542
+ relation: str
543
+ source: RecordRef = Field(alias="from")
544
+ to: RecordRef
545
+ values: dict[str, JsonValue] = Field(default_factory=dict)
546
+
547
+
548
+ class TraverseHop(Model):
549
+ relation: str
550
+ records: list[RecordItem]
551
+ edges: list[Edge]
552
+ truncated: bool = False
553
+
554
+
555
+ class TraverseResult(Served):
556
+ start: RecordItem
557
+ hops: list[TraverseHop]
558
+
559
+
560
+ class NeighborhoodResult(Served):
561
+ nodes: list[RecordItem]
562
+ edges: list[Edge]
563
+ truncated: bool = False
564
+
565
+
566
+ class SearchHit(Model):
567
+ kind: str
568
+ key: str
569
+ title: str | None = None
570
+ snippet: str | None = None
571
+ score: float
572
+ match: Literal["all", "partial"] | None = None
573
+ """`all` when the hit holds every term of q, `partial` when only some (ranked after the
574
+ others); None for hits found by vector only."""
575
+
576
+
577
+ class SearchCorrection(Model):
578
+ """A query word the branch does not hold and the words searched in its place as well."""
579
+
580
+ word: str
581
+ replacements: list[str]
582
+
583
+
584
+ class SearchResult(Served):
585
+ mode: str
586
+ items: list[SearchHit]
587
+ corrections: list[SearchCorrection] = Field(default_factory=list)
588
+ truncated_candidates: bool = False
589
+ """Only a sample of the matches was ranked (every term of q is very common): narrow q."""
590
+
591
+
592
+ class QueryRequest(RequestModel):
593
+ """A query of the JSON DSL and where to run it. Predicates are plain JSON objects."""
594
+
595
+ kind: str | None = None
596
+ trait: str | None = None
597
+ where: dict[str, JsonValue] | None = None
598
+ select: list[str] | None = None
599
+ include: dict[str, dict[str, JsonValue]] | None = None
600
+ order_by: list[dict[str, str]] | None = None
601
+ limit: int | None = Field(default=None, ge=0)
602
+ cursor: str | None = None
603
+ aggregate: dict[str, JsonValue] | None = None
604
+ branch: str | None = None
605
+ at: str | None = None
606
+
607
+
608
+ class RecordAddress(RequestModel):
609
+ kind: str = Field(min_length=1)
610
+ key: str = Field(min_length=1)
611
+
612
+
613
+ class TraverseStep(RequestModel):
614
+ relation: str = Field(min_length=1)
615
+ where: dict[str, JsonValue] | None = None
616
+ limit: int | None = Field(default=None, ge=1)
617
+
618
+
619
+ class TraverseRequest(RequestModel):
620
+ """Follow a path of Relations from one Record."""
621
+
622
+ branch: str | None = None
623
+ at: str | None = None
624
+ start: RecordAddress
625
+ path: list[TraverseStep] = Field(min_length=1)
626
+
627
+
628
+ class NeighborhoodRequest(RequestModel):
629
+ """Breadth-first expansion from seed Records."""
630
+
631
+ branch: str | None = None
632
+ at: str | None = None
633
+ seeds: list[RecordAddress] = Field(min_length=1)
634
+ depth: int = Field(default=1, ge=1)
635
+ relations: list[str] | None = None
636
+ max_nodes: int | None = Field(default=None, ge=1)
637
+ max_edges: int | None = Field(default=None, ge=1)
638
+ values: bool | None = None
639
+ """False: nodes come back as headers (kind, key, title, empty `values`) and edges as
640
+ endpoints (empty `values`); None keeps the server default (with values)."""
641
+
642
+
643
+ class SearchTuning(RequestModel):
644
+ """Ranking knobs of one search in place of the configured ones; unset knobs keep theirs.
645
+
646
+ `candidates` and `typo_candidates` may only be lowered.
647
+ """
648
+
649
+ lexical_weight: float | None = Field(default=None, ge=0, le=100)
650
+ semantic_weight: float | None = Field(default=None, ge=0, le=100)
651
+ rrf_k: int | None = Field(default=None, ge=1, le=10000)
652
+ trigram_threshold: float | None = Field(default=None, ge=0.1, le=1)
653
+ trigram_weight: float | None = Field(default=None, ge=0, le=100)
654
+ typo_candidates: int | None = Field(default=None, ge=0)
655
+ candidates: int | None = Field(default=None, ge=1)
656
+
657
+
658
+ class SearchRequest(RequestModel):
659
+ """Lexical, semantic (caller-supplied vector) or hybrid search."""
660
+
661
+ branch: str | None = None
662
+ at: str | None = None
663
+ q: str | None = Field(default=None, min_length=1)
664
+ kinds: list[str] | None = None
665
+ where: dict[str, JsonValue] | None = None
666
+ mode: SearchMode | None = None
667
+ query_vector: list[float] | None = None
668
+ limit: int | None = Field(default=None, ge=1)
669
+ tuning: SearchTuning | None = None
670
+
671
+
672
+ # Revisions, log and diff ------------------------------------------------------------------
673
+
674
+
675
+ class Revision(Model):
676
+ id: str
677
+ parents: list[str]
678
+ root: str
679
+ generation: int
680
+ author: str
681
+ on_behalf_of: str | None = None
682
+ message: str
683
+ origin: dict[str, Any]
684
+ stats: dict[str, Any]
685
+ created_at: datetime
686
+
687
+
688
+ class LogPage(Model):
689
+ branch: str
690
+ head: str
691
+ items: list[Revision]
692
+ next_cursor: str | None = None
693
+
694
+
695
+ class PartitionCount(Model):
696
+ partition: str
697
+ element: str
698
+ name: str
699
+ count: int
700
+
701
+
702
+ class RevisionCounts(Model):
703
+ revision: str
704
+ partitions: list[PartitionCount]
705
+
706
+
707
+ class ValueChange(Model):
708
+ name: str
709
+ old: JsonValue = None
710
+ new: JsonValue = None
711
+
712
+
713
+ class Change(Model):
714
+ """One changed element between two Revisions."""
715
+
716
+ element: str
717
+ change: Literal["added", "removed", "modified"]
718
+ partition: str
719
+ kind: str | None = None
720
+ relation: str | None = None
721
+ key: str | None = None
722
+ from_key: str | None = None
723
+ to_key: str | None = None
724
+ element_type: str | None = None
725
+ api_name: str | None = None
726
+ old_hash: str | None = None
727
+ new_hash: str | None = None
728
+ changes: list[ValueChange] | None = None
729
+ provenance: dict[str, str] | None = None
730
+
731
+
732
+ class PartitionSummary(Model):
733
+ partition: str
734
+ element: str
735
+ name: str
736
+ added: int = 0
737
+ removed: int = 0
738
+ modified: int = 0
739
+
740
+
741
+ class ChangesPage(Model):
742
+ """A page of element changes with the summary of all of them."""
743
+
744
+ from_revision: str | None = None
745
+ to_revision: str
746
+ total: int
747
+ summary: list[PartitionSummary]
748
+ items: list[Change]
749
+ next_cursor: str | None = None
750
+
751
+
752
+ # Administration ---------------------------------------------------------------------------
753
+
754
+
755
+ class EraseRequest(RequestModel):
756
+ """Erase the vaulted values of a Record (`key`, optional `attributes`) or a value of a
757
+ vault Attribute in every Record of a Kind (`attribute`, `value`)."""
758
+
759
+ kind: str
760
+ key: str | None = None
761
+ attributes: list[str] | None = None
762
+ attribute: str | None = None
763
+ value: JsonValue = None
764
+ branch: str | None = None
765
+
766
+
767
+ class EraseResult(Model):
768
+ """Outcome of an erasure: `erased` vault values were erased by this request."""
769
+
770
+ domain: str
771
+ kind: str
772
+ mode: str
773
+ erased: int
774
+ unopenable: int = 0
775
+
776
+
777
+ class VaultKeys(Model):
778
+ """Which keys the vault rows use (`vault.manage`)."""
779
+
780
+ key_id: str
781
+ token_key_id: str
782
+ fingerprint_key: str
783
+ encryption_keys: dict[str, int]
784
+ fingerprint_keys: dict[str, int]
785
+ tombstone_keys: dict[str, int]
786
+ outdated: int
787
+ removable_keys: list[str]
788
+ removable_token_keys: list[str]
789
+ digests: str
790
+
791
+
792
+ class VaultRotation(Model):
793
+ """A queued vault key rotation."""
794
+
795
+ job_id: int
796
+
797
+
798
+ class CreateApiKey(RequestModel):
799
+ """Request to create an API key for a Domain."""
800
+
801
+ name: str = Field(min_length=1, max_length=120)
802
+ role: str | None = None
803
+ ttl_days: int | None = Field(default=None, ge=1)
804
+ principal_id: str | None = None
805
+ allowed_networks: list[str] | None = None
806
+
807
+
808
+ class ApiKey(Model):
809
+ """An API key; the secret is shown only when it is created."""
810
+
811
+ id: str
812
+ principal_id: str
813
+ prefix: str
814
+ scopes: list[str]
815
+ expires_at: datetime | None = None
816
+ last_used_at: datetime | None = None
817
+ revoked_at: datetime | None = None
818
+ created_at: datetime
819
+ allowed_networks: list[str] = Field(default_factory=list)
820
+
821
+
822
+ class ApiKeyCreated(ApiKey):
823
+ """A new API key: `key` is the secret, returned once."""
824
+
825
+ key: str
826
+ role: str | None = None
827
+
828
+
829
+ class Principal(Model):
830
+ id: str
831
+ type: str
832
+ display_name: str
833
+ external_subject: str | None = None
834
+ created_at: datetime
835
+
836
+
837
+ class CreateGrant(RequestModel):
838
+ """Request to grant a role."""
839
+
840
+ subject: str = Field(min_length=1)
841
+ role: str
842
+ scope_type: Literal["domain", "kind", "operation", "branch"] = "domain"
843
+ scope_ref: str | None = Field(default=None, min_length=1)
844
+
845
+
846
+ class Grant(Model):
847
+ id: str
848
+ domain_id: str
849
+ subject: str
850
+ role: str
851
+ scope_type: str
852
+ scope_ref: str | None = None
853
+ created_by: str
854
+ created_at: datetime
855
+
856
+
857
+ class AuditEntry(Model):
858
+ seq: int
859
+ at: datetime
860
+ domain_id: str | None = None
861
+ actor: str
862
+ action: str
863
+ target: str | None = None
864
+ details: dict[str, Any] = Field(default_factory=dict)
865
+ entry_hash: str
866
+
867
+
868
+ class AuditBreak(Model):
869
+ """The first audit entry of a chain that does not verify."""
870
+
871
+ seq: int
872
+ reason: str
873
+
874
+
875
+ class AuditVerification(Model):
876
+ """Result of recomputing a Domain's audit hash chain."""
877
+
878
+ domain_id: str
879
+ intact: bool
880
+ checked: int
881
+ after_seq: int
882
+ last_seq: int | None = None
883
+ first_broken: AuditBreak | None = None
884
+
885
+
886
+ # Proposals --------------------------------------------------------------------------------
887
+
888
+
889
+ class ProposalCreate(RequestModel):
890
+ """A new Proposal: merge `source_ref` into `target_ref`."""
891
+
892
+ source_ref: str = Field(min_length=1, max_length=200)
893
+ target_ref: str = Field(min_length=1, max_length=200)
894
+ title: str = Field(min_length=1, max_length=300)
895
+ description: str | None = Field(default=None, max_length=20000)
896
+
897
+
898
+ class ProposalUpdate(RequestModel):
899
+ """Title and description; omitted members are left unchanged."""
900
+
901
+ title: str | None = Field(default=None, min_length=1, max_length=300)
902
+ description: str | None = Field(default=None, max_length=20000)
903
+
904
+
905
+ class ReviewIn(RequestModel):
906
+ """A review of a Proposal."""
907
+
908
+ verdict: ReviewVerdict
909
+ comment: str | None = Field(default=None, max_length=20000)
910
+
911
+
912
+ class MergeIn(RequestModel):
913
+ """Options of a merge."""
914
+
915
+ message: str | None = Field(default=None, max_length=2000)
916
+
917
+
918
+ class ResolutionIn(RequestModel):
919
+ """A decision for one Conflict (`value` or per-member `values` for `custom` only)."""
920
+
921
+ choice: str
922
+ value: JsonValue = None
923
+ values: dict[str, JsonValue] | None = None
924
+
925
+
926
+ class FingerprintedResolutionIn(ResolutionIn):
927
+ """A decision for the Conflict with this fingerprint."""
928
+
929
+ fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
930
+
931
+
932
+ class ResolveManyIn(RequestModel):
933
+ """Decisions for several Conflicts at once."""
934
+
935
+ resolutions: list[FingerprintedResolutionIn]
936
+
937
+
938
+ class Check(Model):
939
+ """One Proposal check."""
940
+
941
+ name: str
942
+ status: CheckStatus
943
+ message: str | None = None
944
+
945
+
946
+ class Review(Model):
947
+ id: str
948
+ reviewer: str
949
+ verdict: ReviewVerdict
950
+ comment: str | None
951
+ at: datetime
952
+
953
+
954
+ class Approval(Model):
955
+ """Review state against the protection of the target branch."""
956
+
957
+ protected: bool
958
+ required: int
959
+ approvals: int
960
+ approvers: list[str]
961
+ changes_requested: list[str]
962
+ satisfied: bool
963
+
964
+
965
+ class Resolution(Model):
966
+ """A stored resolution; `matched` is false when its Conflict is gone from the attempt."""
967
+
968
+ fingerprint: str
969
+ choice: str
970
+ value: JsonValue = None
971
+ values: dict[str, JsonValue] | None = None
972
+ resolver: str
973
+ at: datetime
974
+ matched: bool = True
975
+
976
+
977
+ class Attempt(Model):
978
+ """A merge attempt and the resolutions of its Proposal."""
979
+
980
+ id: str
981
+ proposal: str
982
+ status: str
983
+ result: str | None
984
+ base: str | None
985
+ ours: str
986
+ theirs: str
987
+ virtual_base: bool
988
+ created_at: datetime
989
+ created_by: str | None
990
+ revision: str | None
991
+ policy: dict[str, Any]
992
+ open_conflicts: int
993
+ resolved_conflicts: int
994
+ counts: dict[str, dict[str, int]]
995
+ recomputed: bool
996
+ stale: bool
997
+ resolutions: list[Resolution]
998
+ unmatched_resolutions: list[str]
999
+ auto_resolutions: list[dict[str, Any]]
1000
+
1001
+
1002
+ class ConflictItem(Model):
1003
+ """A Conflict: `id`, `status` and the members the merge engine reports."""
1004
+
1005
+ model_config = ConfigDict(extra="allow")
1006
+
1007
+ id: str
1008
+ status: ConflictStatus
1009
+ resolution: dict[str, Any] | None = None
1010
+
1011
+
1012
+ class Conflicts(Attempt):
1013
+ """An attempt with one page of its Conflicts; walkable with the pagination helpers."""
1014
+
1015
+ conflicts: list[ConflictItem]
1016
+ next_cursor: str | None = None
1017
+
1018
+ @property
1019
+ def items(self) -> list[ConflictItem]:
1020
+ return self.conflicts
1021
+
1022
+
1023
+ class Proposal(Model):
1024
+ """A Proposal as listed."""
1025
+
1026
+ id: str
1027
+ title: str
1028
+ description: str | None
1029
+ source_ref: str
1030
+ target_ref: str
1031
+ origin: ProposalOrigin
1032
+ status: ProposalStatus
1033
+ author: str
1034
+ created_at: datetime
1035
+ updated_at: datetime | None
1036
+ merged_revision: str | None
1037
+ policy: dict[str, Any]
1038
+ checks: list[Check]
1039
+
1040
+
1041
+ class ProposalDetail(Proposal):
1042
+ """A Proposal with its reviews, approval state and last merge attempt."""
1043
+
1044
+ reviews: list[Review]
1045
+ approval: Approval
1046
+ attempt: Attempt | None
1047
+
1048
+
1049
+ class ReviewResult(Model):
1050
+ review: Review
1051
+ approval: Approval
1052
+
1053
+
1054
+ class MergeResult(Model):
1055
+ """A merged Proposal."""
1056
+
1057
+ status: Literal["merged"] = "merged"
1058
+ result: str
1059
+ revision: str
1060
+ proposal: Proposal
1061
+ attempt: Conflicts
1062
+ source_deleted: bool
1063
+ replayed: bool
1064
+
1065
+
1066
+ # Agents -----------------------------------------------------------------------------------
1067
+
1068
+
1069
+ class ContextRequest(RequestModel):
1070
+ """What an agent needs to reason about: seed Records and how much around them."""
1071
+
1072
+ branch: str | None = None
1073
+ at: str | None = None
1074
+ refs: list[str] | None = None
1075
+ query: dict[str, JsonValue] | None = None
1076
+ search: str | None = Field(default=None, min_length=1)
1077
+ budget_tokens: int | None = Field(default=None, ge=1)
1078
+ depth: int | None = Field(default=None, ge=0)
1079
+ relations: list[str] | None = None
1080
+ include_schema: bool = True
1081
+ include_recent_changes: bool = True
1082
+ include_lineage: bool = True
1083
+ since: str | None = None
1084
+ format: ContextFormat = "json"
1085
+
1086
+
1087
+ class ContextPack(Model):
1088
+ """A context pack in JSON; sections are kept as plain data."""
1089
+
1090
+ domain: str
1091
+ branch: str
1092
+ revision: str
1093
+ etag: str
1094
+ seeds: list[dict[str, Any]]
1095
+ neighbors: list[dict[str, Any]]
1096
+ links: dict[str, list[dict[str, Any]]]
1097
+ schema_: dict[str, Any] | None = Field(default=None, alias="schema")
1098
+ recent_changes: list[dict[str, Any]] | None = None
1099
+ lineage: list[dict[str, Any]] | None = None
1100
+ since: str | None = None
1101
+ budget: dict[str, Any]
1102
+
1103
+
1104
+ class Explain(Model):
1105
+ """An explained Record: where its values and Links came from; sections are plain data."""
1106
+
1107
+ domain: str
1108
+ branch: str
1109
+ revision: str
1110
+ record: dict[str, Any]
1111
+ created: dict[str, Any] | None = None
1112
+ attributes: list[dict[str, Any]]
1113
+ links: list[dict[str, Any]]
1114
+ links_truncated: bool
1115
+ history: dict[str, Any]
1116
+ proposals: list[dict[str, Any]]
1117
+
1118
+
1119
+ class Provenance(Model):
1120
+ """Where a change came from."""
1121
+
1122
+ id: str
1123
+ origin_type: str
1124
+ origin: dict[str, JsonValue]
1125
+ actor: str
1126
+ at: datetime
1127
+
1128
+
1129
+ class AttributeChange(Model):
1130
+ name: str
1131
+ before: JsonValue = None
1132
+ after: JsonValue = None
1133
+ provenance: Provenance | None = None
1134
+
1135
+
1136
+ class RecordChange(Model):
1137
+ """One change of a Record in one Revision."""
1138
+
1139
+ revision: str
1140
+ generation: int
1141
+ at: datetime
1142
+ author: str
1143
+ on_behalf_of: str | None = None
1144
+ message: str
1145
+ origin: dict[str, JsonValue]
1146
+ change: ChangeKind
1147
+ attributes: list[AttributeChange]
1148
+ provenance: Provenance | None = None
1149
+
1150
+
1151
+ class RecordHistory(Served):
1152
+ branch: str
1153
+ kind: str
1154
+ key: str
1155
+ items: list[RecordChange]
1156
+ next_cursor: str | None = None
1157
+
1158
+
1159
+ class LineageProposal(Model):
1160
+ id: str
1161
+ title: str
1162
+ status: str
1163
+ merged_revision: str | None = None
1164
+
1165
+
1166
+ class ExtractionEvidence(Model):
1167
+ job_id: str
1168
+ document_id: str
1169
+ span_start: int
1170
+ span_end: int
1171
+ model_id: str
1172
+ prompt_version: str
1173
+
1174
+
1175
+ class ValueLineage(Model):
1176
+ """The current value of an Attribute and the change behind it."""
1177
+
1178
+ name: str
1179
+ value: JsonValue = None
1180
+ provenance: Provenance | None = None
1181
+ revision: str | None = None
1182
+ revision_at: datetime | None = None
1183
+ author: str | None = None
1184
+ message: str | None = None
1185
+ source: str | None = None
1186
+ operation: str | None = None
1187
+ extraction: ExtractionEvidence | None = None
1188
+ proposal: LineageProposal | None = None
1189
+
1190
+
1191
+ class RecordLineage(Served):
1192
+ branch: str
1193
+ kind: str
1194
+ key: str
1195
+ created: RecordChange | None = None
1196
+ changes: int
1197
+ changes_truncated: bool = False
1198
+ attributes: list[ValueLineage]
1199
+ sources: list[str]
1200
+ proposals: list[LineageProposal]
1201
+
1202
+
1203
+ class HealthSummary(Model):
1204
+ """Totals of a health report; `ok` is false when there are violations, orphan Links,
1205
+ required-relation gaps or duplicates of unique Attributes."""
1206
+
1207
+ ok: bool
1208
+ kinds: int
1209
+ records: int
1210
+ links: int
1211
+ invalid_records: int
1212
+ orphan_links: int
1213
+ required_relation_gaps: int
1214
+ duplicate_groups: int
1215
+ stale_kinds: int
1216
+ sampled_kinds: int
1217
+
1218
+
1219
+ class DomainHealth(Model):
1220
+ """Health of a Domain at the head of a branch; Kind and Relation sections are plain data."""
1221
+
1222
+ revision: str
1223
+ served_from: ServedFrom
1224
+ domain: str
1225
+ branch: str
1226
+ summary: HealthSummary
1227
+ kinds: list[dict[str, Any]]
1228
+ relations: list[dict[str, Any]]
1229
+
1230
+
1231
+ class KindHealthReport(Model):
1232
+ """Health of one Kind with the Relations it takes part in."""
1233
+
1234
+ revision: str
1235
+ served_from: ServedFrom
1236
+ domain: str
1237
+ branch: str
1238
+ summary: HealthSummary
1239
+ health: dict[str, Any]
1240
+ relations: list[dict[str, Any]]
1241
+
1242
+
1243
+ class RevisionEvent(Model):
1244
+ """A Revision committed on a branch."""
1245
+
1246
+ sequence: int | None = None
1247
+ type: str
1248
+ domain: str
1249
+ branch: str
1250
+ revision: str
1251
+ parents: list[str]
1252
+ generation: int
1253
+ author: str
1254
+ on_behalf_of: str | None = None
1255
+ message: str
1256
+ origin: dict[str, JsonValue]
1257
+ stats: dict[str, JsonValue]
1258
+ created_at: datetime
1259
+ kinds: dict[str, int]
1260
+ relations: dict[str, int]
1261
+ schema_changed: bool
1262
+ keys: dict[str, list[str]]
1263
+ keys_truncated: bool
1264
+
1265
+
1266
+ class ChangesFeed(Model):
1267
+ """Revision events after a point; `next_since` continues the feed while `more` is true."""
1268
+
1269
+ branch: str
1270
+ head: str
1271
+ since: str | None = None
1272
+ items: list[RevisionEvent]
1273
+ next_since: str
1274
+ more: bool
1275
+
1276
+
1277
+ # Webhooks ---------------------------------------------------------------------------------
1278
+
1279
+
1280
+ class CreateWebhook(RequestModel):
1281
+ """A webhook for events of a Domain, optionally narrowed to some branches."""
1282
+
1283
+ url: str = Field(min_length=1, max_length=2048)
1284
+ events: list[str] | None = None
1285
+ branches: list[str] | None = None
1286
+ description: str | None = Field(default=None, max_length=500)
1287
+ clearances: list[str] | None = None
1288
+ """Sensitivity tags (`pii`, `confidential`, `restricted`) whose values the webhook receives
1289
+ in clear; others are masked."""
1290
+
1291
+
1292
+ class UpdateWebhook(RequestModel):
1293
+ """The new clearances of a webhook, replacing the current ones."""
1294
+
1295
+ clearances: list[str]
1296
+
1297
+
1298
+ class Webhook(Model):
1299
+ id: str
1300
+ url: str
1301
+ description: str | None = None
1302
+ events: list[str]
1303
+ branches: list[str] | None = None
1304
+ clearances: list[str] = Field(default_factory=list)
1305
+ created_by: str
1306
+ created_at: datetime
1307
+
1308
+
1309
+ class WebhookCreated(Webhook):
1310
+ """A new webhook: `secret` signs deliveries and is returned once."""
1311
+
1312
+ secret: str
1313
+
1314
+
1315
+ class WebhookList(Model):
1316
+ items: list[Webhook]
1317
+
1318
+
1319
+ # Sources ----------------------------------------------------------------------------------
1320
+
1321
+
1322
+ class SyncRequest(RequestModel):
1323
+ """A synchronization of a Source; a dry run records the delta without writing."""
1324
+
1325
+ dry_run: bool = False
1326
+
1327
+
1328
+ class SyncQueued(Model):
1329
+ """The queued (or already pending) synchronization job."""
1330
+
1331
+ job_id: int
1332
+ source: str
1333
+ api_name: str
1334
+ dry_run: bool
1335
+
1336
+
1337
+ class SourceRun(Model):
1338
+ id: str
1339
+ source: str
1340
+ status: str
1341
+ started_at: datetime | None = None
1342
+ finished_at: datetime | None = None
1343
+ stats: dict[str, JsonValue]
1344
+ revision: str | None = None
1345
+ proposal_id: str | None = None
1346
+ error: dict[str, JsonValue] | None = None
1347
+
1348
+
1349
+ # Documents --------------------------------------------------------------------------------
1350
+
1351
+
1352
+ class DocumentFromUrl(RequestModel):
1353
+ """A document fetched by the server from an allowlisted URL."""
1354
+
1355
+ url: str = Field(min_length=1, max_length=2000)
1356
+ filename: str | None = Field(default=None, max_length=255)
1357
+ extract: bool | None = None
1358
+ tiers: list[int] | None = None
1359
+
1360
+
1361
+ class Document(Model):
1362
+ """A stored document; `text_sha256` is set once Scribe parsed its text."""
1363
+
1364
+ id: str
1365
+ filename: str
1366
+ media_type: str
1367
+ sha256: str
1368
+ size: int
1369
+ text_sha256: str | None = None
1370
+ metadata: dict[str, JsonValue]
1371
+ created_at: datetime | None = None
1372
+ created_by: str
1373
+
1374
+
1375
+ class ExtractionJob(Model):
1376
+ id: str
1377
+ status: str
1378
+ model: str
1379
+ prompt_version: str
1380
+ branch: str | None = None
1381
+ proposal_id: str | None = None
1382
+ created_at: datetime | None = None
1383
+ finished_at: datetime | None = None
1384
+ usage: dict[str, JsonValue]
1385
+ stats: dict[str, JsonValue]
1386
+ error: dict[str, JsonValue] | None = None
1387
+
1388
+
1389
+ class DocumentDetail(Document):
1390
+ """A document with its extraction jobs, newest first."""
1391
+
1392
+ extraction_jobs: list[ExtractionJob]
1393
+
1394
+
1395
+ class DocumentText(Model):
1396
+ """The parsed text of a document; extraction evidence offsets index it."""
1397
+
1398
+ document_id: str
1399
+ text_sha256: str
1400
+ text: str
1401
+ length: int
1402
+ truncated: bool
1403
+
1404
+
1405
+ class DocumentUploaded(Model):
1406
+ """The stored document; `duplicate` when the same bytes were stored before."""
1407
+
1408
+ document: Document
1409
+ duplicate: bool
1410
+ job_id: int | None = None
1411
+
1412
+
1413
+ # Extraction items -------------------------------------------------------------------------
1414
+
1415
+
1416
+ class ProposalItem(Model):
1417
+ """One extracted Record, value or Link of an extraction Proposal."""
1418
+
1419
+ id: str
1420
+ type: ItemType
1421
+ seq: int
1422
+ decision: ItemDecisionValue
1423
+ confidence: float | None
1424
+ tier: int | None
1425
+ model: str | None
1426
+ kind: str | None = None
1427
+ relation: str | None = None
1428
+ key: str | None = None
1429
+ attribute: str | None = None
1430
+ value: JsonValue = None
1431
+ current: JsonValue = None
1432
+ from_key: str | None = Field(default=None, alias="from")
1433
+ to_key: str | None = Field(default=None, alias="to")
1434
+ values: dict[str, JsonValue] = Field(default_factory=dict)
1435
+ subject: dict[str, JsonValue]
1436
+ evidence: dict[str, JsonValue] | None
1437
+ match: dict[str, JsonValue] | None
1438
+ edits: dict[str, JsonValue] | None
1439
+ decided_by: str | None
1440
+ decided_at: datetime | None
1441
+ not_applied: str | None = None
1442
+
1443
+
1444
+ class ItemSelection(RequestModel):
1445
+ """Items matching every given filter."""
1446
+
1447
+ status: list[ItemDecisionValue] | None = None
1448
+ kind: list[str] | None = None
1449
+ type: list[ItemType] | None = None
1450
+ min_confidence: float | None = Field(default=None, ge=0, le=1)
1451
+ max_confidence: float | None = Field(default=None, ge=0, le=1.01)
1452
+
1453
+
1454
+ class ItemDecision(RequestModel):
1455
+ """A decision on one item; `edits` omitted keeps the stored ones, `{}` clears them."""
1456
+
1457
+ item: str = Field(min_length=1)
1458
+ decision: ItemDecisionValue
1459
+ edits: dict[str, JsonValue] | None = None
1460
+
1461
+
1462
+ class DecideItems(RequestModel):
1463
+ """Decisions by item, and or one decision for every item matching `where`."""
1464
+
1465
+ decisions: list[ItemDecision] = Field(default_factory=list)
1466
+ where: ItemSelection | None = None
1467
+ decision: ItemDecisionValue | None = None
1468
+
1469
+
1470
+ class MaterializedItems(Model):
1471
+ """What the extraction branch holds after the decisions."""
1472
+
1473
+ revision: str | None
1474
+ changes: int
1475
+ applied: int
1476
+ skipped: dict[str, str]
1477
+
1478
+
1479
+ class ItemsDecided(Model):
1480
+ proposal_id: str
1481
+ title: str
1482
+ checks: dict[str, JsonValue]
1483
+ items: list[ProposalItem]
1484
+ materialized: MaterializedItems
1485
+
1486
+
1487
+ class TypeGuess(Model):
1488
+ """The type the samples of a suggested Attribute suggest."""
1489
+
1490
+ type: str
1491
+ constraints: dict[str, JsonValue]
1492
+ confidence: float
1493
+ reasons: list[str]
1494
+
1495
+
1496
+ class SuggestionDuplicate(Model):
1497
+ """The element of the current schema a suggestion duplicates."""
1498
+
1499
+ section: Literal["kinds", "attributes", "relations"]
1500
+ api_name: str
1501
+
1502
+
1503
+ class SchemaSuggestion(Model):
1504
+ """A Kind, Attribute or Relation extraction found outside the schema."""
1505
+
1506
+ id: str
1507
+ type: SuggestionType
1508
+ name: str
1509
+ kind: str | None = None
1510
+ target: str | None = None
1511
+ labels: list[str]
1512
+ support: int
1513
+ documents: int
1514
+ confidence: float
1515
+ status: ItemDecisionValue
1516
+ type_guess: TypeGuess | None = None
1517
+ samples: list[str]
1518
+ examples: list[dict[str, JsonValue]]
1519
+ sources: list[str]
1520
+ duplicate_of: SuggestionDuplicate | None = None
1521
+ withheld: bool = False
1522
+ proposal_id: str | None
1523
+ branch: str | None
1524
+ decided_by: str | None
1525
+ decided_at: datetime | None
1526
+ created_at: datetime | None
1527
+ updated_at: datetime | None
1528
+
1529
+
1530
+ class SuggestionDecision(RequestModel):
1531
+ """A decision on one suggestion; `name` renames an accepted one."""
1532
+
1533
+ suggestion: str = Field(min_length=1)
1534
+ decision: ItemDecisionValue
1535
+ name: str | None = Field(default=None, max_length=100)
1536
+
1537
+
1538
+ class DecideSuggestions(RequestModel):
1539
+ decisions: list[SuggestionDecision] = Field(min_length=1)
1540
+
1541
+
1542
+ class SuggestionsMerged(Model):
1543
+ """What merging stored variants of suggestions did."""
1544
+
1545
+ merged: int
1546
+ kept: int
1547
+
1548
+
1549
+ class SuggestionProposal(Model):
1550
+ """The Proposal opened for the accepted suggestions."""
1551
+
1552
+ id: str
1553
+ title: str
1554
+ branch: str
1555
+ changes: list[ElementChange]
1556
+
1557
+
1558
+ class SuggestionsDecided(Model):
1559
+ suggestions: list[SchemaSuggestion]
1560
+ proposal: SuggestionProposal | None = None
1561
+
1562
+
1563
+ RecordItem.model_rebuild()
1564
+
1565
+
1566
+ # Agent trace ------------------------------------------------------------------------------
1567
+
1568
+
1569
+ class TraceRecordRef(Model):
1570
+ kind: str
1571
+ key: str
1572
+
1573
+
1574
+ class TraceLinkRef(Model):
1575
+ relation: str
1576
+ source: TraceRecordRef = Field(alias="from")
1577
+ to: TraceRecordRef
1578
+
1579
+
1580
+ class TraceRefs(Model):
1581
+ """Records and Links a call touched; `truncated` when more were touched than kept."""
1582
+
1583
+ records: list[TraceRecordRef]
1584
+ links: list[TraceLinkRef]
1585
+ truncated: bool = False
1586
+
1587
+
1588
+ class TraceSession(Model):
1589
+ """Summary of one agent session."""
1590
+
1591
+ session_id: str
1592
+ actor: str
1593
+ started_at: datetime
1594
+ last_at: datetime
1595
+ calls: int
1596
+ notes: int
1597
+ domains: list[str]
1598
+ tools: list[str]
1599
+ proposals: list[str]
1600
+ inferred: bool = False
1601
+ """Grouped heuristically: the calls carried no session id."""
1602
+
1603
+
1604
+ class TraceSessionPage(Model):
1605
+ """One page of sessions; `enabled` tells whether Keeper traces new requests."""
1606
+
1607
+ enabled: bool
1608
+ items: list[TraceSession]
1609
+ next_cursor: str | None = None
1610
+
1611
+
1612
+ class TraceStep(Model):
1613
+ """One call or note of a session. `note` is opaque user text, not markup."""
1614
+
1615
+ step: int
1616
+ seq: int
1617
+ type: Literal["call", "note"]
1618
+ at: datetime
1619
+ actor: str
1620
+ principal_type: str | None = None
1621
+ domain_id: str | None = None
1622
+ tool: str | None = None
1623
+ method: str | None = None
1624
+ route: str | None = None
1625
+ status: int | None = None
1626
+ latency_ms: float | None = None
1627
+ cache: str | None = None
1628
+ branch: str | None = None
1629
+ query: dict[str, Any] | None = None
1630
+ result: dict[str, Any] | None = None
1631
+ refs: TraceRefs | None = None
1632
+ explain: dict[str, Any] | None = None
1633
+ note: str | None = None
1634
+ proposal_id: str | None = None
1635
+ request_id: str | None = None
1636
+ trace_id: str | None = None
1637
+ inferred: bool = False
1638
+ withheld: bool = False
1639
+ """Another principal's step whose query, corrections and note text are not shown."""
1640
+
1641
+
1642
+ class TraceTimeline(Model):
1643
+ """Steps of a session in order; `truncated` when it has more visible steps."""
1644
+
1645
+ session_id: str
1646
+ steps: list[TraceStep]
1647
+ truncated: bool
1648
+
1649
+
1650
+ class NoteRequest(RequestModel):
1651
+ """A note attached to a session."""
1652
+
1653
+ text: str = Field(min_length=1)
1654
+ domain_id: str | None = Field(default=None, max_length=256)
1655
+ step: int | None = Field(default=None, ge=0)
1656
+ """The agent's own step counter."""
1657
+
1658
+
1659
+ class NoteAccepted(Model):
1660
+ """`recorded` is false when tracing is off or the note was dropped."""
1661
+
1662
+ recorded: bool