rabbit-brain 0.6.0__py3-none-win_amd64.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.
- rabbit_brain/__init__.py +33 -0
- rabbit_brain/_rb.py +180 -0
- rabbit_brain/errors.py +71 -0
- rabbit_brain/models.py +33 -0
- rabbit_brain/record.py +1321 -0
- rabbit_brain/sdk.py +626 -0
- rabbit_brain/tui.py +1315 -0
- rabbit_brain-0.6.0.data/scripts/rabbit-brain.exe +0 -0
- rabbit_brain-0.6.0.data/scripts/rb.exe +0 -0
- rabbit_brain-0.6.0.dist-info/METADATA +197 -0
- rabbit_brain-0.6.0.dist-info/RECORD +15 -0
- rabbit_brain-0.6.0.dist-info/WHEEL +4 -0
- rabbit_brain-0.6.0.dist-info/licenses/LICENSE +202 -0
- rabbit_brain-0.6.0.dist-info/licenses/THIRD-PARTY-NOTICES +2958 -0
- rabbit_brain-0.6.0.dist-info/sboms/rabbit-brain.cyclonedx.json +3153 -0
rabbit_brain/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Rabbit Brain: the record of the experiments your coding agents run, kept as plain files in your repo.
|
|
2
|
+
|
|
3
|
+
Experiments with variants and criteria, runs with receipts, and the datasets and checkpoints they used, recorded as YAML
|
|
4
|
+
in `.rb/` beside the code. Agents propose and record; rb checks sources and computes findings; you decide. CLI: `rb`.
|
|
5
|
+
Documentation: `rb docs` (AGENTS.md).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
__version__ = "0.6.0"
|
|
13
|
+
if os.environ.get("RB_TEST_VERSION"): # the tests pin the version, as RB_TEST_NOW pins the clock, so what they expect outlives a release
|
|
14
|
+
__version__ = os.environ["RB_TEST_VERSION"]
|
|
15
|
+
|
|
16
|
+
from .errors import RBError, RBWarning # noqa: E402
|
|
17
|
+
|
|
18
|
+
_LAZY = {
|
|
19
|
+
"open": ".sdk", "init": ".sdk", "clone": ".sdk", "State": ".sdk", "Experiment": ".sdk", "Criterion": ".sdk", "Run": ".sdk",
|
|
20
|
+
"Spec": ".sdk", "DryRun": ".sdk",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
__all__ = ["__version__", "RBError", "RBWarning", *_LAZY]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def __getattr__(name: str) -> Any:
|
|
27
|
+
"""Loaded on first use, so `import rabbit_brain` (and every `rb` command) does not pay for what it does not touch."""
|
|
28
|
+
if name in _LAZY:
|
|
29
|
+
import importlib
|
|
30
|
+
value = getattr(importlib.import_module(_LAZY[name], __name__), name)
|
|
31
|
+
globals()[name] = value
|
|
32
|
+
return value
|
|
33
|
+
raise AttributeError(f"module 'rabbit_brain' has no attribute {name!r}")
|
rabbit_brain/_rb.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Calling rb from its Python package. The SDK and the terminal UI hold no rules of their own: every read and every
|
|
2
|
+
write is asked of rb, through `rb bridge` (one JSON request, one JSON answer), so the Python surface and the command
|
|
3
|
+
line can never disagree about what the record says.
|
|
4
|
+
|
|
5
|
+
Which rb: RB_BIN when set, else the `rb` installed with this package (the scripts directory of this Python).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import sysconfig
|
|
14
|
+
import warnings
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
from . import __version__
|
|
19
|
+
from .errors import EXIT_ENVIRONMENT, RBError, RBWarning
|
|
20
|
+
|
|
21
|
+
UNSET: Any = object()
|
|
22
|
+
# pinned: unpinned, pip installs the newest final release, which can be another version than this one
|
|
23
|
+
REINSTALL = f'pip install --force-reinstall "rabbit-brain=={__version__}"'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _quiet() -> dict:
|
|
27
|
+
"""Windows: a Python with no console window (an editor's, a notebook's) starts rb without one too, so no window
|
|
28
|
+
flashes up for each question. In a terminal rb shares the terminal, as everywhere else."""
|
|
29
|
+
if sys.platform != "win32":
|
|
30
|
+
return {}
|
|
31
|
+
try:
|
|
32
|
+
import ctypes
|
|
33
|
+
if ctypes.windll.kernel32.GetConsoleWindow():
|
|
34
|
+
return {}
|
|
35
|
+
except Exception: # noqa: BLE001 - no way to ask: start rb without a window, which is right in either case
|
|
36
|
+
pass
|
|
37
|
+
return {"creationflags": subprocess.CREATE_NO_WINDOW}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
QUIET = _quiet()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def binary() -> Optional[str]:
|
|
44
|
+
"""The rb binary this package calls, or None when there is none to find."""
|
|
45
|
+
given = os.environ.get("RB_BIN")
|
|
46
|
+
if given:
|
|
47
|
+
return given
|
|
48
|
+
exe = "rb" + (sysconfig.get_config_var("EXE") or "")
|
|
49
|
+
places = []
|
|
50
|
+
try:
|
|
51
|
+
places.append(sysconfig.get_path("scripts"))
|
|
52
|
+
places.append(sysconfig.get_path("scripts", scheme=sysconfig.get_preferred_scheme("user")))
|
|
53
|
+
except Exception: # noqa: BLE001 - an unusual Python layout only means fewer places to look
|
|
54
|
+
pass
|
|
55
|
+
places.append(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "bin")) # pip install --target
|
|
56
|
+
for d in places:
|
|
57
|
+
if d and os.path.isfile(os.path.join(d, exe)):
|
|
58
|
+
return os.path.join(d, exe)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def found() -> str:
|
|
63
|
+
"""The rb this package calls. When there is none, that is said once, with what to do."""
|
|
64
|
+
rb = binary()
|
|
65
|
+
if rb is None:
|
|
66
|
+
raise RBError("E_INTERNAL", message="This Python package found no rb beside it: the SDK and the screen ask rb for everything.",
|
|
67
|
+
fix=f"Install them together: {REINSTALL}. Or set RB_BIN to the rb binary.",
|
|
68
|
+
exit_code=EXIT_ENVIRONMENT)
|
|
69
|
+
return rb
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _jsonable(v: Any) -> Any:
|
|
73
|
+
"""What json does not take as it is: a path, a numpy or torch scalar, a set."""
|
|
74
|
+
if isinstance(v, os.PathLike):
|
|
75
|
+
return os.fspath(v)
|
|
76
|
+
if hasattr(v, "item") and callable(v.item):
|
|
77
|
+
try:
|
|
78
|
+
return v.item()
|
|
79
|
+
except Exception: # noqa: BLE001 - an array is not a scalar: said below
|
|
80
|
+
pass
|
|
81
|
+
if isinstance(v, (set, frozenset)):
|
|
82
|
+
return sorted(v)
|
|
83
|
+
raise RBError("E_OBJECT_INVALID", message=f"rb records text, numbers, true and false, lists and mappings (got {type(v).__name__}).")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def command() -> list:
|
|
87
|
+
"""The command line that runs rb, for what the screen runs as the user would type it."""
|
|
88
|
+
return [found()]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def environment() -> dict:
|
|
92
|
+
"""The environment rb runs in: this one, plus what only this process knows about the Python doing the work."""
|
|
93
|
+
env = dict(os.environ)
|
|
94
|
+
if sys.executable:
|
|
95
|
+
env["RB_PYTHON"] = sys.executable # a run's receipt fingerprints the Python that made it
|
|
96
|
+
torch = sys.modules.get("torch")
|
|
97
|
+
cuda = getattr(getattr(torch, "version", None), "cuda", None) if torch is not None else None
|
|
98
|
+
if cuda:
|
|
99
|
+
env["RB_CUDA"] = str(cuda) # CUDA exists only on an imported torch: said by the process that has it
|
|
100
|
+
else:
|
|
101
|
+
env.pop("RB_CUDA", None)
|
|
102
|
+
return env
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _answer(line: str) -> dict:
|
|
106
|
+
rb = found()
|
|
107
|
+
try:
|
|
108
|
+
r = subprocess.run([rb, "bridge"], input=(line + "\n").encode("utf-8"), stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
109
|
+
env=environment(), **QUIET)
|
|
110
|
+
except OSError as exc:
|
|
111
|
+
raise RBError("E_INTERNAL", message=f"rb could not be started ({rb}): {exc}", exit_code=EXIT_ENVIRONMENT)
|
|
112
|
+
out = r.stdout.decode("utf-8", "replace").strip()
|
|
113
|
+
try:
|
|
114
|
+
answer = json.loads(out.splitlines()[-1]) if out else None
|
|
115
|
+
except ValueError:
|
|
116
|
+
answer = None
|
|
117
|
+
if not isinstance(answer, dict) or "ok" not in answer:
|
|
118
|
+
tail = (r.stderr.decode("utf-8", "replace").strip().splitlines() or [out[:200] or f"exit {r.returncode}"])[-1]
|
|
119
|
+
raise RBError("E_INTERNAL", message=f"rb did not answer ({rb}): {tail}", exit_code=EXIT_ENVIRONMENT,
|
|
120
|
+
fix=f"This Python package and the rb it found may be different versions: {REINSTALL}.")
|
|
121
|
+
return answer
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class DryRun:
|
|
126
|
+
"""A write asked with dry_run=True: rb made every check the write makes and wrote nothing. `value` is what the write
|
|
127
|
+
would give back; `would_write` what it would write, as `rb <command> --dry-run --json` gives it in data.would_write:
|
|
128
|
+
each record file with its content (`files`), each file it would remove (`removed`), each line it would add to the
|
|
129
|
+
log (`log`). Ids and times are drawn again when the write is made."""
|
|
130
|
+
value: Any
|
|
131
|
+
would_write: dict
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def call(op: str, args: Optional[dict] = None, *, root: Any = None, via: str = "sdk", agent: Optional[str] = None,
|
|
135
|
+
words: Any = UNSET, dry_run: bool = False, here: Any = None, place: Optional[str] = None, at: Optional[str] = None) -> Any:
|
|
136
|
+
"""Ask rb for one thing. Returns its value, or with `dry_run` a DryRun; raises RBError with what rb said when it
|
|
137
|
+
refuses. `here` is the checkout the caller works in (a linked worktree nested in the record's), as op open gave it.
|
|
138
|
+
`place` and `at` ask what a screen reads of another place or a past version, by the id and commit op places lists:
|
|
139
|
+
read there, never written."""
|
|
140
|
+
req: dict = {"op": op, "root": os.fspath(root) if root is not None else None, "via": via, "agent": agent, "args": args or {}}
|
|
141
|
+
if here is not None:
|
|
142
|
+
req["here"] = os.fspath(here)
|
|
143
|
+
if place is not None:
|
|
144
|
+
req["place"] = place
|
|
145
|
+
if at is not None:
|
|
146
|
+
req["at"] = at
|
|
147
|
+
if words is not UNSET:
|
|
148
|
+
req["words"] = words
|
|
149
|
+
if dry_run:
|
|
150
|
+
req["dry_run"] = True
|
|
151
|
+
answer = _answer(json.dumps(req, default=_jsonable))
|
|
152
|
+
if answer["ok"]:
|
|
153
|
+
_said(answer.get("warnings"))
|
|
154
|
+
if dry_run:
|
|
155
|
+
return DryRun(answer.get("value"), answer.get("would_write") or {})
|
|
156
|
+
return answer.get("value")
|
|
157
|
+
e = dict(answer.get("error") or {})
|
|
158
|
+
raise RBError(e.pop("code", "E_INTERNAL"), message=e.pop("message", None), fix=e.pop("fix", None),
|
|
159
|
+
exit_code=e.pop("exit_code", 2), problems=list(e.pop("problems", []) or []), extra=e)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _said(texts: Any) -> None:
|
|
163
|
+
"""What rb says after a write it made (`data.warnings` on the command line), as an RBWarning at the line of code
|
|
164
|
+
that asked for the write: the first one outside this package."""
|
|
165
|
+
if not texts:
|
|
166
|
+
return
|
|
167
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
168
|
+
frame, level = sys._getframe(1), 2
|
|
169
|
+
while frame is not None and os.path.dirname(os.path.abspath(frame.f_code.co_filename)) == here:
|
|
170
|
+
frame, level = frame.f_back, level + 1
|
|
171
|
+
for text in texts:
|
|
172
|
+
warnings.warn(str(text), RBWarning, stacklevel=level)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def check_version(answered: Any) -> None:
|
|
176
|
+
"""An opening answer says which rb gave it. A different version beside this package means a broken install."""
|
|
177
|
+
theirs = answered.get("rb") if isinstance(answered, dict) else None
|
|
178
|
+
if theirs is not None and theirs != __version__:
|
|
179
|
+
raise RBError("E_INTERNAL", message=f"rb {theirs} answered, and this Python package is rabbit-brain {__version__} ({binary()}).",
|
|
180
|
+
fix=f"Reinstall so the two match: {REINSTALL}.", exit_code=EXIT_ENVIRONMENT)
|
rabbit_brain/errors.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Error codes: one table feeds `errors[]` in the JSON envelope, `rb docs`, and the tests.
|
|
2
|
+
|
|
3
|
+
Exit codes: 0 ok · 1 a check failed (rb status --fail-on, rb spec verify, rb fmt --check, rb doctor) · 2 rb could not do what was asked, and says why
|
|
4
|
+
· 3 a fault in rb itself, or an install that cannot start it.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
EXIT_OK = 0
|
|
11
|
+
EXIT_CHECK_FAILED = 1
|
|
12
|
+
EXIT_INVALID = 2
|
|
13
|
+
EXIT_ENVIRONMENT = 3
|
|
14
|
+
|
|
15
|
+
# code -> (meaning, what to do)
|
|
16
|
+
ERRORS: dict[str, tuple[str, str]] = {
|
|
17
|
+
"E_FILE_NOT_FOUND": ("A file given on the command line does not exist, or could not be read.", "Check the path; it is relative to the current directory."),
|
|
18
|
+
"E_NOT_AVAILABLE": ("This command, or the hosted service it needs, is not available in this version of rabbit-brain.", "The message says what to use instead. `rb --help` lists every command in this version."),
|
|
19
|
+
"E_WRITE_FAILED": ("A file could not be written.", "The message names the file and says why; check that its directory is one you can write to."),
|
|
20
|
+
"E_NO_PROJECT": ("No record here: no .rb/ in this directory or any directory above it.", "Run `rb init \"<the work's theme>\"` in the project root (it creates .rb/), or cd into a project that has one. In a project already under way, `rb docs --start` says how to record the work done."),
|
|
21
|
+
"E_PROJECT_EXISTS": ("A project already exists here or in a directory above, or in another worktree of this repository.", "Use it: `rb status`. One .rb/ per line of work; start another in a different directory."),
|
|
22
|
+
"E_YAML": ("The file is not Rabbit Brain YAML.", "The message says where and what to write instead. Rabbit Brain YAML is plain YAML without anchors, tags, dates or yes/no; `rb fmt <file>` rewrites a file that reads in the canonical form."),
|
|
23
|
+
"E_OBJECT_NOT_FOUND": ("No record with that id in this project.", "`rb status --json` lists every record; `rb show <experiment>` lists its variants, settings, criteria and runs; a setting is addressed as <experiment>/<name>."),
|
|
24
|
+
"E_OBJECT_INVALID": ("The values given do not make a valid record.", "Do what the message says; `rb <command> --help` lists the arguments and `rb schema <kind>` prints a record's shape (experiment, criterion, run, ...)."),
|
|
25
|
+
"E_FROZEN": ("The experiment is frozen, and this would change what it locked: a setting, a variant, what it varies, or a criterion.", "Run the same command with --amend --why \"<reason>\"; the change, the reason, who made it and the hash before and after are kept."),
|
|
26
|
+
"E_USAGE": ("The command line could not be parsed: an unknown option, a missing argument, or a value that is not allowed.", "`rb <command> --help` lists the arguments. A negative number is written as it is (--at-most -0.001); text that starts with '-' goes after --."),
|
|
27
|
+
"E_ACTOR_INVALID": ("RB_ACTOR is not in the form human:<name> or agent:<name>.", "Set it to human:<name> or agent:<name> (letters, digits and _.@+-), or unset it: rb then detects the agent runtime, or names the human from git."),
|
|
28
|
+
"E_SOURCE_UNRESOLVED": ("The source was read and does not state the value, or could not be read.", "The setting stays provisional. Check the path, key, line, quote or commit, or set the value the source actually states: `rb spec set <exp> <name> <value> --source <file#key>`."),
|
|
29
|
+
"E_SOURCE_OUTSIDE": ("The source is outside the project, so nobody else can check it.", "Copy it into the repository, commit it, and point --source at the copy."),
|
|
30
|
+
"E_SOURCE_UNVERIFIABLE": ("This source cannot be checked by reading a file: a url, a note, or a file source without a line or a quote.", "Point at a config key (file#key), a file line, a quote in a saved copy of the page, or a run's JSON (run:PATH#/pointer) that states the value; or someone vouches for it: `rb decide <exp>/<name> accept --why \"...\"`."),
|
|
31
|
+
"E_STATE_EDITED": ("A record under .rb/ was changed outside rb (its content edited, its file moved or deleted, or written by hand), and rb will not write over it. Comments and formatting are not changes.", "`rb doctor` lists every such file. `rb doctor --restore` puts back what rb last wrote; if the change is right, `rb doctor --adopt --why \"...\"` takes it as it stands, recorded as yours."),
|
|
32
|
+
"E_STATE_CORRUPT": ("A file under .rb/ is not a valid record.", "It was probably edited by hand or left mid-merge. `rb doctor` lists every such file and says where it breaks; `rb doctor --restore` puts back what rb last wrote."),
|
|
33
|
+
"E_STATE_NEWER": ("Part of this .rb/ was written by a newer rb than this one.", "Upgrade: `pip install --upgrade rabbit-brain`. Until then rb will not guess at state it cannot read, so gates fail closed with this reason."),
|
|
34
|
+
"E_BUNDLE_INVALID": ("The bundle is not a valid rb export, or its files do not match its manifest.", "Get a fresh copy from where it was published; every file must hash to what manifest.json records. rb export --with-record writes one."),
|
|
35
|
+
"E_HUB_REFUSED": ("The hub refused the request.", "The message carries the hub's reason and fix; the record on this machine is unchanged."),
|
|
36
|
+
"E_HUB_UNREACHABLE": ("The hub could not be reached, or what answered at its address is not the hub.", "Check the address (--hub, or RB_HUB_URL) and the network; nothing local was affected."),
|
|
37
|
+
"E_ADOPT_AUTHORITY": ("The edit changes who wrote or decided something, or through what, which an adoption must never launder into the record.", "`rb doctor --restore` puts the file back. If the authorship or decision is really wrong, make the change through rb as yourself, where it is logged as yours."),
|
|
38
|
+
"E_STALE_REVIEW": ("The files changed between the review and the adoption, so what was reviewed is not what would be adopted.", "Look again: `rb doctor` lists every file changed outside rb as it is now."),
|
|
39
|
+
"E_INTERNAL": ("A fault in rb itself, or an install that cannot run a part of it.", "Report it with the link its fix line gives (report_url under --json)."),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RBWarning(UserWarning):
|
|
44
|
+
"""What rb says after a write that went through, for the writer to act on: the SDK's `data.warnings`. A run's input
|
|
45
|
+
whose copy here is not the bytes it names gets one, and so do numbers that differ from the run already recorded with
|
|
46
|
+
the same output files, and an agent's new text that holds LaTeX."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class RBError(Exception):
|
|
51
|
+
code: str
|
|
52
|
+
message: str | None = None
|
|
53
|
+
fix: str | None = None
|
|
54
|
+
exit_code: int = EXIT_INVALID
|
|
55
|
+
problems: list[str] = field(default_factory=list)
|
|
56
|
+
extra: dict = field(default_factory=dict) # structured detail for agents
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
meaning, default_fix = ERRORS.get(self.code, ("Unexpected failure.", ERRORS["E_INTERNAL"][1]))
|
|
60
|
+
if self.message is None:
|
|
61
|
+
self.message = meaning
|
|
62
|
+
if self.fix is None:
|
|
63
|
+
self.fix = default_fix
|
|
64
|
+
super().__init__(self.message)
|
|
65
|
+
|
|
66
|
+
def to_dict(self) -> dict:
|
|
67
|
+
d = {"code": self.code, "message": self.message, "fix": self.fix}
|
|
68
|
+
if self.problems:
|
|
69
|
+
d["problems"] = list(self.problems)
|
|
70
|
+
d.update(self.extra)
|
|
71
|
+
return d
|
rabbit_brain/models.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""The envelope every command prints with --json, and the registry `rb schema` prints from."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ErrorItem(BaseModel):
|
|
10
|
+
model_config = ConfigDict(extra="allow")
|
|
11
|
+
code: str
|
|
12
|
+
message: str
|
|
13
|
+
fix: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Envelope(BaseModel):
|
|
17
|
+
"""What every command prints with --json: exactly one object on stdout."""
|
|
18
|
+
model_config = ConfigDict(extra="forbid")
|
|
19
|
+
ok: bool
|
|
20
|
+
rb_version: str
|
|
21
|
+
command: str
|
|
22
|
+
data: dict = Field(default_factory=dict)
|
|
23
|
+
errors: list[ErrorItem] = Field(default_factory=list)
|
|
24
|
+
next: list[str] = Field(default_factory=list)
|
|
25
|
+
rb_delta: list[dict] = Field(default_factory=list) # in an agent session: what changed in the research since it last looked
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _schemas() -> dict[str, type[BaseModel]]:
|
|
29
|
+
from .record import OBJECT_SCHEMAS
|
|
30
|
+
return {**OBJECT_SCHEMAS, "envelope": Envelope}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
SCHEMAS: dict[str, type[BaseModel]] = _schemas()
|