Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
pull_request:

jobs:
test:
python:
runs-on: ubuntu-latest
services:
redis:
Expand Down Expand Up @@ -40,13 +40,46 @@ jobs:
AGENT_LEDGER_REDIS_URL: redis://localhost:6379/0
AGENT_LEDGER_POSTGRES_URL: postgresql+asyncpg://ledger:ledger@localhost:5432/ledger
AGENT_LEDGER_MYSQL_URL: mysql+asyncmy://ledger:ledger@localhost:3306/ledger
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: "3.11"
enable-cache: true
cache-dependency-glob: python/uv.lock
- run: uv sync --all-extras
- run: make lint
- run: make test

typescript:
runs-on: ubuntu-latest
defaults:
run:
working-directory: typescript
steps:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 devloop code-review · deepseek-v4-pro · ready in 1m 44s

Both the new typescript and go jobs lack a timeout-minutes setting. If a test hangs (e.g., infinite loop, deadlocked async operation, or a make test that waits forever on a missing service), the job will run until GitHub's default 6-hour timeout, wasting runner minutes and delaying other queued workflows.

Add timeout-minutes: 10 (or whatever is appropriate for these test suites) to each job:

  typescript:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    ...
  go:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    ...

ccr:fp=55e1a7720aef

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ccr:label=debatable — GitHub Actions 已有 6 小时硬上限,仓库现有 Python job 同样未设 timeout-minutes;为新 job 增加更短预算是运维偏好,不构成功能或可靠性缺陷 #padding

- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
cache: npm
cache-dependency-path: typescript/package-lock.json
- run: npm ci
- run: make lint
- run: make test

go:
runs-on: ubuntu-latest
defaults:
run:
working-directory: go
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
cache-dependency-path: go/go.sum
- run: make lint
- run: make test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
.venv/
.worktree/
.worktrees/
node_modules/
*.tsbuildinfo
.mypy_cache/
.pytest_cache/
.ruff_cache/
Expand Down
19 changes: 11 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
.PHONY: fix lint test build

fix:
uv run ruff format .
uv run ruff check --fix .
$(MAKE) -C python fix
$(MAKE) -C go fix

lint:
uv run ruff format --check .
uv run ruff check .
uv run mypy
$(MAKE) -C python lint
$(MAKE) -C typescript lint
$(MAKE) -C go lint

test:
uv run pytest
$(MAKE) -C python test
$(MAKE) -C typescript test
$(MAKE) -C go test

build:
uv build

$(MAKE) -C python build
$(MAKE) -C typescript build
$(MAKE) -C go build
129 changes: 58 additions & 71 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,84 +1,71 @@
# Agent Ledger

Agent Ledger is a framework-neutral event ledger for durable agent sessions. It records agent
steps before model and tool execution, keeps causal links across distributed agent runs, and gives
framework adapters enough facts to rebuild their own run context after a restart.

The ledger is the source of truth for **what happened**. Recovery remains owned by the framework
integration that understands its checkpoint and `RunContext` types.

## Why another event log?

Traditional logs and traces explain service execution. Agent Ledger adds agent-native invariants:

- `Session` groups one end-to-end task across processes and agents.
- `Run` is the optimistic-concurrency stream written by one agent loop.
- `Step` is a logical unit; `Attempt` is one physical model or tool invocation.
- requested events are committed before external calls, so interrupted calls remain visible.
- `parent_run_id` and `caused_by_event_id` form a causal DAG without relying on timestamps.
- trajectories such as ATIF are projections, not the durable source of truth.

## Quick start

```python
from agent_ledger import Actor, SessionRecorder
from agent_ledger.stores.memory import MemoryEventStore

store = MemoryEventStore()
recorder = SessionRecorder(
store=store,
session_id="session-1",
run_id="run-1",
actor=Actor(type="agent", id="researcher"),
)

await recorder.start_run(payload={"task": "summarize"})
attempt = await recorder.before_model_call(
step_id="step-1",
payload={"model": "example-model", "messages": [{"role": "user", "content": "Hi"}]},
)

# The real model call starts only after model.requested is durably appended.
response = await model.generate()
await recorder.model_completed(attempt, payload={"message": response})
Agent Ledger is a framework-neutral specification and a set of polyglot adapters for durable agent
sessions. It records model and tool attempts before execution, preserves causal timelines across
distributed agents, and lets each framework rebuild its own native session after interruption.

The specification is the stable product. Language SDKs are deliberately small; most project code
lives in adapters that understand a framework's hooks, messages, checkpoints, and resume APIs.

## Model

- `Session` groups one end-to-end task across processes, languages, and agents.
- `Run` identifies one semantic agent execution and participates in the causal DAG.
- `EventStream` is an optimistic-concurrency partition. It may contain one run's execution events or
framework-native state that survives several runtime runs.
- `Step` is logical work that survives retries; `Attempt` is one physical model or tool invocation.
- Normalized events are the source for timelines and trajectories. Framework-native records are the
source for resume.

Requested events are committed before an external call. A requested event without a terminal event
is unresolved after a crash. It is input to the adapter's reconciliation policy; an adapter must
not silently replay a side-effecting tool.

## Repository

| Area | Responsibility |
| --- | --- |
| `spec/` | Event, append, adapter capability, and recovery contracts |
| `conformance/` | Cross-language golden vectors and adapter contract tests |
| `python/` | Python core SDK plus memory, Redis, and SQLAlchemy stores |
| `typescript/` | TypeScript core SDK and Pi adapter |
| `go/` | Go core SDK and AgentGo adapter |

Current framework profiles:

| Adapter | Recording | Recovery |
| --- | --- | --- |
| Pi AgentHarness | Awaited model/tool hooks | Ledger-backed Pi `SessionStorage` |
| AgentGo | `ChatModel` wrapper, turn hooks, message committer, tool middleware | Native message codec with `HoldRuns` + `SetMessages` + `Continue` |
| Plain Python loop | Explicit recorder calls | Snapshot plus tail replay |

Every adapter publishes machine-readable capabilities such as `strict`, `best_effort`, and
`unsupported`; installing a telemetry-only hook never silently claims durable recovery.

## Store contract

Applications inject an `EventStore`. V1 has no mandatory collector or `/agent-session` service:
framework processes write directly to an in-memory, Redis, or database implementation selected by
the host.

```text
append(stream, expected_version, append_id, events)
read_stream(stream, after_version)
scan_session(session_id, after_cursor)
```

If the process stops after `before_model_call`, inspection reports an unresolved attempt. An adapter
can then ask the provider for a result, require human confirmation, or retry with a new
`attempt_id`; the generic library never silently repeats an external side effect.

## Stores

`EventStore` has three implementations:

- `MemoryEventStore`: process-local reference implementation and test double.
- `RedisEventStore`: atomic append through Lua, with per-session cluster key co-location.
- `SqlEventStore`: one SQLAlchemy 2.x implementation for SQLite, MySQL, and PostgreSQL.

Redis and SQL clients are supplied by the application so pool size, connection timeout, and
deployment-specific durability are explicit. Install optional dependencies with
`agent-ledger[redis]`, `agent-ledger[sql]`, `agent-ledger[mysql]`, or
`agent-ledger[postgres]`.

## Design boundaries

- No collector or mandatory network service in v1.
- No generic cross-framework `RunContext` serializer.
- No automatic replay of an unresolved tool side effect.
- No exactly-once claim. Appends are atomic and idempotent; external calls are not transactional
with the ledger.
- No global ordering claim. `commit_cursor` orders one session's stored events for display and
pagination; causal links define execution relationships.

See [RFC 0001](spec/rfcs/0001-agent-ledger.md) for the contract and
[the plain-loop example](examples/plain_loop.py) for adapter-owned recovery.
Appends are atomic, idempotent by RFC 8785 canonical event content, and protected by optimistic
concurrency. `commit_cursor` gives one session a display/pagination order; causal links, not cursor
or timestamps, define execution relationships.

## Development

```bash
uv sync --all-extras
make fix
make lint
make test
make build
```

See [RFC 0001](spec/rfcs/0001-agent-ledger.md) for the ledger contract and
[RFC 0002](spec/rfcs/0002-polyglot-adapters.md) for framework recording and recovery boundaries.
8 changes: 8 additions & 0 deletions conformance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Conformance

Language SDKs consume the same golden vectors to verify event encoding and append identity. Adapter
contract suites live beside their implementations. The conformance target includes failure
injection before model calls, before tools, after external outcomes, and during native-state
restoration.

Passing these vectors is required before an SDK can claim Agent Ledger schema `1.x` compatibility.
20 changes: 20 additions & 0 deletions conformance/vectors/append.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"canonicalization": "RFC8785",
"canonical": "[{\"actor\":{\"framework\":\"pi\",\"id\":\"writer\",\"type\":\"agent\"},\"attempt_id\":\"attempt-1\",\"event_id\":\"event-1\",\"event_type\":\"model.requested\",\"extensions\":{},\"occurred_at\":\"2026-01-02T03:04:05Z\",\"payload\":{\"model\":\"claude\",\"temperature\":0.25,\"tokens\":1024},\"run_id\":\"run-1\",\"schema_version\":\"1.0\",\"session_id\":\"session-1\",\"step_id\":\"step-1\"}]",
"sha256": "b83e0ae0fcf7e76613c66f34b6c034cb0d35e517271aea5cf6c8820ac7ef5721",
"events": [
{
"schema_version": "1.0",
"event_id": "event-1",
"event_type": "model.requested",
"session_id": "session-1",
"run_id": "run-1",
"actor": { "type": "agent", "id": "writer", "framework": "pi" },
"occurred_at": "2026-01-02T03:04:05Z",
"step_id": "step-1",
"attempt_id": "attempt-1",
"payload": { "model": "claude", "temperature": 0.25, "tokens": 1024 },
"extensions": {}
}
]
}
13 changes: 13 additions & 0 deletions go/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.PHONY: fix lint test build

fix:
go fmt ./...

lint:
go vet ./...

test:
go test ./...

build:
go build ./...
Loading
Loading