actionlens 1.4.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.
Files changed (70) hide show
  1. actionlens-1.4.1/LICENSE +21 -0
  2. actionlens-1.4.1/MANIFEST.in +5 -0
  3. actionlens-1.4.1/PKG-INFO +27 -0
  4. actionlens-1.4.1/README.md +557 -0
  5. actionlens-1.4.1/README.zh-CN.md +555 -0
  6. actionlens-1.4.1/examples/dbos_integration_example.py +57 -0
  7. actionlens-1.4.1/examples/provider_reconciliation_cookbook.md +70 -0
  8. actionlens-1.4.1/examples/temporal_integration_example.py +84 -0
  9. actionlens-1.4.1/pyproject.toml +33 -0
  10. actionlens-1.4.1/setup.cfg +4 -0
  11. actionlens-1.4.1/src/actionlens/__init__.py +126 -0
  12. actionlens-1.4.1/src/actionlens/artifacts/__init__.py +16 -0
  13. actionlens-1.4.1/src/actionlens/artifacts/base.py +91 -0
  14. actionlens-1.4.1/src/actionlens/artifacts/fs.py +1251 -0
  15. actionlens-1.4.1/src/actionlens/cli.py +290 -0
  16. actionlens-1.4.1/src/actionlens/context.py +47 -0
  17. actionlens-1.4.1/src/actionlens/contracts.py +111 -0
  18. actionlens-1.4.1/src/actionlens/errors.py +69 -0
  19. actionlens-1.4.1/src/actionlens/evals.py +129 -0
  20. actionlens-1.4.1/src/actionlens/exporters/__init__.py +27 -0
  21. actionlens-1.4.1/src/actionlens/exporters/core.py +646 -0
  22. actionlens-1.4.1/src/actionlens/exporters/html.py +65 -0
  23. actionlens-1.4.1/src/actionlens/integrations/__init__.py +20 -0
  24. actionlens-1.4.1/src/actionlens/integrations/common.py +224 -0
  25. actionlens-1.4.1/src/actionlens/integrations/dbos.py +110 -0
  26. actionlens-1.4.1/src/actionlens/integrations/langchain.py +45 -0
  27. actionlens-1.4.1/src/actionlens/integrations/openai_agents.py +24 -0
  28. actionlens-1.4.1/src/actionlens/integrations/pydantic_ai.py +20 -0
  29. actionlens-1.4.1/src/actionlens/integrations/temporal.py +138 -0
  30. actionlens-1.4.1/src/actionlens/ledger/__init__.py +11 -0
  31. actionlens-1.4.1/src/actionlens/ledger/memory.py +144 -0
  32. actionlens-1.4.1/src/actionlens/ledger/sqlite.py +290 -0
  33. actionlens-1.4.1/src/actionlens/ledger/tickets.py +267 -0
  34. actionlens-1.4.1/src/actionlens/models.py +296 -0
  35. actionlens-1.4.1/src/actionlens/outbox.py +154 -0
  36. actionlens-1.4.1/src/actionlens/policy.py +94 -0
  37. actionlens-1.4.1/src/actionlens/reconciliation.py +91 -0
  38. actionlens-1.4.1/src/actionlens/redaction.py +75 -0
  39. actionlens-1.4.1/src/actionlens/remote.py +44 -0
  40. actionlens-1.4.1/src/actionlens/repositories/__init__.py +10 -0
  41. actionlens-1.4.1/src/actionlens/repositories/memory.py +18 -0
  42. actionlens-1.4.1/src/actionlens/repositories/postgres.py +586 -0
  43. actionlens-1.4.1/src/actionlens/repositories/sqlite.py +616 -0
  44. actionlens-1.4.1/src/actionlens/repository.py +104 -0
  45. actionlens-1.4.1/src/actionlens/runtime.py +1550 -0
  46. actionlens-1.4.1/src/actionlens/schema.py +44 -0
  47. actionlens-1.4.1/src/actionlens/sinks/__init__.py +12 -0
  48. actionlens-1.4.1/src/actionlens/sinks/composite.py +42 -0
  49. actionlens-1.4.1/src/actionlens/sinks/jsonl.py +139 -0
  50. actionlens-1.4.1/src/actionlens/sinks/memory.py +17 -0
  51. actionlens-1.4.1/src/actionlens/sinks/metrics.py +30 -0
  52. actionlens-1.4.1/src/actionlens/sinks/otel.py +111 -0
  53. actionlens-1.4.1/src/actionlens/sinks/webhook.py +165 -0
  54. actionlens-1.4.1/src/actionlens/trajectory.py +49 -0
  55. actionlens-1.4.1/src/actionlens.egg-info/PKG-INFO +27 -0
  56. actionlens-1.4.1/src/actionlens.egg-info/SOURCES.txt +68 -0
  57. actionlens-1.4.1/src/actionlens.egg-info/dependency_links.txt +1 -0
  58. actionlens-1.4.1/src/actionlens.egg-info/entry_points.txt +2 -0
  59. actionlens-1.4.1/src/actionlens.egg-info/requires.txt +28 -0
  60. actionlens-1.4.1/src/actionlens.egg-info/top_level.txt +1 -0
  61. actionlens-1.4.1/tests/test_actionlens_core.py +404 -0
  62. actionlens-1.4.1/tests/test_actionlens_v03.py +549 -0
  63. actionlens-1.4.1/tests/test_actionlens_v05.py +496 -0
  64. actionlens-1.4.1/tests/test_actionlens_v10.py +242 -0
  65. actionlens-1.4.1/tests/test_actionlens_v11.py +279 -0
  66. actionlens-1.4.1/tests/test_benchmark_tools.py +34 -0
  67. actionlens-1.4.1/tests/test_eval_evidence_otel.py +396 -0
  68. actionlens-1.4.1/tests/test_postgres_integration.py +365 -0
  69. actionlens-1.4.1/tests/test_streaming_artifacts.py +241 -0
  70. actionlens-1.4.1/tests/test_v14_durable_media.py +538 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JieNEUer
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,5 @@
1
+ include README.md
2
+ include README.zh-CN.md
3
+ include LICENSE
4
+ prune docs
5
+ recursive-include examples *.md *.py
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: actionlens
3
+ Version: 1.4.1
4
+ Summary: Low-intrusion tool governance and trajectory capture for Python agent systems.
5
+ Requires-Python: >=3.10
6
+ License-File: LICENSE
7
+ Requires-Dist: pydantic>=2
8
+ Provides-Extra: pydantic-ai
9
+ Requires-Dist: pydantic-ai<3,>=1; extra == "pydantic-ai"
10
+ Provides-Extra: openai-agents
11
+ Requires-Dist: openai-agents<1,>=0.8; extra == "openai-agents"
12
+ Provides-Extra: langchain
13
+ Requires-Dist: langchain-core<2,>=0.3; extra == "langchain"
14
+ Provides-Extra: langgraph
15
+ Requires-Dist: langgraph<2,>=0.2; extra == "langgraph"
16
+ Provides-Extra: postgres
17
+ Requires-Dist: psycopg[binary,pool]<4,>=3.1; extra == "postgres"
18
+ Provides-Extra: otel
19
+ Requires-Dist: opentelemetry-api<2,>=1.20; extra == "otel"
20
+ Requires-Dist: opentelemetry-sdk<2,>=1.20; extra == "otel"
21
+ Provides-Extra: webhook
22
+ Requires-Dist: httpx<1,>=0.27; extra == "webhook"
23
+ Provides-Extra: test
24
+ Requires-Dist: build>=1; extra == "test"
25
+ Requires-Dist: pytest>=8; extra == "test"
26
+ Requires-Dist: ruff>=0.8; extra == "test"
27
+ Dynamic: license-file
@@ -0,0 +1,557 @@
1
+ # ActionLens
2
+
3
+ [English](README.md) | [简体中文](README.zh-CN.md)
4
+
5
+ ActionLens is a low-intrusion Python library for agent tool governance and trajectory capture.
6
+
7
+ It sits at the tool boundary instead of replacing your agent framework. Wrap an existing Python function, get structured tool outputs, bounded model-visible results, local artifacts for large payloads, idempotency protection, approval handoff, and JSONL trajectories that can later feed eval or monitoring workflows.
8
+
9
+ ## Where ActionLens Fits
10
+
11
+ ```mermaid
12
+ %%{init: {"theme":"base","themeVariables":{"fontFamily":"ui-sans-serif, system-ui, sans-serif","primaryColor":"#F7FBF7","primaryTextColor":"#1F2933","primaryBorderColor":"#3F474A","lineColor":"#4A5559","tertiaryColor":"#FFFFFF"}}}%%
13
+ flowchart LR
14
+ host["Agent / host runtime<br/>LangChain, LangGraph, PydanticAI, OpenAI Agents SDK"]:::host
15
+ durable["Optional durable control<br/>Temporal Activities / DBOS Steps"]:::durable
16
+ provider["Business tools / providers<br/>SaaS APIs, databases, local systems"]:::provider
17
+ consumer["Observability / eval / audit<br/>metrics, traces, evidence consumers"]:::consumer
18
+
19
+ subgraph actionlens["ActionLens: governance + evidence plane"]
20
+ direction TB
21
+ runtime["Governed tool runtime"]:::core
22
+ policy["Policy + approvals<br/>budgets + redaction"]:::governance
23
+ ledger["Ledger + idempotency<br/>outbox + recovery"]:::evidence
24
+ artifacts["Artifacts + provenance<br/>retention + access checks"]:::evidence
25
+ trajectory["Trajectory + exporters<br/>events + evidence bundles"]:::evidence
26
+ runtime --> policy
27
+ runtime --> ledger
28
+ ledger --> artifacts
29
+ ledger --> trajectory
30
+ end
31
+
32
+ host -->|"tools + explicit context"| runtime
33
+ durable -->|"stable workflow / step identity"| runtime
34
+ policy -->|"authorized invocation"| provider
35
+ provider -->|"result + provider evidence"| runtime
36
+ ledger --> consumer
37
+ artifacts --> consumer
38
+ trajectory --> consumer
39
+
40
+ classDef host fill:#FFFFFF,stroke:#3F474A,stroke-width:1.25px,color:#1F2933;
41
+ classDef durable fill:#F4F8F4,stroke:#586661,stroke-width:1.25px,color:#1F2933;
42
+ classDef core fill:#EAF6E6,stroke:#6DAE4A,stroke-width:1.6px,color:#1F2933;
43
+ classDef governance fill:#F7FBF7,stroke:#6B7972,stroke-width:1.2px,color:#1F2933;
44
+ classDef evidence fill:#FFFFFF,stroke:#6B7972,stroke-width:1.2px,color:#1F2933;
45
+ classDef provider fill:#FFFFFF,stroke:#3F474A,stroke-width:1.25px,color:#1F2933;
46
+ classDef consumer fill:#F1F8EF,stroke:#6DAE4A,stroke-width:1.25px,color:#1F2933;
47
+ style actionlens fill:#FAFCFA,stroke:#3F474A,stroke-width:1.25px,stroke-dasharray:2 3
48
+ ```
49
+
50
+ ## Why
51
+
52
+ Modern agents often fail at the tool boundary:
53
+
54
+ - A crawler returns 20,000 words and blows up the model context.
55
+ - A model retries the same mutation with a slightly different timestamp.
56
+ - A Python exception is sent back to the model as raw traceback noise.
57
+ - A high-risk operation needs human approval, but the approval state is not replayable.
58
+ - Production runs cannot be converted into eval or debugging traces later.
59
+
60
+ ActionLens turns these into explicit runtime protocols while keeping the host framework in charge.
61
+
62
+ ## Status
63
+
64
+ This repository contains the v1.4.1 stable protocol focused on multi-instance-safe governance, bounded production data paths, evidence-backed recovery, durable audit delivery, explicit artifact confidentiality, and low-intrusion durable-runtime bridges:
65
+
66
+ - `@lens.tool(...)` decorator for sync and async functions
67
+ - `StructuredToolOutput` for model-visible results
68
+ - local artifact storage for large outputs
69
+ - JSONL trajectory events
70
+ - explicit `ToolCallContext` passing for resume/distributed workers
71
+ - public signature injection for required `idempotency_key`
72
+ - interchangeable governance repositories for PostgreSQL, SQLite, and ephemeral tests
73
+ - lease ownership, heartbeat, fencing tokens, args/schema conflict detection, and `UNCERTAIN`
74
+ - atomic approval ticket + ledger + transactional outbox transitions
75
+ - at-least-once outbox dispatch with retry, claim leases, and dead-letter handling
76
+ - persistent approval pending / approve / deny / resume flow
77
+ - pluggable policy chain and redactors
78
+ - artifact metadata plus dry-run / size-aware GC
79
+ - optional thread-mode timeout for sync tools
80
+ - ordered `invoke_many()` for concurrency-safe read tools
81
+ - `actionlens summary`, `actionlens export`, and `actionlens gc`
82
+ - thin PydanticAI, OpenAI Agents SDK, and LangChain/LangGraph adapters
83
+ - Inspect-oriented transcript and conservative SFT JSONL exporters
84
+ - `EvalCaseCandidate` with stable case IDs, host-supplied task/environment/rubric facts, explicit readiness, and versioned Inspect sample mapping
85
+ - evidence bundles with source digests, a per-event SHA-256 chain, redaction/retention metadata, and explicit missing-evidence declarations
86
+ - static local HTML trajectory reports
87
+ - optional bounded-queue JSONL writing with explicit drop policies
88
+ - expiring approval tickets, schema-checked modified arguments, and ticket/ledger inspection
89
+ - `ArtifactPolicy` with write-before-redaction guarantees, reference-only/deny modes, quotas, and encryption provider SPI
90
+ - optional `MediaMetadata` and compact `ArtifactProvenance` for metadata-first, derived media artifacts
91
+ - host-provided `MediaMetadataExtractor` SPI for local plaintext image/audio/video artifacts
92
+ - provenance-aware local GC that preserves a source while a retained derivative still references it, with explicit cascade mode
93
+ - signed webhook, composite, low-cardinality metrics, and a pinned OpenTelemetry GenAI mapping profile
94
+ - versioned schema readers/golden fixtures and reproducible SFT dataset manifests
95
+ - framework-neutral `RemoteToolRunner` SPI
96
+ - dependency-free Temporal Activity and DBOS Step context bridges that preserve stable workflow/step identity
97
+ - evidence-backed `UNCERTAIN` reconciliation with atomic audit events
98
+ - background outbox lifecycle, health state, and controlled dead-letter replay/termination
99
+ - authorized artifact read/decrypt/checksum verification and reference URI policy
100
+ - webhook key rotation, replay-window verification, event deduplication hook, and SSRF controls
101
+ - directly runnable repository and sink contract checks for third-party implementations
102
+ - pooled PostgreSQL connections with bounded acquire/statement/lock/transaction timeouts
103
+ - explicit advisory-lock migrations and startup schema compatibility checks
104
+ - event-loop isolation for synchronous governance I/O around async tools
105
+ - phase-aware governance failures before and after external side effects
106
+ - outbox backlog/lag health, bounded retention, and single-round-trip PostgreSQL claims
107
+ - cross-process artifact read/GC leases plus symlink/reparse-point rejection
108
+ - bounded streaming artifact upload, authenticated decryption, and atomic destination promotion
109
+ - reproducible benchmark and soak probes with percentile and memory evidence
110
+
111
+ PostgreSQL is the preferred multi-instance backend because ledger, approval, and outbox facts share one transaction. Redis is intentionally not implemented in v1.4; the repository protocol permits a future backend without changing `ToolRuntime`.
112
+
113
+ ## Install For Local Development
114
+
115
+ ```bash
116
+ python -m pip install -e .
117
+ python -m pytest -q
118
+ ```
119
+
120
+ The runtime dependency is intentionally light:
121
+
122
+ - Python 3.10+
123
+ - Pydantic v2
124
+
125
+ Install production PostgreSQL support separately:
126
+
127
+ ```bash
128
+ python -m pip install -e ".[postgres]"
129
+ ```
130
+
131
+ ## PostgreSQL Repository
132
+
133
+ Production startup does not run DDL. Apply migrations as a deployment step, preferably with the DSN in the environment rather than the process command line:
134
+
135
+ ```bash
136
+ ACTIONLENS_POSTGRES_DSN=postgresql://actionlens:secret@db.internal/actionlens actionlens migrate
137
+ ACTIONLENS_POSTGRES_DSN=postgresql://actionlens:secret@db.internal/actionlens actionlens schema-status
138
+ ```
139
+
140
+ ```python
141
+ import actionlens as al
142
+
143
+ repository = al.PostgresGovernanceRepository(
144
+ "postgresql://actionlens:secret@db.internal/actionlens",
145
+ min_pool_size=2,
146
+ max_pool_size=20,
147
+ pool_timeout=5,
148
+ statement_timeout_ms=30_000,
149
+ lock_timeout_ms=5_000,
150
+ transaction_timeout_ms=60_000,
151
+ )
152
+ lens = al.ActionLens(project="demo-agent", repository=repository)
153
+
154
+ # At the owning application lifecycle boundary:
155
+ lens.close()
156
+ repository.close()
157
+ ```
158
+
159
+ Migrations are idempotent, serialized by a PostgreSQL advisory transaction lock, and recorded in `actionlens_schema_migrations`. `auto_migrate=True` remains available for isolated development only. PostgreSQL uses row locks and `SKIP LOCKED` outbox claims. External side effects are not advertised as exactly-once: an expired non-fenceable execution becomes `UNCERTAIN` and must be reconciled explicitly.
160
+
161
+ SQLite keeps `synchronous="FULL"` as the durability default. Latency-sensitive local deployments that accept SQLite WAL's `NORMAL` power-loss tradeoff may opt in explicitly:
162
+
163
+ ```python
164
+ repository = al.SQLiteGovernanceRepository(".actionlens/ledger.sqlite3", synchronous="NORMAL")
165
+ ```
166
+
167
+ ## Artifact Confidentiality
168
+
169
+ ```python
170
+ policy = al.ArtifactPolicy(
171
+ raw_mode="redact_then_store", # store, redact_then_store, reference_only, deny
172
+ encryption="provider",
173
+ max_bytes_per_run=10_000_000,
174
+ retention_days=30,
175
+ )
176
+ lens = al.ActionLens(
177
+ artifact_policy=policy,
178
+ encryption_provider=my_kms_provider,
179
+ )
180
+ ```
181
+
182
+ An encryption provider supplies `provider_id` and `encrypt(payload, context=...)`. ActionLens never stores a master key. `reference_only` accepts an existing `ArtifactRef`; `deny` prevents artifact writes.
183
+
184
+ ## Media Metadata And Provenance
185
+
186
+ Media support is metadata-first and dependency-free. `ArtifactRef.media_metadata` and `ArtifactRef.provenance` are optional additive fields; ActionLens does not import FFmpeg, OCR, ASR, or vision-model SDKs.
187
+
188
+ ```python
189
+ source = lens.artifact_store.put(video_bytes, media_type="video/mp4")
190
+ thumbnail = lens.artifact_store.put(
191
+ thumbnail_bytes,
192
+ media_type="image/jpeg",
193
+ media_metadata=al.MediaMetadata(width=320, height=180, codec="jpeg"),
194
+ provenance=al.ArtifactProvenance.from_source(
195
+ source,
196
+ operation="thumbnail",
197
+ operation_version="ffmpeg-7.0",
198
+ parameters={"time_sec": 12.5, "max_width": 320},
199
+ created_by_tool="extract_thumbnail",
200
+ ),
201
+ )
202
+ ```
203
+
204
+ To populate metadata automatically for local plaintext image/audio/video writes, inject a small host adapter. The extractor runs only after ActionLens has atomically persisted the local plaintext file. It is best-effort so decoder failure cannot turn a completed artifact write into an orphan; failures are emitted through the `actionlens.artifacts.fs` logger so operators can detect a broken extractor. Encrypted artifacts are never handed to the extractor; provide trusted `media_metadata=` at write time when extraction happens before encryption.
205
+
206
+ When content-addressed local storage deduplicates identical derived bytes from multiple sources, its sidecar retains every observed local provenance record. Local GC treats all of those sources as parents, so deleting one source cannot leave a retained derivative without its evidence chain. The local sidecar is capped at 64 distinct source/transformation identities per content-addressed file and fails closed rather than silently dropping lineage; hosts needing a larger many-to-one index should provide their own artifact store.
207
+
208
+ ```python
209
+ class MyMediaExtractor:
210
+ def extract(self, path, media_type):
211
+ return al.MediaMetadata(width=1920, height=1080, codec="h264")
212
+
213
+ lens = al.ActionLens(media_metadata_extractor=MyMediaExtractor())
214
+ ```
215
+
216
+ ## Quick Start
217
+
218
+ ```python
219
+ import actionlens as al
220
+
221
+ lens = al.ActionLens(project="demo-agent", storage_dir=".actionlens")
222
+
223
+ @lens.tool(max_bytes=1000)
224
+ def fetch_page() -> str:
225
+ return "very long page..." * 1000
226
+
227
+ with lens.session(session_id="chat-001"):
228
+ output = fetch_page()
229
+
230
+ print(output.status)
231
+ print(output.result_summary)
232
+ print(output.artifact_refs)
233
+ ```
234
+
235
+ If the result exceeds `max_bytes`, ActionLens stores the raw result under `.actionlens/artifacts/` and returns a bounded preview plus an `ArtifactRef`.
236
+
237
+ ## Idempotency
238
+
239
+ For mutation tools, require an explicit idempotency key:
240
+
241
+ ```python
242
+ @lens.tool(
243
+ risk=al.RiskLevel.MUTATION,
244
+ idempotency=al.IdempotencyPolicy.REQUIRED,
245
+ )
246
+ def send_message(user_id: str, text: str) -> dict:
247
+ return {"sent": True, "user_id": user_id}
248
+
249
+ with lens.session(session_id="chat-001"):
250
+ first = send_message("u1", "hello", idempotency_key="msg-u1-001")
251
+ second = send_message("u1", "hello", idempotency_key="msg-u1-001")
252
+ ```
253
+
254
+ The wrapper modifies the public function signature so schema extractors can see the required `idempotency_key`:
255
+
256
+ ```python
257
+ import inspect
258
+ print(inspect.signature(send_message))
259
+ # (user_id: str, text: str, *, idempotency_key: str) -> dict
260
+ ```
261
+
262
+ For auto-hash mode, ignore unstable fields that models may invent:
263
+
264
+ ```python
265
+ @lens.tool(
266
+ risk=al.RiskLevel.MUTATION,
267
+ idempotency=al.IdempotencyPolicy.AUTO_HASH,
268
+ hash_ignore_keys=["timestamp", "nonce", "uuid"],
269
+ )
270
+ def write_note(message: str, timestamp: int) -> dict:
271
+ return {"ok": True}
272
+ ```
273
+
274
+ ## Explicit Context For Resume
275
+
276
+ `ContextVar` works for normal in-process request scopes, but distributed resume systems such as LangGraph checkpointing, Temporal, or background workers need explicit context passing.
277
+
278
+ ```python
279
+ ctx = al.ToolCallContext(
280
+ project="demo-agent",
281
+ session_id="serialized-session",
282
+ run_id="resume-run",
283
+ call_id="old-call",
284
+ tool_name="lookup",
285
+ )
286
+
287
+ result = lookup("query", __al_ctx=ctx)
288
+ ```
289
+
290
+ `__al_ctx` is consumed by ActionLens and hidden from the public tool signature.
291
+
292
+ ## Durable Workflow Bridges
293
+
294
+ The `temporal` and `dbos` integration modules are dependency-free mapping layers: the durable runtime controls replay, scheduling, signals, and durable waits; ActionLens controls the governed tool call inside an Activity or Step. They do not manage workflow state or introduce a core Temporal/DBOS dependency.
295
+
296
+ ```python
297
+ from actionlens.integrations.temporal import (
298
+ TemporalActivityRunner,
299
+ context_from_temporal_workflow,
300
+ )
301
+
302
+ # Inside a Temporal Activity, pass values from activity.info().
303
+ ctx = context_from_temporal_workflow(
304
+ workflow_id,
305
+ run_id,
306
+ tool_name="send_message",
307
+ activity_id=activity_id, # preferred for call-level correlation
308
+ attempt=attempt,
309
+ project="messaging",
310
+ )
311
+ output = await TemporalActivityRunner(send_message).arun(
312
+ ctx, "hello", idempotency_key="message-123"
313
+ )
314
+ ```
315
+
316
+ `workflow_id` / DBOS `workflow_id` becomes the stable ActionLens `session_id`. Retry `attempt` is observability metadata only and is never part of the auto-hash idempotency identity. Use a stable business idempotency key for mutations. Approval signals/messages are wake-ups only: commit `lens.approve(...)` first, then let the resumed Activity/Step re-read the ActionLens ticket and ledger. See [Temporal](examples/temporal_integration_example.py) and [DBOS](examples/dbos_integration_example.py) examples.
317
+
318
+ ## Human Approval Flow
319
+
320
+ ```python
321
+ @lens.tool(
322
+ risk=al.RiskLevel.DESTRUCTIVE,
323
+ idempotency=al.IdempotencyPolicy.REQUIRED,
324
+ approval_required=True,
325
+ )
326
+ def drop_table(name: str) -> dict:
327
+ return {"dropped": name}
328
+
329
+ with lens.session(session_id="ops-001"):
330
+ pending = drop_table("users", idempotency_key="drop-users")
331
+
332
+ ticket_id = pending.result["ticket_id"]
333
+ lens.approve(ticket_id=ticket_id)
334
+
335
+ with lens.session(session_id="ops-001"):
336
+ success = drop_table("users", idempotency_key="drop-users")
337
+ ```
338
+
339
+ The first call returns `PENDING_APPROVAL` and does not execute the function. After approval, the same idempotency key is allowed to execute.
340
+
341
+ ## Reconcile Uncertain Side Effects
342
+
343
+ An expired non-fenceable mutation remains blocked as `UNCERTAIN`. Resolve it only through a business-specific reconciler that returns evidence:
344
+
345
+ ```python
346
+ class PaymentReconciler:
347
+ def inspect(self, record):
348
+ return al.ReconciliationResult(
349
+ outcome="CONFIRMED_SUCCEEDED",
350
+ summary="provider transaction exists",
351
+ output={"status": "SUCCESS", "result_summary": "payment confirmed"},
352
+ evidence_ref="https://audit.internal/payments/txn-123",
353
+ )
354
+
355
+ lens.reconcile_uncertain(idempotency_key, PaymentReconciler())
356
+ ```
357
+
358
+ `MANUAL_OVERRIDE` additionally requires `actor_id`, `reason`, `evidence_ref`, and an explicit override target. The ledger transition and reconciliation event are committed atomically.
359
+
360
+ For provider adapters, `ProviderStatusReconciler` maps a read-only `APPLIED`, `NOT_APPLIED`, `PENDING`, or `UNKNOWN` observation onto these outcomes and requires evidence for terminal conclusions. The [provider reconciliation cookbook](examples/provider_reconciliation_cookbook.md) covers payment, email, GitHub PR, and object-storage identity, query, consistency-window, and evidence rules.
361
+
362
+ ## CLI
363
+
364
+ Summarize local trajectory events:
365
+
366
+ ```bash
367
+ actionlens summary --storage-dir .actionlens
368
+ ```
369
+
370
+ Export native JSONL or a summary JSON:
371
+
372
+ ```bash
373
+ actionlens export --storage-dir .actionlens --format actionlens-jsonl --output trajectories.jsonl
374
+ actionlens export --storage-dir .actionlens --format summary-json --output summary.json
375
+ actionlens export --storage-dir .actionlens --format inspect-ai --output inspect.jsonl
376
+ actionlens export --storage-dir .actionlens --format eval-candidates --output eval-candidates.jsonl
377
+ actionlens export --storage-dir .actionlens --format sft-jsonl --output sft.jsonl
378
+ ```
379
+
380
+ Exporters skip corrupt or partial JSONL lines and report the skipped count. SFT export only includes completed successful calls. It uses redacted, bounded trajectory output and never reads raw artifact bodies.
381
+
382
+ ## Eval Case Bridge
383
+
384
+ Tool-boundary events alone do not contain the original user task, a reproducible environment, a grading target, or proof of the business outcome. ActionLens therefore exports an explicitly incomplete `EvalCaseCandidate` by default instead of pretending a trajectory is an Inspect `EvalLog`.
385
+
386
+ Supply facts owned by the host in a versioned JSON file keyed by `project/session/run` (the shorter `session/run` and `run` keys are also accepted):
387
+
388
+ ```json
389
+ {
390
+ "schema_version": "actionlens.eval-case-contexts.v1",
391
+ "runs": {
392
+ "demo-agent/chat-001/run-001": {
393
+ "task_input": "Send the approved invoice once",
394
+ "environment_spec": {
395
+ "name": "billing-sandbox",
396
+ "version": "2026-07-01",
397
+ "spec_ref": "https://eval.internal/environments/billing-v3"
398
+ },
399
+ "target": {"invoice_status": "sent"},
400
+ "rubric": {"no_duplicate_send": true},
401
+ "outcome_evidence": [
402
+ {
403
+ "kind": "provider_status",
404
+ "summary": "provider accepted exactly one message",
405
+ "ref": "https://audit.internal/messages/msg-123"
406
+ }
407
+ ],
408
+ "scorer_version": "billing-state.v2"
409
+ }
410
+ }
411
+ }
412
+ ```
413
+
414
+ ```bash
415
+ actionlens export --storage-dir .actionlens --format eval-candidates \
416
+ --case-contexts eval-contexts.json --output eval-candidates.jsonl
417
+
418
+ actionlens export --storage-dir .actionlens --format inspect-samples \
419
+ --case-contexts eval-contexts.json --require-ready --output inspect-samples.jsonl
420
+ ```
421
+
422
+ Each export writes a sidecar manifest containing source hashes, output hash, mapper version, redaction policy, and readiness/filter counts. `inspect-samples` is a dataset mapper only; the host still owns the Inspect task, sandbox, scorer, and replay lifecycle.
423
+
424
+ ## Audit Evidence Bundle
425
+
426
+ Generate a bounded tool-boundary evidence package without reading artifact bodies:
427
+
428
+ ```bash
429
+ actionlens export --storage-dir .actionlens --format evidence-bundle \
430
+ --output evidence/run-001 \
431
+ --retention-policy-id regulated-six-months.v1 --retention-days 180 \
432
+ --host-context-ref https://audit.internal/context/run-001 \
433
+ --actor-authorization-ref https://audit.internal/authz/run-001 \
434
+ --signature-manifest-ref https://audit.internal/signatures/run-001 \
435
+ --worm-archive-ref s3://audit-archive/run-001
436
+ ```
437
+
438
+ The directory contains `events.jsonl`, `integrity.jsonl`, and `manifest.json`. The manifest records source and output hashes, the event-chain root, retention metadata, redaction behavior, host evidence references, and missing evidence. Signature manifests and WORM attestations remain host-owned: the optional reference flags record sanitized references and clear the corresponding gaps, but ActionLens does not sign data, manage keys, or claim that it controls the archive.
439
+
440
+ ## OpenTelemetry GenAI Mapping
441
+
442
+ `OpenTelemetrySink` uses the pinned `actionlens.otel-genai.v1` profile. It maps `gen_ai.operation.name`, `gen_ai.tool.name`, and `gen_ai.tool_call.id`, while approval, run, and evidence facts remain in the `actionlens.*` namespace. The mapping contract records its upstream development snapshot because the standalone GenAI semantic-conventions repository is still evolving.
443
+
444
+ Prompt/context, tool arguments, model output, artifact URI, and raw evidence are never copied into span attributes. OpenTelemetry remains a sampled observability output; trajectory storage remains the evidence source of truth.
445
+
446
+ Create a static report without a server or frontend build chain:
447
+
448
+ ```bash
449
+ actionlens report --storage-dir .actionlens --html --output report.html
450
+ ```
451
+
452
+ Inspect governance state:
453
+
454
+ ```bash
455
+ actionlens tickets --storage-dir .actionlens --status PENDING
456
+ actionlens inspect-ledger --storage-dir .actionlens
457
+ actionlens outbox --storage-dir .actionlens list
458
+ actionlens outbox --storage-dir .actionlens status
459
+ actionlens outbox --storage-dir .actionlens cleanup --retention 30d --limit 1000
460
+ actionlens outbox --storage-dir .actionlens replay --delivery-id delivery-123
461
+ actionlens outbox --storage-dir .actionlens terminate --delivery-id delivery-123 --reason "invalid endpoint"
462
+ ```
463
+
464
+ Remove old local artifacts:
465
+
466
+ ```bash
467
+ actionlens gc --storage-dir .actionlens --older-than 7d
468
+ actionlens gc --storage-dir .actionlens --older-than 30d --cascade-derived
469
+ ```
470
+
471
+ Default GC protects a source artifact while any retained local derivative references it. `--cascade-derived` is an explicit operator choice to remove eligible derivatives with an eligible source; active read leases still win. A GC run with no deletion candidates skips provenance sidecar reads. When candidates exist, the local store must inspect lineage evidence to preserve retained ancestors; deployments that need a cross-store or continuously indexed lineage service should provide that at the host storage layer.
472
+
473
+ ## Framework Adapters
474
+
475
+ Adapters keep framework dependencies optional and preserve the ActionLens-managed signature:
476
+
477
+ ```python
478
+ from actionlens.integrations import (
479
+ wrap_langchain_tool,
480
+ wrap_openai_agent_tool,
481
+ wrap_pydantic_ai_tool,
482
+ )
483
+
484
+ pydantic_tool = wrap_pydantic_ai_tool(send_message)
485
+ openai_tool = wrap_openai_agent_tool(send_message)
486
+ langchain_tool = wrap_langchain_tool(send_message)
487
+
488
+ assert "idempotency_key" in openai_tool.parameters_json_schema["required"]
489
+ ```
490
+
491
+ Each adapter exposes `invoke()` / `ainvoke()` for explicit framework context mapping. Native framework object factories are lazy imports in the respective integration modules, so importing ActionLens never imports those frameworks.
492
+
493
+ For native LangGraph `StateGraph` / `ToolNode` execution, install the independent optional extra:
494
+
495
+ ```bash
496
+ python -m pip install -e ".[langgraph]"
497
+ ```
498
+
499
+ For LangGraph resume, map serialized state explicitly:
500
+
501
+ ```python
502
+ from actionlens.integrations.langchain import context_from_langgraph_state
503
+
504
+ ctx = context_from_langgraph_state(
505
+ {"config": {"configurable": {"thread_id": "chat-1", "run_id": "run-1"}}},
506
+ tool_name="send_message",
507
+ )
508
+ result = send_message("hello", idempotency_key="msg-1", __al_ctx=ctx)
509
+ ```
510
+
511
+ ## Bounded JSONL Queue
512
+
513
+ Synchronous writing remains the default. Enable a bounded single-writer queue explicitly:
514
+
515
+ ```python
516
+ from actionlens.sinks import JsonlSink
517
+
518
+ sink = JsonlSink(
519
+ ".actionlens",
520
+ queue_maxsize=10_000,
521
+ drop_policy="drop_oldest", # block, drop_oldest, or drop_newest
522
+ strict=False,
523
+ )
524
+ lens = al.ActionLens(project="demo", storage_dir=".actionlens", sink=sink)
525
+
526
+ # At process shutdown or an application lifecycle boundary:
527
+ lens.close()
528
+ print(sink.stats())
529
+ ```
530
+
531
+ With `strict=False`, sink failures are isolated from business tools and counted. With `strict=True`, write failures propagate through `emit()`, `flush()`, or `close()`.
532
+
533
+ ## Design Notes
534
+
535
+ Key boundaries:
536
+
537
+ - ActionLens is not an agent framework.
538
+ - It does not own graph control flow, model selection, scorer logic, or environment reset.
539
+ - Local JSONL trajectories are the source of truth; OpenTelemetry, Prometheus, dashboards, and eval adapters are optional outputs.
540
+ - Naive key redaction is only an MVP fallback. Production users should plug in a stronger redaction/DLP engine.
541
+
542
+ ## Development
543
+
544
+ ```bash
545
+ python -m pytest -q
546
+ python -m compileall -q src tests
547
+ python -m ruff check src tests benchmarks
548
+ python benchmarks/benchmark.py --iterations 1000 --output benchmark.json
549
+ python benchmarks/soak.py --duration 86400 --output soak-24h.json
550
+ ```
551
+
552
+ For the PostgreSQL query-plan check, supply only an isolated local test cluster:
553
+
554
+ ```bash
555
+ ACTIONLENS_POSTGRES_DSN=postgresql://postgres@127.0.0.1:55432/postgres \
556
+ python benchmarks/postgres.py --output postgres-query-plan.json
557
+ ```