fathom-read 0.1.0__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.
Files changed (32) hide show
  1. fathom_read-0.1.0/LICENSE +21 -0
  2. fathom_read-0.1.0/PKG-INFO +141 -0
  3. fathom_read-0.1.0/README.md +98 -0
  4. fathom_read-0.1.0/pyproject.toml +38 -0
  5. fathom_read-0.1.0/setup.cfg +4 -0
  6. fathom_read-0.1.0/src/fathom_read/__init__.py +17 -0
  7. fathom_read-0.1.0/src/fathom_read/adapters/__init__.py +47 -0
  8. fathom_read-0.1.0/src/fathom_read/adapters/_tools.py +88 -0
  9. fathom_read-0.1.0/src/fathom_read/adapters/crewai.py +34 -0
  10. fathom_read-0.1.0/src/fathom_read/adapters/dbos.py +25 -0
  11. fathom_read-0.1.0/src/fathom_read/adapters/edits.py +51 -0
  12. fathom_read-0.1.0/src/fathom_read/adapters/events.py +17 -0
  13. fathom_read-0.1.0/src/fathom_read/adapters/langgraph.py +56 -0
  14. fathom_read-0.1.0/src/fathom_read/adapters/letta.py +32 -0
  15. fathom_read-0.1.0/src/fathom_read/adapters/openinference.py +66 -0
  16. fathom_read-0.1.0/src/fathom_read/cli.py +128 -0
  17. fathom_read-0.1.0/src/fathom_read/client.py +40 -0
  18. fathom_read-0.1.0/src/fathom_read/examples/crewai_events.json +31 -0
  19. fathom_read-0.1.0/src/fathom_read/examples/knowledge_update.json +26 -0
  20. fathom_read-0.1.0/src/fathom_read/examples/langgraph_history.json +37 -0
  21. fathom_read-0.1.0/src/fathom_read/examples/order_duplicate.json +31 -0
  22. fathom_read-0.1.0/src/fathom_read/examples/rename_coherent.json +63 -0
  23. fathom_read-0.1.0/src/fathom_read/examples/rename_starved.json +63 -0
  24. fathom_read-0.1.0/src/fathom_read/ops.py +79 -0
  25. fathom_read-0.1.0/src/fathom_read.egg-info/PKG-INFO +141 -0
  26. fathom_read-0.1.0/src/fathom_read.egg-info/SOURCES.txt +30 -0
  27. fathom_read-0.1.0/src/fathom_read.egg-info/dependency_links.txt +1 -0
  28. fathom_read-0.1.0/src/fathom_read.egg-info/entry_points.txt +2 -0
  29. fathom_read-0.1.0/src/fathom_read.egg-info/requires.txt +3 -0
  30. fathom_read-0.1.0/src/fathom_read.egg-info/top_level.txt +1 -0
  31. fathom_read-0.1.0/tests/test_adapters.py +53 -0
  32. fathom_read-0.1.0/tests/test_client.py +20 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Peter Galligan, Embedded Risk Analytics
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.
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: fathom-read
3
+ Version: 0.1.0
4
+ Summary: Catch the step where an AI agent contradicts a decision it already made. Adapters and a CLI for the Fathom committed-state read over LangGraph, CrewAI, Letta, OpenInference, DBOS, and coding-agent traces.
5
+ Author-email: Embedded Risk Analytics <contact@embeddedriskanalytics.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Peter Galligan, Embedded Risk Analytics
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://embeddedriskanalytics.com
29
+ Project-URL: Repository, https://github.com/ERA-Fathom/fathom
30
+ Project-URL: Research, https://embeddedriskanalytics.com/research.html
31
+ Keywords: ai-agents,llm,langgraph,crewai,letta,openinference,agent-memory,context-engineering,agent-evaluation,coherence
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Topic :: Software Development :: Testing
36
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
37
+ Requires-Python: >=3.9
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Provides-Extra: dev
41
+ Requires-Dist: pytest>=7; extra == "dev"
42
+ Dynamic: license-file
43
+
44
+ # fathom-read
45
+
46
+ **Catch the step where an AI agent contradicts a decision it already made.**
47
+
48
+ On a long task, an agent loses track of what it already decided and starts acting against it. It renames `guest_id` to `customer_id` at step 1, then writes new code against `guest_id` at step 6. The change compiles, imports, and passes the tests. It fails at runtime.
49
+
50
+ `fathom-read` turns the traces your framework already records into an action stream and sends it to the Fathom read, which reconstructs the state the agent committed and flags the step that contradicts it. Deterministic. No model access. Nothing runs in your production path.
51
+
52
+ ![fathom demo](docs/demo.gif)
53
+
54
+ ## Install
55
+
56
+ ```
57
+ pip install fathom-read
58
+ ```
59
+
60
+ ## Run
61
+
62
+ ```
63
+ fathom demo # the bundled rename example, coherent and not
64
+ fathom read trace.json --supersede guest_id=customer_id # your own trace
65
+ fathom read history.json --format langgraph # or name the format
66
+ fathom read trace.json --ops # see the action stream before anything is sent
67
+ fathom formats # the formats it reads
68
+ ```
69
+
70
+ `fathom read` exits 0 when the committed state is coherent and 2 when it finds a contradiction, so it drops into a test suite or a CI step as it is. Add `--json` for a machine-readable verdict.
71
+
72
+ The package ships with a demo key that is rate-limited per day. For your own key, which lifts the limit and keeps your traces on a private tier, write to [contact@embeddedriskanalytics.com](mailto:contact@embeddedriskanalytics.com?subject=fathom-read%20key) and set `FATHOM_API_KEY`. `--ops` shows exactly what would be sent: the ops the adapter produced, and nothing else.
73
+
74
+ ## What it reads
75
+
76
+ | Format | What you export | How |
77
+ |---|---|---|
78
+ | `langgraph` | The checkpoint lineage | `[{"values": s.values, "step": s.metadata["step"]} for s in graph.get_state_history(config)]` |
79
+ | `openinference` | The spans Arize Phoenix stores | Export the trace's spans as JSON; only TOOL spans matter |
80
+ | `crewai` | The crew's event log | A listener on the event bus, capturing `tool_usage_finished`, `tool_usage_error`, `task_completed` |
81
+ | `letta` | Blocks, passages, and the memory-edit tool calls | `agents.blocks.list`, `agents.passages.list`, the tool calls from `agents.messages.list` |
82
+ | `dbos` | A workflow's step stream | `{"workflow_id": ..., "steps": [{"step_name", "args", "result", "ok"}]}` |
83
+ | `edits` | A coding agent's edit log | `{"initial_files": {...}, "edits": [{"tool": "str_replace_editor", "args": {...}, "ok": true}]}` |
84
+ | `events` | The native op stream | One op per line: `{"op": "set", "kind": "file", "key": "a.py", "value": "...", "ok": true}` |
85
+
86
+ Your tools have their own names. Map them once with `--map tools.json`:
87
+
88
+ ```json
89
+ {"save_decision": {"op": "set", "kind": "decision", "key": "topic", "value": "text"},
90
+ "book_seat": {"op": "add", "kind": "flight", "key": "seats", "value": "seat"},
91
+ "confirm_booking": {"op": "commit", "kind": "flight", "key": "booking"}}
92
+ ```
93
+
94
+ ## What it finds
95
+
96
+ | Finding | The agent... |
97
+ |---|---|
98
+ | `stale_reference` | acts on a fact it already removed or renamed away |
99
+ | `superseded_value` | writes or answers with a value it already replaced |
100
+ | `authored_contradiction` | reintroduces a token into a record it had already migrated |
101
+ | `residual` | ends the run with a record still carrying a value it replaced elsewhere |
102
+ | `duplicate_commit` | adds an entity a collection already holds |
103
+ | `post_commit_mutation` | changes a thing after committing it |
104
+
105
+ Every finding cites the earlier step it contradicts, so the readout is a diff between what the agent decided and what it did.
106
+
107
+ ## How it reads
108
+
109
+ The read folds the agent's successful actions into a ledger of committed facts and checks every later action against the ledger. Two rules make this a reconstruction rather than a transcript. A failed action is a no-op: an edit the tool rejected leaves nothing behind. And the read consults only the agent's own actions and their results, never an answer key, so it attaches the same way on any framework. The adapters and the CLI in this repository build the action stream; the read itself runs in ERA's service.
110
+
111
+ ## Use it from Python
112
+
113
+ ```python
114
+ from fathom_read import Op, read
115
+
116
+ ops = [
117
+ Op("set", "fact", "user.city", value="Denver"),
118
+ Op("set", "fact", "user.city", value="Austin"),
119
+ Op("answer", "fact", "user.city", value="The user lives in Denver."),
120
+ ]
121
+ verdict = read(ops) # uses FATHOM_API_KEY, or the demo key
122
+ for f in verdict.findings:
123
+ print(f.kind, f.step, f.detail)
124
+ # superseded_value 2 step 2 answers 'Denver' for fact 'user.city', a value the agent replaced with 'Austin' at step 1.
125
+ ```
126
+
127
+ ## What it does not do
128
+
129
+ It does not run your agent, call a model, or need one. It does not say why the agent contradicted itself or which repair would fix it; that is the [design-partner engagement](https://embeddedriskanalytics.com/contact.html). It reads agents whose committed state lives in tool calls, checkpoints, memory writes, or edits; an agent that keeps state only in free-text logs is out of scope.
130
+
131
+ ## Research
132
+
133
+ The read comes out of the Fathom program at [Embedded Risk Analytics](https://embeddedriskanalytics.com). Case studies on LangGraph, CrewAI, Letta, OpenHands, Agent-E, ContextPilot, and τ-bench are at [embeddedriskanalytics.com/research](https://embeddedriskanalytics.com/research.html). The theory is in [Records, Reflexive Modeling, and the Conditions for Stable Physical Histories](https://ssrn.com/abstract=6683578) (SSRN, 2026). See [CITATION.cff](CITATION.cff).
134
+
135
+ ## Send us a trace
136
+
137
+ If you run long-horizon agents and want a readout on your own traces, send a batch: [embeddedriskanalytics.com/contact](https://embeddedriskanalytics.com/contact.html).
138
+
139
+ ## License
140
+
141
+ MIT. Fathom is a trademark of Embedded Risk Analytics.
@@ -0,0 +1,98 @@
1
+ # fathom-read
2
+
3
+ **Catch the step where an AI agent contradicts a decision it already made.**
4
+
5
+ On a long task, an agent loses track of what it already decided and starts acting against it. It renames `guest_id` to `customer_id` at step 1, then writes new code against `guest_id` at step 6. The change compiles, imports, and passes the tests. It fails at runtime.
6
+
7
+ `fathom-read` turns the traces your framework already records into an action stream and sends it to the Fathom read, which reconstructs the state the agent committed and flags the step that contradicts it. Deterministic. No model access. Nothing runs in your production path.
8
+
9
+ ![fathom demo](docs/demo.gif)
10
+
11
+ ## Install
12
+
13
+ ```
14
+ pip install fathom-read
15
+ ```
16
+
17
+ ## Run
18
+
19
+ ```
20
+ fathom demo # the bundled rename example, coherent and not
21
+ fathom read trace.json --supersede guest_id=customer_id # your own trace
22
+ fathom read history.json --format langgraph # or name the format
23
+ fathom read trace.json --ops # see the action stream before anything is sent
24
+ fathom formats # the formats it reads
25
+ ```
26
+
27
+ `fathom read` exits 0 when the committed state is coherent and 2 when it finds a contradiction, so it drops into a test suite or a CI step as it is. Add `--json` for a machine-readable verdict.
28
+
29
+ The package ships with a demo key that is rate-limited per day. For your own key, which lifts the limit and keeps your traces on a private tier, write to [contact@embeddedriskanalytics.com](mailto:contact@embeddedriskanalytics.com?subject=fathom-read%20key) and set `FATHOM_API_KEY`. `--ops` shows exactly what would be sent: the ops the adapter produced, and nothing else.
30
+
31
+ ## What it reads
32
+
33
+ | Format | What you export | How |
34
+ |---|---|---|
35
+ | `langgraph` | The checkpoint lineage | `[{"values": s.values, "step": s.metadata["step"]} for s in graph.get_state_history(config)]` |
36
+ | `openinference` | The spans Arize Phoenix stores | Export the trace's spans as JSON; only TOOL spans matter |
37
+ | `crewai` | The crew's event log | A listener on the event bus, capturing `tool_usage_finished`, `tool_usage_error`, `task_completed` |
38
+ | `letta` | Blocks, passages, and the memory-edit tool calls | `agents.blocks.list`, `agents.passages.list`, the tool calls from `agents.messages.list` |
39
+ | `dbos` | A workflow's step stream | `{"workflow_id": ..., "steps": [{"step_name", "args", "result", "ok"}]}` |
40
+ | `edits` | A coding agent's edit log | `{"initial_files": {...}, "edits": [{"tool": "str_replace_editor", "args": {...}, "ok": true}]}` |
41
+ | `events` | The native op stream | One op per line: `{"op": "set", "kind": "file", "key": "a.py", "value": "...", "ok": true}` |
42
+
43
+ Your tools have their own names. Map them once with `--map tools.json`:
44
+
45
+ ```json
46
+ {"save_decision": {"op": "set", "kind": "decision", "key": "topic", "value": "text"},
47
+ "book_seat": {"op": "add", "kind": "flight", "key": "seats", "value": "seat"},
48
+ "confirm_booking": {"op": "commit", "kind": "flight", "key": "booking"}}
49
+ ```
50
+
51
+ ## What it finds
52
+
53
+ | Finding | The agent... |
54
+ |---|---|
55
+ | `stale_reference` | acts on a fact it already removed or renamed away |
56
+ | `superseded_value` | writes or answers with a value it already replaced |
57
+ | `authored_contradiction` | reintroduces a token into a record it had already migrated |
58
+ | `residual` | ends the run with a record still carrying a value it replaced elsewhere |
59
+ | `duplicate_commit` | adds an entity a collection already holds |
60
+ | `post_commit_mutation` | changes a thing after committing it |
61
+
62
+ Every finding cites the earlier step it contradicts, so the readout is a diff between what the agent decided and what it did.
63
+
64
+ ## How it reads
65
+
66
+ The read folds the agent's successful actions into a ledger of committed facts and checks every later action against the ledger. Two rules make this a reconstruction rather than a transcript. A failed action is a no-op: an edit the tool rejected leaves nothing behind. And the read consults only the agent's own actions and their results, never an answer key, so it attaches the same way on any framework. The adapters and the CLI in this repository build the action stream; the read itself runs in ERA's service.
67
+
68
+ ## Use it from Python
69
+
70
+ ```python
71
+ from fathom_read import Op, read
72
+
73
+ ops = [
74
+ Op("set", "fact", "user.city", value="Denver"),
75
+ Op("set", "fact", "user.city", value="Austin"),
76
+ Op("answer", "fact", "user.city", value="The user lives in Denver."),
77
+ ]
78
+ verdict = read(ops) # uses FATHOM_API_KEY, or the demo key
79
+ for f in verdict.findings:
80
+ print(f.kind, f.step, f.detail)
81
+ # superseded_value 2 step 2 answers 'Denver' for fact 'user.city', a value the agent replaced with 'Austin' at step 1.
82
+ ```
83
+
84
+ ## What it does not do
85
+
86
+ It does not run your agent, call a model, or need one. It does not say why the agent contradicted itself or which repair would fix it; that is the [design-partner engagement](https://embeddedriskanalytics.com/contact.html). It reads agents whose committed state lives in tool calls, checkpoints, memory writes, or edits; an agent that keeps state only in free-text logs is out of scope.
87
+
88
+ ## Research
89
+
90
+ The read comes out of the Fathom program at [Embedded Risk Analytics](https://embeddedriskanalytics.com). Case studies on LangGraph, CrewAI, Letta, OpenHands, Agent-E, ContextPilot, and τ-bench are at [embeddedriskanalytics.com/research](https://embeddedriskanalytics.com/research.html). The theory is in [Records, Reflexive Modeling, and the Conditions for Stable Physical Histories](https://ssrn.com/abstract=6683578) (SSRN, 2026). See [CITATION.cff](CITATION.cff).
91
+
92
+ ## Send us a trace
93
+
94
+ If you run long-horizon agents and want a readout on your own traces, send a batch: [embeddedriskanalytics.com/contact](https://embeddedriskanalytics.com/contact.html).
95
+
96
+ ## License
97
+
98
+ MIT. Fathom is a trademark of Embedded Risk Analytics.
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "fathom-read"
7
+ version = "0.1.0"
8
+ description = "Catch the step where an AI agent contradicts a decision it already made. Adapters and a CLI for the Fathom committed-state read over LangGraph, CrewAI, Letta, OpenInference, DBOS, and coding-agent traces."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {file = "LICENSE"}
12
+ authors = [{name = "Embedded Risk Analytics", email = "contact@embeddedriskanalytics.com"}]
13
+ keywords = ["ai-agents", "llm", "langgraph", "crewai", "letta", "openinference", "agent-memory", "context-engineering", "agent-evaluation", "coherence"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Software Development :: Testing",
19
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.optional-dependencies]
24
+ dev = ["pytest>=7"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://embeddedriskanalytics.com"
28
+ Repository = "https://github.com/ERA-Fathom/fathom"
29
+ Research = "https://embeddedriskanalytics.com/research.html"
30
+
31
+ [project.scripts]
32
+ fathom = "fathom_read.cli:main"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ [tool.setuptools.package-data]
38
+ fathom_read = ["examples/*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ """fathom-read: catch the step where an AI agent contradicts a decision it already made."""
2
+ from .ops import Op, Finding, Verdict # noqa: F401
3
+ from .client import read, ReadError # noqa: F401
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["Op", "Finding", "Verdict", "read", "ReadError", "read_file", "load_ops"]
7
+
8
+
9
+ def load_ops(path: str, fmt: str = "auto", mapping_path: str = None):
10
+ """Turn a trace file in any supported format into the op stream."""
11
+ from .cli import load_ops as _load
12
+ return _load(path, fmt, mapping_path)
13
+
14
+
15
+ def read_file(path: str, fmt: str = "auto", supersede=None, mapping_path: str = None, **kw) -> Verdict:
16
+ """Read a trace file and return the hosted read's verdict."""
17
+ return read(load_ops(path, fmt, mapping_path), supersede=supersede, **kw)
@@ -0,0 +1,47 @@
1
+ """Adapters turn what a framework already records into the op stream the read consumes."""
2
+ from . import events, edits, openinference, langgraph, crewai, letta, dbos # noqa: F401
3
+
4
+ FORMATS = {
5
+ "events": events,
6
+ "edits": edits,
7
+ "openinference": openinference,
8
+ "langgraph": langgraph,
9
+ "crewai": crewai,
10
+ "letta": letta,
11
+ "dbos": dbos,
12
+ }
13
+
14
+
15
+ def detect(doc) -> str:
16
+ """Guess the format of a loaded JSON document."""
17
+ if isinstance(doc, dict):
18
+ if "ops" in doc:
19
+ return "events"
20
+ if "spans" in doc or "resourceSpans" in doc:
21
+ return "openinference"
22
+ if "edits" in doc or "initial_files" in doc:
23
+ return "edits"
24
+ if "blocks" in doc or "passages" in doc:
25
+ return "letta"
26
+ if "steps" in doc and "workflow_id" in doc:
27
+ return "dbos"
28
+ if "events" in doc:
29
+ return "crewai"
30
+ if "snapshots" in doc or "history" in doc:
31
+ return "langgraph"
32
+ if isinstance(doc, list) and doc:
33
+ first = doc[0]
34
+ if isinstance(first, dict):
35
+ if "op" in first and "key" in first:
36
+ return "events"
37
+ if "attributes" in first or "span_kind" in first:
38
+ return "openinference"
39
+ if "values" in first and ("next" in first or "config" in first or "metadata" in first or "step" in first):
40
+ return "langgraph"
41
+ if "type" in first and ("tool_name" in first or "task_name" in first or first.get("type", "").startswith(("tool_", "task_"))):
42
+ return "crewai"
43
+ if "step_name" in first:
44
+ return "dbos"
45
+ if "tool" in first and "args" in first:
46
+ return "edits"
47
+ raise ValueError("could not detect the trace format; pass --format")
@@ -0,0 +1,88 @@
1
+ """Shared mapping from tool calls to ops. Adapters that see (tool name, args, result, ok) use this."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any, Dict, Optional
6
+
7
+ from ..ops import Op
8
+
9
+ # Default mapping: tool name -> how to build an op from its arguments.
10
+ # "key" and "value" name the argument fields; "kind" labels the fact; "op" is the ledger op.
11
+ DEFAULT_MAP: Dict[str, Dict[str, Any]] = {
12
+ # coding agents (OpenHands, CodeAct-style editors); content folding lives in the edits adapter
13
+ "str_replace_editor": {"op": "edit"},
14
+ "edit_file": {"op": "edit"},
15
+ "write_file": {"op": "set", "kind": "file", "key": "path", "value": "content"},
16
+ "create_file": {"op": "set", "kind": "file", "key": "path", "value": "content"},
17
+ "delete_file": {"op": "remove", "kind": "file", "key": "path"},
18
+ # record-keeping crews and workflows
19
+ "write_record": {"op": "set", "kind": "record", "key": "record", "value": "content"},
20
+ "update_record": {"op": "set", "kind": "record", "key": "record", "value": "content"},
21
+ "commit_record": {"op": "set", "kind": "record", "key": "record", "value": "content"},
22
+ "set_value": {"op": "set", "kind": "fact", "key": "key", "value": "value"},
23
+ "update_value": {"op": "set", "kind": "fact", "key": "key", "value": "value"},
24
+ "delete_value": {"op": "remove", "kind": "fact", "key": "key"},
25
+ "rename_key": {"op": "rename", "kind": "fact", "key": "old", "to": "new"},
26
+ # Letta memory tools
27
+ "core_memory_append": {"op": "append", "kind": "block", "key": "label", "value": "content"},
28
+ "core_memory_replace": {"op": "set", "kind": "block", "key": "label", "value": "new_content"},
29
+ "memory_replace": {"op": "set", "kind": "block", "key": "label", "value": "new_str"},
30
+ "memory_insert": {"op": "append", "kind": "block", "key": "label", "value": "new_str"},
31
+ "memory_rethink": {"op": "set", "kind": "block", "key": "label", "value": "new_memory"},
32
+ "archival_memory_insert": {"op": "add", "kind": "passage", "key": "archival", "value": "content"},
33
+ # ordering and transactions (web agents, customer-service agents)
34
+ "add_item": {"op": "add", "kind": "order", "key": "cart", "value": "item"},
35
+ "add_to_cart": {"op": "add", "kind": "order", "key": "cart", "value": "item"},
36
+ "remove_item": {"op": "remove_member", "kind": "order", "key": "cart", "value": "item"},
37
+ "place_order": {"op": "commit", "kind": "order", "key": "order"},
38
+ "checkout": {"op": "commit", "kind": "order", "key": "order"},
39
+ "submit": {"op": "commit", "kind": "order", "key": "order"},
40
+ "book_reservation": {"op": "set", "kind": "reservation", "key": "reservation_id", "value": "details"},
41
+ "cancel_reservation": {"op": "remove", "kind": "reservation", "key": "reservation_id"},
42
+ }
43
+
44
+
45
+ def _first(args: Dict[str, Any], *names: str) -> Optional[str]:
46
+ for n in names:
47
+ if n in args and args[n] is not None:
48
+ v = args[n]
49
+ return v if isinstance(v, str) else json.dumps(v, sort_keys=True)
50
+ return None
51
+
52
+
53
+ def load_map(path: Optional[str]) -> Dict[str, Dict[str, Any]]:
54
+ m = dict(DEFAULT_MAP)
55
+ if path:
56
+ with open(path) as f:
57
+ m.update(json.load(f))
58
+ return m
59
+
60
+
61
+ def op_from_tool(name: str, args: Any, ok: bool, step: int, mapping: Dict[str, Dict[str, Any]],
62
+ source: str = "") -> Optional[Op]:
63
+ """Build an op from one tool call, or None when the tool does not write committed state."""
64
+ name = (name or "").strip().lower().replace(" ", "_")
65
+ spec = mapping.get(name)
66
+ if spec is None:
67
+ return None
68
+ if not isinstance(args, dict):
69
+ try:
70
+ args = json.loads(args) if args else {}
71
+ except (TypeError, ValueError):
72
+ args = {}
73
+ kind = spec.get("kind", "fact")
74
+ op = spec["op"]
75
+ key = _first(args, spec.get("key", "key"), "key", "record", "path", "label", "name", "id") or spec.get("key", name)
76
+ value = _first(args, spec.get("value", "value"), "value", "content", "new_str", "new_content", "text")
77
+ if op == "edit":
78
+ return None # the edits adapter folds these with file contents
79
+ if op == "append":
80
+ return Op("set", kind, key, value=value, ok=ok, step=step, source=source or name)
81
+ if op == "remove_member":
82
+ return Op("remove", kind, f"{key}:{value}", ok=ok, step=step, source=source or name)
83
+ if op == "rename":
84
+ to = _first(args, spec.get("to", "new"), "new", "to")
85
+ return Op("rename", kind, key, to=to, ok=ok, step=step, source=source or name)
86
+ if op == "commit":
87
+ return Op("commit", kind, key, ok=ok, step=step, source=source or name)
88
+ return Op(op, kind, key, value=value, ok=ok, step=step, source=source or name)
@@ -0,0 +1,34 @@
1
+ """
2
+ CrewAI event logs: the events the crew's event bus already emits.
3
+
4
+ Input: a list of events (or {"events": [...]}) with "type" in tool_usage_finished,
5
+ tool_usage_error, task_completed; tool events carry tool_name and tool_args. A write tool's
6
+ arguments name the record and the content committed to it; a task_completed event commits
7
+ the task's output under the task name. Capture them with a listener on crewai's event bus.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, List
12
+
13
+ from ..ops import Op
14
+ from ._tools import op_from_tool, load_map
15
+
16
+
17
+ def load(doc: Any, mapping_path: str = None, **_) -> List[Op]:
18
+ events = doc.get("events", []) if isinstance(doc, dict) else doc
19
+ mapping = load_map(mapping_path)
20
+ ops: List[Op] = []
21
+ for i, ev in enumerate(events):
22
+ t = (ev.get("type") or ev.get("_type") or "").replace("Event", "").lower()
23
+ t = {"toolusagefinished": "tool_usage_finished", "toolusageerror": "tool_usage_error",
24
+ "taskcompleted": "task_completed"}.get(t, t)
25
+ if t in ("tool_usage_finished", "tool_usage_error"):
26
+ ok = t == "tool_usage_finished" and not bool(ev.get("failure"))
27
+ op = op_from_tool(ev.get("tool_name", ""), ev.get("tool_args"), ok, i, mapping, source=t)
28
+ if op is not None:
29
+ ops.append(op)
30
+ elif t == "task_completed":
31
+ name = ev.get("task_name") or ev.get("task") or f"task_{i}"
32
+ out = ev.get("output") or ev.get("raw") or ""
33
+ ops.append(Op("set", "task", str(name), value=str(out), ok=True, step=i, source="task_completed"))
34
+ return ops
@@ -0,0 +1,25 @@
1
+ """
2
+ DBOS step streams: the inputs and outputs of each step in a workflow, in order.
3
+
4
+ Input: {"workflow_id": "...", "steps": [{"step_name", "args", "result", "ok"}]} or a bare list.
5
+ Step names map to ops through the tool mapping (pass --map for your own step names).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, List
10
+
11
+ from ..ops import Op
12
+ from ._tools import op_from_tool, load_map
13
+
14
+
15
+ def load(doc: Any, mapping_path: str = None, **_) -> List[Op]:
16
+ steps = doc.get("steps", []) if isinstance(doc, dict) else doc
17
+ mapping = load_map(mapping_path)
18
+ ops: List[Op] = []
19
+ for i, s in enumerate(steps):
20
+ ok = bool(s.get("ok", True)) and not bool(s.get("error"))
21
+ args = s.get("args") or s.get("inputs") or {}
22
+ op = op_from_tool(s.get("step_name") or s.get("name", ""), args, ok, i, mapping, source="step")
23
+ if op is not None:
24
+ ops.append(op)
25
+ return ops
@@ -0,0 +1,51 @@
1
+ """
2
+ Coding-agent edit logs: str_replace-style edits over a set of files.
3
+
4
+ Input: {"initial_files": {path: content}, "edits": [{"tool": "str_replace_editor",
5
+ "args": {"path", "old_str", "new_str"}, "ok": true}, ...], "done": true}
6
+ or a bare list of edit records. The adapter folds each successful edit onto the file it
7
+ targets and emits one "set" op per edit with the file's new content, so the read can see what
8
+ the agent actually left in each file. A rejected edit changes nothing.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Dict, List
13
+
14
+ from ..ops import Op
15
+
16
+ EDIT_TOOLS = {"str_replace_editor", "edit_file", "str_replace", "apply_patch"}
17
+
18
+
19
+ def load(doc: Any, **_) -> List[Op]:
20
+ if isinstance(doc, list):
21
+ doc = {"edits": doc}
22
+ files: Dict[str, str] = dict(doc.get("initial_files", {}))
23
+ ops: List[Op] = []
24
+ step = 0
25
+ for path, content in files.items():
26
+ ops.append(Op("set", "file", path, value=content, ok=True, step=step, source="initial"))
27
+ step += 1
28
+ for e in doc.get("edits", []):
29
+ tool = (e.get("tool") or e.get("name") or "").strip().lower()
30
+ args = e.get("args") or e.get("parameters") or {}
31
+ ok = bool(e.get("ok", True)) and not bool(e.get("error"))
32
+ if tool in EDIT_TOOLS:
33
+ path, old, new = args.get("path"), args.get("old_str"), args.get("new_str")
34
+ applied = False
35
+ if ok and path in files and old and old in files[path]:
36
+ files[path] = files[path].replace(old, new or "")
37
+ applied = True
38
+ ops.append(Op("set", "file", str(path), value=files.get(str(path)), ok=applied, step=step, source=tool))
39
+ elif tool in ("write_file", "create_file"):
40
+ if ok:
41
+ files[str(args.get("path"))] = str(args.get("content", ""))
42
+ ops.append(Op("set", "file", str(args.get("path")), value=str(args.get("content", "")), ok=ok, step=step, source=tool))
43
+ elif tool in ("delete_file",):
44
+ if ok:
45
+ files.pop(str(args.get("path")), None)
46
+ ops.append(Op("remove", "file", str(args.get("path")), ok=ok, step=step, source=tool))
47
+ else:
48
+ step += 1
49
+ continue
50
+ step += 1
51
+ return ops
@@ -0,0 +1,17 @@
1
+ """Native format: a JSON list (or {"ops": [...]}) of op dicts, or JSON Lines with one op per line."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any, List
6
+
7
+ from ..ops import Op
8
+
9
+
10
+ def load(doc: Any, **_) -> List[Op]:
11
+ if isinstance(doc, dict):
12
+ doc = doc.get("ops", [])
13
+ return [Op.from_dict(d) for d in doc]
14
+
15
+
16
+ def load_jsonl(text: str) -> List[Op]:
17
+ return [Op.from_dict(json.loads(line)) for line in text.splitlines() if line.strip()]
@@ -0,0 +1,56 @@
1
+ """
2
+ LangGraph state history: the checkpoint lineage from graph.get_state_history(config).
3
+
4
+ Input: a list of snapshots, each with "values" (the channel values at that step), newest
5
+ first as LangGraph returns them, or {"snapshots": [...]}. The adapter diffs consecutive
6
+ snapshots and emits one op per changed channel: a scalar or string channel that changed is a
7
+ "set"; a list channel that grew is an "add" per new element. Every checkpoint is a
8
+ successful commit, so ok is always true here; the read then checks what those commits say
9
+ against one another.
10
+
11
+ To export from a running graph:
12
+ import json
13
+ history = [{"values": s.values, "step": s.metadata.get("step")} for s in graph.get_state_history(config)]
14
+ json.dump(history, open("history.json", "w"), default=str)
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from typing import Any, List
20
+
21
+ from ..ops import Op
22
+
23
+
24
+ def _as_text(v: Any) -> str:
25
+ return v if isinstance(v, str) else json.dumps(v, sort_keys=True, default=str)
26
+
27
+
28
+ def load(doc: Any, **_) -> List[Op]:
29
+ snaps = doc.get("snapshots", doc.get("history", [])) if isinstance(doc, dict) else doc
30
+ snaps = list(snaps)
31
+ if len(snaps) > 1:
32
+ # get_state_history returns newest first; put them in run order.
33
+ s0 = snaps[0].get("step", snaps[0].get("metadata", {}).get("step"))
34
+ s1 = snaps[-1].get("step", snaps[-1].get("metadata", {}).get("step"))
35
+ if s0 is None or s1 is None or s0 > s1:
36
+ snaps = snaps[::-1]
37
+ ops: List[Op] = []
38
+ prev: dict = {}
39
+ step = 0
40
+ for snap in snaps:
41
+ values = snap.get("values", {}) or {}
42
+ for chan, val in values.items():
43
+ old = prev.get(chan)
44
+ if isinstance(val, list) and isinstance(old, list) and len(val) >= len(old) and val[:len(old)] == old:
45
+ for item in val[len(old):]:
46
+ ops.append(Op("add", "channel", chan, value=_as_text(item), step=step, source="checkpoint"))
47
+ step += 1
48
+ elif isinstance(val, list) and old is None:
49
+ for item in val:
50
+ ops.append(Op("add", "channel", chan, value=_as_text(item), step=step, source="checkpoint"))
51
+ step += 1
52
+ elif val != old:
53
+ ops.append(Op("set", "channel", chan, value=_as_text(val), step=step, source="checkpoint"))
54
+ step += 1
55
+ prev = values
56
+ return ops