anagrapheus 0.1.1__tar.gz
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.
- anagrapheus-0.1.1/PKG-INFO +89 -0
- anagrapheus-0.1.1/README.md +72 -0
- anagrapheus-0.1.1/pyproject.toml +89 -0
- anagrapheus-0.1.1/pyproject.toml.orig +92 -0
- anagrapheus-0.1.1/src/anagrapheus/__init__.py +45 -0
- anagrapheus-0.1.1/src/anagrapheus/assembly/__init__.py +81 -0
- anagrapheus-0.1.1/src/anagrapheus/assembly/assemble.py +202 -0
- anagrapheus-0.1.1/src/anagrapheus/assembly/errors.py +41 -0
- anagrapheus-0.1.1/src/anagrapheus/assembly/parts.py +167 -0
- anagrapheus-0.1.1/src/anagrapheus/assembly/render.py +40 -0
- anagrapheus-0.1.1/src/anagrapheus/assembly/slices.py +374 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/__init__.py +120 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/divergence.py +53 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/errors.py +102 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/events.py +175 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/fold.py +267 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/index.py +56 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/migrations.py +44 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/ops.py +131 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/predicates.py +317 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/rekey.py +411 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/sqlite.py +139 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/store.py +58 -0
- anagrapheus-0.1.1/src/anagrapheus/beliefs/writer.py +135 -0
- anagrapheus-0.1.1/src/anagrapheus/chronicle/__init__.py +88 -0
- anagrapheus-0.1.1/src/anagrapheus/chronicle/errors.py +36 -0
- anagrapheus-0.1.1/src/anagrapheus/chronicle/fold.py +328 -0
- anagrapheus-0.1.1/src/anagrapheus/chronicle/migrations.py +39 -0
- anagrapheus-0.1.1/src/anagrapheus/chronicle/reading.py +171 -0
- anagrapheus-0.1.1/src/anagrapheus/chronicle/reconsolidation.py +144 -0
- anagrapheus-0.1.1/src/anagrapheus/chronicle/sections.py +62 -0
- anagrapheus-0.1.1/src/anagrapheus/chronicle/sqlite.py +283 -0
- anagrapheus-0.1.1/src/anagrapheus/chronicle/store.py +117 -0
- anagrapheus-0.1.1/src/anagrapheus/identity/__init__.py +105 -0
- anagrapheus-0.1.1/src/anagrapheus/identity/decisions.py +271 -0
- anagrapheus-0.1.1/src/anagrapheus/identity/errors.py +84 -0
- anagrapheus-0.1.1/src/anagrapheus/identity/events.py +184 -0
- anagrapheus-0.1.1/src/anagrapheus/identity/fold.py +83 -0
- anagrapheus-0.1.1/src/anagrapheus/identity/ladder.py +69 -0
- anagrapheus-0.1.1/src/anagrapheus/identity/migrations.py +117 -0
- anagrapheus-0.1.1/src/anagrapheus/identity/registry.py +153 -0
- anagrapheus-0.1.1/src/anagrapheus/identity/sqlite.py +522 -0
- anagrapheus-0.1.1/src/anagrapheus/identity/store.py +157 -0
- anagrapheus-0.1.1/src/anagrapheus/identity/vocabulary.py +149 -0
- anagrapheus-0.1.1/src/anagrapheus/provider/__init__.py +47 -0
- anagrapheus-0.1.1/src/anagrapheus/provider/errors.py +9 -0
- anagrapheus-0.1.1/src/anagrapheus/provider/model.py +309 -0
- anagrapheus-0.1.1/src/anagrapheus/provider/policy.py +274 -0
- anagrapheus-0.1.1/src/anagrapheus/provider/stream.py +82 -0
- anagrapheus-0.1.1/src/anagrapheus/provider/tools.py +183 -0
- anagrapheus-0.1.1/src/anagrapheus/py.typed +0 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/__init__.py +110 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/_canonical.py +82 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/envelope.py +78 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/errors.py +41 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/folds.py +544 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/migrations.py +142 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/pins.py +459 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/recording.py +136 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/registry.py +125 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/replay.py +342 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/sqlite.py +260 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/store.py +130 -0
- anagrapheus-0.1.1/src/anagrapheus/timeline/upcast.py +145 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: anagrapheus
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Event-sourced timeline, belief store, and identity layer for long-running agents.
|
|
5
|
+
Author: Anentropic
|
|
6
|
+
Author-email: Anentropic <ego@anentropic.com>
|
|
7
|
+
Requires-Dist: anyio>=4.0
|
|
8
|
+
Requires-Dist: doxastica>=0.2
|
|
9
|
+
Requires-Dist: pydantic>=2.11
|
|
10
|
+
Requires-Dist: pydantic-ai-slim>=2
|
|
11
|
+
Requires-Dist: pydantic-ai-slim[anthropic]>=2 ; extra == 'anthropic'
|
|
12
|
+
Requires-Dist: pydantic-ai-slim[openai]>=2 ; extra == 'openai'
|
|
13
|
+
Requires-Python: >=3.14
|
|
14
|
+
Provides-Extra: anthropic
|
|
15
|
+
Provides-Extra: openai
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# anagrapheus
|
|
19
|
+
|
|
20
|
+
[](https://github.com/anentropic/anagrapheus/actions/workflows/ci.yml)
|
|
21
|
+
[](https://pypi.org/project/anagrapheus/)
|
|
22
|
+
[](https://www.python.org/)
|
|
23
|
+
[](https://github.com/anentropic/anagrapheus/blob/main/LICENSE)
|
|
24
|
+
|
|
25
|
+
An event-sourced timeline, belief store and identity layer for long-running LLM agents. One append-only event log is the authoritative record of a run; per-agent beliefs, entity identity, a consolidated history and assembled prompts are all projections of it that can be dropped and rebuilt. Every model call made through `PinnedModel`, and every tool wrapped with `pinned_tool`, is pinned to the log, so a recorded run replays offline, exactly, without reaching the model, and a run whose prompts have changed reports where it diverged instead of quietly drawing new answers.
|
|
26
|
+
|
|
27
|
+
It is for Python developers building agents and simulations that run for many turns and hold private beliefs about each other: negotiations, games, interactive fiction. It builds on [pydantic-ai](https://ai.pydantic.dev/) for the model call and works with any provider pydantic-ai supports.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
anagrapheus needs Python 3.14 or later.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install anagrapheus # or: uv add anagrapheus
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The base install runs offline. To call a real provider, add its extra: `anagrapheus[anthropic]` or `anagrapheus[openai]` (the OpenAI SDK, also used for OpenAI-compatible routes such as OpenRouter).
|
|
38
|
+
|
|
39
|
+
## Documentation
|
|
40
|
+
|
|
41
|
+
**[anentropic.github.io/anagrapheus](https://anentropic.github.io/anagrapheus/)** has a getting-started tutorial, how-to guides for each layer, explanations of the design, and the API reference.
|
|
42
|
+
|
|
43
|
+
A worked example, a four-round EU and UK negotiation that uses every layer, lives in [`examples/negotiation/`](examples/negotiation/). From a clone, run it offline with `uv run python -m examples.negotiation`.
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
See [DEVELOP.md](DEVELOP.md) for the full guide.
|
|
48
|
+
|
|
49
|
+
### Setup
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Install dependencies (the dev group includes the anthropic and openai extras)
|
|
53
|
+
uv sync --dev
|
|
54
|
+
|
|
55
|
+
# Install git hooks
|
|
56
|
+
prek install
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Quality gates
|
|
60
|
+
|
|
61
|
+
Before committing, run the hooks:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
prek run --all-files # file hygiene, ruff lint and format, uv lock, basedpyright, blacken-docs
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The hooks do not run the tests. Run each gate individually with:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv run basedpyright # Type checking (strict mode)
|
|
71
|
+
uv run ruff check # Linting
|
|
72
|
+
uv run ruff format --check # Formatting
|
|
73
|
+
uv run pytest # Tests
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`uv run pytest` skips the live smoke tests, which reach a real provider. Opt in with `uv run pytest -m tier3`, which needs an API key for at least one of the routes in `tests/tier3`.
|
|
77
|
+
|
|
78
|
+
### Docs
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv sync --dev --group docs
|
|
82
|
+
just docs-serve # or: just docs-build
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Both recipes regenerate `CHANGELOG.md` with git-cliff before building.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
BSD-3-Clause. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# anagrapheus
|
|
2
|
+
|
|
3
|
+
[](https://github.com/anentropic/anagrapheus/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/anagrapheus/)
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
[](https://github.com/anentropic/anagrapheus/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
An event-sourced timeline, belief store and identity layer for long-running LLM agents. One append-only event log is the authoritative record of a run; per-agent beliefs, entity identity, a consolidated history and assembled prompts are all projections of it that can be dropped and rebuilt. Every model call made through `PinnedModel`, and every tool wrapped with `pinned_tool`, is pinned to the log, so a recorded run replays offline, exactly, without reaching the model, and a run whose prompts have changed reports where it diverged instead of quietly drawing new answers.
|
|
9
|
+
|
|
10
|
+
It is for Python developers building agents and simulations that run for many turns and hold private beliefs about each other: negotiations, games, interactive fiction. It builds on [pydantic-ai](https://ai.pydantic.dev/) for the model call and works with any provider pydantic-ai supports.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
anagrapheus needs Python 3.14 or later.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install anagrapheus # or: uv add anagrapheus
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The base install runs offline. To call a real provider, add its extra: `anagrapheus[anthropic]` or `anagrapheus[openai]` (the OpenAI SDK, also used for OpenAI-compatible routes such as OpenRouter).
|
|
21
|
+
|
|
22
|
+
## Documentation
|
|
23
|
+
|
|
24
|
+
**[anentropic.github.io/anagrapheus](https://anentropic.github.io/anagrapheus/)** has a getting-started tutorial, how-to guides for each layer, explanations of the design, and the API reference.
|
|
25
|
+
|
|
26
|
+
A worked example, a four-round EU and UK negotiation that uses every layer, lives in [`examples/negotiation/`](examples/negotiation/). From a clone, run it offline with `uv run python -m examples.negotiation`.
|
|
27
|
+
|
|
28
|
+
## Development
|
|
29
|
+
|
|
30
|
+
See [DEVELOP.md](DEVELOP.md) for the full guide.
|
|
31
|
+
|
|
32
|
+
### Setup
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Install dependencies (the dev group includes the anthropic and openai extras)
|
|
36
|
+
uv sync --dev
|
|
37
|
+
|
|
38
|
+
# Install git hooks
|
|
39
|
+
prek install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Quality gates
|
|
43
|
+
|
|
44
|
+
Before committing, run the hooks:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
prek run --all-files # file hygiene, ruff lint and format, uv lock, basedpyright, blacken-docs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The hooks do not run the tests. Run each gate individually with:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv run basedpyright # Type checking (strict mode)
|
|
54
|
+
uv run ruff check # Linting
|
|
55
|
+
uv run ruff format --check # Formatting
|
|
56
|
+
uv run pytest # Tests
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`uv run pytest` skips the live smoke tests, which reach a real provider. Opt in with `uv run pytest -m tier3`, which needs an API key for at least one of the routes in `tests/tier3`.
|
|
60
|
+
|
|
61
|
+
### Docs
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv sync --dev --group docs
|
|
65
|
+
just docs-serve # or: just docs-build
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Both recipes regenerate `CHANGELOG.md` with git-cliff before building.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
BSD-3-Clause. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "anagrapheus"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Event-sourced timeline, belief store, and identity layer for long-running agents."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"anyio>=4.0",
|
|
9
|
+
"doxastica>=0.2",
|
|
10
|
+
"pydantic>=2.11",
|
|
11
|
+
"pydantic-ai-slim>=2",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[project.authors]]
|
|
15
|
+
name = "Anentropic"
|
|
16
|
+
email = "ego@anentropic.com"
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
anthropic = ["pydantic-ai-slim[anthropic]>=2"]
|
|
20
|
+
openai = ["pydantic-ai-slim[openai]>=2"]
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.9.18,<1.0.0"]
|
|
24
|
+
build-backend = "uv_build"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"anagrapheus[anthropic,openai]",
|
|
29
|
+
"basedpyright>=1.38.0",
|
|
30
|
+
"ipython>=9.10.0",
|
|
31
|
+
"pdbpp>=0.12.0.post1",
|
|
32
|
+
"pytest>=8.0.0",
|
|
33
|
+
"pytest-cov>=6.0.0",
|
|
34
|
+
"ruff>=0.15.1",
|
|
35
|
+
"hypothesis>=6.130",
|
|
36
|
+
]
|
|
37
|
+
docs = [
|
|
38
|
+
"mkdocs>=1.6.0",
|
|
39
|
+
"mkdocs-material>=9.7.0",
|
|
40
|
+
"mkdocstrings[python]>=0.26.0",
|
|
41
|
+
"mkdocs-gen-files>=0.5.0",
|
|
42
|
+
"mkdocs-literate-nav>=0.6.0",
|
|
43
|
+
"mkdocs-section-index>=0.3.0",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.pyright]
|
|
47
|
+
pythonVersion = "3.14"
|
|
48
|
+
typeCheckingMode = "strict"
|
|
49
|
+
include = [
|
|
50
|
+
"src",
|
|
51
|
+
"tests",
|
|
52
|
+
]
|
|
53
|
+
reportPrivateUsage = false
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
target-version = "py314"
|
|
57
|
+
line-length = 100
|
|
58
|
+
src = ["src"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = [
|
|
62
|
+
"E",
|
|
63
|
+
"F",
|
|
64
|
+
"W",
|
|
65
|
+
"I",
|
|
66
|
+
"UP",
|
|
67
|
+
"B",
|
|
68
|
+
"SIM",
|
|
69
|
+
"TCH",
|
|
70
|
+
"D",
|
|
71
|
+
]
|
|
72
|
+
ignore = [
|
|
73
|
+
"D1",
|
|
74
|
+
"D202",
|
|
75
|
+
"D203",
|
|
76
|
+
"D212",
|
|
77
|
+
"D401",
|
|
78
|
+
"D413",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint.flake8-type-checking]
|
|
82
|
+
runtime-evaluated-base-classes = [
|
|
83
|
+
"pydantic.BaseModel",
|
|
84
|
+
"anagrapheus.timeline.envelope.DomainEvent",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
[tool.pytest.ini_options]
|
|
88
|
+
addopts = "-v -m 'not tier3'"
|
|
89
|
+
markers = ["tier3: reaches a real provider; needs a key for at least one route in tests/tier3"]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "anagrapheus"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Event-sourced timeline, belief store, and identity layer for long-running agents."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Anentropic", email = "ego@anentropic.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"anyio>=4.0",
|
|
12
|
+
"doxastica>=0.2",
|
|
13
|
+
"pydantic>=2.11",
|
|
14
|
+
"pydantic-ai-slim>=2",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.9.18,<1.0.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
# Only tier 3 -- the live smoke test -- needs a real provider. Kept out of the base
|
|
23
|
+
# install so the library stays provider-agnostic: pydantic-ai is the seam, and which
|
|
24
|
+
# provider sits behind it is the consumer's choice.
|
|
25
|
+
anthropic = ["pydantic-ai-slim[anthropic]>=2"]
|
|
26
|
+
# The OpenAI SDK, which pydantic-ai uses for OpenAI itself and for every
|
|
27
|
+
# OpenAI-compatible route -- OpenRouter among them. Named for what it installs, as
|
|
28
|
+
# pydantic-ai names its own extra, rather than for one provider that uses it.
|
|
29
|
+
openai = ["pydantic-ai-slim[openai]>=2"]
|
|
30
|
+
|
|
31
|
+
[dependency-groups]
|
|
32
|
+
dev = [
|
|
33
|
+
"anagrapheus[anthropic,openai]",
|
|
34
|
+
"basedpyright>=1.38.0",
|
|
35
|
+
"ipython>=9.10.0",
|
|
36
|
+
"pdbpp>=0.12.0.post1",
|
|
37
|
+
"pytest>=8.0.0",
|
|
38
|
+
"pytest-cov>=6.0.0",
|
|
39
|
+
"ruff>=0.15.1",
|
|
40
|
+
"hypothesis>=6.130",
|
|
41
|
+
]
|
|
42
|
+
docs = [
|
|
43
|
+
"mkdocs>=1.6.0",
|
|
44
|
+
"mkdocs-material>=9.7.0",
|
|
45
|
+
"mkdocstrings[python]>=0.26.0",
|
|
46
|
+
"mkdocs-gen-files>=0.5.0",
|
|
47
|
+
"mkdocs-literate-nav>=0.6.0",
|
|
48
|
+
"mkdocs-section-index>=0.3.0",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.pyright]
|
|
52
|
+
# The minimum supported version, not the dev version: an explicit pythonVersion
|
|
53
|
+
# overrides the running interpreter, so setting it to the dev version lets newer
|
|
54
|
+
# stdlib and typing APIs type-check clean on *every* CI leg -- including the
|
|
55
|
+
# minimum-version leg whose job is to catch them -- and fail at runtime instead.
|
|
56
|
+
# Kept in step with requires-python and ruff's target-version below.
|
|
57
|
+
pythonVersion = "3.14"
|
|
58
|
+
typeCheckingMode = "strict"
|
|
59
|
+
include = ["src", "tests"]
|
|
60
|
+
reportPrivateUsage = false
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
target-version = "py314"
|
|
64
|
+
line-length = 100
|
|
65
|
+
src = ["src"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint.flake8-type-checking]
|
|
68
|
+
# pydantic resolves field annotations at model build; imports its models use must
|
|
69
|
+
# stay runtime even when they look annotation-only.
|
|
70
|
+
runtime-evaluated-base-classes = [
|
|
71
|
+
"pydantic.BaseModel",
|
|
72
|
+
"anagrapheus.timeline.envelope.DomainEvent",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint]
|
|
76
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM", "TCH", "D"]
|
|
77
|
+
ignore = [
|
|
78
|
+
"D1", # don't require docstrings everywhere
|
|
79
|
+
"D202", # allow blank line after docstring (common in tests)
|
|
80
|
+
"D203", # conflicts with D211 (no blank line before class docstring)
|
|
81
|
+
"D212", # conflicts with D213 (multiline summary on second line)
|
|
82
|
+
"D401", # imperative mood is awkward for dunder methods
|
|
83
|
+
"D413", # blank line after last section not required
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
[tool.pytest.ini_options]
|
|
87
|
+
addopts = "-v -m 'not tier3'"
|
|
88
|
+
markers = [
|
|
89
|
+
# Tier 3 costs money and needs a key, so it is excluded by default rather than
|
|
90
|
+
# skipped noisily on every local run. `-m tier3` opts in.
|
|
91
|
+
"tier3: reaches a real provider; needs a key for at least one route in tests/tier3",
|
|
92
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Event-sourced timeline, belief store, identity and chronicle for long-running agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from . import assembly, beliefs, chronicle, identity, provider, timeline
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from anagrapheus.timeline.registry import EventTypeRegistry
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"assembly",
|
|
15
|
+
"beliefs",
|
|
16
|
+
"chronicle",
|
|
17
|
+
"identity",
|
|
18
|
+
"provider",
|
|
19
|
+
"register_builtin_events",
|
|
20
|
+
"timeline",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# A library logs and never configures: without this, a consumer that has not set up
|
|
24
|
+
# logging would see Python's last-resort handler print our warnings to stderr. Every
|
|
25
|
+
# module logs under `anagrapheus.<subpackage>.<module>`, so one `getLogger("anagrapheus")`
|
|
26
|
+
# turns all of it up or down.
|
|
27
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_builtin_events(registry: EventTypeRegistry) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Register every event type this library emits, across all its subpackages.
|
|
33
|
+
|
|
34
|
+
Under-registering is a live failure mode rather than an untidiness: an application
|
|
35
|
+
that uses the pin ledger and the identity layer over one stream, but registers only
|
|
36
|
+
one subpackage's events, gets an
|
|
37
|
+
:class:`~anagrapheus.timeline.errors.UnknownEventType` the first time a reader
|
|
38
|
+
reaches the other's. Library events all live under the reserved namespace and can
|
|
39
|
+
never collide with an application's own, so registering them all is free.
|
|
40
|
+
|
|
41
|
+
The per-subpackage functions remain for a consumer using only one of them.
|
|
42
|
+
"""
|
|
43
|
+
timeline.register_builtin_events(registry)
|
|
44
|
+
identity.register_builtin_events(registry)
|
|
45
|
+
beliefs.register_builtin_events(registry)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt assembly: what a model is shown, built only from what its role was granted.
|
|
3
|
+
|
|
4
|
+
Prompt assembly is a projection of the log, like any other read model. This package
|
|
5
|
+
gives it a shape:
|
|
6
|
+
|
|
7
|
+
- a **grant** of :mod:`~anagrapheus.assembly.slices`, each reading at one position and
|
|
8
|
+
remembering what it handed out, so a role can read nothing it was not granted and a
|
|
9
|
+
context always knows its sources;
|
|
10
|
+
- **parts** carrying a register, a rank and their sources, structured rather than
|
|
11
|
+
rendered (:mod:`~anagrapheus.assembly.parts`);
|
|
12
|
+
- :func:`~anagrapheus.assembly.assemble.assemble`, which checks citations and labels
|
|
13
|
+
and fits a :class:`~anagrapheus.assembly.assemble.Budget` that never sheds a binding
|
|
14
|
+
part.
|
|
15
|
+
|
|
16
|
+
Rendering, slot names, budgets and every schema are the application's. Pinning does
|
|
17
|
+
not change: the rendered messages go through
|
|
18
|
+
:class:`~anagrapheus.provider.PinnedModel` as before, and its content-addressed key
|
|
19
|
+
makes any assembly change a miss.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Design: note 07; upstream agent-core doc 00 §2.
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from anagrapheus.assembly.assemble import DEFAULT_SHED_ORDER, Budget, assemble, slices_of
|
|
27
|
+
from anagrapheus.assembly.errors import (
|
|
28
|
+
AssemblyError,
|
|
29
|
+
BudgetExceeded,
|
|
30
|
+
MislabelledPart,
|
|
31
|
+
NotGranted,
|
|
32
|
+
StaleRead,
|
|
33
|
+
UncitedSource,
|
|
34
|
+
)
|
|
35
|
+
from anagrapheus.assembly.parts import (
|
|
36
|
+
Context,
|
|
37
|
+
EventRef,
|
|
38
|
+
GivenRef,
|
|
39
|
+
Part,
|
|
40
|
+
Register,
|
|
41
|
+
SectionRef,
|
|
42
|
+
Source,
|
|
43
|
+
)
|
|
44
|
+
from anagrapheus.assembly.render import render_labelled
|
|
45
|
+
from anagrapheus.assembly.slices import (
|
|
46
|
+
BeliefSlice,
|
|
47
|
+
ChronicleSlice,
|
|
48
|
+
EventsSlice,
|
|
49
|
+
Given,
|
|
50
|
+
LogSlice,
|
|
51
|
+
Slice,
|
|
52
|
+
handed_out,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"DEFAULT_SHED_ORDER",
|
|
57
|
+
"AssemblyError",
|
|
58
|
+
"BeliefSlice",
|
|
59
|
+
"Budget",
|
|
60
|
+
"BudgetExceeded",
|
|
61
|
+
"ChronicleSlice",
|
|
62
|
+
"Context",
|
|
63
|
+
"EventRef",
|
|
64
|
+
"EventsSlice",
|
|
65
|
+
"Given",
|
|
66
|
+
"GivenRef",
|
|
67
|
+
"LogSlice",
|
|
68
|
+
"MislabelledPart",
|
|
69
|
+
"NotGranted",
|
|
70
|
+
"Part",
|
|
71
|
+
"Register",
|
|
72
|
+
"SectionRef",
|
|
73
|
+
"Slice",
|
|
74
|
+
"Source",
|
|
75
|
+
"StaleRead",
|
|
76
|
+
"UncitedSource",
|
|
77
|
+
"assemble",
|
|
78
|
+
"handed_out",
|
|
79
|
+
"render_labelled",
|
|
80
|
+
"slices_of",
|
|
81
|
+
]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Putting a context together: run the assembler, check what it built, fit the budget.
|
|
3
|
+
|
|
4
|
+
``assemble`` is small on purpose. The assembler is the application's and is required
|
|
5
|
+
to be pure; what the library adds is the four checks that make a context trustworthy
|
|
6
|
+
whatever the assembler does:
|
|
7
|
+
|
|
8
|
+
- **The grant holds slices and nothing else.** A grant is a dataclass, and a field
|
|
9
|
+
that is not a :class:`~anagrapheus.assembly.slices.Slice` is refused, because a
|
|
10
|
+
store or a table in a grant is a way round every other check here.
|
|
11
|
+
- **The slices agree on a position.** Two slices at different positions would build a
|
|
12
|
+
context no single moment of the log describes.
|
|
13
|
+
- **A part cites only what was handed out**, and **prose is labelled as prose**: a
|
|
14
|
+
part citing a chronicle section must be ``generated``, and every section read must
|
|
15
|
+
be cited by one. That catches prose that loses its citation; it cannot stop an
|
|
16
|
+
assembler that cites a section *and* copies its words elsewhere, which is the limit
|
|
17
|
+
of checking structure rather than text.
|
|
18
|
+
- **A grant is read for one assembly.** Its slices must be unread when it arrives, so
|
|
19
|
+
the context's ``read`` is exactly this call's.
|
|
20
|
+
- **The budget never sheds a binding part**, and says what it shed.
|
|
21
|
+
|
|
22
|
+
**Determinism is the contract this rests on.** Same log, same position, same grant: same
|
|
23
|
+
parts, so the same rendered bytes, so a replay hits its pins. Nothing here can check
|
|
24
|
+
that an assembler is pure. The application's own tests can: build a context over a fixed
|
|
25
|
+
log at a fixed position, and compare
|
|
26
|
+
:meth:`~anagrapheus.assembly.parts.Context.describe` with a stored golden.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# Design: note 07 §4; upstream NVM U42 (the binding layer never sheds).
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import dataclasses
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from typing import TYPE_CHECKING
|
|
36
|
+
|
|
37
|
+
from anagrapheus.assembly.errors import (
|
|
38
|
+
AssemblyError,
|
|
39
|
+
BudgetExceeded,
|
|
40
|
+
MislabelledPart,
|
|
41
|
+
UncitedSource,
|
|
42
|
+
)
|
|
43
|
+
from anagrapheus.assembly.parts import Context, Register, SectionRef
|
|
44
|
+
from anagrapheus.assembly.slices import Slice, handed_out
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
48
|
+
|
|
49
|
+
from anagrapheus.assembly.parts import Part
|
|
50
|
+
|
|
51
|
+
__all__ = ["DEFAULT_SHED_ORDER", "Budget", "assemble", "slices_of"]
|
|
52
|
+
|
|
53
|
+
#: Which registers a budget sheds first. ``binding`` is never in it.
|
|
54
|
+
DEFAULT_SHED_ORDER: tuple[Register, ...] = (
|
|
55
|
+
Register.considered,
|
|
56
|
+
Register.generated,
|
|
57
|
+
Register.craft,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class Budget:
|
|
63
|
+
"""
|
|
64
|
+
How much a context may hold, and what goes first when it holds too much.
|
|
65
|
+
|
|
66
|
+
``count`` is the application's token counter — the library does not guess at one.
|
|
67
|
+
Parts are shed register by register in ``order``, lowest ``rank`` first and, among
|
|
68
|
+
equals, the later part first. ``binding`` may not appear in ``order``: the binding
|
|
69
|
+
layer never sheds, because a context missing a binding fact is wrong, not short.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
total: int
|
|
73
|
+
count: Callable[[Part], int]
|
|
74
|
+
order: tuple[Register, ...] = DEFAULT_SHED_ORDER
|
|
75
|
+
|
|
76
|
+
def __post_init__(self) -> None:
|
|
77
|
+
if Register.binding in self.order:
|
|
78
|
+
raise ValueError("a budget may never shed binding parts")
|
|
79
|
+
|
|
80
|
+
def fit(self, parts: Sequence[Part]) -> tuple[tuple[Part, ...], tuple[Part, ...]]:
|
|
81
|
+
"""``(kept, shed)``, with ``kept`` in assembly order."""
|
|
82
|
+
sizes = [self.count(p) for p in parts]
|
|
83
|
+
binding = sum(
|
|
84
|
+
n for p, n in zip(parts, sizes, strict=True) if p.register is Register.binding
|
|
85
|
+
)
|
|
86
|
+
if binding > self.total:
|
|
87
|
+
raise BudgetExceeded(
|
|
88
|
+
f"the binding parts alone come to {binding} against a total of {self.total}; "
|
|
89
|
+
f"a call that cannot carry its invariants is not made"
|
|
90
|
+
)
|
|
91
|
+
used = sum(sizes)
|
|
92
|
+
dropped: set[int] = set()
|
|
93
|
+
for register in self.order:
|
|
94
|
+
candidates = sorted(
|
|
95
|
+
(i for i, p in enumerate(parts) if p.register is register),
|
|
96
|
+
key=lambda i: (parts[i].rank, -i),
|
|
97
|
+
)
|
|
98
|
+
for i in candidates:
|
|
99
|
+
if used <= self.total:
|
|
100
|
+
break
|
|
101
|
+
dropped.add(i)
|
|
102
|
+
used -= sizes[i]
|
|
103
|
+
if used > self.total:
|
|
104
|
+
raise BudgetExceeded(
|
|
105
|
+
f"{used} remain against a total of {self.total} after shedding every "
|
|
106
|
+
f"register this budget may shed"
|
|
107
|
+
)
|
|
108
|
+
kept = tuple(p for i, p in enumerate(parts) if i not in dropped)
|
|
109
|
+
shed = tuple(p for i, p in enumerate(parts) if i in dropped)
|
|
110
|
+
return kept, shed
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def slices_of(grant: object) -> tuple[Slice, ...]:
|
|
114
|
+
"""
|
|
115
|
+
A grant's slices, refusing a grant that holds anything else.
|
|
116
|
+
|
|
117
|
+
Frozen, so the assembler cannot swap a field for a store after the check has run.
|
|
118
|
+
"""
|
|
119
|
+
if not dataclasses.is_dataclass(grant) or isinstance(grant, type):
|
|
120
|
+
raise TypeError(f"a grant is a dataclass instance of slices, not {type(grant).__name__}")
|
|
121
|
+
params: object = getattr(type(grant), "__dataclass_params__", None)
|
|
122
|
+
if not getattr(params, "frozen", False):
|
|
123
|
+
raise TypeError(
|
|
124
|
+
f"grant {type(grant).__name__} is not frozen; an assembler could replace a "
|
|
125
|
+
f"slice with a store after the grant was checked"
|
|
126
|
+
)
|
|
127
|
+
found: list[Slice] = []
|
|
128
|
+
for field in dataclasses.fields(grant):
|
|
129
|
+
value: object = getattr(grant, field.name)
|
|
130
|
+
if not isinstance(value, Slice):
|
|
131
|
+
raise TypeError(
|
|
132
|
+
f"grant field {field.name!r} holds a {type(value).__name__}, which is not a "
|
|
133
|
+
f"slice; a grant that carries a store is a way round the firewall"
|
|
134
|
+
)
|
|
135
|
+
found.append(value)
|
|
136
|
+
return tuple(found)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def assemble[G](
|
|
140
|
+
role: str,
|
|
141
|
+
assembler: Callable[[G], Iterable[Part]],
|
|
142
|
+
grant: G,
|
|
143
|
+
*,
|
|
144
|
+
budget: Budget | None = None,
|
|
145
|
+
) -> Context:
|
|
146
|
+
"""
|
|
147
|
+
Run ``assembler`` over ``grant`` and return what ``role`` will be shown.
|
|
148
|
+
|
|
149
|
+
Raises :class:`~anagrapheus.assembly.errors.UncitedSource` for a part citing what
|
|
150
|
+
the grant never handed out, :class:`~anagrapheus.assembly.errors.MislabelledPart`
|
|
151
|
+
for prose presented as anything but ``generated``, and
|
|
152
|
+
:class:`~anagrapheus.assembly.errors.BudgetExceeded` when the binding parts alone
|
|
153
|
+
do not fit.
|
|
154
|
+
"""
|
|
155
|
+
slices = slices_of(grant)
|
|
156
|
+
positions = {s.upto_seq for s in slices if s.upto_seq is not None}
|
|
157
|
+
if len(positions) > 1:
|
|
158
|
+
raise AssemblyError(
|
|
159
|
+
f"the slices in this grant read at different positions {sorted(positions)}; a "
|
|
160
|
+
f"context must describe one moment of the log"
|
|
161
|
+
)
|
|
162
|
+
if handed_out(slices):
|
|
163
|
+
# Ledgers live on the slices, so a reused grant would carry the last call's
|
|
164
|
+
# reads into this one's context, and let a part cite what this call never read.
|
|
165
|
+
raise AssemblyError(
|
|
166
|
+
"this grant was already read from; a grant is opened for one assembly, so "
|
|
167
|
+
"its context describes that call and no other"
|
|
168
|
+
)
|
|
169
|
+
parts = tuple(assembler(grant))
|
|
170
|
+
read = handed_out(slices)
|
|
171
|
+
known = set(read)
|
|
172
|
+
labelled: set[SectionRef] = set()
|
|
173
|
+
for part in parts:
|
|
174
|
+
for source in part.sources:
|
|
175
|
+
if source not in known:
|
|
176
|
+
raise UncitedSource(
|
|
177
|
+
f"part {part.slot!r} cites {source}, which this grant never handed out"
|
|
178
|
+
)
|
|
179
|
+
if isinstance(source, SectionRef):
|
|
180
|
+
if part.register is not Register.generated:
|
|
181
|
+
raise MislabelledPart(
|
|
182
|
+
f"part {part.slot!r} cites chronicle prose as {part.register.value}; "
|
|
183
|
+
f"model-written prose is advisory and must be labelled generated"
|
|
184
|
+
)
|
|
185
|
+
labelled.add(source)
|
|
186
|
+
unlabelled = [s for s in read if isinstance(s, SectionRef) and s not in labelled]
|
|
187
|
+
if unlabelled:
|
|
188
|
+
# Checking only what parts cite would let prose in under another register by
|
|
189
|
+
# leaving the citation off. Every section read must be accounted for by a
|
|
190
|
+
# generated part, which makes an omitted citation an error rather than a bypass.
|
|
191
|
+
raise MislabelledPart(
|
|
192
|
+
f"{len(unlabelled)} chronicle section(s) were read and no generated part cites "
|
|
193
|
+
f"them ({unlabelled[0].section_id!r} first); read prose only to show it as prose"
|
|
194
|
+
)
|
|
195
|
+
kept, shed = (parts, ()) if budget is None else budget.fit(parts)
|
|
196
|
+
return Context(
|
|
197
|
+
role=role,
|
|
198
|
+
upto_seq=next(iter(positions), None),
|
|
199
|
+
parts=kept,
|
|
200
|
+
shed=shed,
|
|
201
|
+
read=read,
|
|
202
|
+
)
|