flowscript-agents 0.2.8__tar.gz → 0.2.9__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.8 → flowscript_agents-0.2.9}/PKG-INFO +1 -1
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/__init__.py +1 -1
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/audit.py +5 -1
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/cloud.py +94 -28
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/pyproject.toml +1 -1
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_cloud.py +137 -15
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/.github/workflows/test.yml +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/.gitignore +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/AUDIT_TRAIL_DESIGN.md +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/README.md +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/docs/adapters.md +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/docs/api-reference.md +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/docs/audit-trail.md +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/docs/brand/logo-512.png +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/docs/brand/social-preview.png +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/docs/flowscript-demo.png +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/docs/lifecycle.md +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/examples/CLAUDE.md.example +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/examples/langgraph_live_test.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/examples/temporal_e2e_test.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/camel_ai.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/crewai.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/embeddings/__init__.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/embeddings/_utils.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/embeddings/consolidate.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/embeddings/extract.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/embeddings/index.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/embeddings/providers.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/embeddings/search.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/explain.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/fixpoint.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/google_adk.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/haystack.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/langgraph.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/llamaindex.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/mcp.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/memory.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/openai_agents.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/pydantic_ai.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/query.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/smolagents.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/tool-integrity.json +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/types.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/unified.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/scripts/validate_dedup_threshold.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/conftest.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_audit.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_camel_ai.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_consolidation.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_crewai.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_embeddings.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_explain.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_fixpoint.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_google_adk.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_haystack.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_langgraph.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_llamaindex.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_mcp.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_memory.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_openai_agents.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_pydantic_ai.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/tests/test_smolagents.py +0 -0
- {flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/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.9
|
|
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
|
|
@@ -19,6 +19,7 @@ File layout:
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
+
import copy
|
|
22
23
|
import gzip
|
|
23
24
|
import atexit
|
|
24
25
|
import hashlib
|
|
@@ -407,7 +408,10 @@ class AuditWriter:
|
|
|
407
408
|
# Fire on_event callback (failure must never block audit, but log to stderr)
|
|
408
409
|
if self._config.on_event:
|
|
409
410
|
if self._config.on_event_async:
|
|
410
|
-
|
|
411
|
+
# Deep copy to prevent caller mutation between write() return and
|
|
412
|
+
# async callback execution from producing different canonical JSON
|
|
413
|
+
# (which would break hash chain verification in CloudClient).
|
|
414
|
+
self._get_executor().submit(self._fire_on_event, copy.deepcopy(entry))
|
|
411
415
|
else:
|
|
412
416
|
self._fire_on_event(entry)
|
|
413
417
|
|
|
@@ -40,7 +40,7 @@ import threading
|
|
|
40
40
|
import urllib.error
|
|
41
41
|
import urllib.request
|
|
42
42
|
from dataclasses import dataclass, field
|
|
43
|
-
from typing import Any, Optional
|
|
43
|
+
from typing import Any, Callable, Optional
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
# =============================================================================
|
|
@@ -49,8 +49,16 @@ from typing import Any, Optional
|
|
|
49
49
|
|
|
50
50
|
DEFAULT_CLOUD_URL = "https://api.flowscript.org"
|
|
51
51
|
DEFAULT_BATCH_SIZE = 100
|
|
52
|
+
DEFAULT_MAX_BUFFER_SIZE = 10000
|
|
52
53
|
DEFAULT_FLUSH_INTERVAL = 5.0 # seconds (for future auto-flush)
|
|
53
|
-
|
|
54
|
+
def _get_user_agent() -> str:
|
|
55
|
+
try:
|
|
56
|
+
from flowscript_agents import __version__
|
|
57
|
+
return f"flowscript-agents-python/{__version__}"
|
|
58
|
+
except ImportError:
|
|
59
|
+
return "flowscript-agents-python/unknown"
|
|
60
|
+
|
|
61
|
+
USER_AGENT = _get_user_agent()
|
|
54
62
|
|
|
55
63
|
|
|
56
64
|
# =============================================================================
|
|
@@ -101,6 +109,12 @@ class CloudClient:
|
|
|
101
109
|
env var, then https://api.flowscript.org.
|
|
102
110
|
batch_size: Flush automatically when buffer reaches this size.
|
|
103
111
|
Default 100.
|
|
112
|
+
max_buffer_size: Maximum events to buffer before dropping new events.
|
|
113
|
+
Prevents unbounded memory growth when Cloud is unreachable.
|
|
114
|
+
Dropped events are still in the local audit trail (AuditWriter
|
|
115
|
+
writes to disk before calling on_event). Use the backfill
|
|
116
|
+
endpoint to recover after Cloud connectivity is restored.
|
|
117
|
+
Default 10000.
|
|
104
118
|
timeout: HTTP request timeout in seconds. Default 30.
|
|
105
119
|
on_witness: Optional callback invoked with each CloudWitness after
|
|
106
120
|
successful flush. Use for logging or monitoring.
|
|
@@ -114,9 +128,10 @@ class CloudClient:
|
|
|
114
128
|
namespace: Optional[str] = None,
|
|
115
129
|
endpoint: Optional[str] = None,
|
|
116
130
|
batch_size: int = DEFAULT_BATCH_SIZE,
|
|
131
|
+
max_buffer_size: int = DEFAULT_MAX_BUFFER_SIZE,
|
|
117
132
|
timeout: int = 30,
|
|
118
|
-
on_witness: Optional[
|
|
119
|
-
on_error: Optional[
|
|
133
|
+
on_witness: Optional[Callable] = None,
|
|
134
|
+
on_error: Optional[Callable] = None,
|
|
120
135
|
) -> None:
|
|
121
136
|
self._api_key = api_key or os.environ.get("FLOWSCRIPT_API_KEY", "")
|
|
122
137
|
self._namespace = namespace or os.environ.get("FLOWSCRIPT_NAMESPACE", "")
|
|
@@ -124,6 +139,7 @@ class CloudClient:
|
|
|
124
139
|
endpoint or os.environ.get("FLOWSCRIPT_CLOUD_URL", DEFAULT_CLOUD_URL)
|
|
125
140
|
).rstrip("/")
|
|
126
141
|
self._batch_size = batch_size
|
|
142
|
+
self._max_buffer_size = max_buffer_size
|
|
127
143
|
self._timeout = timeout
|
|
128
144
|
self._on_witness = on_witness
|
|
129
145
|
self._on_error = on_error
|
|
@@ -131,6 +147,7 @@ class CloudClient:
|
|
|
131
147
|
self._lock = threading.Lock()
|
|
132
148
|
self._total_sent = 0
|
|
133
149
|
self._total_accepted = 0
|
|
150
|
+
self._total_dropped = 0
|
|
134
151
|
self._last_witness: Optional[CloudWitness] = None
|
|
135
152
|
|
|
136
153
|
if not self._api_key:
|
|
@@ -159,6 +176,16 @@ class CloudClient:
|
|
|
159
176
|
"""Total events accepted by Cloud (excludes replays)."""
|
|
160
177
|
return self._total_accepted
|
|
161
178
|
|
|
179
|
+
@property
|
|
180
|
+
def total_dropped(self) -> int:
|
|
181
|
+
"""Total events dropped due to buffer overflow.
|
|
182
|
+
|
|
183
|
+
Dropped events are NOT lost — they exist in the local audit trail
|
|
184
|
+
(AuditWriter writes to disk before calling on_event). Use the
|
|
185
|
+
backfill endpoint to sync them to Cloud after connectivity is restored.
|
|
186
|
+
"""
|
|
187
|
+
return self._total_dropped
|
|
188
|
+
|
|
162
189
|
@property
|
|
163
190
|
def last_witness(self) -> Optional[CloudWitness]:
|
|
164
191
|
"""Most recent witness attestation received."""
|
|
@@ -175,6 +202,10 @@ class CloudClient:
|
|
|
175
202
|
The entry dict is re-serialized to canonical JSON (matching the
|
|
176
203
|
AuditWriter's serialization) for hash-chain-compatible transmission.
|
|
177
204
|
|
|
205
|
+
If the buffer has reached max_buffer_size, the event is dropped with
|
|
206
|
+
a warning to stderr. Dropped events are NOT lost — they exist in the
|
|
207
|
+
local audit trail. Use the backfill endpoint to recover.
|
|
208
|
+
|
|
178
209
|
Args:
|
|
179
210
|
entry: Audit event dict as produced by AuditWriter.write()
|
|
180
211
|
"""
|
|
@@ -182,10 +213,28 @@ class CloudClient:
|
|
|
182
213
|
# Python json.dumps with sort_keys=True is deterministic for the same dict.
|
|
183
214
|
json_line = json.dumps(entry, sort_keys=True, separators=(",", ":"))
|
|
184
215
|
|
|
216
|
+
batch_to_send = None
|
|
185
217
|
with self._lock:
|
|
218
|
+
if len(self._buffer) >= self._max_buffer_size:
|
|
219
|
+
self._total_dropped += 1
|
|
220
|
+
if self._total_dropped == 1 or self._total_dropped % 1000 == 0:
|
|
221
|
+
print(
|
|
222
|
+
f"CloudClient: buffer full ({self._max_buffer_size} events). "
|
|
223
|
+
f"Dropping Cloud sync ({self._total_dropped} dropped total). "
|
|
224
|
+
f"Local audit trail is unaffected. Use backfill to recover.",
|
|
225
|
+
file=sys.stderr,
|
|
226
|
+
)
|
|
227
|
+
return
|
|
228
|
+
|
|
186
229
|
self._buffer.append(json_line)
|
|
187
230
|
if len(self._buffer) >= self._batch_size:
|
|
188
|
-
self.
|
|
231
|
+
batch_to_send = list(self._buffer)
|
|
232
|
+
self._buffer.clear()
|
|
233
|
+
|
|
234
|
+
# Auto-flush outside the lock — same pattern as flush() to avoid
|
|
235
|
+
# blocking other queue_event() callers during HTTP I/O.
|
|
236
|
+
if batch_to_send is not None:
|
|
237
|
+
self._send_batch(batch_to_send)
|
|
189
238
|
|
|
190
239
|
def flush(self) -> Optional[CloudFlushResult]:
|
|
191
240
|
"""Send all buffered events to Cloud.
|
|
@@ -195,18 +244,22 @@ class CloudClient:
|
|
|
195
244
|
|
|
196
245
|
Safe to call from any thread, including atexit handlers.
|
|
197
246
|
"""
|
|
247
|
+
# Take events from buffer under lock, then release lock for HTTP I/O.
|
|
248
|
+
# This allows other threads to queue_event() while we're sending.
|
|
198
249
|
with self._lock:
|
|
199
|
-
|
|
250
|
+
if not self._buffer:
|
|
251
|
+
return None
|
|
252
|
+
events = list(self._buffer)
|
|
253
|
+
self._buffer.clear()
|
|
200
254
|
|
|
201
|
-
|
|
202
|
-
"""Internal flush — must be called with self._lock held."""
|
|
203
|
-
if not self._buffer:
|
|
204
|
-
return None
|
|
255
|
+
return self._send_batch(events)
|
|
205
256
|
|
|
206
|
-
|
|
207
|
-
|
|
257
|
+
def _send_batch(self, events: list[str]) -> CloudFlushResult:
|
|
258
|
+
"""Send a batch of canonical JSON event strings to Cloud.
|
|
208
259
|
|
|
209
|
-
|
|
260
|
+
Handles response parsing, witness tracking, error handling, and retry
|
|
261
|
+
(putting events back in buffer on 5xx/network errors).
|
|
262
|
+
"""
|
|
210
263
|
body = json.dumps({
|
|
211
264
|
"namespace": self._namespace,
|
|
212
265
|
"events": events,
|
|
@@ -231,18 +284,27 @@ class CloudClient:
|
|
|
231
284
|
accepted = resp_body.get("accepted", 0)
|
|
232
285
|
self._total_accepted += accepted
|
|
233
286
|
|
|
234
|
-
# Parse witness
|
|
287
|
+
# Parse witness — defensive against malformed responses.
|
|
288
|
+
# Events are already accepted; witness parse failure should not
|
|
289
|
+
# cause data loss or propagate errors.
|
|
235
290
|
witness_data = resp_body.get("witness")
|
|
236
291
|
witness = None
|
|
237
292
|
if witness_data:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
293
|
+
try:
|
|
294
|
+
witness = CloudWitness(
|
|
295
|
+
id=witness_data["id"],
|
|
296
|
+
chain_head_seq=witness_data["chain_head_seq"],
|
|
297
|
+
chain_head_hash=witness_data["chain_head_hash"],
|
|
298
|
+
witnessed_at=witness_data["witnessed_at"],
|
|
299
|
+
)
|
|
300
|
+
self._last_witness = witness
|
|
301
|
+
except (KeyError, TypeError) as e:
|
|
302
|
+
print(
|
|
303
|
+
f"CloudClient: malformed witness response: {e}",
|
|
304
|
+
file=sys.stderr,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
if witness and self._on_witness:
|
|
246
308
|
try:
|
|
247
309
|
self._on_witness(witness)
|
|
248
310
|
except Exception as e:
|
|
@@ -274,14 +336,16 @@ class CloudClient:
|
|
|
274
336
|
# that needs human attention. On 4xx, also don't retry (client error).
|
|
275
337
|
# On 5xx, put events back in buffer for retry.
|
|
276
338
|
if e.code >= 500:
|
|
277
|
-
|
|
339
|
+
with self._lock:
|
|
340
|
+
self._buffer = events + self._buffer
|
|
278
341
|
|
|
279
342
|
self._handle_error(result)
|
|
280
343
|
return result
|
|
281
344
|
|
|
282
345
|
except (urllib.error.URLError, OSError, TimeoutError) as e:
|
|
283
346
|
# Network error — put events back for retry
|
|
284
|
-
|
|
347
|
+
with self._lock:
|
|
348
|
+
self._buffer = events + self._buffer
|
|
285
349
|
result = CloudFlushResult(
|
|
286
350
|
accepted=0,
|
|
287
351
|
error=f"Network error: {e}",
|
|
@@ -311,10 +375,12 @@ class CloudClient:
|
|
|
311
375
|
|
|
312
376
|
Useful for backfill or one-shot uploads.
|
|
313
377
|
"""
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
378
|
+
json_lines = [
|
|
379
|
+
json.dumps(entry, sort_keys=True, separators=(",", ":"))
|
|
380
|
+
for entry in entries
|
|
381
|
+
]
|
|
382
|
+
with self._lock:
|
|
383
|
+
self._buffer.extend(json_lines)
|
|
318
384
|
return self.flush() or CloudFlushResult(accepted=0)
|
|
319
385
|
|
|
320
386
|
def health(self) -> dict[str, Any]:
|
|
@@ -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.9"
|
|
8
8
|
description = "Complete agent memory: reasoning queries + vector search + auto-extraction. Decision intelligence for LangGraph, CrewAI, Google ADK, OpenAI Agents SDK, Pydantic AI, smolagents, LlamaIndex, Haystack, and CAMEL-AI."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -107,21 +107,19 @@ class TestCloudClientBuffering:
|
|
|
107
107
|
client = CloudClient(api_key="fsk_test", namespace="test/agent", batch_size=3)
|
|
108
108
|
entry = {"v": 1, "seq": 0, "event": "test", "data": {}, "timestamp": "T", "prev_hash": "sha256:GENESIS", "session_id": None, "adapter": None}
|
|
109
109
|
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
client.queue_event(entry) # This should trigger auto-flush
|
|
124
|
-
assert client.buffered_count == 0
|
|
110
|
+
# Patch urlopen to return a mock response
|
|
111
|
+
mock_resp = MagicMock()
|
|
112
|
+
mock_resp.read.return_value = json.dumps({"accepted": 3, "witness": None}).encode()
|
|
113
|
+
mock_resp.status = 200
|
|
114
|
+
mock_resp.__enter__ = lambda s: s
|
|
115
|
+
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
116
|
+
|
|
117
|
+
with patch("urllib.request.urlopen", return_value=mock_resp):
|
|
118
|
+
client.queue_event(entry)
|
|
119
|
+
client.queue_event(entry)
|
|
120
|
+
assert client.buffered_count == 2
|
|
121
|
+
client.queue_event(entry) # This should trigger auto-flush
|
|
122
|
+
assert client.buffered_count == 0
|
|
125
123
|
|
|
126
124
|
def test_thread_safety(self):
|
|
127
125
|
"""Verify concurrent queue_event calls don't corrupt buffer."""
|
|
@@ -141,6 +139,64 @@ class TestCloudClientBuffering:
|
|
|
141
139
|
assert client.buffered_count == 1000
|
|
142
140
|
|
|
143
141
|
|
|
142
|
+
class TestCloudClientBufferOverflow:
|
|
143
|
+
"""Test buffer overflow protection."""
|
|
144
|
+
|
|
145
|
+
def test_drops_events_at_max_buffer_size(self):
|
|
146
|
+
"""Events beyond max_buffer_size are dropped (not lost — on disk)."""
|
|
147
|
+
client = CloudClient(api_key="fsk_test", namespace="test/agent", batch_size=10000, max_buffer_size=5)
|
|
148
|
+
entry = {"v": 1, "seq": 0, "event": "test", "data": {}, "timestamp": "T", "prev_hash": "sha256:GENESIS", "session_id": None, "adapter": None}
|
|
149
|
+
|
|
150
|
+
for _ in range(10):
|
|
151
|
+
client.queue_event(entry)
|
|
152
|
+
|
|
153
|
+
assert client.buffered_count == 5
|
|
154
|
+
assert client.total_dropped == 5
|
|
155
|
+
|
|
156
|
+
def test_drop_warning_rate_limited(self, capsys):
|
|
157
|
+
"""Warning prints on first drop and every 1000th, not every drop."""
|
|
158
|
+
client = CloudClient(api_key="fsk_test", namespace="test/agent", batch_size=10000, max_buffer_size=1)
|
|
159
|
+
entry = {"v": 1, "seq": 0, "event": "test", "data": {}, "timestamp": "T", "prev_hash": "sha256:GENESIS", "session_id": None, "adapter": None}
|
|
160
|
+
|
|
161
|
+
# Fill buffer
|
|
162
|
+
client.queue_event(entry)
|
|
163
|
+
# Drop 3 more
|
|
164
|
+
for _ in range(3):
|
|
165
|
+
client.queue_event(entry)
|
|
166
|
+
|
|
167
|
+
captured = capsys.readouterr()
|
|
168
|
+
# Should have exactly 1 warning (first drop), not 3
|
|
169
|
+
assert captured.err.count("buffer full") == 1
|
|
170
|
+
assert client.total_dropped == 3
|
|
171
|
+
|
|
172
|
+
def test_buffer_accepts_again_after_flush(self):
|
|
173
|
+
"""After flushing, buffer accepts new events even if drops occurred."""
|
|
174
|
+
client = CloudClient(api_key="fsk_test", namespace="test/agent", batch_size=10000, max_buffer_size=2)
|
|
175
|
+
entry = {"v": 1, "seq": 0, "event": "test", "data": {}, "timestamp": "T", "prev_hash": "sha256:GENESIS", "session_id": None, "adapter": None}
|
|
176
|
+
|
|
177
|
+
# Fill and overflow
|
|
178
|
+
for _ in range(5):
|
|
179
|
+
client.queue_event(entry)
|
|
180
|
+
assert client.buffered_count == 2
|
|
181
|
+
assert client.total_dropped == 3
|
|
182
|
+
|
|
183
|
+
# Flush (mock HTTP)
|
|
184
|
+
mock_resp = MagicMock()
|
|
185
|
+
mock_resp.read.return_value = json.dumps({"accepted": 2, "witness": None}).encode()
|
|
186
|
+
mock_resp.status = 200
|
|
187
|
+
mock_resp.__enter__ = lambda s: s
|
|
188
|
+
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
189
|
+
|
|
190
|
+
with patch("urllib.request.urlopen", return_value=mock_resp):
|
|
191
|
+
client.flush()
|
|
192
|
+
|
|
193
|
+
assert client.buffered_count == 0
|
|
194
|
+
|
|
195
|
+
# Should accept again
|
|
196
|
+
client.queue_event(entry)
|
|
197
|
+
assert client.buffered_count == 1
|
|
198
|
+
|
|
199
|
+
|
|
144
200
|
class TestCloudClientErrorHandling:
|
|
145
201
|
"""Test error handling and retry behavior."""
|
|
146
202
|
|
|
@@ -192,6 +248,72 @@ class TestCloudClientErrorHandling:
|
|
|
192
248
|
assert client.buffered_count == 1 # Put back
|
|
193
249
|
|
|
194
250
|
|
|
251
|
+
class TestCloudClientMalformedResponse:
|
|
252
|
+
"""Test handling of malformed server responses."""
|
|
253
|
+
|
|
254
|
+
def test_malformed_witness_does_not_lose_events(self):
|
|
255
|
+
"""If witness response is malformed, events are still counted as accepted."""
|
|
256
|
+
client = CloudClient(api_key="fsk_test", namespace="test/agent")
|
|
257
|
+
entry = {"v": 1, "seq": 0, "event": "test", "data": {}, "timestamp": "T", "prev_hash": "sha256:GENESIS", "session_id": None, "adapter": None}
|
|
258
|
+
client.queue_event(entry)
|
|
259
|
+
|
|
260
|
+
mock_resp = MagicMock()
|
|
261
|
+
mock_resp.read.return_value = json.dumps({
|
|
262
|
+
"accepted": 1,
|
|
263
|
+
"witness": {"garbage": True}, # missing required fields
|
|
264
|
+
}).encode()
|
|
265
|
+
mock_resp.status = 200
|
|
266
|
+
mock_resp.__enter__ = lambda s: s
|
|
267
|
+
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
268
|
+
|
|
269
|
+
with patch("urllib.request.urlopen", return_value=mock_resp):
|
|
270
|
+
result = client.flush()
|
|
271
|
+
|
|
272
|
+
assert result.accepted == 1
|
|
273
|
+
assert client.total_accepted == 1
|
|
274
|
+
assert client.last_witness is None # Not updated with garbage
|
|
275
|
+
assert client.buffered_count == 0 # Events NOT put back
|
|
276
|
+
|
|
277
|
+
def test_on_error_callback_fires_on_failure(self):
|
|
278
|
+
"""Verify on_error callback is invoked on flush failure."""
|
|
279
|
+
errors = []
|
|
280
|
+
client = CloudClient(
|
|
281
|
+
api_key="fsk_test",
|
|
282
|
+
namespace="test/agent",
|
|
283
|
+
on_error=lambda r: errors.append(r),
|
|
284
|
+
)
|
|
285
|
+
entry = {"v": 1, "seq": 0, "event": "test", "data": {}, "timestamp": "T", "prev_hash": "sha256:GENESIS", "session_id": None, "adapter": None}
|
|
286
|
+
client.queue_event(entry)
|
|
287
|
+
|
|
288
|
+
import urllib.error
|
|
289
|
+
error = urllib.error.HTTPError("http://test", 409, "Conflict", {}, None)
|
|
290
|
+
with patch("urllib.request.urlopen", side_effect=error):
|
|
291
|
+
client.flush()
|
|
292
|
+
|
|
293
|
+
assert len(errors) == 1
|
|
294
|
+
assert errors[0].status_code == 409
|
|
295
|
+
|
|
296
|
+
def test_on_error_exception_swallowed(self):
|
|
297
|
+
"""Exceptions in on_error callback must not propagate."""
|
|
298
|
+
def bad_callback(result):
|
|
299
|
+
raise RuntimeError("callback boom")
|
|
300
|
+
|
|
301
|
+
client = CloudClient(
|
|
302
|
+
api_key="fsk_test",
|
|
303
|
+
namespace="test/agent",
|
|
304
|
+
on_error=bad_callback,
|
|
305
|
+
)
|
|
306
|
+
entry = {"v": 1, "seq": 0, "event": "test", "data": {}, "timestamp": "T", "prev_hash": "sha256:GENESIS", "session_id": None, "adapter": None}
|
|
307
|
+
client.queue_event(entry)
|
|
308
|
+
|
|
309
|
+
import urllib.error
|
|
310
|
+
error = urllib.error.HTTPError("http://test", 400, "Bad Request", {}, None)
|
|
311
|
+
with patch("urllib.request.urlopen", side_effect=error):
|
|
312
|
+
# Should not raise despite bad callback
|
|
313
|
+
result = client.flush()
|
|
314
|
+
assert result.error is not None
|
|
315
|
+
|
|
316
|
+
|
|
195
317
|
class TestAuditWriterIntegration:
|
|
196
318
|
"""Test CloudClient integration with AuditWriter via on_event callback."""
|
|
197
319
|
|
|
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
|
{flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/embeddings/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/flowscript_agents/embeddings/consolidate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flowscript_agents-0.2.8 → flowscript_agents-0.2.9}/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
|
|
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
|