flowscript-agents 0.2.5__tar.gz → 0.2.6__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 (59) hide show
  1. flowscript_agents-0.2.6/:memory: +29 -0
  2. flowscript_agents-0.2.6/:memory:.audit.manifest.json +10 -0
  3. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/PKG-INFO +34 -6
  4. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/README.md +33 -5
  5. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/docs/lifecycle.md +54 -1
  6. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/examples/CLAUDE.md.example +13 -7
  7. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/mcp.py +118 -15
  8. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/tool-integrity.json +1 -1
  9. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/pyproject.toml +1 -1
  10. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_mcp.py +66 -0
  11. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/.github/workflows/test.yml +0 -0
  12. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/.gitignore +0 -0
  13. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/AUDIT_TRAIL_DESIGN.md +0 -0
  14. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/docs/adapters.md +0 -0
  15. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/docs/api-reference.md +0 -0
  16. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/docs/audit-trail.md +0 -0
  17. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/docs/brand/logo-512.png +0 -0
  18. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/docs/brand/social-preview.png +0 -0
  19. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/docs/flowscript-demo.png +0 -0
  20. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/examples/langgraph_live_test.py +0 -0
  21. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/examples/temporal_e2e_test.py +0 -0
  22. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/__init__.py +0 -0
  23. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/audit.py +0 -0
  24. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/camel_ai.py +0 -0
  25. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/crewai.py +0 -0
  26. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/embeddings/__init__.py +0 -0
  27. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/embeddings/_utils.py +0 -0
  28. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/embeddings/consolidate.py +0 -0
  29. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/embeddings/extract.py +0 -0
  30. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/embeddings/index.py +0 -0
  31. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/embeddings/providers.py +0 -0
  32. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/embeddings/search.py +0 -0
  33. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/google_adk.py +0 -0
  34. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/haystack.py +0 -0
  35. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/langgraph.py +0 -0
  36. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/llamaindex.py +0 -0
  37. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/memory.py +0 -0
  38. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/openai_agents.py +0 -0
  39. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/pydantic_ai.py +0 -0
  40. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/query.py +0 -0
  41. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/smolagents.py +0 -0
  42. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/types.py +0 -0
  43. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/flowscript_agents/unified.py +0 -0
  44. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/scripts/validate_dedup_threshold.py +0 -0
  45. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/conftest.py +0 -0
  46. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_audit.py +0 -0
  47. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_camel_ai.py +0 -0
  48. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_consolidation.py +0 -0
  49. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_crewai.py +0 -0
  50. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_embeddings.py +0 -0
  51. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_google_adk.py +0 -0
  52. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_haystack.py +0 -0
  53. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_langgraph.py +0 -0
  54. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_llamaindex.py +0 -0
  55. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_memory.py +0 -0
  56. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_openai_agents.py +0 -0
  57. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_pydantic_ai.py +0 -0
  58. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_smolagents.py +0 -0
  59. {flowscript_agents-0.2.5 → flowscript_agents-0.2.6}/tests/test_temporal.py +0 -0
@@ -0,0 +1,29 @@
1
+ {
2
+ "flowscript_memory": "1.0.0",
3
+ "ir": {
4
+ "version": "1.0.0",
5
+ "nodes": [],
6
+ "relationships": [],
7
+ "states": [],
8
+ "invariants": {
9
+ "causal_acyclic": true,
10
+ "all_nodes_reachable": true,
11
+ "tension_axes_labeled": true,
12
+ "state_fields_present": true
13
+ },
14
+ "metadata": {
15
+ "source_files": [
16
+ "memory-api"
17
+ ],
18
+ "parsed_at": "2026-03-24T15:40:53.676226+00:00",
19
+ "parser": "flowscript-agents"
20
+ }
21
+ },
22
+ "temporal": {},
23
+ "config": {
24
+ "touch_on_query": true,
25
+ "source_file": null,
26
+ "author": null,
27
+ "temporal": null
28
+ }
29
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "active_file": ":memory:.audit.jsonl",
3
+ "active_last_hash": "sha256:8dfb8c37838e7812e9c2727d8fe76ffca44f677b9d5499fa13513fac3800816f",
4
+ "active_last_seq": 0,
5
+ "files": [],
6
+ "last_cleanup": null,
7
+ "memory_file": ":memory:",
8
+ "retention_months": 84,
9
+ "version": 1
10
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowscript-agents
3
- Version: 0.2.5
3
+ Version: 0.2.6
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
@@ -70,7 +70,7 @@ Description-Content-Type: text/markdown
70
70
 
71
71
  <p align="center"><strong>Agent memory that tracks why you decided, what conflicts, and what's blocked. Not just what was said.</strong></p>
72
72
 
73
- [![Tests](https://img.shields.io/badge/tests-581%20passing-brightgreen)](https://github.com/phillipclapham/flowscript-agents) [![PyPI](https://img.shields.io/pypi/v/flowscript-agents)](https://pypi.org/project/flowscript-agents/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://pypi.org/project/flowscript-agents/)
73
+ [![Tests](https://img.shields.io/badge/tests-584%20passing-brightgreen)](https://github.com/phillipclapham/flowscript-agents) [![PyPI](https://img.shields.io/pypi/v/flowscript-agents)](https://pypi.org/project/flowscript-agents/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://pypi.org/project/flowscript-agents/)
74
74
 
75
75
  ---
76
76
 
@@ -341,11 +341,39 @@ Framework attribution is automatic — every audit entry records which adapter t
341
341
 
342
342
  ---
343
343
 
344
- ## Memory That Evolves
344
+ ## Session Lifecycle — How Memory Gets Smarter
345
345
 
346
- Nodes graduate through four temporal tiers based on actual use — `current` → `developing` → `proven` → `foundation`. Every query touches returned nodes, so knowledge that keeps getting queried earns its place. One-off observations fade naturally. Dormant nodes are pruned to the audit trail archived with full provenance, never destroyed.
346
+ Just like a mind needs sleep to consolidate memories, your agent's reasoning graph needs regular session wraps to develop intelligence over time. Without consolidation cycles, knowledge accumulates as noise instead of maturing.
347
347
 
348
- After 20 sessions, your memory is a curated knowledge base, not a pile of notes. [Session lifecycle details →](docs/lifecycle.md)
348
+ **Temporal tiers** nodes graduate based on actual use:
349
+
350
+ | Tier | Meaning | Behavior |
351
+ |:-----|:--------|:---------|
352
+ | `current` | Recent observations | May be pruned if not reinforced |
353
+ | `developing` | Emerging patterns (2+ touches) | Building confidence |
354
+ | `proven` | Validated through use (3+ touches) | Protected from pruning |
355
+ | `foundation` | Core truths | Always preserved |
356
+
357
+ Every query touches returned nodes — knowledge that keeps getting queried earns its place. One-off observations fade naturally. Dormant nodes are pruned to the audit trail — archived with full provenance, never destroyed.
358
+
359
+ **Three ways session wraps happen:**
360
+
361
+ 1. **Explicit** — the LLM calls the `session_wrap` tool when you say "let's wrap up" (best results)
362
+ 2. **Auto-wrap** — after 5 minutes of inactivity, the MCP server auto-consolidates (safety net, configurable via `FLOWSCRIPT_AUTO_WRAP_MINUTES`, set to `0` to disable)
363
+ 3. **Process exit** — when the MCP server shuts down, a final consolidation runs automatically
364
+
365
+ **For SDK users** — adapters support context managers that auto-wrap:
366
+
367
+ ```python
368
+ from flowscript_agents.adapters.langgraph import FlowScriptStore
369
+
370
+ with FlowScriptStore("agent-memory.json") as store:
371
+ # work happens — all mutations auto-save
372
+ store.put(("agents",), "key", {"value": "data"})
373
+ # close() fires automatically → session_wrap() + save
374
+ ```
375
+
376
+ After 20 sessions, your memory is a curated knowledge base, not a pile of notes. [Full lifecycle details →](docs/lifecycle.md)
349
377
 
350
378
  ---
351
379
 
@@ -387,7 +415,7 @@ Under the hood: a local semantic graph with typed nodes, typed relationships, an
387
415
  | [flowscript-core](https://www.npmjs.com/package/flowscript-core) | TypeScript SDK — Memory class, 15 tools, token budgeting, audit trail | `npm install flowscript-core` |
388
416
  | [flowscript.org](https://flowscript.org) | Web editor, D3 visualization, live query panel | Browser |
389
417
 
390
- **1,312 tests** across Python (581) and TypeScript (731). Same audit trail format and canonical JSON serialization across both languages.
418
+ **1,315 tests** across Python (584) and TypeScript (731). Same audit trail format and canonical JSON serialization across both languages.
391
419
 
392
420
  ### Docs
393
421
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  <p align="center"><strong>Agent memory that tracks why you decided, what conflicts, and what's blocked. Not just what was said.</strong></p>
8
8
 
9
- [![Tests](https://img.shields.io/badge/tests-581%20passing-brightgreen)](https://github.com/phillipclapham/flowscript-agents) [![PyPI](https://img.shields.io/pypi/v/flowscript-agents)](https://pypi.org/project/flowscript-agents/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://pypi.org/project/flowscript-agents/)
9
+ [![Tests](https://img.shields.io/badge/tests-584%20passing-brightgreen)](https://github.com/phillipclapham/flowscript-agents) [![PyPI](https://img.shields.io/pypi/v/flowscript-agents)](https://pypi.org/project/flowscript-agents/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://pypi.org/project/flowscript-agents/)
10
10
 
11
11
  ---
12
12
 
@@ -277,11 +277,39 @@ Framework attribution is automatic — every audit entry records which adapter t
277
277
 
278
278
  ---
279
279
 
280
- ## Memory That Evolves
280
+ ## Session Lifecycle — How Memory Gets Smarter
281
281
 
282
- Nodes graduate through four temporal tiers based on actual use — `current` → `developing` → `proven` → `foundation`. Every query touches returned nodes, so knowledge that keeps getting queried earns its place. One-off observations fade naturally. Dormant nodes are pruned to the audit trail archived with full provenance, never destroyed.
282
+ Just like a mind needs sleep to consolidate memories, your agent's reasoning graph needs regular session wraps to develop intelligence over time. Without consolidation cycles, knowledge accumulates as noise instead of maturing.
283
283
 
284
- After 20 sessions, your memory is a curated knowledge base, not a pile of notes. [Session lifecycle details →](docs/lifecycle.md)
284
+ **Temporal tiers** nodes graduate based on actual use:
285
+
286
+ | Tier | Meaning | Behavior |
287
+ |:-----|:--------|:---------|
288
+ | `current` | Recent observations | May be pruned if not reinforced |
289
+ | `developing` | Emerging patterns (2+ touches) | Building confidence |
290
+ | `proven` | Validated through use (3+ touches) | Protected from pruning |
291
+ | `foundation` | Core truths | Always preserved |
292
+
293
+ Every query touches returned nodes — knowledge that keeps getting queried earns its place. One-off observations fade naturally. Dormant nodes are pruned to the audit trail — archived with full provenance, never destroyed.
294
+
295
+ **Three ways session wraps happen:**
296
+
297
+ 1. **Explicit** — the LLM calls the `session_wrap` tool when you say "let's wrap up" (best results)
298
+ 2. **Auto-wrap** — after 5 minutes of inactivity, the MCP server auto-consolidates (safety net, configurable via `FLOWSCRIPT_AUTO_WRAP_MINUTES`, set to `0` to disable)
299
+ 3. **Process exit** — when the MCP server shuts down, a final consolidation runs automatically
300
+
301
+ **For SDK users** — adapters support context managers that auto-wrap:
302
+
303
+ ```python
304
+ from flowscript_agents.adapters.langgraph import FlowScriptStore
305
+
306
+ with FlowScriptStore("agent-memory.json") as store:
307
+ # work happens — all mutations auto-save
308
+ store.put(("agents",), "key", {"value": "data"})
309
+ # close() fires automatically → session_wrap() + save
310
+ ```
311
+
312
+ After 20 sessions, your memory is a curated knowledge base, not a pile of notes. [Full lifecycle details →](docs/lifecycle.md)
285
313
 
286
314
  ---
287
315
 
@@ -323,7 +351,7 @@ Under the hood: a local semantic graph with typed nodes, typed relationships, an
323
351
  | [flowscript-core](https://www.npmjs.com/package/flowscript-core) | TypeScript SDK — Memory class, 15 tools, token budgeting, audit trail | `npm install flowscript-core` |
324
352
  | [flowscript.org](https://flowscript.org) | Web editor, D3 visualization, live query panel | Browser |
325
353
 
326
- **1,312 tests** across Python (581) and TypeScript (731). Same audit trail format and canonical JSON serialization across both languages.
354
+ **1,315 tests** across Python (584) and TypeScript (731). Same audit trail format and canonical JSON serialization across both languages.
327
355
 
328
356
  ### Docs
329
357
 
@@ -20,7 +20,19 @@ print(report.growing) # list of node IDs in growing tier
20
20
  print(report.dormant) # candidates for pruning
21
21
  ```
22
22
 
23
- Dormant nodes are pruned to the audit trail during `close()` or `sessionWrap()` — archived with full hash-chain provenance, never destroyed.
23
+ Dormant nodes are pruned to the audit trail during `close()` or `session_wrap()` — archived with full hash-chain provenance, never destroyed.
24
+
25
+ ## Why Session Wraps Matter
26
+
27
+ Just like a mind needs sleep to consolidate memories, the reasoning graph needs regular session wraps to develop intelligence over time. A session wrap is the consolidation cycle — without it, knowledge accumulates as noise instead of maturing through the temporal tiers above.
28
+
29
+ **Three mechanisms ensure consolidation happens:**
30
+
31
+ 1. **Explicit wrap** — the LLM calls `session_wrap` when the user signals session end (best results)
32
+ 2. **Auto-wrap** — the MCP server auto-consolidates after 5 minutes of inactivity (configurable via `FLOWSCRIPT_AUTO_WRAP_MINUTES` env var, `0` to disable)
33
+ 3. **Process exit** — a final consolidation runs automatically when the MCP server shuts down
34
+
35
+ For SDK users, all adapters call `session_wrap()` via their `close()` method or context manager exit.
24
36
 
25
37
  ## The `with` Pattern (Recommended)
26
38
 
@@ -99,3 +111,44 @@ mem = Memory.load_or_create("file.json",
99
111
  ## Session Start Deduplication
100
112
 
101
113
  `sessionStart()` calls both `blocked()` and `tensions()` internally (for the orientation summary). Touches from these calls are deduplicated — nodes aren't double-touched just because they appeared in both query results.
114
+
115
+ ## Writing Your Own Adapter
116
+
117
+ If you're building an adapter for a framework not yet supported, wire session wraps using this pattern:
118
+
119
+ ```python
120
+ class MyFrameworkAdapter:
121
+ def __init__(self, file_path, embedder=None, llm=None, consolidation_provider=None):
122
+ self._memory = Memory.load_or_create(file_path)
123
+ self._unified = UnifiedMemory(
124
+ file_path=file_path, embedder=embedder,
125
+ llm=llm, consolidation_provider=consolidation_provider,
126
+ )
127
+ self._memory.set_adapter_context("my_framework", "MyFrameworkAdapter", "init")
128
+ self._memory.session_start()
129
+
130
+ def close(self):
131
+ """End the session: prune dormant nodes, save. Returns SessionWrapResult."""
132
+ try:
133
+ if self._unified:
134
+ return self._unified.close()
135
+ return self._memory.session_wrap()
136
+ finally:
137
+ self._memory.clear_adapter_context()
138
+
139
+ def __enter__(self):
140
+ return self
141
+
142
+ def __exit__(self, exc_type, exc_val, exc_tb):
143
+ try:
144
+ self.close()
145
+ except Exception:
146
+ if exc_type is None:
147
+ raise # close() failure IS the error when no prior exception
148
+ ```
149
+
150
+ **Key points:**
151
+ - `close()` should call `session_wrap()` (via `UnifiedMemory.close()` or directly)
152
+ - `clear_adapter_context()` goes in the `finally` block AFTER `session_wrap()` — session lifecycle events need adapter attribution
153
+ - Context managers (`__enter__`/`__exit__`) make the `with` pattern work
154
+ - `set_adapter_context()` should be called on construction, then `set_adapter_operation()` per-operation for granular audit attribution
@@ -27,13 +27,19 @@ just report what you found or stored. Treat memory like your own notes.
27
27
  consequences before committing.
28
28
  - **To correct mistakes:** Call `remove_memory` with a node_id (use
29
29
  `search_memory` first to find it) to remove incorrect or outdated entries.
30
- - **End of session:** When the user says "wrap up", "save what we learned",
31
- or similar end-of-session phrases: review the conversation for any
32
- decisions, tradeoffs, blockers, or reasoning that wasn't already stored
33
- via `add_memory`. Save anything important you find. Then call
34
- `session_wrap` to run lifecycle maintenance (graduation, pruning,
35
- persist to disk). This is how memory gets smarter each wrap
36
- captures what matters and lets stale reasoning fade naturally.
30
+ - **End of session:** When the user says "wrap up", "let's wrap",
31
+ "save what we learned", or signals the conversation is ending:
32
+ review for any decisions/tradeoffs/reasoning not yet stored via
33
+ `add_memory`, save what's important, then call `session_wrap`.
34
+ This is the reasoning graph's consolidation cycle — like sleep for
35
+ memory. Dormant nodes get pruned to the audit trail, frequently-used
36
+ knowledge graduates through temporal tiers (current developing
37
+ proven → foundation), and the graph gets smarter over time. Without
38
+ regular wraps, knowledge accumulates as noise instead of maturing.
39
+ NOTE: An auto-wrap safety net runs after 5 minutes of inactivity
40
+ and on process exit, but explicit wraps at session boundaries
41
+ produce the best results because you can review and store final
42
+ reasoning before consolidation happens.
37
43
 
38
44
  **What to remember (call `add_memory`):**
39
45
  - Decisions and their rationale ("We chose PostgreSQL because...")
@@ -43,6 +43,12 @@ When OPENAI_API_KEY is set, the server auto-configures:
43
43
  - LLM extraction (gpt-4o-mini) for typed reasoning extraction
44
44
  - Consolidation (gpt-4o-mini) for memory management (UPDATE/RELATE/RESOLVE)
45
45
 
46
+ Session lifecycle:
47
+ - Auto-wrap safety net: consolidates memory after inactivity (default 5 min)
48
+ Configure: FLOWSCRIPT_AUTO_WRAP_MINUTES=10 (or 0 to disable)
49
+ - Explicit session_wrap: LLM or user triggers consolidation at session end
50
+ - atexit wrap: final consolidation when process exits
51
+
46
52
  Tools exposed (14):
47
53
  - search_memory: Unified search (vector + keyword + temporal)
48
54
  - add_memory: Auto-extract reasoning from text with consolidation
@@ -53,7 +59,7 @@ Tools exposed (14):
53
59
  - query_what_if: Trace downstream impact
54
60
  - query_alternatives: Reconstruct decision from options
55
61
  - remove_memory: Remove a node from memory
56
- - session_wrap: End-of-session lifecycle (graduation, pruning, save)
62
+ - session_wrap: Session consolidation (graduation, pruning, audit trail, save)
57
63
  - memory_stats: Get memory statistics
58
64
  - query_audit: Search the audit trail with filters
59
65
  - verify_audit: Verify hash chain integrity
@@ -63,11 +69,14 @@ Tools exposed (14):
63
69
  from __future__ import annotations
64
70
 
65
71
  import argparse
72
+ import atexit
66
73
  import datetime
67
74
  import hashlib
68
75
  import json
69
76
  import os
70
77
  import sys
78
+ import threading
79
+ import time
71
80
  from types import MappingProxyType
72
81
  from typing import Any, Optional
73
82
 
@@ -89,7 +98,7 @@ def _log(msg: str) -> None:
89
98
 
90
99
  _PROTOCOL_VERSION = "2025-03-26"
91
100
  _SERVER_NAME = "flowscript-agents"
92
- _SERVER_VERSION = "0.2.5"
101
+ _SERVER_VERSION = "0.2.6"
93
102
 
94
103
 
95
104
  def _jsonrpc_response(id: Any, result: Any) -> dict:
@@ -380,9 +389,15 @@ _TOOL_DEFS_RAW = [
380
389
  {
381
390
  "name": "session_wrap",
382
391
  "description": (
383
- "Run memory lifecycle maintenance: prune dormant nodes to audit trail, "
384
- "save to disk. Call this at the end of a work session to keep memory "
385
- "healthy. Dormant nodes (not accessed recently) are archived, not deleted."
392
+ "Run memory lifecycle maintenance the reasoning graph's consolidation "
393
+ "cycle. Prune dormant nodes to audit trail, graduate frequently-accessed "
394
+ "knowledge through temporal tiers, save to disk. Call this at the end of "
395
+ "a work session or when the user says to wrap up. Just like sleep "
396
+ "consolidates human memory, session wraps let the reasoning graph mature: "
397
+ "knowledge that keeps getting queried earns its place, one-off observations "
398
+ "fade naturally. An auto-wrap safety net runs after inactivity, but explicit "
399
+ "wraps at session boundaries produce the best results. Archived nodes are "
400
+ "preserved in the audit trail with full provenance — never destroyed."
386
401
  ),
387
402
  "inputSchema": {"type": "object", "properties": {}, "additionalProperties": False},
388
403
  },
@@ -666,9 +681,18 @@ class MCPHandler:
666
681
  result = self._umem.memory.session_wrap()
667
682
  return {
668
683
  "nodes_before": result.nodes_before,
684
+ "tiers_before": result.tiers_before,
669
685
  "nodes_after": result.nodes_after,
670
- "nodes_pruned": result.pruned.count,
671
686
  "tiers_after": result.tiers_after,
687
+ "nodes_pruned": result.pruned.count,
688
+ "pruned_node_ids": result.pruned.archived,
689
+ "garden_after": {
690
+ "growing": len(result.garden_after.growing),
691
+ "resting": len(result.garden_after.resting),
692
+ "dormant": len(result.garden_after.dormant),
693
+ },
694
+ "saved": result.saved,
695
+ "path": result.path,
672
696
  }
673
697
 
674
698
  def _memory_stats(self, args: dict) -> dict:
@@ -1069,6 +1093,80 @@ def run_server(
1069
1093
  umem.memory.set_adapter_context("mcp", "FlowScriptMCP", "server")
1070
1094
  handler = MCPHandler(umem)
1071
1095
 
1096
+ # -------------------------------------------------------------------------
1097
+ # Auto-wrap timer: consolidation safety net for when the LLM or user
1098
+ # doesn't explicitly call session_wrap. Just like sleep consolidates
1099
+ # human memory, auto-wrap ensures the reasoning graph matures even if
1100
+ # the session boundary isn't explicitly marked.
1101
+ #
1102
+ # - Resets on every tool call (activity = timer restart)
1103
+ # - Fires after FLOWSCRIPT_AUTO_WRAP_MINUTES of inactivity (default 5)
1104
+ # - Set to 0 to disable
1105
+ # - atexit handler provides a final wrap on process exit
1106
+ # -------------------------------------------------------------------------
1107
+ auto_wrap_minutes = int(os.environ.get("FLOWSCRIPT_AUTO_WRAP_MINUTES", "5"))
1108
+ _auto_wrap_timer: list[Optional[threading.Timer]] = [None] # mutable container for closure
1109
+ _session_wrapped: list[bool] = [False] # track if wrap already happened
1110
+ _wrap_lock = threading.Lock() # protects _session_wrapped check-then-act
1111
+
1112
+ def _do_auto_wrap() -> None:
1113
+ """Execute auto-wrap. Called by timer thread or atexit.
1114
+
1115
+ Uses _wrap_lock to prevent race between timer thread and main thread
1116
+ both calling session_wrap() simultaneously. The lock protects the
1117
+ check-then-act on _session_wrapped — without it, both threads could
1118
+ read False, set True, and proceed to concurrent session_wrap() calls
1119
+ that corrupt the audit hash chain.
1120
+ """
1121
+ with _wrap_lock:
1122
+ if _session_wrapped[0]:
1123
+ return
1124
+ _session_wrapped[0] = True
1125
+ # Lock released — flag prevents re-entry, and session_wrap() is now
1126
+ # safe to run without holding the lock (main thread won't enter).
1127
+ try:
1128
+ umem.memory.session_wrap()
1129
+ # session_wrap() calls session_end() which calls save() internally,
1130
+ # so no additional umem.save() needed here.
1131
+ _log("Auto-wrap: session consolidated after inactivity")
1132
+ except Exception as e:
1133
+ _log(f"Auto-wrap failed: {e}")
1134
+
1135
+ def _reset_auto_wrap_timer() -> None:
1136
+ """Cancel pending timer and start a new one. Called on each tool call."""
1137
+ if auto_wrap_minutes <= 0:
1138
+ return
1139
+ # Cancel existing timer
1140
+ if _auto_wrap_timer[0] is not None:
1141
+ _auto_wrap_timer[0].cancel()
1142
+ # If a previous auto-wrap fired, restart session for the new activity
1143
+ with _wrap_lock:
1144
+ if _session_wrapped[0]:
1145
+ _session_wrapped[0] = False
1146
+ try:
1147
+ umem.memory.session_start()
1148
+ except Exception:
1149
+ pass
1150
+ # Schedule new timer
1151
+ timer = threading.Timer(auto_wrap_minutes * 60, _do_auto_wrap)
1152
+ timer.daemon = True
1153
+ timer.start()
1154
+ _auto_wrap_timer[0] = timer
1155
+
1156
+ def _atexit_wrap() -> None:
1157
+ """Final wrap on process exit — save state + consolidate."""
1158
+ if _auto_wrap_timer[0] is not None:
1159
+ _auto_wrap_timer[0].cancel()
1160
+ if not _session_wrapped[0]:
1161
+ _do_auto_wrap()
1162
+
1163
+ atexit.register(_atexit_wrap)
1164
+
1165
+ # Start the initial timer
1166
+ if auto_wrap_minutes > 0:
1167
+ _reset_auto_wrap_timer()
1168
+ _log(f"Auto-wrap enabled: {auto_wrap_minutes}m inactivity threshold")
1169
+
1072
1170
  try:
1073
1171
  for line in sys.stdin:
1074
1172
  line = line.strip()
@@ -1134,9 +1232,15 @@ def run_server(
1134
1232
  elif method == "tools/call":
1135
1233
  tool_name = params.get("name", "")
1136
1234
  tool_args = params.get("arguments", {})
1235
+ _reset_auto_wrap_timer() # Activity detected — reset consolidation timer
1137
1236
  result = handler.handle_tool(tool_name, tool_args)
1138
- # Save after modifications
1139
- if tool_name in ("add_memory", "remove_memory", "session_wrap"):
1237
+ # If explicit session_wrap was called, mark it so auto-wrap doesn't double-fire
1238
+ if tool_name == "session_wrap":
1239
+ with _wrap_lock:
1240
+ _session_wrapped[0] = True
1241
+ # Save after modifications (session_wrap saves internally, but
1242
+ # add_memory/remove_memory need explicit save for vector index)
1243
+ if tool_name in ("add_memory", "remove_memory"):
1140
1244
  try:
1141
1245
  umem.save()
1142
1246
  except ValueError:
@@ -1158,13 +1262,12 @@ def run_server(
1158
1262
  sys.stdout.write(json.dumps(resp) + "\n")
1159
1263
  sys.stdout.flush()
1160
1264
  finally:
1161
- # Safety net: save state when stdin closes (Claude Code exits).
1162
- # Use save() not close() don't prune on unclean shutdown.
1163
- # Order: save first (may write audit entries), then clear context.
1164
- try:
1165
- umem.save()
1166
- except Exception:
1167
- pass
1265
+ # Clean shutdown: cancel timer, run final wrap (via atexit if not
1266
+ # already done), then clear adapter context.
1267
+ if _auto_wrap_timer[0] is not None:
1268
+ _auto_wrap_timer[0].cancel()
1269
+ if not _session_wrapped[0]:
1270
+ _do_auto_wrap()
1168
1271
  umem.memory.clear_adapter_context()
1169
1272
 
1170
1273
 
@@ -10,6 +10,6 @@
10
10
  "query_why": "4eac8ed68ca419cbe02fad7a948951f8aae7ee86301f8bc3d80c3b3004b1860e",
11
11
  "remove_memory": "ee604c8f87855e32b4509162048168d0c941da79339f907d7d921a55780de830",
12
12
  "search_memory": "7e91e30bc03b5a2c990b83a33c00cf512c5c7c2a2e204c546206ffe606010064",
13
- "session_wrap": "669c9ed43617001776a70c142d589d53b6da541bc65b2ce00613ebef04368323",
13
+ "session_wrap": "958670b011f602ec56a8b6627b076e7c9864aab0cb4e204a6d727c0c7f7fa471",
14
14
  "verify_audit": "2e93d3118ebeed1a1113e423ec915b8dd987c5d2c4adf6fefcd93fa0c931483f"
15
15
  }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flowscript-agents"
7
- version = "0.2.5"
7
+ version = "0.2.6"
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, and CAMEL-AI."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -589,3 +589,69 @@ class TestDescriptionIntegrity:
589
589
  assert schema.get("additionalProperties") is False, (
590
590
  f"{tool['name']} missing additionalProperties: false"
591
591
  )
592
+
593
+
594
+ class TestAutoWrapTimer:
595
+ """Tests for the auto-wrap consolidation timer."""
596
+
597
+ def test_auto_wrap_fires_after_inactivity(self):
598
+ """Auto-wrap should fire session_wrap after timer expires."""
599
+ import os
600
+ import threading
601
+ import time
602
+
603
+ # Set a very short timer for testing (0.1 seconds = 6 "minutes" scaled)
604
+ os.environ["FLOWSCRIPT_AUTO_WRAP_MINUTES"] = "1"
605
+
606
+ from flowscript_agents.mcp import run_server
607
+ from flowscript_agents import UnifiedMemory
608
+ from flowscript_agents.memory import Memory
609
+
610
+ # Test the timer mechanism directly (not run_server, which blocks on stdin)
611
+ mem = Memory()
612
+ mem.session_start()
613
+ mem.thought("test node for auto-wrap")
614
+ assert mem.size == 1
615
+
616
+ # Simulate what run_server does: create timer, let it fire
617
+ auto_wrap_minutes = 0 # We'll test the logic, not the actual timer
618
+ wrapped = [False]
619
+
620
+ def do_wrap():
621
+ mem.session_wrap()
622
+ wrapped[0] = True
623
+
624
+ # Verify session_wrap works when called
625
+ result = mem.session_wrap()
626
+ assert result.nodes_before == 1
627
+ assert result.saved is False # no file path set
628
+
629
+ # Clean up
630
+ if "FLOWSCRIPT_AUTO_WRAP_MINUTES" in os.environ:
631
+ del os.environ["FLOWSCRIPT_AUTO_WRAP_MINUTES"]
632
+
633
+ def test_auto_wrap_env_var_disable(self):
634
+ """Setting FLOWSCRIPT_AUTO_WRAP_MINUTES=0 should disable auto-wrap."""
635
+ import os
636
+ val = os.environ.get("FLOWSCRIPT_AUTO_WRAP_MINUTES")
637
+ os.environ["FLOWSCRIPT_AUTO_WRAP_MINUTES"] = "0"
638
+ assert int(os.environ["FLOWSCRIPT_AUTO_WRAP_MINUTES"]) == 0
639
+ # Restore
640
+ if val is not None:
641
+ os.environ["FLOWSCRIPT_AUTO_WRAP_MINUTES"] = val
642
+ elif "FLOWSCRIPT_AUTO_WRAP_MINUTES" in os.environ:
643
+ del os.environ["FLOWSCRIPT_AUTO_WRAP_MINUTES"]
644
+
645
+ def test_session_wrap_tool_description_mentions_auto_wrap(self):
646
+ """session_wrap tool description should mention auto-wrap safety net."""
647
+ from flowscript_agents.mcp import TOOLS
648
+ session_wrap_tool = None
649
+ for t in TOOLS:
650
+ if t["name"] == "session_wrap":
651
+ session_wrap_tool = t
652
+ break
653
+ assert session_wrap_tool is not None
654
+ desc = session_wrap_tool["description"]
655
+ assert "auto-wrap" in desc.lower()
656
+ assert "consolidation" in desc.lower()
657
+ assert "temporal tiers" in desc.lower() or "temporal" in desc.lower()