flowscript-agents 0.2.7__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/PKG-INFO +1 -1
  2. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/__init__.py +5 -1
  3. flowscript_agents-0.2.8/flowscript_agents/cloud.py +325 -0
  4. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/extract.py +30 -1
  5. flowscript_agents-0.2.8/flowscript_agents/fixpoint.py +339 -0
  6. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/pyproject.toml +1 -1
  7. flowscript_agents-0.2.8/tests/test_cloud.py +312 -0
  8. flowscript_agents-0.2.8/tests/test_fixpoint.py +671 -0
  9. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/.github/workflows/test.yml +0 -0
  10. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/.gitignore +0 -0
  11. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/AUDIT_TRAIL_DESIGN.md +0 -0
  12. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/README.md +0 -0
  13. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/adapters.md +0 -0
  14. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/api-reference.md +0 -0
  15. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/audit-trail.md +0 -0
  16. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/brand/logo-512.png +0 -0
  17. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/brand/social-preview.png +0 -0
  18. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/flowscript-demo.png +0 -0
  19. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/lifecycle.md +0 -0
  20. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/examples/CLAUDE.md.example +0 -0
  21. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/examples/langgraph_live_test.py +0 -0
  22. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/examples/temporal_e2e_test.py +0 -0
  23. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/audit.py +0 -0
  24. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/camel_ai.py +0 -0
  25. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/crewai.py +0 -0
  26. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/__init__.py +0 -0
  27. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/_utils.py +0 -0
  28. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/consolidate.py +0 -0
  29. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/index.py +0 -0
  30. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/providers.py +0 -0
  31. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/search.py +0 -0
  32. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/explain.py +0 -0
  33. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/google_adk.py +0 -0
  34. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/haystack.py +0 -0
  35. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/langgraph.py +0 -0
  36. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/llamaindex.py +0 -0
  37. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/mcp.py +0 -0
  38. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/memory.py +0 -0
  39. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/openai_agents.py +0 -0
  40. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/pydantic_ai.py +0 -0
  41. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/query.py +0 -0
  42. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/smolagents.py +0 -0
  43. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/tool-integrity.json +0 -0
  44. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/types.py +0 -0
  45. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/unified.py +0 -0
  46. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/scripts/validate_dedup_threshold.py +0 -0
  47. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/conftest.py +0 -0
  48. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_audit.py +0 -0
  49. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_camel_ai.py +0 -0
  50. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_consolidation.py +0 -0
  51. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_crewai.py +0 -0
  52. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_embeddings.py +0 -0
  53. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_explain.py +0 -0
  54. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_google_adk.py +0 -0
  55. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_haystack.py +0 -0
  56. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_langgraph.py +0 -0
  57. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_llamaindex.py +0 -0
  58. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_mcp.py +0 -0
  59. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_memory.py +0 -0
  60. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_openai_agents.py +0 -0
  61. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_pydantic_ai.py +0 -0
  62. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_smolagents.py +0 -0
  63. {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_temporal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowscript-agents
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: Complete agent memory: reasoning queries + vector search + auto-extraction. Decision intelligence for LangGraph, CrewAI, Google ADK, OpenAI Agents SDK, Pydantic AI, smolagents, LlamaIndex, Haystack, and CAMEL-AI.
5
5
  Project-URL: Homepage, https://flowscript.org
6
6
  Project-URL: Repository, https://github.com/phillipclapham/flowscript-agents
@@ -27,6 +27,7 @@ Usage:
27
27
  """
28
28
 
29
29
  from .audit import AuditConfig, AuditQueryResult, AuditVerifyResult
30
+ from .cloud import CloudClient, CloudFlushResult, CloudWitness
30
31
  from .memory import (
31
32
  Memory,
32
33
  MemoryOptions,
@@ -44,12 +45,15 @@ from .memory import (
44
45
  from .unified import UnifiedMemory
45
46
  from .explain import explain
46
47
 
47
- __version__ = "0.2.7"
48
+ __version__ = "0.2.8"
48
49
  __all__ = [
49
50
  "explain",
50
51
  "AuditConfig",
51
52
  "AuditQueryResult",
52
53
  "AuditVerifyResult",
54
+ "CloudClient",
55
+ "CloudFlushResult",
56
+ "CloudWitness",
53
57
  "Memory",
54
58
  "MemoryOptions",
55
59
  "NodeRef",
@@ -0,0 +1,325 @@
1
+ """
2
+ CloudClient — Batching client for FlowScript Cloud API.
3
+
4
+ Sends audit trail events to api.flowscript.org for independent cryptographic
5
+ witnessing. Events are buffered locally and flushed in batches.
6
+
7
+ Integration with AuditWriter via on_event callback:
8
+
9
+ from flowscript_agents import AuditConfig
10
+ from flowscript_agents.cloud import CloudClient
11
+
12
+ cloud = CloudClient(api_key="fsk_...", namespace="myorg/myagent")
13
+ config = AuditConfig(on_event=cloud.queue_event, on_event_async=True)
14
+ writer = AuditWriter(Path("agent.json"), config)
15
+ # Events are automatically queued and flushed to Cloud
16
+
17
+ # At shutdown:
18
+ cloud.flush() # Send any remaining buffered events
19
+
20
+ Manual usage:
21
+
22
+ cloud = CloudClient(api_key="fsk_...", namespace="myorg/myagent")
23
+ cloud.queue_event(entry_dict)
24
+ result = cloud.flush()
25
+ print(result.accepted, result.witness)
26
+
27
+ Configuration via environment:
28
+
29
+ FLOWSCRIPT_API_KEY=fsk_...
30
+ FLOWSCRIPT_CLOUD_URL=https://api.flowscript.org (default)
31
+ FLOWSCRIPT_NAMESPACE=myorg/myagent
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import json
37
+ import os
38
+ import sys
39
+ import threading
40
+ import urllib.error
41
+ import urllib.request
42
+ from dataclasses import dataclass, field
43
+ from typing import Any, Optional
44
+
45
+
46
+ # =============================================================================
47
+ # Configuration
48
+ # =============================================================================
49
+
50
+ DEFAULT_CLOUD_URL = "https://api.flowscript.org"
51
+ DEFAULT_BATCH_SIZE = 100
52
+ DEFAULT_FLUSH_INTERVAL = 5.0 # seconds (for future auto-flush)
53
+ USER_AGENT = "flowscript-agents-python/0.2.8"
54
+
55
+
56
+ # =============================================================================
57
+ # Result Types
58
+ # =============================================================================
59
+
60
+
61
+ @dataclass
62
+ class CloudFlushResult:
63
+ """Result of a flush operation."""
64
+
65
+ accepted: int
66
+ witness: Optional[dict[str, Any]] = None
67
+ error: Optional[str] = None
68
+ status_code: Optional[int] = None
69
+
70
+
71
+ @dataclass
72
+ class CloudWitness:
73
+ """Witness attestation from Cloud."""
74
+
75
+ id: str
76
+ chain_head_seq: int
77
+ chain_head_hash: str
78
+ witnessed_at: str
79
+
80
+
81
+ # =============================================================================
82
+ # CloudClient
83
+ # =============================================================================
84
+
85
+
86
+ class CloudClient:
87
+ """Batching client for FlowScript Cloud event ingestion.
88
+
89
+ Events are buffered locally and sent in batches to minimize network
90
+ round-trips. Each flush returns a witness attestation from the Cloud
91
+ service, proving independent verification of the hash chain.
92
+
93
+ Thread-safe: multiple threads can call queue_event() concurrently.
94
+
95
+ Args:
96
+ api_key: FlowScript Cloud API key (fsk_...). Falls back to
97
+ FLOWSCRIPT_API_KEY env var.
98
+ namespace: Agent namespace in "owner/agent" format. Falls back to
99
+ FLOWSCRIPT_NAMESPACE env var.
100
+ endpoint: Cloud API base URL. Falls back to FLOWSCRIPT_CLOUD_URL
101
+ env var, then https://api.flowscript.org.
102
+ batch_size: Flush automatically when buffer reaches this size.
103
+ Default 100.
104
+ timeout: HTTP request timeout in seconds. Default 30.
105
+ on_witness: Optional callback invoked with each CloudWitness after
106
+ successful flush. Use for logging or monitoring.
107
+ on_error: Optional callback invoked on flush errors. Receives the
108
+ CloudFlushResult with error details. Default: print to stderr.
109
+ """
110
+
111
+ def __init__(
112
+ self,
113
+ api_key: Optional[str] = None,
114
+ namespace: Optional[str] = None,
115
+ endpoint: Optional[str] = None,
116
+ batch_size: int = DEFAULT_BATCH_SIZE,
117
+ timeout: int = 30,
118
+ on_witness: Optional[callable] = None,
119
+ on_error: Optional[callable] = None,
120
+ ) -> None:
121
+ self._api_key = api_key or os.environ.get("FLOWSCRIPT_API_KEY", "")
122
+ self._namespace = namespace or os.environ.get("FLOWSCRIPT_NAMESPACE", "")
123
+ self._endpoint = (
124
+ endpoint or os.environ.get("FLOWSCRIPT_CLOUD_URL", DEFAULT_CLOUD_URL)
125
+ ).rstrip("/")
126
+ self._batch_size = batch_size
127
+ self._timeout = timeout
128
+ self._on_witness = on_witness
129
+ self._on_error = on_error
130
+ self._buffer: list[str] = []
131
+ self._lock = threading.Lock()
132
+ self._total_sent = 0
133
+ self._total_accepted = 0
134
+ self._last_witness: Optional[CloudWitness] = None
135
+
136
+ if not self._api_key:
137
+ raise ValueError(
138
+ "API key required. Pass api_key= or set FLOWSCRIPT_API_KEY env var."
139
+ )
140
+ if not self._namespace:
141
+ raise ValueError(
142
+ "Namespace required. Pass namespace= or set FLOWSCRIPT_NAMESPACE env var. "
143
+ "Format: owner/agent"
144
+ )
145
+
146
+ @property
147
+ def buffered_count(self) -> int:
148
+ """Number of events currently buffered."""
149
+ with self._lock:
150
+ return len(self._buffer)
151
+
152
+ @property
153
+ def total_sent(self) -> int:
154
+ """Total events sent to Cloud (may include retransmissions)."""
155
+ return self._total_sent
156
+
157
+ @property
158
+ def total_accepted(self) -> int:
159
+ """Total events accepted by Cloud (excludes replays)."""
160
+ return self._total_accepted
161
+
162
+ @property
163
+ def last_witness(self) -> Optional[CloudWitness]:
164
+ """Most recent witness attestation received."""
165
+ return self._last_witness
166
+
167
+ # -------------------------------------------------------------------------
168
+ # Queue + Flush
169
+ # -------------------------------------------------------------------------
170
+
171
+ def queue_event(self, entry: dict[str, Any]) -> None:
172
+ """Queue an audit event for batch upload to Cloud.
173
+
174
+ This is designed to be used as the AuditConfig.on_event callback.
175
+ The entry dict is re-serialized to canonical JSON (matching the
176
+ AuditWriter's serialization) for hash-chain-compatible transmission.
177
+
178
+ Args:
179
+ entry: Audit event dict as produced by AuditWriter.write()
180
+ """
181
+ # Re-serialize to canonical JSON — identical to AuditWriter line 390.
182
+ # Python json.dumps with sort_keys=True is deterministic for the same dict.
183
+ json_line = json.dumps(entry, sort_keys=True, separators=(",", ":"))
184
+
185
+ with self._lock:
186
+ self._buffer.append(json_line)
187
+ if len(self._buffer) >= self._batch_size:
188
+ self._flush_locked()
189
+
190
+ def flush(self) -> Optional[CloudFlushResult]:
191
+ """Send all buffered events to Cloud.
192
+
193
+ Returns None if buffer is empty. Otherwise returns CloudFlushResult
194
+ with accepted count and witness attestation.
195
+
196
+ Safe to call from any thread, including atexit handlers.
197
+ """
198
+ with self._lock:
199
+ return self._flush_locked()
200
+
201
+ def _flush_locked(self) -> Optional[CloudFlushResult]:
202
+ """Internal flush — must be called with self._lock held."""
203
+ if not self._buffer:
204
+ return None
205
+
206
+ events = list(self._buffer)
207
+ self._buffer.clear()
208
+
209
+ # Build request body
210
+ body = json.dumps({
211
+ "namespace": self._namespace,
212
+ "events": events,
213
+ }).encode("utf-8")
214
+
215
+ url = f"{self._endpoint}/v1/events"
216
+ req = urllib.request.Request(
217
+ url,
218
+ data=body,
219
+ headers={
220
+ "Content-Type": "application/json",
221
+ "Authorization": f"Bearer {self._api_key}",
222
+ "User-Agent": USER_AGENT,
223
+ },
224
+ method="POST",
225
+ )
226
+
227
+ try:
228
+ with urllib.request.urlopen(req, timeout=self._timeout) as resp:
229
+ resp_body = json.loads(resp.read().decode("utf-8"))
230
+ self._total_sent += len(events)
231
+ accepted = resp_body.get("accepted", 0)
232
+ self._total_accepted += accepted
233
+
234
+ # Parse witness
235
+ witness_data = resp_body.get("witness")
236
+ witness = None
237
+ if witness_data:
238
+ witness = CloudWitness(
239
+ id=witness_data["id"],
240
+ chain_head_seq=witness_data["chain_head_seq"],
241
+ chain_head_hash=witness_data["chain_head_hash"],
242
+ witnessed_at=witness_data["witnessed_at"],
243
+ )
244
+ self._last_witness = witness
245
+ if self._on_witness:
246
+ try:
247
+ self._on_witness(witness)
248
+ except Exception as e:
249
+ print(
250
+ f"CloudClient: on_witness callback failed: {e}",
251
+ file=sys.stderr,
252
+ )
253
+
254
+ return CloudFlushResult(
255
+ accepted=accepted,
256
+ witness=witness_data,
257
+ status_code=resp.status,
258
+ )
259
+
260
+ except urllib.error.HTTPError as e:
261
+ error_body = ""
262
+ try:
263
+ error_body = e.read().decode("utf-8")
264
+ except Exception:
265
+ pass
266
+
267
+ result = CloudFlushResult(
268
+ accepted=0,
269
+ error=f"HTTP {e.code}: {error_body}",
270
+ status_code=e.code,
271
+ )
272
+
273
+ # On 409 (chain break), don't retry — this is a data integrity issue
274
+ # that needs human attention. On 4xx, also don't retry (client error).
275
+ # On 5xx, put events back in buffer for retry.
276
+ if e.code >= 500:
277
+ self._buffer = events + self._buffer # prepend for order preservation
278
+
279
+ self._handle_error(result)
280
+ return result
281
+
282
+ except (urllib.error.URLError, OSError, TimeoutError) as e:
283
+ # Network error — put events back for retry
284
+ self._buffer = events + self._buffer
285
+ result = CloudFlushResult(
286
+ accepted=0,
287
+ error=f"Network error: {e}",
288
+ )
289
+ self._handle_error(result)
290
+ return result
291
+
292
+ def _handle_error(self, result: CloudFlushResult) -> None:
293
+ """Handle flush errors."""
294
+ if self._on_error:
295
+ try:
296
+ self._on_error(result)
297
+ except Exception:
298
+ pass
299
+ else:
300
+ print(
301
+ f"CloudClient: flush failed: {result.error}",
302
+ file=sys.stderr,
303
+ )
304
+
305
+ # -------------------------------------------------------------------------
306
+ # Convenience
307
+ # -------------------------------------------------------------------------
308
+
309
+ def send_events(self, entries: list[dict[str, Any]]) -> CloudFlushResult:
310
+ """Send a list of events immediately (no buffering).
311
+
312
+ Useful for backfill or one-shot uploads.
313
+ """
314
+ for entry in entries:
315
+ json_line = json.dumps(entry, sort_keys=True, separators=(",", ":"))
316
+ with self._lock:
317
+ self._buffer.append(json_line)
318
+ return self.flush() or CloudFlushResult(accepted=0)
319
+
320
+ def health(self) -> dict[str, Any]:
321
+ """Check Cloud API health. Returns health response dict."""
322
+ url = f"{self._endpoint}/v1/health"
323
+ req = urllib.request.Request(url, method="GET", headers={"User-Agent": USER_AGENT})
324
+ with urllib.request.urlopen(req, timeout=self._timeout) as resp:
325
+ return json.loads(resp.read().decode("utf-8"))
@@ -611,6 +611,18 @@ class AutoExtract:
611
611
  4. Create extraction relationships for surviving nodes
612
612
  5. Apply extraction states for surviving nodes
613
613
  """
614
+ # Capture pre-extraction graph hash for attestation.
615
+ # MUST be before node creation — the certificate's initial_graph_hash
616
+ # must represent the graph BEFORE any extraction/consolidation work.
617
+ # Otherwise ADD actions (novel nodes already in memory from step 1)
618
+ # produce initial_hash == final_hash, contradicting delta > 0.
619
+ pre_hash = None
620
+ try:
621
+ from ..fixpoint import FixpointContext
622
+ pre_hash = FixpointContext._compute_graph_hash_static(self._memory)
623
+ except Exception:
624
+ pass # Attestation is optional — consolidation always runs
625
+
614
626
  # Create all nodes and embed them
615
627
  node_refs: list[NodeRef | None] = []
616
628
  extracted_node_dicts: list[dict[str, Any]] = []
@@ -624,11 +636,28 @@ class AutoExtract:
624
636
  "content": extracted_node.content,
625
637
  })
626
638
 
627
- # Run consolidation
639
+ # Run consolidation (always runs — this is the core operation)
628
640
  consolidation_result = self._consolidation_engine.consolidate(
629
641
  extracted_node_dicts, node_refs
630
642
  )
631
643
 
644
+ # Wrap with fixpoint attestation (fail-open: never blocks consolidation)
645
+ try:
646
+ from ..fixpoint import FixpointContext
647
+
648
+ with FixpointContext(self._memory, name="consolidation", constraint="L1",
649
+ _pre_hash=pre_hash) as fix_ctx:
650
+ delta = (
651
+ consolidation_result.nodes_added
652
+ + consolidation_result.nodes_updated
653
+ + consolidation_result.nodes_related
654
+ + consolidation_result.nodes_resolved
655
+ )
656
+ fix_ctx.record_iteration(delta)
657
+ fix_ctx.record_iteration(0) # convergence marker (degenerate @fix)
658
+ except Exception as e:
659
+ print(f"AutoExtract: fixpoint attestation failed: {e}", file=sys.stderr)
660
+
632
661
  # Create extraction relationships for surviving nodes only
633
662
  # (consolidation may have removed some via UPDATE/NONE)
634
663
  rels_created = self._create_extraction_relationships(extraction, node_refs)