flowscript-agents 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/PKG-INFO +40 -12
  2. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/README.md +39 -11
  3. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/docs/audit-trail.md +13 -2
  4. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/audit.py +15 -3
  5. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/camel_ai.py +10 -3
  6. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/crewai.py +9 -3
  7. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/extract.py +68 -2
  8. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/google_adk.py +10 -3
  9. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/haystack.py +9 -3
  10. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/langgraph.py +9 -3
  11. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/llamaindex.py +9 -3
  12. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/mcp.py +118 -1
  13. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/memory.py +19 -2
  14. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/openai_agents.py +9 -3
  15. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/pydantic_ai.py +9 -3
  16. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/smolagents.py +9 -3
  17. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/pyproject.toml +1 -1
  18. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_mcp.py +3 -2
  19. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/.gitignore +0 -0
  20. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/AUDIT_TRAIL_DESIGN.md +0 -0
  21. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/docs/adapters.md +0 -0
  22. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/docs/api-reference.md +0 -0
  23. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/docs/flowscript-demo.png +0 -0
  24. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/docs/lifecycle.md +0 -0
  25. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/examples/CLAUDE.md.example +0 -0
  26. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/examples/langgraph_live_test.py +0 -0
  27. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/examples/temporal_e2e_test.py +0 -0
  28. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/__init__.py +0 -0
  29. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/__init__.py +0 -0
  30. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/_utils.py +0 -0
  31. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/consolidate.py +0 -0
  32. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/index.py +0 -0
  33. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/providers.py +0 -0
  34. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/search.py +0 -0
  35. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/query.py +0 -0
  36. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/types.py +0 -0
  37. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/unified.py +0 -0
  38. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/scripts/validate_dedup_threshold.py +0 -0
  39. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/conftest.py +0 -0
  40. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_audit.py +0 -0
  41. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_camel_ai.py +0 -0
  42. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_consolidation.py +0 -0
  43. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_crewai.py +0 -0
  44. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_embeddings.py +0 -0
  45. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_google_adk.py +0 -0
  46. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_haystack.py +0 -0
  47. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_langgraph.py +0 -0
  48. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_llamaindex.py +0 -0
  49. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_memory.py +0 -0
  50. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_openai_agents.py +0 -0
  51. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_pydantic_ai.py +0 -0
  52. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_smolagents.py +0 -0
  53. {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/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.0
3
+ Version: 0.2.1
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, CAMEL-AI, and Vercel AI SDK.
5
5
  Project-URL: Homepage, https://flowscript.org
6
6
  Project-URL: Repository, https://github.com/phillipclapham/flowscript-agents
@@ -83,19 +83,20 @@ llm = lambda prompt: (client.chat.completions.create(
83
83
  ).choices[0].message.content or "")
84
84
 
85
85
  with UnifiedMemory("agent-memory.json", embedder=OpenAIEmbeddings(), llm=llm) as mem:
86
- mem.add("We chose Redis for session storage — sub-ms reads are critical for UX")
87
- mem.add("Redis cluster costs are killing us at $200/mo for 3 nodes")
88
- mem.add("Decided: switch to PostgreSQL handles our scale at $15/mo")
86
+ mem.add("Redis gives sub-ms reads which is critical for our UX requirements")
87
+ mem.add("Redis clustering costs $200/month which exceeds our infrastructure budget of $50/month")
88
+ mem.add("PostgreSQL gives us rich queries at $15/month but read latency is 10-50ms")
89
89
 
90
- print(mem.memory.query.tensions())
91
- # → Tension: "sub-ms reads critical for UX" vs "cluster costs $200/mo"
92
- # axis: "performance vs cost"
90
+ tensions = mem.memory.query.tensions()
91
+ # → TensionsResult(1 tension, axes=['cost vs budget'])
92
+ # The LLM detected the $200/month vs $50/month contradiction
93
+ # and preserved both sides as a queryable tension
93
94
 
94
- print(mem.memory.query.blocked())
95
- # → what's stuck and why, with downstream impact
95
+ blocked = mem.memory.query.blocked()
96
+ # → BlockedResult(0 blockers)
96
97
 
97
- print(mem.memory.query.why(node_id))
98
- # → full causal chain backward from any decision
98
+ why = mem.memory.query.why(node_id)
99
+ # → CausalAncestry: full chain backward from any node
99
100
  ```
100
101
 
101
102
  Five queries that no vector store can answer — `why()`, `tensions()`, `blocked()`, `alternatives()`, `whatIf()` — over a typed semantic graph. Drop-in adapters for [9 agent frameworks](#works-with-your-stack). Hash-chained audit trail. And when memories contradict, we don't delete the old one — we create a queryable *tension*.
@@ -128,7 +129,9 @@ Five queries that no vector store can answer — `why()`, `tensions()`, `blocked
128
129
  pip install flowscript-agents
129
130
  ```
130
131
 
131
- Auto-detects your API key and configures the full stack vector search, typed extraction, and contradiction handling. Also supports `ANTHROPIC_API_KEY`. 11 tools, zero additional setup.
132
+ Auto-detects your API key and configures the full stack: vector search, typed extraction, and contradiction handling. Also supports `ANTHROPIC_API_KEY`. 13 reasoning tools.
133
+
134
+ **Then add the [CLAUDE.md snippet](examples/CLAUDE.md.example) to your project.** This is what turns tools into a workflow. It tells your agent *when* to record decisions, surface tensions before new choices, and check blockers at session start. Without it, the tools are available but passive. With it, your agent proactively tracks your project's reasoning.
132
135
 
133
136
  ### Python SDK
134
137
 
@@ -155,6 +158,31 @@ FlowScript operates at three levels. Pick where you start:
155
158
 
156
159
  ---
157
160
 
161
+ ## First 5 Minutes
162
+
163
+ With the MCP server running and the CLAUDE.md snippet in your project, try this conversation:
164
+
165
+ > "I need to decide between PostgreSQL and MongoDB for our user data. We need ACID compliance for payments but flexibility for user profiles."
166
+
167
+ Your agent stores the decision context, tradeoffs, and rationale automatically. Now introduce contradictory information:
168
+
169
+ > "Actually, I've been looking at DynamoDB. The scale requirements might matter more than I thought."
170
+
171
+ Now ask:
172
+
173
+ > "What tensions do we have in our architecture decisions?"
174
+
175
+ FlowScript preserved both perspectives (PostgreSQL's ACID compliance vs DynamoDB's scalability) as a queryable tension instead of deleting the first decision. That's what **RELATE > DELETE** means in practice.
176
+
177
+ After a few sessions, try:
178
+ - *"What's blocking our progress?"* surfaces blockers and their downstream impact
179
+ - *"Why did we choose PostgreSQL originally?"* traces the full causal chain
180
+ - *"What if we switch to DynamoDB?"* maps the downstream consequences
181
+
182
+ After 20 sessions, you have a curated knowledge base of your project's decisions, not a pile of notes. Knowledge that stays relevant graduates through temporal tiers. One-off observations fade naturally.
183
+
184
+ ---
185
+
158
186
  ## Works With Your Stack
159
187
 
160
188
  Drop-in adapters that implement your framework's native interface. Same API you already use — plus `query.tensions()`.
@@ -19,19 +19,20 @@ llm = lambda prompt: (client.chat.completions.create(
19
19
  ).choices[0].message.content or "")
20
20
 
21
21
  with UnifiedMemory("agent-memory.json", embedder=OpenAIEmbeddings(), llm=llm) as mem:
22
- mem.add("We chose Redis for session storage — sub-ms reads are critical for UX")
23
- mem.add("Redis cluster costs are killing us at $200/mo for 3 nodes")
24
- mem.add("Decided: switch to PostgreSQL handles our scale at $15/mo")
22
+ mem.add("Redis gives sub-ms reads which is critical for our UX requirements")
23
+ mem.add("Redis clustering costs $200/month which exceeds our infrastructure budget of $50/month")
24
+ mem.add("PostgreSQL gives us rich queries at $15/month but read latency is 10-50ms")
25
25
 
26
- print(mem.memory.query.tensions())
27
- # → Tension: "sub-ms reads critical for UX" vs "cluster costs $200/mo"
28
- # axis: "performance vs cost"
26
+ tensions = mem.memory.query.tensions()
27
+ # → TensionsResult(1 tension, axes=['cost vs budget'])
28
+ # The LLM detected the $200/month vs $50/month contradiction
29
+ # and preserved both sides as a queryable tension
29
30
 
30
- print(mem.memory.query.blocked())
31
- # → what's stuck and why, with downstream impact
31
+ blocked = mem.memory.query.blocked()
32
+ # → BlockedResult(0 blockers)
32
33
 
33
- print(mem.memory.query.why(node_id))
34
- # → full causal chain backward from any decision
34
+ why = mem.memory.query.why(node_id)
35
+ # → CausalAncestry: full chain backward from any node
35
36
  ```
36
37
 
37
38
  Five queries that no vector store can answer — `why()`, `tensions()`, `blocked()`, `alternatives()`, `whatIf()` — over a typed semantic graph. Drop-in adapters for [9 agent frameworks](#works-with-your-stack). Hash-chained audit trail. And when memories contradict, we don't delete the old one — we create a queryable *tension*.
@@ -64,7 +65,9 @@ Five queries that no vector store can answer — `why()`, `tensions()`, `blocked
64
65
  pip install flowscript-agents
65
66
  ```
66
67
 
67
- Auto-detects your API key and configures the full stack vector search, typed extraction, and contradiction handling. Also supports `ANTHROPIC_API_KEY`. 11 tools, zero additional setup.
68
+ Auto-detects your API key and configures the full stack: vector search, typed extraction, and contradiction handling. Also supports `ANTHROPIC_API_KEY`. 13 reasoning tools.
69
+
70
+ **Then add the [CLAUDE.md snippet](examples/CLAUDE.md.example) to your project.** This is what turns tools into a workflow. It tells your agent *when* to record decisions, surface tensions before new choices, and check blockers at session start. Without it, the tools are available but passive. With it, your agent proactively tracks your project's reasoning.
68
71
 
69
72
  ### Python SDK
70
73
 
@@ -91,6 +94,31 @@ FlowScript operates at three levels. Pick where you start:
91
94
 
92
95
  ---
93
96
 
97
+ ## First 5 Minutes
98
+
99
+ With the MCP server running and the CLAUDE.md snippet in your project, try this conversation:
100
+
101
+ > "I need to decide between PostgreSQL and MongoDB for our user data. We need ACID compliance for payments but flexibility for user profiles."
102
+
103
+ Your agent stores the decision context, tradeoffs, and rationale automatically. Now introduce contradictory information:
104
+
105
+ > "Actually, I've been looking at DynamoDB. The scale requirements might matter more than I thought."
106
+
107
+ Now ask:
108
+
109
+ > "What tensions do we have in our architecture decisions?"
110
+
111
+ FlowScript preserved both perspectives (PostgreSQL's ACID compliance vs DynamoDB's scalability) as a queryable tension instead of deleting the first decision. That's what **RELATE > DELETE** means in practice.
112
+
113
+ After a few sessions, try:
114
+ - *"What's blocking our progress?"* surfaces blockers and their downstream impact
115
+ - *"Why did we choose PostgreSQL originally?"* traces the full causal chain
116
+ - *"What if we switch to DynamoDB?"* maps the downstream consequences
117
+
118
+ After 20 sessions, you have a curated knowledge base of your project's decisions, not a pile of notes. Knowledge that stays relevant graduates through temporal tiers. One-off observations fade naturally.
119
+
120
+ ---
121
+
94
122
  ## Works With Your Stack
95
123
 
96
124
  Drop-in adapters that implement your framework's native interface. Same API you already use — plus `query.tensions()`.
@@ -22,8 +22,10 @@ mem = Memory.load_or_create("agent-memory.json",
22
22
 
23
23
  ## Event Types
24
24
 
25
- ### Python (13 events)
26
- `node_create`, `node_update`, `node_merge`, `node_remove`, `relationship_create`, `state_change`, `graduation`, `prune`, `session_start`, `session_end`, `session_wrap`, `consolidation`, `audit_cleanup`
25
+ ### Python (15 events)
26
+ `node_create`, `update_node`, `update_node_merge`, `node_remove`, `relationship_create`, `state_change`, `graduation`, `prune`, `session_start`, `session_end`, `session_wrap`, `consolidation`, `transcript_extract`, `consolidation_batch`, `audit_cleanup`
27
+
28
+ The `transcript_extract` event fires after each auto-extraction call with stats (nodes extracted/created/deduplicated, type breakdown, node IDs). The `consolidation_batch` event fires after consolidation with full metrics (contested/updated/related/resolved counts, collision stats, health status).
27
29
 
28
30
  ### TypeScript (14 events)
29
31
  `node_create`, `relationship_create`, `state_change`, `modifier_add`, `session_start`, `session_end`, `session_wrap`, `graduation`, `prune`, `snapshot`, `restore`, `transcript_extract`, `budget_apply`, `audit_cleanup`
@@ -70,6 +72,15 @@ result = Memory.query_audit("agent-memory.audit.jsonl",
70
72
  # → AuditQueryResult(entries=[...], total_scanned=42, files_searched=1)
71
73
  ```
72
74
 
75
+ ## MCP Audit Tools
76
+
77
+ The Python MCP server exposes `query_audit` and `verify_audit` as tools (13 tools total). When configured with an `AuditConfig`, your agent can query and verify the audit trail through natural conversation:
78
+
79
+ - **`query_audit`** filters by time range, event types, node ID, session ID, adapter, and limit. Supports optional chain verification.
80
+ - **`verify_audit`** checks the full hash chain integrity and returns entry counts.
81
+
82
+ Both handle missing audit files gracefully (returns `valid: null` for verify, empty results for query).
83
+
73
84
  ## SIEM Integration
74
85
 
75
86
  Stream audit events to Datadog, Splunk, or any monitoring system via the `on_event` callback:
@@ -28,7 +28,7 @@ import sys
28
28
  from dataclasses import dataclass, field
29
29
  from datetime import datetime, timezone
30
30
  from pathlib import Path
31
- from typing import Any, Callable, Optional
31
+ from typing import Any, Callable, Literal, Optional
32
32
 
33
33
 
34
34
  # =============================================================================
@@ -50,6 +50,10 @@ class AuditConfig:
50
50
  for testing or when tamper-evidence is not needed.
51
51
  verbosity: "standard" (default) = mutation events only. "full" =
52
52
  mutations + read/query access events (for HIPAA access auditing).
53
+ encryption: "none" (default) or "aes-256-gcm". Encryption at rest for
54
+ audit trail files. NOT YET IMPLEMENTED — v2 commitment for SOC2/
55
+ enterprise compliance. Setting to anything other than "none" raises
56
+ NotImplementedError.
53
57
  on_event: Optional callback invoked for every audit entry. Receives the
54
58
  full entry dict AFTER disk write. Callback failure never blocks
55
59
  audit persistence. Use for SIEM integration, Observatory, or custom
@@ -61,8 +65,16 @@ class AuditConfig:
61
65
  retention_months: Optional[int] = 84
62
66
  hash_chain: bool = True
63
67
  verbosity: str = "standard"
68
+ encryption: Literal["none", "aes-256-gcm"] = "none"
64
69
  on_event: Optional[Callable[[dict[str, Any]], None]] = None
65
70
 
71
+ def __post_init__(self) -> None:
72
+ if self.encryption != "none":
73
+ raise NotImplementedError(
74
+ f"Encryption at rest ('{self.encryption}') is not yet implemented. "
75
+ "This is a documented v2 commitment. Currently only 'none' is supported."
76
+ )
77
+
66
78
 
67
79
  # =============================================================================
68
80
  # Result Types
@@ -84,7 +96,7 @@ class AuditQueryResult:
84
96
  class AuditVerifyResult:
85
97
  """Result of verify_audit()."""
86
98
 
87
- valid: bool
99
+ valid: Optional[bool] # True = chain intact, False = chain broken, None = no audit trail found
88
100
  total_entries: int
89
101
  files_verified: int
90
102
  legacy_entries: int = 0
@@ -654,7 +666,7 @@ class AuditWriter:
654
666
  files_to_verify.append(active_path)
655
667
 
656
668
  if not files_to_verify:
657
- return AuditVerifyResult(valid=True, total_entries=0, files_verified=0)
669
+ return AuditVerifyResult(valid=None, total_entries=0, files_verified=0)
658
670
 
659
671
  # Verify chain across all files
660
672
  total_entries = 0
@@ -131,6 +131,7 @@ class FlowScriptCamelMemory(_CamelAgentMemory):
131
131
  self._max_tokens = max_tokens
132
132
  self._agent_id: str | None = None
133
133
  self._memory.session_start()
134
+ self._memory.set_adapter_context("camel_ai", "FlowScriptCamelMemory", "init")
134
135
 
135
136
  @property
136
137
  def memory(self) -> Memory:
@@ -173,6 +174,7 @@ class FlowScriptCamelMemory(_CamelAgentMemory):
173
174
  Returns:
174
175
  List of ContextRecord objects scored for context assembly.
175
176
  """
177
+ self._memory.set_adapter_operation("retrieve")
176
178
  records: list[ContextRecord] = []
177
179
 
178
180
  # Order by tier priority
@@ -273,6 +275,7 @@ class FlowScriptCamelMemory(_CamelAgentMemory):
273
275
  Args:
274
276
  records: List of MemoryRecord-like objects.
275
277
  """
278
+ self._memory.set_adapter_operation("write_records")
276
279
  # Extract content from all records
277
280
  contents = []
278
281
  for record in records:
@@ -399,6 +402,7 @@ class FlowScriptCamelMemory(_CamelAgentMemory):
399
402
  Uses unified search (vector + keyword + temporal) when available,
400
403
  falls back to word-level matching.
401
404
  """
405
+ self._memory.set_adapter_operation("recall")
402
406
  if self._unified:
403
407
  unified_results = self._unified.search(query, top_k=limit)
404
408
  if unified_results:
@@ -452,9 +456,12 @@ class FlowScriptCamelMemory(_CamelAgentMemory):
452
456
 
453
457
  def close(self):
454
458
  """End session: prune dormant, save. Returns SessionWrapResult."""
455
- if self._unified:
456
- return self._unified.close()
457
- return self._memory.session_wrap()
459
+ try:
460
+ if self._unified:
461
+ return self._unified.close()
462
+ return self._memory.session_wrap()
463
+ finally:
464
+ self._memory.clear_adapter_context()
458
465
 
459
466
  def __enter__(self):
460
467
  return self
@@ -101,6 +101,7 @@ class FlowScriptStorage:
101
101
  self._rebuild_index()
102
102
  # Start temporal session
103
103
  self._memory.session_start()
104
+ self._memory.set_adapter_context("crewai", "FlowScriptStorage", "init")
104
105
 
105
106
  @property
106
107
  def memory(self) -> Memory:
@@ -157,6 +158,7 @@ class FlowScriptStorage:
157
158
 
158
159
  def save(self, records: list[Any]) -> None:
159
160
  """Save MemoryRecord objects."""
161
+ self._memory.set_adapter_operation("save")
160
162
  for record in records:
161
163
  rec_id = getattr(record, "id", str(uuid.uuid4()))
162
164
  content = getattr(record, "content", str(record))
@@ -222,6 +224,7 @@ class FlowScriptStorage:
222
224
  for scoring (compares query_embedding against indexed node vectors).
223
225
  Falls back to per-record embeddings or content matching otherwise.
224
226
  """
227
+ self._memory.set_adapter_operation("search")
225
228
  results: list[tuple[_RecordEntry, float]] = []
226
229
 
227
230
  # Build vector scores from VectorIndex when available
@@ -486,9 +489,12 @@ class FlowScriptStorage:
486
489
 
487
490
  def close(self):
488
491
  """End the session: prune dormant nodes, save. Returns SessionWrapResult."""
489
- if self._unified:
490
- return self._unified.close()
491
- return self._memory.session_wrap()
492
+ try:
493
+ if self._unified:
494
+ return self._unified.close()
495
+ return self._memory.session_wrap()
496
+ finally:
497
+ self._memory.clear_adapter_context()
492
498
 
493
499
  def __enter__(self):
494
500
  return self
@@ -581,7 +581,7 @@ class AutoExtract:
581
581
  rels_created = self._create_extraction_relationships(extraction, node_refs)
582
582
  states_created = self._apply_extraction_states(extraction, node_refs)
583
583
 
584
- return IngestResult(
584
+ result = IngestResult(
585
585
  nodes_created=created,
586
586
  nodes_deduplicated=deduped,
587
587
  relationships_created=rels_created,
@@ -589,6 +589,14 @@ class AutoExtract:
589
589
  node_ids=[ref.id for ref in node_refs],
590
590
  )
591
591
 
592
+ # Audit: extraction provenance (never crash ingest for audit failures)
593
+ try:
594
+ self._write_extraction_audit(extraction, result)
595
+ except Exception:
596
+ print("AutoExtract: audit write failed (transcript_extract)", file=sys.stderr)
597
+
598
+ return result
599
+
592
600
  def _ingest_with_consolidation(
593
601
  self,
594
602
  extraction: ExtractionResult,
@@ -636,7 +644,7 @@ class AutoExtract:
636
644
  if action.target_node_id not in surviving_ids:
637
645
  surviving_ids.append(action.target_node_id)
638
646
 
639
- return IngestResult(
647
+ result = IngestResult(
640
648
  nodes_created=consolidation_result.nodes_added,
641
649
  nodes_deduplicated=consolidation_result.nodes_skipped,
642
650
  relationships_created=rels_created + consolidation_result.nodes_related + consolidation_result.nodes_resolved,
@@ -651,6 +659,20 @@ class AutoExtract:
651
659
  fallback_count=consolidation_result.fallback_count,
652
660
  )
653
661
 
662
+ # Audit: extraction provenance (never crash ingest for audit failures)
663
+ try:
664
+ self._write_extraction_audit(extraction, result)
665
+ except Exception:
666
+ print("AutoExtract: audit write failed (transcript_extract)", file=sys.stderr)
667
+
668
+ # Audit: consolidation batch summary
669
+ try:
670
+ self._write_consolidation_batch_audit(consolidation_result)
671
+ except Exception:
672
+ print("AutoExtract: audit write failed (consolidation_batch)", file=sys.stderr)
673
+
674
+ return result
675
+
654
676
  # -------------------------------------------------------------------------
655
677
  # Shared helpers
656
678
  # -------------------------------------------------------------------------
@@ -775,6 +797,50 @@ class AutoExtract:
775
797
 
776
798
  return self.ingest(transcript, metadata=metadata, actor=actor)
777
799
 
800
+ def _write_extraction_audit(
801
+ self,
802
+ extraction: ExtractionResult,
803
+ result: IngestResult,
804
+ ) -> None:
805
+ """Write a transcript_extract audit event summarizing what was extracted."""
806
+ # Build type breakdown from extraction
807
+ type_counts: dict[str, int] = {}
808
+ for node in extraction.nodes:
809
+ t = node.type if node.type in _VALID_NODE_TYPES else "thought"
810
+ type_counts[t] = type_counts.get(t, 0) + 1
811
+
812
+ self._memory.write_audit("transcript_extract", {
813
+ "nodes_extracted": len(extraction.nodes),
814
+ "nodes_created": result.nodes_created,
815
+ "nodes_deduplicated": result.nodes_deduplicated,
816
+ "relationships_extracted": len(extraction.relationships),
817
+ "relationships_created": result.relationships_created,
818
+ "states_created": result.states_created,
819
+ "type_breakdown": type_counts,
820
+ "node_ids": result.node_ids,
821
+ "consolidation_used": result.consolidation_used,
822
+ })
823
+
824
+ def _write_consolidation_batch_audit(
825
+ self,
826
+ consolidation_result: Any,
827
+ ) -> None:
828
+ """Write a consolidation_batch audit event summarizing batch results."""
829
+ self._memory.write_audit("consolidation_batch", {
830
+ "nodes_added": consolidation_result.nodes_added,
831
+ "nodes_updated": consolidation_result.nodes_updated,
832
+ "nodes_related": consolidation_result.nodes_related,
833
+ "nodes_resolved": consolidation_result.nodes_resolved,
834
+ "nodes_skipped": consolidation_result.nodes_skipped,
835
+ "nodes_novel": consolidation_result.nodes_novel,
836
+ "collision_count": consolidation_result.collision_count,
837
+ "collisions_retried": consolidation_result.collisions_retried,
838
+ "error_count": consolidation_result.error_count,
839
+ "total_contested": consolidation_result.total_contested,
840
+ "llm_calls": consolidation_result.llm_calls,
841
+ "health_ok": consolidation_result.health_ok,
842
+ })
843
+
778
844
  def _get_node_creator(self, type_str: str) -> Callable[[str], NodeRef]:
779
845
  """Get the Memory node creation method for a type string.
780
846
 
@@ -90,6 +90,7 @@ class FlowScriptMemoryService(_ADKBaseMemoryService):
90
90
  self._file_path = file_path
91
91
  # Start temporal session
92
92
  self._memory.session_start()
93
+ self._memory.set_adapter_context("google_adk", "FlowScriptMemoryService", "init")
93
94
 
94
95
  @property
95
96
  def memory(self) -> Memory:
@@ -128,6 +129,7 @@ class FlowScriptMemoryService(_ADKBaseMemoryService):
128
129
  to create typed reasoning nodes from session content. Otherwise,
129
130
  stores raw content as thought nodes.
130
131
  """
132
+ self._memory.set_adapter_operation("add_session")
131
133
  app_name = getattr(session, "app_name", "unknown")
132
134
  user_id = getattr(session, "user_id", "unknown")
133
135
  session_id = getattr(session, "id", "unknown")
@@ -206,6 +208,7 @@ class FlowScriptMemoryService(_ADKBaseMemoryService):
206
208
 
207
209
  Returns ADK SearchMemoryResponse with MemoryEntry objects.
208
210
  """
211
+ self._memory.set_adapter_operation("search_memory")
209
212
  # Use unified search when available (vector + keyword + temporal)
210
213
  if self._unified:
211
214
  unified_results = self._unified.search(query, top_k=10)
@@ -321,6 +324,7 @@ class FlowScriptMemoryService(_ADKBaseMemoryService):
321
324
  custom_metadata: Mapping[str, object] | None = None,
322
325
  ) -> None:
323
326
  """Incremental event addition."""
327
+ self._memory.set_adapter_operation("add_events")
324
328
  prev_ref = None
325
329
  for event in events:
326
330
  content = _extract_event_content(event)
@@ -356,9 +360,12 @@ class FlowScriptMemoryService(_ADKBaseMemoryService):
356
360
 
357
361
  def close(self):
358
362
  """End the session: prune dormant nodes, save. Returns SessionWrapResult."""
359
- if self._unified:
360
- return self._unified.close()
361
- return self._memory.session_wrap()
363
+ try:
364
+ if self._unified:
365
+ return self._unified.close()
366
+ return self._memory.session_wrap()
367
+ finally:
368
+ self._memory.clear_adapter_context()
362
369
 
363
370
  def __enter__(self):
364
371
  return self
@@ -101,6 +101,7 @@ class FlowScriptMemoryStore:
101
101
  self._id_map: dict[str, str] = {}
102
102
  self._rebuild_index()
103
103
  self._memory.session_start()
104
+ self._memory.set_adapter_context("haystack", "FlowScriptMemoryStore", "init")
104
105
 
105
106
  @property
106
107
  def memory(self) -> Memory:
@@ -158,6 +159,7 @@ class FlowScriptMemoryStore:
158
159
  user_id: Optional user identifier for scoping.
159
160
  **kwargs: Additional args (agent_id, run_id, etc.)
160
161
  """
162
+ self._memory.set_adapter_operation("add_memories")
161
163
  haystack_meta = {"haystack_user_id": user_id}
162
164
  for k, v in kwargs.items():
163
165
  haystack_meta[f"haystack_{k}"] = v
@@ -245,6 +247,7 @@ class FlowScriptMemoryStore:
245
247
  Returns:
246
248
  List of ChatMessage-compatible dicts with memory content.
247
249
  """
250
+ self._memory.set_adapter_operation("search_memories")
248
251
  # Use unified search when available (vector + keyword + temporal)
249
252
  scored: list[tuple[NodeRef, float]] = []
250
253
  if query and self._unified:
@@ -392,9 +395,12 @@ class FlowScriptMemoryStore:
392
395
 
393
396
  def close(self):
394
397
  """End session: prune dormant nodes, save. Returns SessionWrapResult."""
395
- if self._unified:
396
- return self._unified.close()
397
- return self._memory.session_wrap()
398
+ try:
399
+ if self._unified:
400
+ return self._unified.close()
401
+ return self._memory.session_wrap()
402
+ finally:
403
+ self._memory.clear_adapter_context()
398
404
 
399
405
  def __enter__(self):
400
406
  return self
@@ -124,6 +124,7 @@ class FlowScriptStore(BaseStore):
124
124
  self._rebuild_index()
125
125
  # Start temporal session (resets touch dedup)
126
126
  self._memory.session_start()
127
+ self._memory.set_adapter_context("langgraph", "FlowScriptStore", "init")
127
128
 
128
129
  @property
129
130
  def memory(self) -> Memory:
@@ -204,6 +205,7 @@ class FlowScriptStore(BaseStore):
204
205
  return self.batch(ops)
205
206
 
206
207
  def _handle_get(self, op: GetOp) -> Item | None:
208
+ self._memory.set_adapter_operation("get")
207
209
  stored = self._items.get((op.namespace, op.key))
208
210
  if stored is None:
209
211
  return None
@@ -218,6 +220,7 @@ class FlowScriptStore(BaseStore):
218
220
  )
219
221
 
220
222
  def _handle_put(self, op: PutOp) -> None:
223
+ self._memory.set_adapter_operation("put")
221
224
  ns = op.namespace
222
225
  key = op.key
223
226
 
@@ -381,9 +384,12 @@ class FlowScriptStore(BaseStore):
381
384
 
382
385
  def close(self):
383
386
  """End the session: prune dormant nodes, save. Returns SessionWrapResult."""
384
- if self._unified:
385
- return self._unified.close()
386
- return self._memory.session_wrap()
387
+ try:
388
+ if self._unified:
389
+ return self._unified.close()
390
+ return self._memory.session_wrap()
391
+ finally:
392
+ self._memory.clear_adapter_context()
387
393
 
388
394
  def __enter__(self):
389
395
  return self
@@ -148,6 +148,7 @@ class FlowScriptMemoryBlock(BaseMemoryBlock[str]):
148
148
  else:
149
149
  self._memory = Memory(options=options)
150
150
  self._memory.session_start()
151
+ self._memory.set_adapter_context("llamaindex", "FlowScriptMemoryBlock", "init")
151
152
 
152
153
  @property
153
154
  def memory(self) -> Memory:
@@ -337,6 +338,7 @@ class FlowScriptMemoryBlock(BaseMemoryBlock[str]):
337
338
 
338
339
  def store(self, content: str, **metadata: Any) -> NodeRef:
339
340
  """Store a thought directly. Returns NodeRef for chaining."""
341
+ self._memory.set_adapter_operation("store")
340
342
  ref = self._memory.thought(content)
341
343
  if metadata:
342
344
  ref.node.ext = ref.node.ext or {}
@@ -348,6 +350,7 @@ class FlowScriptMemoryBlock(BaseMemoryBlock[str]):
348
350
 
349
351
  Uses unified search (vector + keyword + temporal) when available.
350
352
  """
353
+ self._memory.set_adapter_operation("recall")
351
354
  if self._unified:
352
355
  unified_results = self._unified.search(query, top_k=limit)
353
356
  if unified_results:
@@ -401,9 +404,12 @@ class FlowScriptMemoryBlock(BaseMemoryBlock[str]):
401
404
 
402
405
  def close(self):
403
406
  """End session: prune dormant nodes, save. Returns SessionWrapResult."""
404
- if self._unified:
405
- return self._unified.close()
406
- return self._memory.session_wrap()
407
+ try:
408
+ if self._unified:
409
+ return self._unified.close()
410
+ return self._memory.session_wrap()
411
+ finally:
412
+ self._memory.clear_adapter_context()
407
413
 
408
414
  def __enter__(self):
409
415
  return self
@@ -32,15 +32,20 @@ When OPENAI_API_KEY is set, the server auto-configures:
32
32
  - LLM extraction (gpt-4o-mini) for typed reasoning extraction
33
33
  - Consolidation (gpt-4o-mini) for memory management (UPDATE/RELATE/RESOLVE)
34
34
 
35
- Tools exposed:
35
+ Tools exposed (13):
36
36
  - search_memory: Unified search (vector + keyword + temporal)
37
37
  - add_memory: Auto-extract reasoning from text with consolidation
38
38
  - get_context: Get formatted memory for prompt injection
39
39
  - query_tensions: Find all tensions/tradeoffs in memory
40
40
  - query_blocked: Find all blocked items with impact analysis
41
41
  - query_why: Trace causal chain for a node
42
+ - query_what_if: Trace downstream impact
42
43
  - query_alternatives: Reconstruct decision from options
44
+ - remove_memory: Remove a node from memory
45
+ - session_wrap: End-of-session lifecycle (graduation, pruning, save)
43
46
  - memory_stats: Get memory statistics
47
+ - query_audit: Search the audit trail with filters
48
+ - verify_audit: Verify hash chain integrity
44
49
  """
45
50
 
46
51
  from __future__ import annotations
@@ -266,6 +271,50 @@ TOOLS = [
266
271
  ),
267
272
  "inputSchema": {"type": "object", "properties": {}},
268
273
  },
274
+ {
275
+ "name": "query_audit",
276
+ "description": (
277
+ "Search the audit trail for reasoning provenance. Call this to understand "
278
+ "how memory evolved — what was extracted, what consolidation decided, "
279
+ "which adapter made changes, or what happened in a specific session. "
280
+ "Returns hash-chained audit entries matching the filters."
281
+ ),
282
+ "inputSchema": {
283
+ "type": "object",
284
+ "properties": {
285
+ "after": {"type": "string", "description": "Only entries after this ISO timestamp"},
286
+ "before": {"type": "string", "description": "Only entries before this ISO timestamp"},
287
+ "events": {
288
+ "type": "array",
289
+ "items": {"type": "string"},
290
+ "description": (
291
+ "Filter by event types. Available: node_create, relationship_create, "
292
+ "state_change, graduation, prune, session_start, session_end, "
293
+ "session_wrap, consolidation, consolidation_batch, transcript_extract, "
294
+ "node_remove, update_node, update_node_merge, audit_cleanup"
295
+ ),
296
+ },
297
+ "node_id": {"type": "string", "description": "Filter by node involvement"},
298
+ "session_id": {"type": "string", "description": "Filter by session ID"},
299
+ "adapter": {"type": "string", "description": "Filter by adapter framework name"},
300
+ "limit": {"type": "integer", "description": "Maximum entries (default 100)", "default": 100},
301
+ "verify_chain": {
302
+ "type": "boolean",
303
+ "description": "Also verify hash chain integrity of matched entries",
304
+ "default": False,
305
+ },
306
+ },
307
+ },
308
+ },
309
+ {
310
+ "name": "verify_audit",
311
+ "description": (
312
+ "Verify hash chain integrity of the entire audit trail. Call this to "
313
+ "confirm the audit trail has not been tampered with. Returns chain "
314
+ "validity status, total entries verified, and location of any break."
315
+ ),
316
+ "inputSchema": {"type": "object", "properties": {}},
317
+ },
269
318
  ]
270
319
 
271
320
 
@@ -293,6 +342,8 @@ class MCPHandler:
293
342
  "remove_memory": self._remove_memory,
294
343
  "session_wrap": self._session_wrap,
295
344
  "memory_stats": self._memory_stats,
345
+ "query_audit": self._query_audit,
346
+ "verify_audit": self._verify_audit,
296
347
  }
297
348
  handler = handlers.get(name)
298
349
  if handler is None:
@@ -467,6 +518,69 @@ class MCPHandler:
467
518
  return stats
468
519
 
469
520
 
521
+ def _get_audit_path(self) -> str | None:
522
+ """Derive audit trail path from memory file path."""
523
+ mem_path = self._umem.memory._file_path
524
+ if not mem_path:
525
+ return None
526
+ from pathlib import Path as _P
527
+ return str(_P(mem_path).parent / (_P(mem_path).stem + ".audit.jsonl"))
528
+
529
+ def _query_audit(self, args: dict) -> dict:
530
+ audit_path = self._get_audit_path()
531
+ if not audit_path:
532
+ return {"error": "No memory file path — audit trail requires file-based persistence"}
533
+ try:
534
+ result = Memory.query_audit(
535
+ audit_path,
536
+ after=args.get("after"),
537
+ before=args.get("before"),
538
+ events=args.get("events"),
539
+ node_id=args.get("node_id"),
540
+ session_id=args.get("session_id"),
541
+ adapter=args.get("adapter"),
542
+ limit=args.get("limit", 100),
543
+ verify_chain=args.get("verify_chain", False),
544
+ )
545
+ resp: dict[str, Any] = {
546
+ "entries": result.entries,
547
+ "total_scanned": result.total_scanned,
548
+ "files_searched": result.files_searched,
549
+ "count": len(result.entries),
550
+ }
551
+ if result.chain_valid is not None:
552
+ resp["chain_valid"] = result.chain_valid
553
+ if result.chain_break_at is not None:
554
+ resp["chain_break_at"] = result.chain_break_at
555
+ return resp
556
+ except FileNotFoundError:
557
+ return {"entries": [], "total_scanned": 0, "files_searched": 0, "count": 0,
558
+ "note": "No audit trail file found — audit may not be configured"}
559
+
560
+ def _verify_audit(self, args: dict) -> dict:
561
+ audit_path = self._get_audit_path()
562
+ if not audit_path:
563
+ return {"error": "No memory file path — audit trail requires file-based persistence"}
564
+ try:
565
+ result = Memory.verify_audit(audit_path)
566
+ resp: dict[str, Any] = {
567
+ "valid": result.valid,
568
+ "total_entries": result.total_entries,
569
+ "files_verified": result.files_verified,
570
+ "legacy_entries": result.legacy_entries,
571
+ }
572
+ if result.valid is False:
573
+ if result.chain_break_at is not None:
574
+ resp["chain_break_at"] = result.chain_break_at
575
+ if result.chain_break_file is not None:
576
+ resp["chain_break_file"] = result.chain_break_file
577
+ return resp
578
+ except FileNotFoundError:
579
+ return {"valid": None, "total_entries": 0, "files_verified": 0,
580
+ "status": "no_audit_trail",
581
+ "note": "No audit trail file found — auditing may not be configured"}
582
+
583
+
470
584
  def _serialize_query_result(result: Any, _seen: set | None = None) -> dict:
471
585
  """Best-effort serialization of query result dataclasses."""
472
586
  if _seen is None:
@@ -711,6 +825,7 @@ def run_server(
711
825
  # Start session tracking — enables touch deduplication and temporal
712
826
  # intelligence across the lifetime of this MCP server instance.
713
827
  umem.memory.session_start()
828
+ umem.memory.set_adapter_context("mcp", "FlowScriptMCP", "server")
714
829
  handler = MCPHandler(umem)
715
830
 
716
831
  try:
@@ -778,10 +893,12 @@ def run_server(
778
893
  finally:
779
894
  # Safety net: save state when stdin closes (Claude Code exits).
780
895
  # Use save() not close() — don't prune on unclean shutdown.
896
+ # Order: save first (may write audit entries), then clear context.
781
897
  try:
782
898
  umem.save()
783
899
  except Exception:
784
900
  pass
901
+ umem.memory.clear_adapter_context()
785
902
 
786
903
 
787
904
  # =============================================================================
@@ -376,6 +376,15 @@ class Memory:
376
376
  "operation": operation,
377
377
  }
378
378
 
379
+ def set_adapter_operation(self, operation: str) -> None:
380
+ """Update the operation field of existing adapter context.
381
+
382
+ Use this for per-operation attribution without resetting framework/class.
383
+ No-op if no adapter context is set.
384
+ """
385
+ if self._adapter_context is not None:
386
+ self._adapter_context["operation"] = operation
387
+
379
388
  def clear_adapter_context(self) -> None:
380
389
  """Clear adapter attribution."""
381
390
  self._adapter_context = None
@@ -1357,8 +1366,13 @@ class Memory:
1357
1366
  deduped_states.append(state)
1358
1367
  self._states = deduped_states
1359
1368
 
1360
- def _write_audit(self, event: str, data: dict[str, Any]) -> None:
1361
- """Write an audit trail entry via AuditWriter (hash-chained, rotatable)."""
1369
+ def write_audit(self, event: str, data: dict[str, Any]) -> None:
1370
+ """Write an audit trail entry via AuditWriter (hash-chained, rotatable).
1371
+
1372
+ Public API for audit event emission. Used by Memory internals and
1373
+ by cross-module callers (AutoExtract, ConsolidationEngine) that need
1374
+ to record provenance events in the same hash chain.
1375
+ """
1362
1376
  writer = self._ensure_audit_writer()
1363
1377
  if writer is None:
1364
1378
  return
@@ -1369,6 +1383,9 @@ class Memory:
1369
1383
  adapter=self._adapter_context,
1370
1384
  )
1371
1385
 
1386
+ # Backwards compat alias — internal callers use this, will migrate over time
1387
+ _write_audit = write_audit
1388
+
1372
1389
  def _merge_temporal(self, old_id: str, target_id: str) -> None:
1373
1390
  """Merge temporal metadata from old node into target — preserve the richer history.
1374
1391
 
@@ -106,6 +106,7 @@ class FlowScriptSession:
106
106
  self._rebuild_items()
107
107
  # Start temporal session
108
108
  self._memory.session_start()
109
+ self._memory.set_adapter_context("openai_agents", "FlowScriptSession", "init")
109
110
 
110
111
  @property
111
112
  def memory(self) -> Memory:
@@ -148,6 +149,7 @@ class FlowScriptSession:
148
149
 
149
150
  async def get_items(self, limit: int | None = None) -> list[dict[str, Any]]:
150
151
  """Get conversation items, optionally limited."""
152
+ self._memory.set_adapter_operation("get_items")
151
153
  items = self._items[-limit:] if limit is not None else self._items
152
154
  # Touch retrieved nodes — retrieval is engagement
153
155
  touched_ids = []
@@ -161,6 +163,7 @@ class FlowScriptSession:
161
163
 
162
164
  async def add_items(self, items: list[dict[str, Any]]) -> None:
163
165
  """Add conversation items to the session."""
166
+ self._memory.set_adapter_operation("add_items")
164
167
  base_order = len(self._items)
165
168
  for i, item in enumerate(items):
166
169
  content = _extract_item_content(item)
@@ -242,9 +245,12 @@ class FlowScriptSession:
242
245
 
243
246
  def close(self):
244
247
  """End the session: prune dormant nodes, save. Returns SessionWrapResult."""
245
- if self._unified:
246
- return self._unified.close()
247
- return self._memory.session_wrap()
248
+ try:
249
+ if self._unified:
250
+ return self._unified.close()
251
+ return self._memory.session_wrap()
252
+ finally:
253
+ self._memory.clear_adapter_context()
248
254
 
249
255
  def __enter__(self):
250
256
  return self
@@ -98,6 +98,7 @@ class FlowScriptDeps:
98
98
  else:
99
99
  self._memory = Memory(options=self.options)
100
100
  self._memory.session_start()
101
+ self._memory.set_adapter_context("pydantic_ai", "FlowScriptDeps", "init")
101
102
 
102
103
  @property
103
104
  def memory(self) -> Memory:
@@ -141,6 +142,7 @@ class FlowScriptDeps:
141
142
  Returns:
142
143
  NodeRef for building relationships (causes, tension_with, etc.)
143
144
  """
145
+ self._memory.set_adapter_operation("store")
144
146
  if self._unified and self._unified.extractor:
145
147
  result = self._unified.add(content, metadata=metadata if metadata else None)
146
148
  # Return first extracted node for chaining
@@ -175,6 +177,7 @@ class FlowScriptDeps:
175
177
  Returns:
176
178
  List of dicts with 'content', 'id', 'tier', 'frequency' keys.
177
179
  """
180
+ self._memory.set_adapter_operation("recall")
178
181
  # Use unified search when available (vector + keyword + temporal)
179
182
  if self._unified:
180
183
  unified_results = self._unified.search(query, top_k=limit)
@@ -256,9 +259,12 @@ class FlowScriptDeps:
256
259
 
257
260
  def close(self):
258
261
  """End session: prune dormant nodes, save. Returns SessionWrapResult."""
259
- if self._unified:
260
- return self._unified.close()
261
- return self._memory.session_wrap()
262
+ try:
263
+ if self._unified:
264
+ return self._unified.close()
265
+ return self._memory.session_wrap()
266
+ finally:
267
+ self._memory.clear_adapter_context()
262
268
 
263
269
  def __enter__(self):
264
270
  return self
@@ -91,6 +91,7 @@ class FlowScriptMemoryTools:
91
91
  self._memory = Memory(options=options)
92
92
  self._file_path = file_path
93
93
  self._memory.session_start()
94
+ self._memory.set_adapter_context("smolagents", "FlowScriptMemoryTools", "init")
94
95
 
95
96
  @property
96
97
  def memory(self) -> Memory:
@@ -139,9 +140,12 @@ class FlowScriptMemoryTools:
139
140
 
140
141
  def close(self):
141
142
  """End session: prune dormant nodes, save. Returns SessionWrapResult."""
142
- if self._unified:
143
- return self._unified.close()
144
- return self._memory.session_wrap()
143
+ try:
144
+ if self._unified:
145
+ return self._unified.close()
146
+ return self._memory.session_wrap()
147
+ finally:
148
+ self._memory.clear_adapter_context()
145
149
 
146
150
  def __enter__(self):
147
151
  return self
@@ -196,6 +200,7 @@ class _StoreMemoryTool(_BaseFSTool):
196
200
  output_type = "string"
197
201
 
198
202
  def forward(self, content: str, category: str = "observation") -> str:
203
+ self._memory.set_adapter_operation("store")
199
204
  # Use auto-extraction when available
200
205
  if self._unified and self._unified.extractor:
201
206
  result = self._unified.add(content, metadata={"category": category})
@@ -232,6 +237,7 @@ class _RecallMemoryTool(_BaseFSTool):
232
237
  output_type = "string"
233
238
 
234
239
  def forward(self, query: str, limit: int = 5) -> str:
240
+ self._memory.set_adapter_operation("recall")
235
241
  # Use unified search when available (vector + keyword + temporal)
236
242
  if self._unified:
237
243
  unified_results = self._unified.search(query, top_k=limit)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flowscript-agents"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Complete agent memory: reasoning queries + vector search + auto-extraction. Decision intelligence for LangGraph, CrewAI, Google ADK, OpenAI Agents SDK, Pydantic AI, smolagents, LlamaIndex, Haystack, CAMEL-AI, and Vercel AI SDK."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -31,7 +31,7 @@ class TestToolDefinitions:
31
31
  assert "inputSchema" in tool
32
32
 
33
33
  def test_tool_count(self):
34
- assert len(TOOLS) == 11
34
+ assert len(TOOLS) == 13
35
35
 
36
36
  def test_tool_names(self):
37
37
  names = {t["name"] for t in TOOLS}
@@ -40,6 +40,7 @@ class TestToolDefinitions:
40
40
  "query_tensions", "query_blocked", "query_why",
41
41
  "query_what_if", "query_alternatives",
42
42
  "remove_memory", "session_wrap", "memory_stats",
43
+ "query_audit", "verify_audit",
43
44
  }
44
45
  assert names == expected
45
46
 
@@ -294,7 +295,7 @@ class TestMCPStdioProtocol:
294
295
  "jsonrpc": "2.0", "id": 2, "method": "tools/list",
295
296
  })
296
297
  tools = resp["result"]["tools"]
297
- assert len(tools) == 11
298
+ assert len(tools) == 13
298
299
  names = {t["name"] for t in tools}
299
300
  assert "search_memory" in names
300
301
  assert "query_what_if" in names