█████╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗██╗ ███████╗███╗ ██╗███████╗ ██╔══██╗██╔════╝╚══██╔══╝██║██╔═══██╗████╗ ██║██║ ██╔════╝████╗ ██║██╔════╝ ███████║██║ ██║ ██║██║ ██║██╔██╗ ██║██║ █████╗ ██╔██╗ ██║███████╗ ██╔══██║██║ ██║ ██║██║ ██║██║╚██╗██║██║ ██╔══╝ ██║╚██╗██║╚════██║ ██║ ██║╚██████╗ ██║ ██║╚██████╔╝██║ ╚████║███████╗███████╗██║ ╚████║███████║ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚══════╝╚═╝ ╚═══╝╚══════╝
ActionLens
pip install actionlens
ActionLens is a low-intrusion Python library for agent tool governance and trajectory capture.
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.
Where ActionLens Fits
%%{init: {"theme":"base","themeVariables":{"fontFamily":"ui-sans-serif, system-ui, sans-serif","primaryColor":"#F7FBF7","primaryTextColor":"#1F2933","primaryBorderColor":"#3F474A","lineColor":"#4A5559","tertiaryColor":"#FFFFFF"}}}%%
flowchart LR
host["Agent / host runtime<br/>LangChain, LangGraph, PydanticAI, OpenAI Agents SDK"]:::host
durable["Optional durable control<br/>Temporal Activities / DBOS Steps"]:::durable
provider["Business tools / providers<br/>SaaS APIs, databases, local systems"]:::provider
consumer["Observability / eval / audit<br/>metrics, traces, evidence consumers"]:::consumer
subgraph actionlens["ActionLens: governance + evidence plane"]
direction TB
runtime["Governed tool runtime"]:::core
policy["Policy + approvals<br/>budgets + redaction"]:::governance
ledger["Ledger + idempotency<br/>outbox + recovery"]:::evidence
artifacts["Artifacts + provenance<br/>retention + access checks"]:::evidence
trajectory["Trajectory + exporters<br/>events + evidence bundles"]:::evidence
runtime --> policy
runtime --> ledger
ledger --> artifacts
ledger --> trajectory
end
host -->|"tools + explicit context"| runtime
durable -->|"stable workflow / step identity"| runtime
policy -->|"authorized invocation"| provider
provider -->|"result + provider evidence"| runtime
ledger --> consumer
artifacts --> consumer
trajectory --> consumer
classDef host fill:#FFFFFF,stroke:#3F474A,stroke-width:1.25px,color:#1F2933;
classDef durable fill:#F4F8F4,stroke:#586661,stroke-width:1.25px,color:#1F2933;
classDef core fill:#EAF6E6,stroke:#6DAE4A,stroke-width:1.6px,color:#1F2933;
classDef governance fill:#F7FBF7,stroke:#6B7972,stroke-width:1.2px,color:#1F2933;
classDef evidence fill:#FFFFFF,stroke:#6B7972,stroke-width:1.2px,color:#1F2933;
classDef provider fill:#FFFFFF,stroke:#3F474A,stroke-width:1.25px,color:#1F2933;
classDef consumer fill:#F1F8EF,stroke:#6DAE4A,stroke-width:1.25px,color:#1F2933;
style actionlens fill:#FAFCFA,stroke:#3F474A,stroke-width:1.25px,stroke-dasharray:2 3
Why
Modern agents often fail at the tool boundary:
- A crawler returns 20,000 words and blows up the model context.
- A model retries the same mutation with a slightly different timestamp.
- A Python exception is sent back to the model as raw traceback noise.
- A high-risk operation needs human approval, but the approval state is not replayable.
- Production runs cannot be converted into eval or debugging traces later.
ActionLens turns these into explicit runtime protocols while keeping the host framework in charge.
Status
This repository contains the v1.5.0 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:
@lens.tool(...)decorator for sync and async functionsStructuredToolOutputfor model-visible results- local artifact storage for large outputs
- UTF-8 byte and top-level-item output budgets, safe trajectory defaults, and explicit summary controls
- JSONL trajectory events
- explicit
ToolCallContextpassing for resume/distributed workers - child contexts that retain
parent_call_idand share the parent run budget - public signature injection for required
idempotency_key - interchangeable governance repositories for PostgreSQL, SQLite, and ephemeral tests
- lease ownership, heartbeat, fencing tokens, args/schema conflict detection, and
UNCERTAIN - atomic approval ticket + ledger + transactional outbox transitions
- at-least-once outbox dispatch with retry, claim leases, and dead-letter handling
- persistent approval pending / approve / deny / resume flow
- pluggable policy chain and redactors
- artifact metadata plus dry-run / size-aware GC
- generator and async-generator tools with bounded-memory artifact capture and configurable model-visible tails
- optional thread-mode timeout for sync tools; high-risk timeouts are
UNCERTAINbecause Python cannot stop a running thread - real TTL-scoped
CACHE_READreuse for read-only tools - synchronous approval resolver support alongside durable approval tickets
- governed, authorized artifact paging and literal grep tools for model navigation
- governed MCP
tools/callproxy with JSON Schema validation, including approval-safe dynamic argument changes RemoteToolAdapter/lens.remote_tool(...)bridge that carries ActionLens request identity into aRemoteToolRunner- ordered
invoke_many()for concurrency-safe read tools actionlens summary,actionlens export, andactionlens gc- thin PydanticAI, OpenAI Agents SDK, and LangChain/LangGraph adapters
- Inspect-oriented transcript and conservative SFT JSONL exporters
EvalCaseCandidatewith stable case IDs, host-supplied task/environment/rubric facts, explicit readiness, and versioned Inspect sample mapping- evidence bundles with source digests, a per-event SHA-256 chain, redaction/retention metadata, and explicit missing-evidence declarations
- static local HTML trajectory reports
- optional bounded-queue JSONL writing with explicit drop policies
- expiring approval tickets, schema-checked modified arguments, and ticket/ledger inspection
ArtifactPolicywith write-before-redaction guarantees, reference-only/deny modes, quotas, and encryption provider SPI- optional
MediaMetadataand compactArtifactProvenancefor metadata-first, derived media artifacts - host-provided
MediaMetadataExtractorSPI for local plaintext image/audio/video artifacts - provenance-aware local GC that preserves a source while a retained derivative still references it, with explicit cascade mode
- signed webhook, composite, low-cardinality metrics, and a pinned OpenTelemetry GenAI mapping profile
- versioned schema readers/golden fixtures and reproducible SFT dataset manifests
- framework-neutral
RemoteToolRunnerSPI with a concrete governed adapter - dependency-free Temporal Activity and DBOS Step context bridges that preserve stable workflow/step identity
- evidence-backed
UNCERTAINreconciliation with atomic audit events - background outbox lifecycle, health state, and controlled dead-letter replay/termination
- authorized artifact read/decrypt/checksum verification and reference URI policy
- webhook key rotation, replay-window verification, event deduplication hook, and SSRF controls
- directly runnable repository and sink contract checks for third-party implementations
- pooled PostgreSQL connections with bounded acquire/statement/lock/transaction timeouts
- explicit advisory-lock migrations and startup schema compatibility checks
- event-loop isolation for synchronous governance I/O around async tools
- phase-aware governance failures before and after external side effects
- outbox backlog/lag health, bounded retention, and single-round-trip PostgreSQL claims
- cross-process artifact read/GC leases plus symlink/reparse-point rejection
- bounded streaming artifact upload, authenticated decryption, and atomic destination promotion
- reproducible benchmark and soak probes with percentile and memory evidence
PostgreSQL is the preferred multi-instance backend because ledger, approval, and outbox facts share one transaction. Redis is intentionally not implemented in v1.5; the repository protocol permits a future backend without changing ToolRuntime.
Install For Local Development
python -m pip install -e .
python -m pytest -q
The runtime dependency is intentionally light:
- Python 3.10+
- Pydantic v2
Install production PostgreSQL support separately:
python -m pip install -e ".[postgres]"
Install MCP JSON Schema validation when proxying MCP tools:
python -m pip install -e ".[mcp]"
PostgreSQL Repository
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:
ACTIONLENS_POSTGRES_DSN=postgresql://actionlens:secret@db.internal/actionlens actionlens migrate
ACTIONLENS_POSTGRES_DSN=postgresql://actionlens:secret@db.internal/actionlens actionlens schema-status
import actionlens as al
repository = al.PostgresGovernanceRepository(
"postgresql://actionlens:secret@db.internal/actionlens",
min_pool_size=2,
max_pool_size=20,
pool_timeout=5,
statement_timeout_ms=30_000,
lock_timeout_ms=5_000,
transaction_timeout_ms=60_000,
)
lens = al.ActionLens(project="demo-agent", repository=repository)
# At the owning application lifecycle boundary:
lens.close()
repository.close()
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.
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:
repository = al.SQLiteGovernanceRepository(".actionlens/ledger.sqlite3", synchronous="NORMAL")
Artifact Confidentiality
policy = al.ArtifactPolicy(
raw_mode="redact_then_store", # store, redact_then_store, reference_only, deny
encryption="provider",
max_bytes_per_run=10_000_000,
retention_days=30,
)
lens = al.ActionLens(
artifact_policy=policy,
encryption_provider=my_kms_provider,
)
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.
OutputPolicy(include_raw_in_trajectory=False) is the default, so the full
StructuredToolOutput.result is not copied into trajectory events. Set it to
True only for an explicitly approved training or diagnostic sink. When an
encryption provider is configured, artifact previews are withheld from the
unencrypted sidecar; the current caller can still receive its independently
bounded, redacted inline preview.
Media Metadata And Provenance
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.
source = lens.artifact_store.put(video_bytes, media_type="video/mp4")
thumbnail = lens.artifact_store.put(
thumbnail_bytes,
media_type="image/jpeg",
media_metadata=al.MediaMetadata(width=320, height=180, codec="jpeg"),
provenance=al.ArtifactProvenance.from_source(
source,
operation="thumbnail",
operation_version="ffmpeg-7.0",
parameters={"time_sec": 12.5, "max_width": 320},
created_by_tool="extract_thumbnail",
),
)
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.
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.
class MyMediaExtractor:
def extract(self, path, media_type):
return al.MediaMetadata(width=1920, height=1080, codec="h264")
lens = al.ActionLens(media_metadata_extractor=MyMediaExtractor())
Quick Start
import actionlens as al
lens = al.ActionLens(project="demo-agent", storage_dir=".actionlens")
@lens.tool(max_bytes=1000)
def fetch_page() -> str:
return "very long page..." * 1000
with lens.session(session_id="chat-001"):
output = fetch_page()
print(output.status)
print(output.result_summary)
print(output.artifact_refs)
If the result exceeds max_bytes, ActionLens stores the raw result under .actionlens/artifacts/ and returns a bounded preview plus an ArtifactRef.
v1.5 Execution Semantics
max_bytes is a UTF-8 byte budget for the model-visible result. Collections
are also limited by OutputPolicy.max_inline_items; an oversized value is
stored as an artifact and the caller receives a preview bounded by the same
byte budget. summary_fields can select safe dictionary fields for an inline
summary, while summary_includes_content=False disables content snippets.
For a synchronous function, run_sync_in_thread=True only releases the caller
after a timeout. Python cannot force-stop that business thread. A timeout for a
MUTATION or DESTRUCTIVE tool is therefore returned and persisted as
UNCERTAIN, blocks automatic retry, and must be reconciled against the business
system. Read-only tool timeouts remain retryable TIMEOUT results.
Idempotency
For mutation tools, require an explicit idempotency key:
@lens.tool(
risk=al.RiskLevel.MUTATION,
idempotency=al.IdempotencyPolicy.REQUIRED,
)
def send_message(user_id: str, text: str) -> dict:
return {"sent": True, "user_id": user_id}
with lens.session(session_id="chat-001"):
first = send_message("u1", "hello", idempotency_key="msg-u1-001")
second = send_message("u1", "hello", idempotency_key="msg-u1-001")
The wrapper modifies the public function signature so schema extractors can see the required idempotency_key:
import inspect
print(inspect.signature(send_message))
# (user_id: str, text: str, *, idempotency_key: str) -> dict
For auto-hash mode, ignore unstable fields that models may invent:
@lens.tool(
risk=al.RiskLevel.MUTATION,
idempotency=al.IdempotencyPolicy.AUTO_HASH,
hash_ignore_keys=["timestamp", "nonce", "uuid"],
)
def write_note(message: str, timestamp: int) -> dict:
return {"ok": True}
Read-only tools can opt into a real shared TTL cache. Its identity includes project, environment, tenant, tool name, and arguments, but intentionally not session or run identity:
@lens.tool(idempotency=al.IdempotencyPolicy.CACHE_READ, cache_ttl_sec=60)
def lookup_customer(customer_id: str) -> dict:
return provider.lookup(customer_id)
Explicit Context For Resume
ContextVar works for normal in-process request scopes, but distributed resume systems such as LangGraph checkpointing, Temporal, or background workers need explicit context passing.
ctx = al.ToolCallContext(
project="demo-agent",
session_id="serialized-session",
run_id="resume-run",
call_id="old-call",
tool_name="lookup",
)
result = lookup("query", __al_ctx=ctx)
__al_ctx is consumed by ActionLens and hidden from the public tool signature.
Use child_context() for a sub-agent or delegated call that must retain a
durable parent link and consume the same run budget:
with lens.session(session_id="chat-001", run_id="run-001") as parent:
child = lens.child_context(parent=parent, tool_name="lookup_customer")
output = lookup_customer("cust-7", __al_ctx=child)
MCP, Remote Tools, And Artifact Navigation
MCPGovernanceProxy turns registered MCP tools/call methods into normal
governed ActionLens tools. The proxy validates the MCP input schema before the
transport call; approvals, leases, output shaping, idempotency, and trajectories
are applied locally.
proxy = al.MCPGovernanceProxy(lens, mcp_transport)
proxy.register_tool(
"search_docs",
input_schema={"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]},
)
result = proxy.handle_request(request)
For an existing asynchronous job provider, lens.remote_tool(runner, name="...")
builds a governed RemoteToolRunner bridge. The request carries the ActionLens
idempotency key, arguments hash, schema hash, context, and deadline. A timeout
attempts cancellation; high-risk outcomes that cannot be confirmed become
UNCERTAIN.
Large artifacts can be navigated through governed tools rather than copied back into context:
tools = lens.artifact_navigation_tools()
page = tools["artifact_read"](artifact_ref, offset=0, limit=4096)
matches = tools["artifact_grep"](artifact_ref, needle="invoice")
Both operations require the configured artifact authorizer, verify checksums,
emit access events, and cap page/search output. Sync and async generators are
persisted as artifacts automatically; OutputPolicy.streaming_tail_lines
controls the tail returned to the model.
Durable Workflow Bridges
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.
from actionlens.integrations.temporal import (
TemporalActivityRunner,
context_from_temporal_workflow,
)
# Inside a Temporal Activity, pass values from activity.info().
ctx = context_from_temporal_workflow(
workflow_id,
run_id,
tool_name="send_message",
activity_id=activity_id, # preferred for call-level correlation
attempt=attempt,
project="messaging",
)
output = await TemporalActivityRunner(send_message).arun(
ctx, "hello", idempotency_key="message-123"
)
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 and DBOS examples.
Human Approval Flow
@lens.tool(
risk=al.RiskLevel.DESTRUCTIVE,
idempotency=al.IdempotencyPolicy.REQUIRED,
approval_required=True,
)
def drop_table(name: str) -> dict:
return {"dropped": name}
with lens.session(session_id="ops-001"):
pending = drop_table("users", idempotency_key="drop-users")
ticket_id = pending.result["ticket_id"]
lens.approve(ticket_id=ticket_id)
with lens.session(session_id="ops-001"):
success = drop_table("users", idempotency_key="drop-users")
The first call returns PENDING_APPROVAL and does not execute the function. After approval, the same idempotency key is allowed to execute.
Interactive hosts can resolve the same ticket synchronously without losing the
durable audit trail. The resolver returns APPROVE, DENY, or PENDING; an
approved decision is persisted before the business function runs:
def prompt_operator(ticket: al.ApprovalTicket, context: al.ToolCallContext):
return {"action": "APPROVE", "approved_by": "on-call"}
lens = al.ActionLens(approval_resolver=prompt_operator)
Reconcile Uncertain Side Effects
An expired non-fenceable mutation remains blocked as UNCERTAIN. Resolve it only through a business-specific reconciler that returns evidence:
class PaymentReconciler:
def inspect(self, record):
return al.ReconciliationResult(
outcome="CONFIRMED_SUCCEEDED",
summary="provider transaction exists",
output={"status": "SUCCESS", "result_summary": "payment confirmed"},
evidence_ref="https://audit.internal/payments/txn-123",
)
lens.reconcile_uncertain(idempotency_key, PaymentReconciler())
MANUAL_OVERRIDE additionally requires actor_id, reason, evidence_ref, and an explicit override target. The ledger transition and reconciliation event are committed atomically.
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 covers payment, email, GitHub PR, and object-storage identity, query, consistency-window, and evidence rules.
CLI
Summarize local trajectory events:
actionlens summary --storage-dir .actionlens
Export native JSONL or a summary JSON:
actionlens export --storage-dir .actionlens --format actionlens-jsonl --output trajectories.jsonl
actionlens export --storage-dir .actionlens --format summary-json --output summary.json
actionlens export --storage-dir .actionlens --format inspect-ai --output inspect.jsonl
actionlens export --storage-dir .actionlens --format eval-candidates --output eval-candidates.jsonl
actionlens export --storage-dir .actionlens --format sft-jsonl --output sft.jsonl
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.
Eval Case Bridge
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.
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):
{
"schema_version": "actionlens.eval-case-contexts.v1",
"runs": {
"demo-agent/chat-001/run-001": {
"task_input": "Send the approved invoice once",
"environment_spec": {
"name": "billing-sandbox",
"version": "2026-07-01",
"spec_ref": "https://eval.internal/environments/billing-v3"
},
"target": {"invoice_status": "sent"},
"rubric": {"no_duplicate_send": true},
"outcome_evidence": [
{
"kind": "provider_status",
"summary": "provider accepted exactly one message",
"ref": "https://audit.internal/messages/msg-123"
}
],
"scorer_version": "billing-state.v2"
}
}
}
actionlens export --storage-dir .actionlens --format eval-candidates \
--case-contexts eval-contexts.json --output eval-candidates.jsonl
actionlens export --storage-dir .actionlens --format inspect-samples \
--case-contexts eval-contexts.json --require-ready --output inspect-samples.jsonl
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.
Audit Evidence Bundle
Generate a bounded tool-boundary evidence package without reading artifact bodies:
actionlens export --storage-dir .actionlens --format evidence-bundle \
--output evidence/run-001 \
--retention-policy-id regulated-six-months.v1 --retention-days 180 \
--host-context-ref https://audit.internal/context/run-001 \
--actor-authorization-ref https://audit.internal/authz/run-001 \
--signature-manifest-ref https://audit.internal/signatures/run-001 \
--worm-archive-ref s3://audit-archive/run-001
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.
OpenTelemetry GenAI Mapping
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.
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.
Create a static report without a server or frontend build chain:
actionlens report --storage-dir .actionlens --html --output report.html
Inspect governance state:
actionlens tickets --storage-dir .actionlens --status PENDING
actionlens inspect-ledger --storage-dir .actionlens
actionlens outbox --storage-dir .actionlens list
actionlens outbox --storage-dir .actionlens status
actionlens outbox --storage-dir .actionlens cleanup --retention 30d --limit 1000
actionlens outbox --storage-dir .actionlens replay --delivery-id delivery-123
actionlens outbox --storage-dir .actionlens terminate --delivery-id delivery-123 --reason "invalid endpoint"
Remove old local artifacts:
actionlens gc --storage-dir .actionlens --older-than 7d
actionlens gc --storage-dir .actionlens --older-than 30d --cascade-derived
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.
Framework Adapters
Adapters keep framework dependencies optional and preserve the ActionLens-managed signature:
from actionlens.integrations import (
wrap_langchain_tool,
wrap_openai_agent_tool,
wrap_pydantic_ai_tool,
)
pydantic_tool = wrap_pydantic_ai_tool(send_message)
openai_tool = wrap_openai_agent_tool(send_message)
langchain_tool = wrap_langchain_tool(send_message)
assert "idempotency_key" in openai_tool.parameters_json_schema["required"]
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.
For native LangGraph StateGraph / ToolNode execution, install the independent optional extra:
python -m pip install -e ".[langgraph]"
For LangGraph resume, map serialized state explicitly:
from actionlens.integrations.langchain import context_from_langgraph_state
ctx = context_from_langgraph_state(
{"config": {"configurable": {"thread_id": "chat-1", "run_id": "run-1"}}},
tool_name="send_message",
)
result = send_message("hello", idempotency_key="msg-1", __al_ctx=ctx)
Bounded JSONL Queue
Synchronous writing remains the default. Enable a bounded single-writer queue explicitly:
from actionlens.sinks import JsonlSink
sink = JsonlSink(
".actionlens",
queue_maxsize=10_000,
drop_policy="drop_oldest", # block, drop_oldest, or drop_newest
strict=False,
)
lens = al.ActionLens(project="demo", storage_dir=".actionlens", sink=sink)
# At process shutdown or an application lifecycle boundary:
lens.close()
print(sink.stats())
With strict=False, sink failures are isolated from business tools and counted. With strict=True, write failures propagate through emit(), flush(), or close().
Design Notes
Key boundaries:
- ActionLens is not an agent framework.
- It does not own graph control flow, model selection, scorer logic, or environment reset.
- Local JSONL trajectories are the source of truth; OpenTelemetry, Prometheus, dashboards, and eval adapters are optional outputs.
- Naive key redaction is only an MVP fallback. Production users should plug in a stronger redaction/DLP engine.
Development
python -m pytest -q
python -m compileall -q src tests
python -m ruff check src tests benchmarks
python benchmarks/benchmark.py --iterations 1000 --output benchmark.json
python benchmarks/soak.py --duration 86400 --output soak-24h.json
For the PostgreSQL query-plan check, supply only an isolated local test cluster:
ACTIONLENS_POSTGRES_DSN=postgresql://postgres@127.0.0.1:55432/postgres \
python benchmarks/postgres.py --output postgres-query-plan.json
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file actionlens-1.5.1.tar.gz.
File metadata
- Download URL: actionlens-1.5.1.tar.gz
- Upload date:
- Size: 169.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d9868a2eb374e799e2f1ab2538b5bb5a698f26ffed5dd0dcc090571c4cc86c1
|
|
| MD5 |
47d40fdde3b303743dd611ef9b9def0d
|
|
| BLAKE2b-256 |
7b8a293dba253753b924ac55021df234dcf60651b07865a09abe44b0a580cc6e
|
Provenance
The following attestation bundles were made for actionlens-1.5.1.tar.gz:
Publisher:
publish.yml on JieNEUer/ActionLens
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
actionlens-1.5.1.tar.gz -
Subject digest:
7d9868a2eb374e799e2f1ab2538b5bb5a698f26ffed5dd0dcc090571c4cc86c1 - Sigstore transparency entry: 2266529765
- Sigstore integration time:
-
Permalink:
JieNEUer/ActionLens@c393fd5442c8d109a9426891c38c067ff12808b2 -
Branch / Tag:
refs/tags/v1.5.1 - Owner: https://github.com/JieNEUer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c393fd5442c8d109a9426891c38c067ff12808b2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file actionlens-1.5.1-py3-none-any.whl.
File metadata
- Download URL: actionlens-1.5.1-py3-none-any.whl
- Upload date:
- Size: 122.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
54956e3aeb0f7dd337b883d4469015c5c058ac3800c743ece94fa5b3548cf7a6
|
|
| MD5 |
68f45743841fd93925ec6e40472ebed4
|
|
| BLAKE2b-256 |
41f6fb42c2a6bfa8e09733334f8bf1ca0b5cac7cfd75ca779050a90600a1cdb6
|
Provenance
The following attestation bundles were made for actionlens-1.5.1-py3-none-any.whl:
Publisher:
publish.yml on JieNEUer/ActionLens
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
actionlens-1.5.1-py3-none-any.whl -
Subject digest:
54956e3aeb0f7dd337b883d4469015c5c058ac3800c743ece94fa5b3548cf7a6 - Sigstore transparency entry: 2266530153
- Sigstore integration time:
-
Permalink:
JieNEUer/ActionLens@c393fd5442c8d109a9426891c38c067ff12808b2 -
Branch / Tag:
refs/tags/v1.5.1 - Owner: https://github.com/JieNEUer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c393fd5442c8d109a9426891c38c067ff12808b2 -
Trigger Event:
release
-
Statement type: