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.
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/PKG-INFO +40 -12
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/README.md +39 -11
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/docs/audit-trail.md +13 -2
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/audit.py +15 -3
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/camel_ai.py +10 -3
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/crewai.py +9 -3
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/extract.py +68 -2
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/google_adk.py +10 -3
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/haystack.py +9 -3
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/langgraph.py +9 -3
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/llamaindex.py +9 -3
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/mcp.py +118 -1
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/memory.py +19 -2
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/openai_agents.py +9 -3
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/pydantic_ai.py +9 -3
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/smolagents.py +9 -3
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/pyproject.toml +1 -1
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_mcp.py +3 -2
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/.gitignore +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/AUDIT_TRAIL_DESIGN.md +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/docs/adapters.md +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/docs/api-reference.md +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/docs/flowscript-demo.png +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/docs/lifecycle.md +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/examples/CLAUDE.md.example +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/examples/langgraph_live_test.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/examples/temporal_e2e_test.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/__init__.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/__init__.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/_utils.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/consolidate.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/index.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/providers.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/search.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/query.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/types.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/unified.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/scripts/validate_dedup_threshold.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/conftest.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_audit.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_camel_ai.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_consolidation.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_crewai.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_embeddings.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_google_adk.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_haystack.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_langgraph.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_llamaindex.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_memory.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_openai_agents.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_pydantic_ai.py +0 -0
- {flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/tests/test_smolagents.py +0 -0
- {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.
|
|
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("
|
|
87
|
-
mem.add("Redis
|
|
88
|
-
mem.add("
|
|
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
|
-
|
|
91
|
-
# →
|
|
92
|
-
#
|
|
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
|
-
|
|
95
|
-
# →
|
|
95
|
+
blocked = mem.memory.query.blocked()
|
|
96
|
+
# → BlockedResult(0 blockers)
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
# → full
|
|
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
|
|
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("
|
|
23
|
-
mem.add("Redis
|
|
24
|
-
mem.add("
|
|
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
|
-
|
|
27
|
-
# →
|
|
28
|
-
#
|
|
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
|
-
|
|
31
|
-
# →
|
|
31
|
+
blocked = mem.memory.query.blocked()
|
|
32
|
+
# → BlockedResult(0 blockers)
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
# → full
|
|
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
|
|
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 (
|
|
26
|
-
`node_create`, `
|
|
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=
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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.
|
|
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) ==
|
|
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) ==
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/consolidate.py
RENAMED
|
File without changes
|
|
File without changes
|
{flowscript_agents-0.2.0 → flowscript_agents-0.2.1}/flowscript_agents/embeddings/providers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|