flowscript-agents 0.2.6__tar.gz → 0.2.8__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.
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/PKG-INFO +165 -18
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/README.md +164 -17
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/__init__.py +7 -1
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/audit.py +39 -4
- flowscript_agents-0.2.8/flowscript_agents/cloud.py +325 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/extract.py +30 -1
- flowscript_agents-0.2.8/flowscript_agents/explain.py +410 -0
- flowscript_agents-0.2.8/flowscript_agents/fixpoint.py +339 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/mcp.py +95 -2
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/tool-integrity.json +2 -1
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/pyproject.toml +1 -1
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_audit.py +146 -0
- flowscript_agents-0.2.8/tests/test_cloud.py +312 -0
- flowscript_agents-0.2.8/tests/test_explain.py +412 -0
- flowscript_agents-0.2.8/tests/test_fixpoint.py +671 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_mcp.py +71 -4
- flowscript_agents-0.2.6/:memory: +0 -29
- flowscript_agents-0.2.6/:memory:.audit.manifest.json +0 -10
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/.github/workflows/test.yml +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/.gitignore +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/AUDIT_TRAIL_DESIGN.md +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/docs/adapters.md +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/docs/api-reference.md +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/docs/audit-trail.md +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/docs/brand/logo-512.png +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/docs/brand/social-preview.png +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/docs/flowscript-demo.png +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/docs/lifecycle.md +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/examples/CLAUDE.md.example +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/examples/langgraph_live_test.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/examples/temporal_e2e_test.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/camel_ai.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/crewai.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/__init__.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/_utils.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/consolidate.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/index.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/providers.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/search.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/google_adk.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/haystack.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/langgraph.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/llamaindex.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/memory.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/openai_agents.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/pydantic_ai.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/query.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/smolagents.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/types.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/flowscript_agents/unified.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/scripts/validate_dedup_threshold.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/conftest.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_camel_ai.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_consolidation.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_crewai.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_embeddings.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_google_adk.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_haystack.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_langgraph.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_llamaindex.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_memory.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_openai_agents.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_pydantic_ai.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_smolagents.py +0 -0
- {flowscript_agents-0.2.6 → flowscript_agents-0.2.8}/tests/test_temporal.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowscript-agents
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Complete agent memory: reasoning queries + vector search + auto-extraction. Decision intelligence for LangGraph, CrewAI, Google ADK, OpenAI Agents SDK, Pydantic AI, smolagents, LlamaIndex, Haystack, and CAMEL-AI.
|
|
5
5
|
Project-URL: Homepage, https://flowscript.org
|
|
6
6
|
Project-URL: Repository, https://github.com/phillipclapham/flowscript-agents
|
|
@@ -68,14 +68,14 @@ Description-Content-Type: text/markdown
|
|
|
68
68
|
|
|
69
69
|
<h1 align="center">flowscript-agents</h1>
|
|
70
70
|
|
|
71
|
-
<p align="center"><strong>
|
|
71
|
+
<p align="center"><strong>Your AI agents make decisions they can't explain. FlowScript makes those decisions queryable.</strong></p>
|
|
72
|
+
|
|
73
|
+
<p align="center">Drop-in adapters for 9 agent frameworks. Plain text in, typed reasoning queries out.<br>Hash-chained audit trail. Structural compliance. MIT licensed.</p>
|
|
72
74
|
|
|
73
75
|
[](https://github.com/phillipclapham/flowscript-agents) [](https://pypi.org/project/flowscript-agents/) [](LICENSE) [](https://pypi.org/project/flowscript-agents/)
|
|
74
76
|
|
|
75
77
|
---
|
|
76
78
|
|
|
77
|
-
Plain text in. Typed reasoning queries out:
|
|
78
|
-
|
|
79
79
|
```python
|
|
80
80
|
from openai import OpenAI
|
|
81
81
|
from flowscript_agents import UnifiedMemory
|
|
@@ -92,18 +92,19 @@ with UnifiedMemory("agent-memory.json", embedder=OpenAIEmbeddings(), llm=llm) as
|
|
|
92
92
|
mem.add("PostgreSQL gives us rich queries at $15/month but read latency is 10-50ms")
|
|
93
93
|
|
|
94
94
|
tensions = mem.memory.query.tensions()
|
|
95
|
-
# →
|
|
96
|
-
#
|
|
97
|
-
# and preserved both sides as a queryable tension
|
|
95
|
+
# → The LLM detected the $200/month vs $50/month contradiction
|
|
96
|
+
# → and preserved both sides as a queryable tension — not a deletion
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
# Pick any node to trace its reasoning:
|
|
99
|
+
first_node = mem.memory.nodes[0]
|
|
100
|
+
why = mem.memory.query.why(first_node.id)
|
|
101
|
+
# → Full causal chain backward from any decision
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
# →
|
|
103
|
+
blocked = mem.memory.query.blocked()
|
|
104
|
+
# → What's stuck + downstream impact
|
|
104
105
|
```
|
|
105
106
|
|
|
106
|
-
Five queries that no vector store can answer — `why()`, `tensions()`, `blocked()`, `alternatives()`, `whatIf()` — over a typed
|
|
107
|
+
Five queries that no vector store can answer — `why()`, `tensions()`, `blocked()`, `alternatives()`, `whatIf()` — over a typed reasoning graph. Drop-in adapters for [9 agent frameworks](#works-with-your-stack). Hash-chained audit trail. And when memories contradict, we don't delete — we create a queryable *tension*.
|
|
107
108
|
|
|
108
109
|
<p align="center">
|
|
109
110
|
<img src="docs/flowscript-demo.png" alt="FlowScript — editor with .fs syntax, D3 reasoning graph, and tensions query results" width="800">
|
|
@@ -113,11 +114,11 @@ Five queries that no vector store can answer — `why()`, `tensions()`, `blocked
|
|
|
113
114
|
|
|
114
115
|
## Why FlowScript
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
Every agent framework gives AI agents memory. None of them make that memory queryable.
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
Vector stores retrieve content that looks similar. That's useful, but it's not reasoning. When an auditor asks "why did your agent deny that claim?" or a developer asks "what breaks if we change this decision?" — similarity search returns a guess. FlowScript returns the actual typed reasoning chain.
|
|
119
120
|
|
|
120
|
-
FlowScript sits above your memory store, not instead of it.
|
|
121
|
+
This is the gap researchers call "[strategic blindness](https://arxiv.org/abs/2603.18718)" — memory that tracks content without tracking reasoning. FlowScript sits above your memory store, not instead of it. Mem0, LangGraph checkpointers, Google Memory Bank — they remember what your agent stored. FlowScript remembers *why it decided*, what it traded off, and what breaks if you change your mind.
|
|
121
122
|
|
|
122
123
|
---
|
|
123
124
|
|
|
@@ -337,7 +338,22 @@ result = Memory.verify_audit("agent.audit.jsonl")
|
|
|
337
338
|
# → AuditVerifyResult(valid=True, total_entries=42, files_verified=1)
|
|
338
339
|
```
|
|
339
340
|
|
|
340
|
-
Framework attribution is automatic — every audit entry records which adapter triggered it. Query by time range, event type, adapter, or session. Rotation with gzip compression.
|
|
341
|
+
Framework attribution is automatic — every audit entry records which adapter triggered it. Query by time range, event type, adapter, or session. Rotation with gzip compression.
|
|
342
|
+
|
|
343
|
+
**SIEM integration:** `on_event` callback fires for every audit entry. Use `on_event_async=True` for non-blocking dispatch — slow webhooks won't block agent operations:
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
from flowscript_agents import Memory, MemoryOptions, AuditConfig
|
|
347
|
+
|
|
348
|
+
def send_to_siem(entry):
|
|
349
|
+
requests.post("https://siem.example.com/ingest", json=entry)
|
|
350
|
+
|
|
351
|
+
mem = Memory.load_or_create("agent.json", options=MemoryOptions(
|
|
352
|
+
audit=AuditConfig(on_event=send_to_siem, on_event_async=True)
|
|
353
|
+
))
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Events are dispatched in order (single worker thread). Callback failures log to stderr but never block audit writes. Call `writer.close()` for graceful shutdown of in-flight callbacks.
|
|
341
357
|
|
|
342
358
|
---
|
|
343
359
|
|
|
@@ -365,7 +381,7 @@ Every query touches returned nodes — knowledge that keeps getting queried earn
|
|
|
365
381
|
**For SDK users** — adapters support context managers that auto-wrap:
|
|
366
382
|
|
|
367
383
|
```python
|
|
368
|
-
from flowscript_agents.
|
|
384
|
+
from flowscript_agents.langgraph import FlowScriptStore
|
|
369
385
|
|
|
370
386
|
with FlowScriptStore("agent-memory.json") as store:
|
|
371
387
|
# work happens — all mutations auto-save
|
|
@@ -391,6 +407,8 @@ Both the Python and [TypeScript](https://www.npmjs.com/package/flowscript-core)
|
|
|
391
407
|
|
|
392
408
|
## Comparison
|
|
393
409
|
|
|
410
|
+
Every agent framework gives AI agents memory. None make that memory auditable, typed, or queryable at the reasoning level. That's the layer FlowScript occupies.
|
|
411
|
+
|
|
394
412
|
| | FlowScript | Mem0 | Vector stores |
|
|
395
413
|
|:---|:---|:---|:---|
|
|
396
414
|
| Find similar content | Vector search | Vector search | Vector search |
|
|
@@ -403,7 +421,128 @@ Both the Python and [TypeScript](https://www.npmjs.com/package/flowscript-core)
|
|
|
403
421
|
| Temporal graduation | Automatic 4-tier | — | — |
|
|
404
422
|
| Token budgeting | 4 strategies | — | — |
|
|
405
423
|
|
|
406
|
-
Under the hood: a local semantic graph with typed nodes, typed relationships, and typed states. Queries traverse structure — no embeddings required, no LLM calls, no network. Sub-ms on project-scale graphs.
|
|
424
|
+
Under the hood: a local semantic graph with typed nodes, typed relationships, and typed states. Queries traverse structure — no embeddings required, no LLM calls, no network. Sub-ms on project-scale graphs.
|
|
425
|
+
|
|
426
|
+
Vector search and reasoning queries are orthogonal — use both. Mem0 for retrieval, FlowScript for reasoning. They're different architectural layers.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Enterprise & Compliance
|
|
431
|
+
|
|
432
|
+
FlowScript's typed reasoning chains are also compliance-ready audit infrastructure. This isn't a separate product — it's a structural property of how FlowScript works.
|
|
433
|
+
|
|
434
|
+
**EU AI Act coverage:**
|
|
435
|
+
|
|
436
|
+
| Requirement | Article | How FlowScript satisfies it |
|
|
437
|
+
|:---|:---|:---|
|
|
438
|
+
| Record-keeping | Art. 12 | Hash-chained audit trail, append-only, tamper-evident, 7yr default retention |
|
|
439
|
+
| Transparency | Art. 13 | `why()` queries return typed causal chains — not reconstructions, actual reasoning records |
|
|
440
|
+
| Right to explanation | Art. 86 | `explain()` generates deterministic, reproducible compliance documents from `why()` results |
|
|
441
|
+
| Monitoring | Art. 72 | `on_event` / `on_event_async` callbacks stream audit events to SIEM/monitoring systems |
|
|
442
|
+
|
|
443
|
+
**Article 86 — Right to Explanation:**
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
from flowscript_agents import Memory, explain
|
|
447
|
+
|
|
448
|
+
mem = Memory.load_or_create("agent.json")
|
|
449
|
+
# ... agent builds reasoning graph during normal work ...
|
|
450
|
+
|
|
451
|
+
result = mem.query.why(decision_node.id)
|
|
452
|
+
print(explain(result, subject="Applicant ID #4821", audience="legal"))
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
```
|
|
456
|
+
AUTOMATED DECISION EXPLANATION
|
|
457
|
+
Issued under EU AI Act Article 86 (Right to Explanation)
|
|
458
|
+
|
|
459
|
+
Subject: Applicant ID #4821
|
|
460
|
+
Decision: loan application denied
|
|
461
|
+
Causal chain depth: 3 steps
|
|
462
|
+
|
|
463
|
+
CAUSAL SEQUENCE
|
|
464
|
+
Step 1 (foundational factor): applicant income below minimum requirement
|
|
465
|
+
Step 2 (derives from): debt-to-income ratio exceeds policy limit
|
|
466
|
+
Step 3 (derives from): risk assessment: HIGH
|
|
467
|
+
Outcome: loan application denied
|
|
468
|
+
|
|
469
|
+
CERTIFICATION
|
|
470
|
+
Generated: 2026-03-27T19:46:46Z
|
|
471
|
+
This explanation is generated from a deterministic causal reasoning graph
|
|
472
|
+
maintained by FlowScript. The complete hash-chained audit trail is available
|
|
473
|
+
upon request and can be verified against the original reasoning record.
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Three audience modes: `"general"` (plain English for affected individuals), `"legal"` (formal compliance language with Article 86 citation and hash-chain reference), `"technical"` (structured debug output). No LLM in the loop — the explanation is deterministic and reproducible.
|
|
477
|
+
|
|
478
|
+
**Via MCP:** call the `explain_decision` tool with a node ID or content search. Same deterministic output, accessible from Claude Code, Cursor, or any MCP client.
|
|
479
|
+
|
|
480
|
+
**Via framework adapters:** access the underlying Memory through `adapter.memory`:
|
|
481
|
+
|
|
482
|
+
```python
|
|
483
|
+
from flowscript_agents import explain
|
|
484
|
+
result = adapter.memory.query.why(node_id)
|
|
485
|
+
text = explain(result, audience="legal")
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**Enforcement begins August 2026.** Audit trails can't be backdated. Organizations using FlowScript today have unbroken reasoning records from day one. You can turn on logging tomorrow — you can't manufacture the last 18 months of decision provenance.
|
|
489
|
+
|
|
490
|
+
**Architecture:**
|
|
491
|
+
|
|
492
|
+
```
|
|
493
|
+
┌─────────────────────────────────────────────────────┐
|
|
494
|
+
│ Your Agent Framework (LangGraph, CrewAI, ADK, ...) │
|
|
495
|
+
├─────────────────────────────────────────────────────┤
|
|
496
|
+
│ FlowScript SDK — Typed Reasoning Layer │
|
|
497
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │
|
|
498
|
+
│ │ Memory │ │ Queries │ │ Audit Trail │ │
|
|
499
|
+
│ │ (graph) │ │ (5 ops) │ │ (SHA-256 hash chain) │ │
|
|
500
|
+
│ │ explain() │ │ on_event_async │ │
|
|
501
|
+
│ └──────────┘ └──────────┘ └──────────────────────┘ │
|
|
502
|
+
├─────────────────────────────────────────────────────┤
|
|
503
|
+
│ Your Storage (files, database, cloud) │
|
|
504
|
+
└─────────────────────────────────────────────────────┘
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
FlowScript doesn't replace your stack. It sits between your agent framework and your storage, adding typed reasoning and audit to whatever you already use.
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Security
|
|
512
|
+
|
|
513
|
+
Three independent CVE clusters dropped in the same week — MCPwned (SSRF via MCP trust boundaries), ClawHub (1,100+ malicious skills in agent marketplaces), and ClawJacked (CVE-2026-25253, CVSS 8.8, 15,200 affected instances). All share the same structural root cause: **unvalidated content flowing through agent invocation paths.**
|
|
514
|
+
|
|
515
|
+
You can't patch this at the application layer. The invocation path itself is untyped.
|
|
516
|
+
|
|
517
|
+
FlowScript's typed intermediate representation doesn't prevent every attack class — SSRF and transport-layer poisoning need different tools. What it makes structurally impossible is the deeper problem: **reasoning corruption.** Untraceable decisions, silent contradictions, unaudited state changes — these can't exist in a well-typed FlowScript graph. The type system makes them unrepresentable.
|
|
518
|
+
|
|
519
|
+
This is the same architectural insight behind [CHERI](https://www.cl.cam.ac.uk/research/security/ctsrd/cheri/): Cambridge proved that making unsafe states hardware-inexpressible eliminates 70% of memory-safety CVEs — structural prevention beats behavioral detection. FlowScript applies this insight at the cognitive layer. The enforcement boundary is different (SDK type system vs. hardware capabilities), but the principle is identical: make the violation unrepresentable rather than hoping to catch it after the fact.
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## What FlowScript Actually Is
|
|
524
|
+
|
|
525
|
+
If you've read this far, you're ready for the deeper structure.
|
|
526
|
+
|
|
527
|
+
The five queries and the audit trail are what FlowScript *does*. Here's what it *is*, and why it matters beyond any single application.
|
|
528
|
+
|
|
529
|
+
**Musical notation didn't record what musicians were already playing.** Before staff notation, European music was monophonic — single melodies, loosely coordinated. Notation made polyphony possible. Bach's fugues are literally unthinkable without it — not "hard to remember," but impossible to *compose*, because the simultaneous interaction of independent voices requires a representational system precise enough to reason about counterpoint.
|
|
530
|
+
|
|
531
|
+
Notation expanded the space of possible musical thought.
|
|
532
|
+
|
|
533
|
+
**FlowScript does the same thing for AI cognition.** It doesn't record what agents are already thinking. It makes a new category of AI reasoning possible — the kind where you can have multiple reasoning chains interacting, where you can query across causal paths, where contradictions become structured tensions instead of silent overwrites. This category of reasoning is impossible in the vector-search paradigm because vector search has no representation for *why*.
|
|
534
|
+
|
|
535
|
+
**FlowScript's type system makes malformed reasoning unrepresentable.** Every decision traces to a question through alternatives. Every contradiction becomes a typed tension with a named axis. Every state change gets an audited reason. These constraints give FlowScript a property familiar from [type theory](https://en.wikipedia.org/wiki/Type_theory): well-typedness implies safety. A well-formed FlowScript graph can always be queried — no stuck states, no silent contradictions, no untraceable decisions. The type structure doesn't constitute formal proofs in the Curry-Howard sense, but it does what good type systems do: make certain classes of malformed state structurally unrepresentable.
|
|
536
|
+
|
|
537
|
+
**Compression reveals structure that verbosity hides.** When you force AI reasoning through typed encoding, you force the extraction of structure that would otherwise remain implicit in natural language. This maps to a deep result in information theory: the minimum description of a dataset *is* its structure. Optimal compression and genuine understanding are the same operation. FlowScript's temporal tiers — where knowledge graduates from observation to principle through use — implement this: each compression cycle distills signal from noise, and the resulting structure is *more useful* than the verbose original.
|
|
538
|
+
|
|
539
|
+
**The metacognitive loop.** When an AI agent writes FlowScript, queries its own reasoning graph, discovers tensions or gaps, and generates new reasoning informed by that structure — it's not just remembering. It's *reasoning about its own reasoning* through a typed, queryable substrate. This is metacognition, and it's the category of thought that FlowScript makes possible that no vector store can touch.
|
|
540
|
+
|
|
541
|
+
**This isn't just good engineering — there's math behind it.** [Recent work in formal epistemology](https://arxiv.org/abs/2603.17244) applied AGM belief revision postulates — the mathematical framework for rational belief change — and proved that deletion violates core rationality requirements. When you delete a contradicted memory, you destroy information that the formal framework says a rational agent must preserve. FlowScript's RELATE > DELETE approach satisfies these postulates: preserve contradictions as tensions, maintain provenance chains, never destroy reasoning history. The formal result says deletion is irrational. FlowScript is the implementation that takes that seriously.
|
|
542
|
+
|
|
543
|
+
**FlowScript is infrastructure.** Not a tool. Not a framework. Not a compliance product. Infrastructure — like SQL gave us queryable data, TCP/IP gave us addressable communication, and Git gave us trackable changes. FlowScript gives AI agents queryable reasoning. Everything else — compliance, security, memory, observability — is an application of that infrastructure.
|
|
544
|
+
|
|
545
|
+
The applications are what you install FlowScript for. The infrastructure is why it matters.
|
|
407
546
|
|
|
408
547
|
---
|
|
409
548
|
|
|
@@ -426,4 +565,12 @@ Under the hood: a local semantic graph with typed nodes, typed relationships, an
|
|
|
426
565
|
|
|
427
566
|
---
|
|
428
567
|
|
|
568
|
+
## Known Limitations
|
|
569
|
+
|
|
570
|
+
- **Single-writer audit**: Two processes writing the same audit file will corrupt the hash chain. One writer per memory file.
|
|
571
|
+
- **File-based persistence**: JSON file storage via `save()`. For shared or multi-agent setups, use separate memory files per agent.
|
|
572
|
+
- **Extraction quality varies by model**: gpt-4o-mini handles most content well. Complex contradictory content may produce fallback ADDs instead of RELATE operations. Results improve with larger models.
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
429
576
|
MIT. Built by [Phillip Clapham](https://phillipclapham.com).
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">flowscript-agents</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center"><strong>
|
|
7
|
+
<p align="center"><strong>Your AI agents make decisions they can't explain. FlowScript makes those decisions queryable.</strong></p>
|
|
8
|
+
|
|
9
|
+
<p align="center">Drop-in adapters for 9 agent frameworks. Plain text in, typed reasoning queries out.<br>Hash-chained audit trail. Structural compliance. MIT licensed.</p>
|
|
8
10
|
|
|
9
11
|
[](https://github.com/phillipclapham/flowscript-agents) [](https://pypi.org/project/flowscript-agents/) [](LICENSE) [](https://pypi.org/project/flowscript-agents/)
|
|
10
12
|
|
|
11
13
|
---
|
|
12
14
|
|
|
13
|
-
Plain text in. Typed reasoning queries out:
|
|
14
|
-
|
|
15
15
|
```python
|
|
16
16
|
from openai import OpenAI
|
|
17
17
|
from flowscript_agents import UnifiedMemory
|
|
@@ -28,18 +28,19 @@ with UnifiedMemory("agent-memory.json", embedder=OpenAIEmbeddings(), llm=llm) as
|
|
|
28
28
|
mem.add("PostgreSQL gives us rich queries at $15/month but read latency is 10-50ms")
|
|
29
29
|
|
|
30
30
|
tensions = mem.memory.query.tensions()
|
|
31
|
-
# →
|
|
32
|
-
#
|
|
33
|
-
# and preserved both sides as a queryable tension
|
|
31
|
+
# → The LLM detected the $200/month vs $50/month contradiction
|
|
32
|
+
# → and preserved both sides as a queryable tension — not a deletion
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
# Pick any node to trace its reasoning:
|
|
35
|
+
first_node = mem.memory.nodes[0]
|
|
36
|
+
why = mem.memory.query.why(first_node.id)
|
|
37
|
+
# → Full causal chain backward from any decision
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
# →
|
|
39
|
+
blocked = mem.memory.query.blocked()
|
|
40
|
+
# → What's stuck + downstream impact
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
Five queries that no vector store can answer — `why()`, `tensions()`, `blocked()`, `alternatives()`, `whatIf()` — over a typed
|
|
43
|
+
Five queries that no vector store can answer — `why()`, `tensions()`, `blocked()`, `alternatives()`, `whatIf()` — over a typed reasoning graph. Drop-in adapters for [9 agent frameworks](#works-with-your-stack). Hash-chained audit trail. And when memories contradict, we don't delete — we create a queryable *tension*.
|
|
43
44
|
|
|
44
45
|
<p align="center">
|
|
45
46
|
<img src="docs/flowscript-demo.png" alt="FlowScript — editor with .fs syntax, D3 reasoning graph, and tensions query results" width="800">
|
|
@@ -49,11 +50,11 @@ Five queries that no vector store can answer — `why()`, `tensions()`, `blocked
|
|
|
49
50
|
|
|
50
51
|
## Why FlowScript
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
Every agent framework gives AI agents memory. None of them make that memory queryable.
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
Vector stores retrieve content that looks similar. That's useful, but it's not reasoning. When an auditor asks "why did your agent deny that claim?" or a developer asks "what breaks if we change this decision?" — similarity search returns a guess. FlowScript returns the actual typed reasoning chain.
|
|
55
56
|
|
|
56
|
-
FlowScript sits above your memory store, not instead of it.
|
|
57
|
+
This is the gap researchers call "[strategic blindness](https://arxiv.org/abs/2603.18718)" — memory that tracks content without tracking reasoning. FlowScript sits above your memory store, not instead of it. Mem0, LangGraph checkpointers, Google Memory Bank — they remember what your agent stored. FlowScript remembers *why it decided*, what it traded off, and what breaks if you change your mind.
|
|
57
58
|
|
|
58
59
|
---
|
|
59
60
|
|
|
@@ -273,7 +274,22 @@ result = Memory.verify_audit("agent.audit.jsonl")
|
|
|
273
274
|
# → AuditVerifyResult(valid=True, total_entries=42, files_verified=1)
|
|
274
275
|
```
|
|
275
276
|
|
|
276
|
-
Framework attribution is automatic — every audit entry records which adapter triggered it. Query by time range, event type, adapter, or session. Rotation with gzip compression.
|
|
277
|
+
Framework attribution is automatic — every audit entry records which adapter triggered it. Query by time range, event type, adapter, or session. Rotation with gzip compression.
|
|
278
|
+
|
|
279
|
+
**SIEM integration:** `on_event` callback fires for every audit entry. Use `on_event_async=True` for non-blocking dispatch — slow webhooks won't block agent operations:
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
from flowscript_agents import Memory, MemoryOptions, AuditConfig
|
|
283
|
+
|
|
284
|
+
def send_to_siem(entry):
|
|
285
|
+
requests.post("https://siem.example.com/ingest", json=entry)
|
|
286
|
+
|
|
287
|
+
mem = Memory.load_or_create("agent.json", options=MemoryOptions(
|
|
288
|
+
audit=AuditConfig(on_event=send_to_siem, on_event_async=True)
|
|
289
|
+
))
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Events are dispatched in order (single worker thread). Callback failures log to stderr but never block audit writes. Call `writer.close()` for graceful shutdown of in-flight callbacks.
|
|
277
293
|
|
|
278
294
|
---
|
|
279
295
|
|
|
@@ -301,7 +317,7 @@ Every query touches returned nodes — knowledge that keeps getting queried earn
|
|
|
301
317
|
**For SDK users** — adapters support context managers that auto-wrap:
|
|
302
318
|
|
|
303
319
|
```python
|
|
304
|
-
from flowscript_agents.
|
|
320
|
+
from flowscript_agents.langgraph import FlowScriptStore
|
|
305
321
|
|
|
306
322
|
with FlowScriptStore("agent-memory.json") as store:
|
|
307
323
|
# work happens — all mutations auto-save
|
|
@@ -327,6 +343,8 @@ Both the Python and [TypeScript](https://www.npmjs.com/package/flowscript-core)
|
|
|
327
343
|
|
|
328
344
|
## Comparison
|
|
329
345
|
|
|
346
|
+
Every agent framework gives AI agents memory. None make that memory auditable, typed, or queryable at the reasoning level. That's the layer FlowScript occupies.
|
|
347
|
+
|
|
330
348
|
| | FlowScript | Mem0 | Vector stores |
|
|
331
349
|
|:---|:---|:---|:---|
|
|
332
350
|
| Find similar content | Vector search | Vector search | Vector search |
|
|
@@ -339,7 +357,128 @@ Both the Python and [TypeScript](https://www.npmjs.com/package/flowscript-core)
|
|
|
339
357
|
| Temporal graduation | Automatic 4-tier | — | — |
|
|
340
358
|
| Token budgeting | 4 strategies | — | — |
|
|
341
359
|
|
|
342
|
-
Under the hood: a local semantic graph with typed nodes, typed relationships, and typed states. Queries traverse structure — no embeddings required, no LLM calls, no network. Sub-ms on project-scale graphs.
|
|
360
|
+
Under the hood: a local semantic graph with typed nodes, typed relationships, and typed states. Queries traverse structure — no embeddings required, no LLM calls, no network. Sub-ms on project-scale graphs.
|
|
361
|
+
|
|
362
|
+
Vector search and reasoning queries are orthogonal — use both. Mem0 for retrieval, FlowScript for reasoning. They're different architectural layers.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Enterprise & Compliance
|
|
367
|
+
|
|
368
|
+
FlowScript's typed reasoning chains are also compliance-ready audit infrastructure. This isn't a separate product — it's a structural property of how FlowScript works.
|
|
369
|
+
|
|
370
|
+
**EU AI Act coverage:**
|
|
371
|
+
|
|
372
|
+
| Requirement | Article | How FlowScript satisfies it |
|
|
373
|
+
|:---|:---|:---|
|
|
374
|
+
| Record-keeping | Art. 12 | Hash-chained audit trail, append-only, tamper-evident, 7yr default retention |
|
|
375
|
+
| Transparency | Art. 13 | `why()` queries return typed causal chains — not reconstructions, actual reasoning records |
|
|
376
|
+
| Right to explanation | Art. 86 | `explain()` generates deterministic, reproducible compliance documents from `why()` results |
|
|
377
|
+
| Monitoring | Art. 72 | `on_event` / `on_event_async` callbacks stream audit events to SIEM/monitoring systems |
|
|
378
|
+
|
|
379
|
+
**Article 86 — Right to Explanation:**
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
from flowscript_agents import Memory, explain
|
|
383
|
+
|
|
384
|
+
mem = Memory.load_or_create("agent.json")
|
|
385
|
+
# ... agent builds reasoning graph during normal work ...
|
|
386
|
+
|
|
387
|
+
result = mem.query.why(decision_node.id)
|
|
388
|
+
print(explain(result, subject="Applicant ID #4821", audience="legal"))
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
```
|
|
392
|
+
AUTOMATED DECISION EXPLANATION
|
|
393
|
+
Issued under EU AI Act Article 86 (Right to Explanation)
|
|
394
|
+
|
|
395
|
+
Subject: Applicant ID #4821
|
|
396
|
+
Decision: loan application denied
|
|
397
|
+
Causal chain depth: 3 steps
|
|
398
|
+
|
|
399
|
+
CAUSAL SEQUENCE
|
|
400
|
+
Step 1 (foundational factor): applicant income below minimum requirement
|
|
401
|
+
Step 2 (derives from): debt-to-income ratio exceeds policy limit
|
|
402
|
+
Step 3 (derives from): risk assessment: HIGH
|
|
403
|
+
Outcome: loan application denied
|
|
404
|
+
|
|
405
|
+
CERTIFICATION
|
|
406
|
+
Generated: 2026-03-27T19:46:46Z
|
|
407
|
+
This explanation is generated from a deterministic causal reasoning graph
|
|
408
|
+
maintained by FlowScript. The complete hash-chained audit trail is available
|
|
409
|
+
upon request and can be verified against the original reasoning record.
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Three audience modes: `"general"` (plain English for affected individuals), `"legal"` (formal compliance language with Article 86 citation and hash-chain reference), `"technical"` (structured debug output). No LLM in the loop — the explanation is deterministic and reproducible.
|
|
413
|
+
|
|
414
|
+
**Via MCP:** call the `explain_decision` tool with a node ID or content search. Same deterministic output, accessible from Claude Code, Cursor, or any MCP client.
|
|
415
|
+
|
|
416
|
+
**Via framework adapters:** access the underlying Memory through `adapter.memory`:
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
from flowscript_agents import explain
|
|
420
|
+
result = adapter.memory.query.why(node_id)
|
|
421
|
+
text = explain(result, audience="legal")
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Enforcement begins August 2026.** Audit trails can't be backdated. Organizations using FlowScript today have unbroken reasoning records from day one. You can turn on logging tomorrow — you can't manufacture the last 18 months of decision provenance.
|
|
425
|
+
|
|
426
|
+
**Architecture:**
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
┌─────────────────────────────────────────────────────┐
|
|
430
|
+
│ Your Agent Framework (LangGraph, CrewAI, ADK, ...) │
|
|
431
|
+
├─────────────────────────────────────────────────────┤
|
|
432
|
+
│ FlowScript SDK — Typed Reasoning Layer │
|
|
433
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │
|
|
434
|
+
│ │ Memory │ │ Queries │ │ Audit Trail │ │
|
|
435
|
+
│ │ (graph) │ │ (5 ops) │ │ (SHA-256 hash chain) │ │
|
|
436
|
+
│ │ explain() │ │ on_event_async │ │
|
|
437
|
+
│ └──────────┘ └──────────┘ └──────────────────────┘ │
|
|
438
|
+
├─────────────────────────────────────────────────────┤
|
|
439
|
+
│ Your Storage (files, database, cloud) │
|
|
440
|
+
└─────────────────────────────────────────────────────┘
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
FlowScript doesn't replace your stack. It sits between your agent framework and your storage, adding typed reasoning and audit to whatever you already use.
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## Security
|
|
448
|
+
|
|
449
|
+
Three independent CVE clusters dropped in the same week — MCPwned (SSRF via MCP trust boundaries), ClawHub (1,100+ malicious skills in agent marketplaces), and ClawJacked (CVE-2026-25253, CVSS 8.8, 15,200 affected instances). All share the same structural root cause: **unvalidated content flowing through agent invocation paths.**
|
|
450
|
+
|
|
451
|
+
You can't patch this at the application layer. The invocation path itself is untyped.
|
|
452
|
+
|
|
453
|
+
FlowScript's typed intermediate representation doesn't prevent every attack class — SSRF and transport-layer poisoning need different tools. What it makes structurally impossible is the deeper problem: **reasoning corruption.** Untraceable decisions, silent contradictions, unaudited state changes — these can't exist in a well-typed FlowScript graph. The type system makes them unrepresentable.
|
|
454
|
+
|
|
455
|
+
This is the same architectural insight behind [CHERI](https://www.cl.cam.ac.uk/research/security/ctsrd/cheri/): Cambridge proved that making unsafe states hardware-inexpressible eliminates 70% of memory-safety CVEs — structural prevention beats behavioral detection. FlowScript applies this insight at the cognitive layer. The enforcement boundary is different (SDK type system vs. hardware capabilities), but the principle is identical: make the violation unrepresentable rather than hoping to catch it after the fact.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## What FlowScript Actually Is
|
|
460
|
+
|
|
461
|
+
If you've read this far, you're ready for the deeper structure.
|
|
462
|
+
|
|
463
|
+
The five queries and the audit trail are what FlowScript *does*. Here's what it *is*, and why it matters beyond any single application.
|
|
464
|
+
|
|
465
|
+
**Musical notation didn't record what musicians were already playing.** Before staff notation, European music was monophonic — single melodies, loosely coordinated. Notation made polyphony possible. Bach's fugues are literally unthinkable without it — not "hard to remember," but impossible to *compose*, because the simultaneous interaction of independent voices requires a representational system precise enough to reason about counterpoint.
|
|
466
|
+
|
|
467
|
+
Notation expanded the space of possible musical thought.
|
|
468
|
+
|
|
469
|
+
**FlowScript does the same thing for AI cognition.** It doesn't record what agents are already thinking. It makes a new category of AI reasoning possible — the kind where you can have multiple reasoning chains interacting, where you can query across causal paths, where contradictions become structured tensions instead of silent overwrites. This category of reasoning is impossible in the vector-search paradigm because vector search has no representation for *why*.
|
|
470
|
+
|
|
471
|
+
**FlowScript's type system makes malformed reasoning unrepresentable.** Every decision traces to a question through alternatives. Every contradiction becomes a typed tension with a named axis. Every state change gets an audited reason. These constraints give FlowScript a property familiar from [type theory](https://en.wikipedia.org/wiki/Type_theory): well-typedness implies safety. A well-formed FlowScript graph can always be queried — no stuck states, no silent contradictions, no untraceable decisions. The type structure doesn't constitute formal proofs in the Curry-Howard sense, but it does what good type systems do: make certain classes of malformed state structurally unrepresentable.
|
|
472
|
+
|
|
473
|
+
**Compression reveals structure that verbosity hides.** When you force AI reasoning through typed encoding, you force the extraction of structure that would otherwise remain implicit in natural language. This maps to a deep result in information theory: the minimum description of a dataset *is* its structure. Optimal compression and genuine understanding are the same operation. FlowScript's temporal tiers — where knowledge graduates from observation to principle through use — implement this: each compression cycle distills signal from noise, and the resulting structure is *more useful* than the verbose original.
|
|
474
|
+
|
|
475
|
+
**The metacognitive loop.** When an AI agent writes FlowScript, queries its own reasoning graph, discovers tensions or gaps, and generates new reasoning informed by that structure — it's not just remembering. It's *reasoning about its own reasoning* through a typed, queryable substrate. This is metacognition, and it's the category of thought that FlowScript makes possible that no vector store can touch.
|
|
476
|
+
|
|
477
|
+
**This isn't just good engineering — there's math behind it.** [Recent work in formal epistemology](https://arxiv.org/abs/2603.17244) applied AGM belief revision postulates — the mathematical framework for rational belief change — and proved that deletion violates core rationality requirements. When you delete a contradicted memory, you destroy information that the formal framework says a rational agent must preserve. FlowScript's RELATE > DELETE approach satisfies these postulates: preserve contradictions as tensions, maintain provenance chains, never destroy reasoning history. The formal result says deletion is irrational. FlowScript is the implementation that takes that seriously.
|
|
478
|
+
|
|
479
|
+
**FlowScript is infrastructure.** Not a tool. Not a framework. Not a compliance product. Infrastructure — like SQL gave us queryable data, TCP/IP gave us addressable communication, and Git gave us trackable changes. FlowScript gives AI agents queryable reasoning. Everything else — compliance, security, memory, observability — is an application of that infrastructure.
|
|
480
|
+
|
|
481
|
+
The applications are what you install FlowScript for. The infrastructure is why it matters.
|
|
343
482
|
|
|
344
483
|
---
|
|
345
484
|
|
|
@@ -362,4 +501,12 @@ Under the hood: a local semantic graph with typed nodes, typed relationships, an
|
|
|
362
501
|
|
|
363
502
|
---
|
|
364
503
|
|
|
504
|
+
## Known Limitations
|
|
505
|
+
|
|
506
|
+
- **Single-writer audit**: Two processes writing the same audit file will corrupt the hash chain. One writer per memory file.
|
|
507
|
+
- **File-based persistence**: JSON file storage via `save()`. For shared or multi-agent setups, use separate memory files per agent.
|
|
508
|
+
- **Extraction quality varies by model**: gpt-4o-mini handles most content well. Complex contradictory content may produce fallback ADDs instead of RELATE operations. Results improve with larger models.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
365
512
|
MIT. Built by [Phillip Clapham](https://phillipclapham.com).
|
|
@@ -27,6 +27,7 @@ Usage:
|
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
from .audit import AuditConfig, AuditQueryResult, AuditVerifyResult
|
|
30
|
+
from .cloud import CloudClient, CloudFlushResult, CloudWitness
|
|
30
31
|
from .memory import (
|
|
31
32
|
Memory,
|
|
32
33
|
MemoryOptions,
|
|
@@ -42,12 +43,17 @@ from .memory import (
|
|
|
42
43
|
SessionWrapResult,
|
|
43
44
|
)
|
|
44
45
|
from .unified import UnifiedMemory
|
|
46
|
+
from .explain import explain
|
|
45
47
|
|
|
46
|
-
__version__ = "0.2.
|
|
48
|
+
__version__ = "0.2.8"
|
|
47
49
|
__all__ = [
|
|
50
|
+
"explain",
|
|
48
51
|
"AuditConfig",
|
|
49
52
|
"AuditQueryResult",
|
|
50
53
|
"AuditVerifyResult",
|
|
54
|
+
"CloudClient",
|
|
55
|
+
"CloudFlushResult",
|
|
56
|
+
"CloudWitness",
|
|
51
57
|
"Memory",
|
|
52
58
|
"MemoryOptions",
|
|
53
59
|
"NodeRef",
|
|
@@ -20,11 +20,13 @@ File layout:
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import gzip
|
|
23
|
+
import atexit
|
|
23
24
|
import hashlib
|
|
24
25
|
import json
|
|
25
26
|
import os
|
|
26
27
|
import shutil
|
|
27
28
|
import sys
|
|
29
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
28
30
|
from dataclasses import dataclass, field
|
|
29
31
|
from datetime import datetime, timezone
|
|
30
32
|
from pathlib import Path
|
|
@@ -58,6 +60,10 @@ class AuditConfig:
|
|
|
58
60
|
full entry dict AFTER disk write. Callback failure never blocks
|
|
59
61
|
audit persistence. Use for SIEM integration, Observatory, or custom
|
|
60
62
|
monitoring.
|
|
63
|
+
on_event_async: If True, fire on_event in a background thread so slow
|
|
64
|
+
webhooks or network I/O don't block agent operations. Events are
|
|
65
|
+
dispatched in order (single worker thread). Callback errors are
|
|
66
|
+
logged to stderr but never propagate. Default False (synchronous).
|
|
61
67
|
"""
|
|
62
68
|
|
|
63
69
|
rotation: str = "monthly"
|
|
@@ -67,6 +73,7 @@ class AuditConfig:
|
|
|
67
73
|
verbosity: str = "standard"
|
|
68
74
|
encryption: Literal["none", "aes-256-gcm"] = "none"
|
|
69
75
|
on_event: Optional[Callable[[dict[str, Any]], None]] = None
|
|
76
|
+
on_event_async: bool = False
|
|
70
77
|
|
|
71
78
|
def __post_init__(self) -> None:
|
|
72
79
|
if self.encryption != "none":
|
|
@@ -147,6 +154,34 @@ class AuditWriter:
|
|
|
147
154
|
self._current_period = ""
|
|
148
155
|
self._initialized = False
|
|
149
156
|
self._in_cleanup = False # Guard against recursive cleanup → write → rotate → cleanup
|
|
157
|
+
self._executor: Optional[ThreadPoolExecutor] = None
|
|
158
|
+
|
|
159
|
+
def _get_executor(self) -> ThreadPoolExecutor:
|
|
160
|
+
"""Lazily create the background executor for async on_event dispatch."""
|
|
161
|
+
if self._executor is None:
|
|
162
|
+
self._executor = ThreadPoolExecutor(
|
|
163
|
+
max_workers=1,
|
|
164
|
+
thread_name_prefix="audit_on_event",
|
|
165
|
+
)
|
|
166
|
+
atexit.register(self.close)
|
|
167
|
+
return self._executor
|
|
168
|
+
|
|
169
|
+
def close(self) -> None:
|
|
170
|
+
"""Shutdown the async executor, waiting for all pending callbacks to complete.
|
|
171
|
+
|
|
172
|
+
Call this to ensure in-flight on_event callbacks (e.g. webhook POSTs)
|
|
173
|
+
have finished before process exit. Safe to call multiple times.
|
|
174
|
+
"""
|
|
175
|
+
if self._executor is not None:
|
|
176
|
+
self._executor.shutdown(wait=True)
|
|
177
|
+
self._executor = None
|
|
178
|
+
|
|
179
|
+
def _fire_on_event(self, entry: dict[str, Any]) -> None:
|
|
180
|
+
"""Fire the on_event callback with error isolation."""
|
|
181
|
+
try:
|
|
182
|
+
self._config.on_event(entry) # type: ignore[misc]
|
|
183
|
+
except Exception as e:
|
|
184
|
+
print(f"AuditWriter: on_event callback failed: {e}", file=sys.stderr)
|
|
150
185
|
|
|
151
186
|
# -------------------------------------------------------------------------
|
|
152
187
|
# Path derivation
|
|
@@ -371,10 +406,10 @@ class AuditWriter:
|
|
|
371
406
|
|
|
372
407
|
# Fire on_event callback (failure must never block audit, but log to stderr)
|
|
373
408
|
if self._config.on_event:
|
|
374
|
-
|
|
375
|
-
self.
|
|
376
|
-
|
|
377
|
-
|
|
409
|
+
if self._config.on_event_async:
|
|
410
|
+
self._get_executor().submit(self._fire_on_event, entry)
|
|
411
|
+
else:
|
|
412
|
+
self._fire_on_event(entry)
|
|
378
413
|
|
|
379
414
|
return entry
|
|
380
415
|
|