Metadata-Version: 2.4
Name: execution-journal
Version: 0.1.0
Summary: A minimal, local-first execution journal for AI agents: goals, justifications, artifacts, and approvals in a replayable graph.
Project-URL: Homepage, https://github.com/Harsh-Dhingra/execution-journal
Project-URL: Repository, https://github.com/Harsh-Dhingra/execution-journal
Project-URL: Issues, https://github.com/Harsh-Dhingra/execution-journal/issues
Author: Harsh Dhingra
License: MIT License
        
        Copyright (c) 2026 Harsh Dhingra
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: agents,audit,local-first,observability,provenance,tracing
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Debuggers
Requires-Python: >=3.10
Provides-Extra: aisuite
Requires-Dist: aisuite; extra == 'aisuite'
Requires-Dist: pydantic>=2; extra == 'aisuite'
Provides-Extra: dev
Requires-Dist: pytest; extra == 'dev'
Description-Content-Type: text/markdown

# execution-journal

[![tests](https://github.com/Harsh-Dhingra/execution-journal/actions/workflows/ci.yml/badge.svg)](https://github.com/Harsh-Dhingra/execution-journal/actions/workflows/ci.yml)
[![python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
[![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![status](https://img.shields.io/badge/status-experimental-orange)](#status)

> Logs record what happened. A journal records why: goals, justifications, artifacts, and approvals, linked into a graph you can search and replay.

A minimal, local-first execution journal for AI agents. One SQLite file per project, zero dependencies in the core, and a decorator or context manager as the entire integration cost.

## Status

**Experimental, pre-1.0.** The event schema and the `Journal` API are the parts I expect to keep;
everything else may move. Treat minor versions as breaking until 1.0.

What works today: the schema, SQLite storage, the three exports, replay, and Tier 2
justifications against Anthropic, OpenAI, and aisuite. 91 tests, CI on Python 3.10 and 3.12,
zero runtime dependencies, ~0.1 ms of overhead per step.

What is deliberately not built yet — each has an [issue](https://github.com/Harsh-Dhingra/execution-journal/issues):
a CLI, an index page across runs, LangGraph and OpenAI Agents SDK adapters, Tier 3 framework
extraction, an OpenTelemetry exporter. There is **no dashboard and no server** — you get a
`.db` file and a self-contained HTML page you generate on demand. If you need a team-visible
view over a fleet of agents, this is not that tool.

Bug reports and adapters are the most useful contributions right now — see
[CONTRIBUTING.md](CONTRIBUTING.md).

## Quickstart

```python
from execution_journal import Journal

journal = Journal(goal="Answer the user's question about the approval gate")

with journal.tool_call("repo_search",
                       justification="The question names a specific codebase; grounding beats recall.",
                       query="approval gate") as step:
    step.record_output(n_hits=3, top_file="agent/approvals.py")

journal.approval("email_findings", granted=False, by="user", reason="Answer locally.")
journal.finish("Answered from code evidence; no email sent.")
```

That is the whole adoption story: construct a `Journal`, wrap the steps you care about, call `finish`. Everything lands in `.execution_journal.db`, which you can search, export, replay, or attach to a bug report as-is.

```bash
# PyPI release pending; install from source for now
pip install git+https://github.com/Harsh-Dhingra/execution-journal
python examples/demo_agent.py     # no API keys, no network — writes a journal and all three exports
```

## The viewer

`export_html` writes a single self-contained file that opens from `file://` — execution tree on the left, entries on the right, justification set off from everything else. Light and dark follow the system theme.

![The HTML journal viewer](https://raw.githubusercontent.com/Harsh-Dhingra/execution-journal/main/docs/screenshot.png)

Search filters the entries and the tree together — here, narrowing a run to the failed doc fetch
and the retry that recovered it, one justification written by the model and one by the developer:

![Searching a run in the viewer](https://raw.githubusercontent.com/Harsh-Dhingra/execution-journal/main/docs/demo.gif)

<sub>Both are the demo run that ships with the repo. Regenerate with
`python examples/demo_agent.py && open examples/demo_output/demo.html`.</sub>

## A journal is not a log

Three families of fields distinguish a journal entry from a log line:

| | Fields | Answers |
|---|---|---|
| **Intent** | `goal`, `justification` | What was this step for, and why this step now? |
| **Evidence** | `inputs`, `outputs`, `artifacts` (content-hashed) | What went in, what came out, which file came from which step? |
| **Authority** | `approval` (`by`, `granted`) | Who allowed the side effect — and who refused one? |

Plus `parent_id` on every event, which makes the run a graph rather than a flat timeline.

| | Application logs | Cloud tracing (LangSmith, Langfuse, OTel GenAI) | execution-journal |
|---|---|---|---|
| Chronology, latency, tool sequence | ✅ | ✅ | ✅ |
| Intent + justification as fields | ✕ | ✕ | ✅ |
| Approval / authority semantics | ✕ | ✕ | ✅ |
| Content-hashed artifact provenance | ✕ | partial | ✅ |
| Local-first, no account or server | ✅ | ✕ | ✅ |
| Deliverable you can attach to a ticket | ✕ | link | the `.db` file |

A recorded entry:

```json
{
  "run_id": "a3f9c2e11b04",
  "event_id": "7d1e0aa93c55",
  "parent_id": "f21bb8d0447a",
  "kind": "tool_call",
  "name": "github_search",
  "goal": "Find repository context before answering",
  "justification": "Question names a specific codebase; grounding beats recall.",
  "status": "completed",
  "ts_start_ms": 1753715000123,
  "ts_end_ms": 1753715000963,
  "duration_ms": 840,
  "inputs": {"query": "approval gate", "repo": "andrewyng/openworker"},
  "outputs": {"n_hits": 3, "top_file": "coworker/approvals.py"},
  "artifacts": [],
  "approval": null,
  "error": null,
  "meta": {}
}
```

Failures are entries too. An exception inside a step is recorded with its type and message, marked `failed`, and then re-raised — the journal never swallows it.

## Where the "why" comes from

Every justification records its own provenance in `meta.justification_source`.

**Tier 1 — developer-declared.** You write it at the call site. Reliable for deterministic paths — retries, fallbacks, verification steps — where the reason is a design decision. Static: the same string every run.

```python
journal.tool_call("fetch_docs_retry", justification="One retry against the cached mirror before falling back.")
```

**Tier 2 — model-declared at decision time.** A required `justification` parameter is injected into every tool schema before it reaches the model, so the same generation that picks an action also states why:

```json
{"name": "github_search",
 "arguments": {"query": "approval gate",
               "justification": "The question references OpenWorker's codebase; I need the approval implementation before answering."}}
```

The integration strips the field before the real tool runs — your tool function never sees it — and writes it to the entry with `meta.justification_source = "model"`. Cost is a few tokens per call. If a model omits it despite `required`, the entry records `justification: null` and `meta.justification_missing: true`; nothing is invented.

**Tier 3 — framework-extracted.** Harvesting ReAct "Thought:" lines and planner scratchpads from framework internals. On the roadmap, not in v0.1.

### The faithfulness caveat

A model-declared justification is the agent's **stated** reason at decision time — not verified ground truth. Models can produce plausible post-hoc rationalizations, and the reasoning-faithfulness literature is clear that a stated reason need not be the operative one. This project's claim is deliberately narrow: it records asserted intent, with provenance and timing, which is the record an auditor asks for and a debugging signal that localizes where the agent's world-model went wrong. Verifying that a justification is *true* is out of scope.

## Architecture

![Architecture](https://raw.githubusercontent.com/Harsh-Dhingra/execution-journal/main/docs/architecture.png)

```
     your agent  →  instrumentation  →  journal event model  →  one SQLite file
                    @step_fn / step()   intent · evidence         ↓        ↓        ↓
                    tool_call()         authority · parent_id   viewer  replay  exports
```

Instrumentation knows about frameworks; the event model does not. Storage knows nothing about semantics — it persists indexed columns plus the full event JSON, so the schema can gain optional fields without a migration. Viewer, replayer, and exporters are peers that only read.

## Exports

```python
from execution_journal import export_json, export_markdown, export_html

export_json(db_path, run_id, "journal.json")       # machine-readable
export_markdown(db_path, run_id, "journal.md")     # readable diff-able record
export_html(db_path, run_id, "journal.html")       # self-contained viewer, no external requests
```

## Replay

Recorded inputs turn a flaky one-off failure into a deterministic replay target. v0.1 replays the *decisions* in order and hands each recorded event to a handler you register — no state snapshotting.

```python
from execution_journal import Replayer

replayer = Replayer(".execution_journal.db")
replayer.register("repo_search", lambda event: search(**event.inputs))

for name, result in replayer.replay_from(run_id, event_id):
    print(name, result)
```

## Tier 2 in your own tool loop

The `integrations/` modules are provider adapters — copy the two files you need next to your agent
(`justification.py` plus one adapter). They import no SDK, so they work with the Anthropic SDK, the
OpenAI SDK, raw HTTP, or anything speaking either wire shape.

```python
from execution_journal import Journal
from provider_journal import journaled_tools, tool_results

journal = Journal(goal="Answer the user's question about the approval gate")

response = client.messages.create(
    model="claude-opus-5",
    max_tokens=4096,
    tools=journaled_tools(TOOLS),      # required `justification` injected into each schema
    messages=messages,
)

results = tool_results(journal, response, {"github_search": github_search})
messages += [{"role": "assistant", "content": response.content},
             {"role": "user", "content": results}]
```

Every tool call becomes a journal entry carrying the model's own justification, and `results` is
already shaped for the provider you called — `tool_result` blocks for Anthropic, `role: "tool"`
messages for OpenAI. Your tool functions never see the injected argument.

The field lives in a different place in each schema dialect, and all three are handled:

| Dialect | Where the schema lives |
|---|---|
| Anthropic Messages | `input_schema` |
| OpenAI chat completions | `function.parameters` |
| aisuite | generated from the function signature — see below |

Prefer to dispatch yourself? `tool_calls(response)` returns one dict per call — `name`, `args` with
the justification already stripped, `justification` (`None` if the model omitted it, never
fabricated), `id`, and `provider` — and you open the journal entry however you like.

## aisuite integration

Optional: install the `[aisuite]` extra. `JournaledClient` journals every completion (hashing
message content rather than storing it) and injects the Tier 2 parameter into your tools.

```python
import aisuite
from execution_journal import Journal
from aisuite_journal import JournaledClient

journal = Journal(goal="Answer the user's question")
client = JournaledClient(journal, aisuite.Client())

response = client.chat.completions.create(
    model="anthropic:claude-opus-5",
    messages=[{"role": "user", "content": question}],
    tools=[github_search],          # plain Python callables, as aisuite expects
)
# each tool call is journaled with the model's own justification
```

Verified end-to-end against aisuite 0.1.14 with a stub provider. Three upstream constraints are
worth knowing, and they are why the provider adapter above exists:

- aisuite builds each tool schema from the **function signature and docstring**, so `justified_tool`
  adds the parameter there rather than to a JSON schema.
- `create()` forwards `tools` to the provider **only when `max_turns` is set**, so the wrapper
  defaults it to 1. Without it aisuite drops tools silently.
- aisuite 0.1.14 pins `docstring-parser <0.16`, which uses `ast.NameConstant` — removed in **Python
  3.12**. So the aisuite extra runs on Python 3.10–3.11 only, and it imports `pydantic` without
  declaring it, which is why our extra pulls it in. Neither limitation touches the core library or
  the provider adapter, both of which are stdlib-only on 3.10+.

## Roadmap

LangGraph and OpenAI Agents SDK adapters · OpenTelemetry span export mapping journal fields to GenAI semantic conventions · run-vs-run diffing · multi-agent runs · signed/tamper-evident journals · richer replay with state snapshots

## Where this sits

Between cloud tracing SaaS (chronological spans, no intent or approval semantics, and your run history lives on someone else's server) and heavyweight governance machinery (cryptographic receipts, formal policy engines). This is the lightweight embeddable middle: a minimal schema, in a local file, at a decorator's adoption cost.

The June 2026 survey [*From Agent Traces to Trust: Evidence Tracing and Execution Provenance in LLM Agents*](https://arxiv.org/abs/2606.04990) (arXiv:2606.04990) maps out this gap; execution-journal is a concrete minimal implementation of what it calls for.

## Citation

```bibtex
@misc{execution-journal,
  title  = {execution-journal: a minimal local-first execution journal for AI agents},
  author = {Dhingra, Harsh},
  year   = {2026},
  note   = {arXiv ID pending}
}
```

## License

MIT — see [LICENSE](LICENSE).
