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.
- actionlens-1.4.1/LICENSE +21 -0
- actionlens-1.4.1/MANIFEST.in +5 -0
- actionlens-1.4.1/PKG-INFO +27 -0
- actionlens-1.4.1/README.md +557 -0
- actionlens-1.4.1/README.zh-CN.md +555 -0
- actionlens-1.4.1/examples/dbos_integration_example.py +57 -0
- actionlens-1.4.1/examples/provider_reconciliation_cookbook.md +70 -0
- actionlens-1.4.1/examples/temporal_integration_example.py +84 -0
- actionlens-1.4.1/pyproject.toml +33 -0
- actionlens-1.4.1/setup.cfg +4 -0
- actionlens-1.4.1/src/actionlens/__init__.py +126 -0
- actionlens-1.4.1/src/actionlens/artifacts/__init__.py +16 -0
- actionlens-1.4.1/src/actionlens/artifacts/base.py +91 -0
- actionlens-1.4.1/src/actionlens/artifacts/fs.py +1251 -0
- actionlens-1.4.1/src/actionlens/cli.py +290 -0
- actionlens-1.4.1/src/actionlens/context.py +47 -0
- actionlens-1.4.1/src/actionlens/contracts.py +111 -0
- actionlens-1.4.1/src/actionlens/errors.py +69 -0
- actionlens-1.4.1/src/actionlens/evals.py +129 -0
- actionlens-1.4.1/src/actionlens/exporters/__init__.py +27 -0
- actionlens-1.4.1/src/actionlens/exporters/core.py +646 -0
- actionlens-1.4.1/src/actionlens/exporters/html.py +65 -0
- actionlens-1.4.1/src/actionlens/integrations/__init__.py +20 -0
- actionlens-1.4.1/src/actionlens/integrations/common.py +224 -0
- actionlens-1.4.1/src/actionlens/integrations/dbos.py +110 -0
- actionlens-1.4.1/src/actionlens/integrations/langchain.py +45 -0
- actionlens-1.4.1/src/actionlens/integrations/openai_agents.py +24 -0
- actionlens-1.4.1/src/actionlens/integrations/pydantic_ai.py +20 -0
- actionlens-1.4.1/src/actionlens/integrations/temporal.py +138 -0
- actionlens-1.4.1/src/actionlens/ledger/__init__.py +11 -0
- actionlens-1.4.1/src/actionlens/ledger/memory.py +144 -0
- actionlens-1.4.1/src/actionlens/ledger/sqlite.py +290 -0
- actionlens-1.4.1/src/actionlens/ledger/tickets.py +267 -0
- actionlens-1.4.1/src/actionlens/models.py +296 -0
- actionlens-1.4.1/src/actionlens/outbox.py +154 -0
- actionlens-1.4.1/src/actionlens/policy.py +94 -0
- actionlens-1.4.1/src/actionlens/reconciliation.py +91 -0
- actionlens-1.4.1/src/actionlens/redaction.py +75 -0
- actionlens-1.4.1/src/actionlens/remote.py +44 -0
- actionlens-1.4.1/src/actionlens/repositories/__init__.py +10 -0
- actionlens-1.4.1/src/actionlens/repositories/memory.py +18 -0
- actionlens-1.4.1/src/actionlens/repositories/postgres.py +586 -0
- actionlens-1.4.1/src/actionlens/repositories/sqlite.py +616 -0
- actionlens-1.4.1/src/actionlens/repository.py +104 -0
- actionlens-1.4.1/src/actionlens/runtime.py +1550 -0
- actionlens-1.4.1/src/actionlens/schema.py +44 -0
- actionlens-1.4.1/src/actionlens/sinks/__init__.py +12 -0
- actionlens-1.4.1/src/actionlens/sinks/composite.py +42 -0
- actionlens-1.4.1/src/actionlens/sinks/jsonl.py +139 -0
- actionlens-1.4.1/src/actionlens/sinks/memory.py +17 -0
- actionlens-1.4.1/src/actionlens/sinks/metrics.py +30 -0
- actionlens-1.4.1/src/actionlens/sinks/otel.py +111 -0
- actionlens-1.4.1/src/actionlens/sinks/webhook.py +165 -0
- actionlens-1.4.1/src/actionlens/trajectory.py +49 -0
- actionlens-1.4.1/src/actionlens.egg-info/PKG-INFO +27 -0
- actionlens-1.4.1/src/actionlens.egg-info/SOURCES.txt +68 -0
- actionlens-1.4.1/src/actionlens.egg-info/dependency_links.txt +1 -0
- actionlens-1.4.1/src/actionlens.egg-info/entry_points.txt +2 -0
- actionlens-1.4.1/src/actionlens.egg-info/requires.txt +28 -0
- actionlens-1.4.1/src/actionlens.egg-info/top_level.txt +1 -0
- actionlens-1.4.1/tests/test_actionlens_core.py +404 -0
- actionlens-1.4.1/tests/test_actionlens_v03.py +549 -0
- actionlens-1.4.1/tests/test_actionlens_v05.py +496 -0
- actionlens-1.4.1/tests/test_actionlens_v10.py +242 -0
- actionlens-1.4.1/tests/test_actionlens_v11.py +279 -0
- actionlens-1.4.1/tests/test_benchmark_tools.py +34 -0
- actionlens-1.4.1/tests/test_eval_evidence_otel.py +396 -0
- actionlens-1.4.1/tests/test_postgres_integration.py +365 -0
- actionlens-1.4.1/tests/test_streaming_artifacts.py +241 -0
- actionlens-1.4.1/tests/test_v14_durable_media.py +538 -0
actionlens-1.4.1/LICENSE
ADDED
|
@@ -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,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
|
+
```
|