Skip to content

Commit 2b5fbbb

Browse files
committed
create project
0 parents  commit 2b5fbbb

32 files changed

Lines changed: 3606 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
services:
11+
redis:
12+
image: redis:7-alpine
13+
ports: ["6379:6379"]
14+
postgres:
15+
image: postgres:16-alpine
16+
env:
17+
POSTGRES_PASSWORD: ledger
18+
POSTGRES_USER: ledger
19+
POSTGRES_DB: ledger
20+
ports: ["5432:5432"]
21+
options: >-
22+
--health-cmd "pg_isready -U ledger"
23+
--health-interval 5s
24+
--health-timeout 5s
25+
--health-retries 10
26+
mysql:
27+
image: mysql:8
28+
env:
29+
MYSQL_ROOT_PASSWORD: ledger
30+
MYSQL_DATABASE: ledger
31+
MYSQL_USER: ledger
32+
MYSQL_PASSWORD: ledger
33+
ports: ["3306:3306"]
34+
options: >-
35+
--health-cmd "mysqladmin ping -h localhost -pledger"
36+
--health-interval 5s
37+
--health-timeout 5s
38+
--health-retries 20
39+
env:
40+
AGENT_LEDGER_REDIS_URL: redis://localhost:6379/0
41+
AGENT_LEDGER_POSTGRES_URL: postgresql+asyncpg://ledger:ledger@localhost:5432/ledger
42+
AGENT_LEDGER_MYSQL_URL: mysql+asyncmy://ledger:ledger@localhost:3306/ledger
43+
steps:
44+
- uses: actions/checkout@v4
45+
- uses: astral-sh/setup-uv@v6
46+
with:
47+
python-version: "3.11"
48+
enable-cache: true
49+
- run: uv sync --all-extras
50+
- run: make lint
51+
- run: make test
52+

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.venv/
2+
.worktree/
3+
.mypy_cache/
4+
.pytest_cache/
5+
.ruff_cache/
6+
__pycache__/
7+
*.py[cod]
8+
*.egg-info/
9+
dist/
10+
build/
11+
.coverage
12+
htmlcov/

LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Agent Ledger contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+

Makefile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.PHONY: fix lint test build
2+
3+
fix:
4+
uv run ruff format .
5+
uv run ruff check --fix .
6+
7+
lint:
8+
uv run ruff format --check .
9+
uv run ruff check .
10+
uv run mypy
11+
12+
test:
13+
uv run pytest
14+
15+
build:
16+
uv build
17+

README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Agent Ledger
2+
3+
Agent Ledger is a framework-neutral event ledger for durable agent sessions. It records agent
4+
steps before model and tool execution, keeps causal links across distributed agent runs, and gives
5+
framework adapters enough facts to rebuild their own run context after a restart.
6+
7+
The ledger is the source of truth for **what happened**. Recovery remains owned by the framework
8+
integration that understands its checkpoint and `RunContext` types.
9+
10+
## Why another event log?
11+
12+
Traditional logs and traces explain service execution. Agent Ledger adds agent-native invariants:
13+
14+
- `Session` groups one end-to-end task across processes and agents.
15+
- `Run` is the optimistic-concurrency stream written by one agent loop.
16+
- `Step` is a logical unit; `Attempt` is one physical model or tool invocation.
17+
- requested events are committed before external calls, so interrupted calls remain visible.
18+
- `parent_run_id` and `caused_by_event_id` form a causal DAG without relying on timestamps.
19+
- trajectories such as ATIF are projections, not the durable source of truth.
20+
21+
## Quick start
22+
23+
```python
24+
from agent_ledger import Actor, SessionRecorder
25+
from agent_ledger.stores.memory import MemoryEventStore
26+
27+
store = MemoryEventStore()
28+
recorder = SessionRecorder(
29+
store=store,
30+
session_id="session-1",
31+
run_id="run-1",
32+
actor=Actor(type="agent", id="researcher"),
33+
)
34+
35+
await recorder.start_run(payload={"task": "summarize"})
36+
attempt = await recorder.before_model_call(
37+
step_id="step-1",
38+
payload={"model": "example-model", "messages": [{"role": "user", "content": "Hi"}]},
39+
)
40+
41+
# The real model call starts only after model.requested is durably appended.
42+
response = await model.generate()
43+
await recorder.model_completed(attempt, payload={"message": response})
44+
```
45+
46+
If the process stops after `before_model_call`, inspection reports an unresolved attempt. An adapter
47+
can then ask the provider for a result, require human confirmation, or retry with a new
48+
`attempt_id`; the generic library never silently repeats an external side effect.
49+
50+
## Stores
51+
52+
`EventStore` has three implementations:
53+
54+
- `MemoryEventStore`: process-local reference implementation and test double.
55+
- `RedisEventStore`: atomic append through Lua, with per-session cluster key co-location.
56+
- `SqlEventStore`: one SQLAlchemy 2.x implementation for SQLite, MySQL, and PostgreSQL.
57+
58+
Redis and SQL clients are supplied by the application so pool size, connection timeout, and
59+
deployment-specific durability are explicit. Install optional dependencies with
60+
`agent-ledger[redis]`, `agent-ledger[sql]`, `agent-ledger[mysql]`, or
61+
`agent-ledger[postgres]`.
62+
63+
## Design boundaries
64+
65+
- No collector or mandatory network service in v1.
66+
- No generic cross-framework `RunContext` serializer.
67+
- No automatic replay of an unresolved tool side effect.
68+
- No exactly-once claim. Appends are atomic and idempotent; external calls are not transactional
69+
with the ledger.
70+
- No global ordering claim. `commit_cursor` orders one session's stored events for display and
71+
pagination; causal links define execution relationships.
72+
73+
See [RFC 0001](spec/rfcs/0001-agent-ledger.md) for the contract and
74+
[the plain-loop example](examples/plain_loop.py) for adapter-owned recovery.
75+
76+
## Development
77+
78+
```bash
79+
uv sync --all-extras
80+
make fix
81+
make lint
82+
make test
83+
```
84+

examples/plain_loop.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import asyncio
2+
3+
from agent_ledger import Actor, SessionRecorder
4+
from agent_ledger.frameworks.plain_loop import PlainLoopContext, PlainLoopProfile
5+
from agent_ledger.stores.memory import MemoryEventStore
6+
7+
8+
async def main() -> None:
9+
store = MemoryEventStore()
10+
actor = Actor(type="agent", id="example", framework="plain-loop")
11+
recorder = SessionRecorder(
12+
store=store,
13+
session_id="session-1",
14+
run_id="run-1",
15+
actor=actor,
16+
)
17+
profile = PlainLoopProfile()
18+
19+
await recorder.start_run(payload={"messages": [{"role": "user", "content": "hello"}]})
20+
await recorder.start_step("step-1")
21+
attempt = await recorder.before_model_call(
22+
"step-1",
23+
payload={"model": "example-model"},
24+
)
25+
await recorder.model_completed(
26+
attempt,
27+
payload={"message": {"role": "assistant", "content": "Hello!"}},
28+
)
29+
await recorder.complete_step("step-1")
30+
await profile.save(
31+
recorder,
32+
PlainLoopContext(
33+
messages=[
34+
{"role": "user", "content": "hello"},
35+
{"role": "assistant", "content": "Hello!"},
36+
],
37+
completed_steps=["step-1"],
38+
),
39+
)
40+
41+
events = [event async for event in store.read_stream(recorder.stream)]
42+
recovered = profile.recover(events)
43+
print(recovered.context.model_dump())
44+
45+
46+
if __name__ == "__main__":
47+
asyncio.run(main())

pyproject.toml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
[build-system]
2+
requires = ["hatchling>=1.27"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "agent-ledger"
7+
version = "0.1.0"
8+
description = "A framework-neutral event ledger for durable agent sessions"
9+
readme = "README.md"
10+
requires-python = ">=3.11"
11+
license = "MIT"
12+
authors = [{ name = "Agent Ledger contributors" }]
13+
keywords = ["agent", "event-sourcing", "session", "trajectory", "recovery"]
14+
classifiers = [
15+
"Development Status :: 3 - Alpha",
16+
"License :: OSI Approved :: MIT License",
17+
"Programming Language :: Python :: 3",
18+
"Programming Language :: Python :: 3.11",
19+
"Programming Language :: Python :: 3.12",
20+
"Programming Language :: Python :: 3.13",
21+
"Typing :: Typed",
22+
]
23+
dependencies = ["pydantic>=2.10,<3"]
24+
25+
[project.urls]
26+
Repository = "https://github.com/compforge/agent-ledger"
27+
Issues = "https://github.com/compforge/agent-ledger/issues"
28+
29+
[project.optional-dependencies]
30+
redis = ["redis>=5,<7"]
31+
sql = ["SQLAlchemy[asyncio]>=2.0,<3", "aiosqlite>=0.20,<1"]
32+
mysql = ["SQLAlchemy[asyncio]>=2.0,<3", "asyncmy>=0.2,<1"]
33+
postgres = ["SQLAlchemy[asyncio]>=2.0,<3", "asyncpg>=0.30,<1"]
34+
all = [
35+
"redis>=5,<7",
36+
"SQLAlchemy[asyncio]>=2.0,<3",
37+
"aiosqlite>=0.20,<1",
38+
"asyncmy>=0.2,<1",
39+
"asyncpg>=0.30,<1",
40+
]
41+
42+
[dependency-groups]
43+
dev = [
44+
"jsonschema>=4.23,<5",
45+
"mypy>=1.15,<2",
46+
"pytest>=8.3,<9",
47+
"pytest-asyncio>=0.25,<1",
48+
"ruff>=0.11,<1",
49+
]
50+
51+
[tool.hatch.build.targets.wheel]
52+
packages = ["src/agent_ledger"]
53+
54+
[tool.pytest.ini_options]
55+
asyncio_mode = "auto"
56+
testpaths = ["tests"]
57+
58+
[tool.ruff]
59+
target-version = "py311"
60+
line-length = 100
61+
62+
[tool.ruff.lint]
63+
select = ["E", "F", "I", "UP", "B", "ASYNC", "RUF"]
64+
65+
[tool.ruff.lint.per-file-ignores]
66+
"**/__init__.py" = ["F401"]
67+
68+
[tool.mypy]
69+
python_version = "3.11"
70+
strict = true
71+
packages = ["agent_ledger"]

0 commit comments

Comments
 (0)