flowscript-agents 0.2.0__tar.gz → 0.2.2__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.2}/PKG-INFO +102 -14
  2. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/README.md +101 -13
  3. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/docs/audit-trail.md +13 -2
  4. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/examples/CLAUDE.md.example +2 -1
  5. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/audit.py +15 -3
  6. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/camel_ai.py +10 -3
  7. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/crewai.py +9 -3
  8. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/embeddings/extract.py +68 -2
  9. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/google_adk.py +10 -3
  10. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/haystack.py +9 -3
  11. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/langgraph.py +9 -3
  12. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/llamaindex.py +9 -3
  13. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/mcp.py +134 -6
  14. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/memory.py +19 -2
  15. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/openai_agents.py +9 -3
  16. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/pydantic_ai.py +9 -3
  17. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/smolagents.py +9 -3
  18. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/pyproject.toml +1 -1
  19. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_mcp.py +3 -2
  20. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/.gitignore +0 -0
  21. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/AUDIT_TRAIL_DESIGN.md +0 -0
  22. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/docs/adapters.md +0 -0
  23. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/docs/api-reference.md +0 -0
  24. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/docs/flowscript-demo.png +0 -0
  25. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/docs/lifecycle.md +0 -0
  26. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/examples/langgraph_live_test.py +0 -0
  27. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/examples/temporal_e2e_test.py +0 -0
  28. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/__init__.py +0 -0
  29. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/embeddings/__init__.py +0 -0
  30. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/embeddings/_utils.py +0 -0
  31. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/embeddings/consolidate.py +0 -0
  32. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/embeddings/index.py +0 -0
  33. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/embeddings/providers.py +0 -0
  34. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/embeddings/search.py +0 -0
  35. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/query.py +0 -0
  36. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/types.py +0 -0
  37. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/flowscript_agents/unified.py +0 -0
  38. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/scripts/validate_dedup_threshold.py +0 -0
  39. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/conftest.py +0 -0
  40. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_audit.py +0 -0
  41. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_camel_ai.py +0 -0
  42. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_consolidation.py +0 -0
  43. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_crewai.py +0 -0
  44. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_embeddings.py +0 -0
  45. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_google_adk.py +0 -0
  46. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_haystack.py +0 -0
  47. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_langgraph.py +0 -0
  48. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_llamaindex.py +0 -0
  49. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_memory.py +0 -0
  50. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_openai_agents.py +0 -0
  51. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_pydantic_ai.py +0 -0
  52. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/tests/test_smolagents.py +0 -0
  53. {flowscript_agents-0.2.0 → flowscript_agents-0.2.2}/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.2
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*.
@@ -110,6 +111,16 @@ Five queries that no vector store can answer — `why()`, `tensions()`, `blocked
110
111
 
111
112
  ### MCP Server (Claude Code / Cursor — zero code)
112
113
 
114
+ ```bash
115
+ pip install flowscript-agents openai
116
+ ```
117
+
118
+ The `openai` package is required for extraction, consolidation, and vector search. Without it, `add_memory` stores raw text and `query_tensions` won't find anything.
119
+
120
+ Add to your editor's MCP config:
121
+
122
+ **Claude Code** — add to `.claude/settings.json` in your project (or `~/.claude/settings.json` for global):
123
+
113
124
  ```json
114
125
  {
115
126
  "mcpServers": {
@@ -124,11 +135,63 @@ Five queries that no vector store can answer — `why()`, `tensions()`, `blocked
124
135
  }
125
136
  ```
126
137
 
138
+ **Cursor / Windsurf / VS Code** — add to `.mcp.json` in your project root:
139
+
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "flowscript": {
144
+ "type": "stdio",
145
+ "command": "flowscript-mcp",
146
+ "args": ["--memory", "./project-memory.json"],
147
+ "env": {
148
+ "OPENAI_API_KEY": "your-key"
149
+ }
150
+ }
151
+ }
152
+ }
153
+ ```
154
+
155
+ **Fallback:** If `env` passthrough doesn't work in your editor, export the key in your shell before launching:
127
156
  ```bash
128
- pip install flowscript-agents
157
+ export OPENAI_API_KEY=your-key
129
158
  ```
130
159
 
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.
160
+ The server auto-detects your API key and configures the full stack:
161
+
162
+ | Key | What you get |
163
+ |:----|:-------------|
164
+ | `OPENAI_API_KEY` | Vector search (text-embedding-3-small) + typed extraction (gpt-4o-mini) + consolidation |
165
+ | `ANTHROPIC_API_KEY` | Typed extraction + consolidation (no embeddings, keyword search fallback) |
166
+ | Neither | Raw text storage only. Tools work, but no typed extraction and `query_tensions` won't find anything. |
167
+
168
+ **Without an API key, you get a degraded experience.** The server warns on startup and in tool responses.
169
+
170
+ ### Embedding Providers
171
+
172
+ The default is OpenAI `text-embedding-3-small`. To use a different provider, pass flags in `args`:
173
+
174
+ ```json
175
+ "args": ["--memory", "./project-memory.json", "--embedder", "ollama", "--embedding-model", "nomic-embed-text"]
176
+ ```
177
+
178
+ | Flag | What it does | Default |
179
+ |:-----|:-------------|:--------|
180
+ | `--embedder` | Embedding provider: `openai`, `sentence-transformers`, or `ollama` | Auto-detected from API key |
181
+ | `--embedding-model` | Model name (provider-specific) | `text-embedding-3-small` (OpenAI) |
182
+ | `--llm-model` | LLM for extraction and consolidation | `gpt-4o-mini` |
183
+ | `--no-auto` | Disable auto-configuration from API keys | Off |
184
+
185
+ **Local embeddings (free, no API key for embeddings):**
186
+
187
+ | Provider | Install | Example model | Notes |
188
+ |:---------|:--------|:--------------|:------|
189
+ | Ollama | [Install Ollama](https://ollama.com), then `ollama pull nomic-embed-text` | `nomic-embed-text` | Beats text-embedding-3-small. 274MB. |
190
+ | SentenceTransformers | `pip install sentence-transformers` | `BAAI/bge-m3` | Runs on CPU. Downloads on first use. |
191
+
192
+ You still need an LLM API key (`OPENAI_API_KEY` or `ANTHROPIC_API_KEY`) for typed extraction and consolidation, even when using local embeddings.
193
+
194
+ **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
195
 
133
196
  ### Python SDK
134
197
 
@@ -155,6 +218,31 @@ FlowScript operates at three levels. Pick where you start:
155
218
 
156
219
  ---
157
220
 
221
+ ## First 5 Minutes
222
+
223
+ With the MCP server running and the CLAUDE.md snippet in your project, try this conversation:
224
+
225
+ > "I need to decide between PostgreSQL and MongoDB for our user data. We need ACID compliance for payments but flexibility for user profiles."
226
+
227
+ Your agent stores the decision context, tradeoffs, and rationale automatically. Now introduce contradictory information:
228
+
229
+ > "Actually, I've been looking at DynamoDB. The scale requirements might matter more than I thought."
230
+
231
+ Now ask:
232
+
233
+ > "What tensions do we have in our architecture decisions?"
234
+
235
+ 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.
236
+
237
+ After a few sessions, try:
238
+ - *"What's blocking our progress?"* surfaces blockers and their downstream impact
239
+ - *"Why did we choose PostgreSQL originally?"* traces the full causal chain
240
+ - *"What if we switch to DynamoDB?"* maps the downstream consequences
241
+
242
+ 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.
243
+
244
+ ---
245
+
158
246
  ## Works With Your Stack
159
247
 
160
248
  Drop-in adapters that implement your framework's native interface. Same API you already use — plus `query.tensions()`.
@@ -261,7 +349,7 @@ Under the hood: a local semantic graph with typed nodes, typed relationships, an
261
349
 
262
350
  | Package | What | Install |
263
351
  |:--------|:-----|:--------|
264
- | [flowscript-agents](https://pypi.org/project/flowscript-agents/) | Python SDK — 9 adapters, unified memory, consolidation, audit trail | `pip install flowscript-agents` |
352
+ | [flowscript-agents](https://pypi.org/project/flowscript-agents/) | Python SDK — 9 adapters, unified memory, consolidation, audit trail | `pip install flowscript-agents openai` |
265
353
  | [flowscript-core](https://www.npmjs.com/package/flowscript-core) | TypeScript SDK — Memory class, 15 tools, token budgeting, audit trail | `npm install flowscript-core` |
266
354
  | [flowscript.org](https://flowscript.org) | Web editor, D3 visualization, live query panel | Browser |
267
355
 
@@ -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*.
@@ -46,6 +47,16 @@ Five queries that no vector store can answer — `why()`, `tensions()`, `blocked
46
47
 
47
48
  ### MCP Server (Claude Code / Cursor — zero code)
48
49
 
50
+ ```bash
51
+ pip install flowscript-agents openai
52
+ ```
53
+
54
+ The `openai` package is required for extraction, consolidation, and vector search. Without it, `add_memory` stores raw text and `query_tensions` won't find anything.
55
+
56
+ Add to your editor's MCP config:
57
+
58
+ **Claude Code** — add to `.claude/settings.json` in your project (or `~/.claude/settings.json` for global):
59
+
49
60
  ```json
50
61
  {
51
62
  "mcpServers": {
@@ -60,11 +71,63 @@ Five queries that no vector store can answer — `why()`, `tensions()`, `blocked
60
71
  }
61
72
  ```
62
73
 
74
+ **Cursor / Windsurf / VS Code** — add to `.mcp.json` in your project root:
75
+
76
+ ```json
77
+ {
78
+ "mcpServers": {
79
+ "flowscript": {
80
+ "type": "stdio",
81
+ "command": "flowscript-mcp",
82
+ "args": ["--memory", "./project-memory.json"],
83
+ "env": {
84
+ "OPENAI_API_KEY": "your-key"
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Fallback:** If `env` passthrough doesn't work in your editor, export the key in your shell before launching:
63
92
  ```bash
64
- pip install flowscript-agents
93
+ export OPENAI_API_KEY=your-key
65
94
  ```
66
95
 
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.
96
+ The server auto-detects your API key and configures the full stack:
97
+
98
+ | Key | What you get |
99
+ |:----|:-------------|
100
+ | `OPENAI_API_KEY` | Vector search (text-embedding-3-small) + typed extraction (gpt-4o-mini) + consolidation |
101
+ | `ANTHROPIC_API_KEY` | Typed extraction + consolidation (no embeddings, keyword search fallback) |
102
+ | Neither | Raw text storage only. Tools work, but no typed extraction and `query_tensions` won't find anything. |
103
+
104
+ **Without an API key, you get a degraded experience.** The server warns on startup and in tool responses.
105
+
106
+ ### Embedding Providers
107
+
108
+ The default is OpenAI `text-embedding-3-small`. To use a different provider, pass flags in `args`:
109
+
110
+ ```json
111
+ "args": ["--memory", "./project-memory.json", "--embedder", "ollama", "--embedding-model", "nomic-embed-text"]
112
+ ```
113
+
114
+ | Flag | What it does | Default |
115
+ |:-----|:-------------|:--------|
116
+ | `--embedder` | Embedding provider: `openai`, `sentence-transformers`, or `ollama` | Auto-detected from API key |
117
+ | `--embedding-model` | Model name (provider-specific) | `text-embedding-3-small` (OpenAI) |
118
+ | `--llm-model` | LLM for extraction and consolidation | `gpt-4o-mini` |
119
+ | `--no-auto` | Disable auto-configuration from API keys | Off |
120
+
121
+ **Local embeddings (free, no API key for embeddings):**
122
+
123
+ | Provider | Install | Example model | Notes |
124
+ |:---------|:--------|:--------------|:------|
125
+ | Ollama | [Install Ollama](https://ollama.com), then `ollama pull nomic-embed-text` | `nomic-embed-text` | Beats text-embedding-3-small. 274MB. |
126
+ | SentenceTransformers | `pip install sentence-transformers` | `BAAI/bge-m3` | Runs on CPU. Downloads on first use. |
127
+
128
+ You still need an LLM API key (`OPENAI_API_KEY` or `ANTHROPIC_API_KEY`) for typed extraction and consolidation, even when using local embeddings.
129
+
130
+ **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
131
 
69
132
  ### Python SDK
70
133
 
@@ -91,6 +154,31 @@ FlowScript operates at three levels. Pick where you start:
91
154
 
92
155
  ---
93
156
 
157
+ ## First 5 Minutes
158
+
159
+ With the MCP server running and the CLAUDE.md snippet in your project, try this conversation:
160
+
161
+ > "I need to decide between PostgreSQL and MongoDB for our user data. We need ACID compliance for payments but flexibility for user profiles."
162
+
163
+ Your agent stores the decision context, tradeoffs, and rationale automatically. Now introduce contradictory information:
164
+
165
+ > "Actually, I've been looking at DynamoDB. The scale requirements might matter more than I thought."
166
+
167
+ Now ask:
168
+
169
+ > "What tensions do we have in our architecture decisions?"
170
+
171
+ 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.
172
+
173
+ After a few sessions, try:
174
+ - *"What's blocking our progress?"* surfaces blockers and their downstream impact
175
+ - *"Why did we choose PostgreSQL originally?"* traces the full causal chain
176
+ - *"What if we switch to DynamoDB?"* maps the downstream consequences
177
+
178
+ 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.
179
+
180
+ ---
181
+
94
182
  ## Works With Your Stack
95
183
 
96
184
  Drop-in adapters that implement your framework's native interface. Same API you already use — plus `query.tensions()`.
@@ -197,7 +285,7 @@ Under the hood: a local semantic graph with typed nodes, typed relationships, an
197
285
 
198
286
  | Package | What | Install |
199
287
  |:--------|:-----|:--------|
200
- | [flowscript-agents](https://pypi.org/project/flowscript-agents/) | Python SDK — 9 adapters, unified memory, consolidation, audit trail | `pip install flowscript-agents` |
288
+ | [flowscript-agents](https://pypi.org/project/flowscript-agents/) | Python SDK — 9 adapters, unified memory, consolidation, audit trail | `pip install flowscript-agents openai` |
201
289
  | [flowscript-core](https://www.npmjs.com/package/flowscript-core) | TypeScript SDK — Memory class, 15 tools, token budgeting, audit trail | `npm install flowscript-core` |
202
290
  | [flowscript.org](https://flowscript.org) | Web editor, D3 visualization, live query panel | Browser |
203
291
 
@@ -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:
@@ -2,7 +2,8 @@
2
2
  #
3
3
  # Paste the section below into your project's CLAUDE.md to enable
4
4
  # proactive reasoning memory. FlowScript MCP tools are available
5
- # when the server is configured via .mcp.json or ~/.claude.json.
5
+ # when the server is configured in your editor's MCP settings
6
+ # (.claude/settings.json for Claude Code, .mcp.json for Cursor/Windsurf).
6
7
 
7
8
  ## Reasoning Memory (FlowScript)
8
9
 
@@ -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