spatialagents 0.1.0__tar.gz → 0.1.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.
Files changed (48) hide show
  1. {spatialagents-0.1.0 → spatialagents-0.1.1}/PKG-INFO +1 -1
  2. {spatialagents-0.1.0 → spatialagents-0.1.1}/pyproject.toml +1 -1
  3. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/__init__.py +1 -1
  4. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/gateway.py +21 -2
  5. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_gateway.py +53 -1
  6. {spatialagents-0.1.0 → spatialagents-0.1.1}/uv.lock +1 -1
  7. {spatialagents-0.1.0 → spatialagents-0.1.1}/.gitignore +0 -0
  8. {spatialagents-0.1.0 → spatialagents-0.1.1}/.python-version +0 -0
  9. {spatialagents-0.1.0 → spatialagents-0.1.1}/README.md +0 -0
  10. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/approvals.py +0 -0
  11. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/backend.py +0 -0
  12. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/cache_plan.py +0 -0
  13. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/__init__.py +0 -0
  14. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/cli.py +0 -0
  15. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/http.py +0 -0
  16. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/server.py +0 -0
  17. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/static/chat.css +0 -0
  18. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/static/chat.js +0 -0
  19. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/static/index.html +0 -0
  20. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/conversations.py +0 -0
  21. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/errors.py +0 -0
  22. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/offload.py +0 -0
  23. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/orchestrator.py +0 -0
  24. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/plans.py +0 -0
  25. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/policy.py +0 -0
  26. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/prefix.py +0 -0
  27. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/py.typed +0 -0
  28. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/runner.py +0 -0
  29. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/schema.py +0 -0
  30. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/serialize.py +0 -0
  31. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/sessions.py +0 -0
  32. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/testing.py +0 -0
  33. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/toolcache.py +0 -0
  34. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/tools.py +0 -0
  35. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/trace.py +0 -0
  36. {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/wire.py +0 -0
  37. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/conftest.py +0 -0
  38. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/js/page_probe.mjs +0 -0
  39. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_approvals.py +0 -0
  40. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_chat_http.py +0 -0
  41. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_chat_page.py +0 -0
  42. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_compaction.py +0 -0
  43. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_conversations.py +0 -0
  44. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_import_light.py +0 -0
  45. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_pieces.py +0 -0
  46. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_plans.py +0 -0
  47. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_runner.py +0 -0
  48. {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_toolcache_offload.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: spatialagents
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: The chat harness runtime SCIMAP Pro, Plexora and other BioCognia products share
5
5
  Author-email: Ajit Johnson Nirmal <ajitjohnson.n@gmail.com>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spatialagents"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "The chat harness runtime SCIMAP Pro, Plexora and other BioCognia products share"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -43,7 +43,7 @@ from __future__ import annotations
43
43
 
44
44
  import importlib
45
45
 
46
- __version__ = "0.1.0"
46
+ __version__ = "0.1.1"
47
47
 
48
48
  _SUBMODULES = frozenset({
49
49
  "approvals", "backend", "cache_plan", "chat", "conversations", "errors", "gateway", "offload",
@@ -22,11 +22,16 @@ Retries: a refusal that happened before anything reached a provider (rate
22
22
  limit, provider outage, unreachable gateway) is retried with the SAME
23
23
  idempotency key, which the gateway frees in exactly those cases; an expired
24
24
  token is refreshed once. Credit, cap and envelope refusals are never retried:
25
- they are surfaced so the run or conversation can pause (`PAUSE_CODES`).
25
+ they are surfaced so the run or conversation can pause (`PAUSE_CODES`). A
26
+ stream that breaks once the answer has begun -- the gateway stops, the
27
+ connection drops, or nothing arrives for `CALL_TIMEOUT` seconds -- is
28
+ `interrupted` and not retried: the call reached the provider, and the gateway
29
+ may have settled it already.
26
30
  """
27
31
 
28
32
  from __future__ import annotations
29
33
 
34
+ import http.client
30
35
  import json
31
36
  import time
32
37
 
@@ -61,6 +66,20 @@ __all__ = ["CALL_TIMEOUT", "CIRCUIT_ATTEMPTS", "CREDIT_CODES", "ENV_DEV", "ENV_G
61
66
  "gateway_url", "iter_sse"]
62
67
 
63
68
 
69
+ def _events(response):
70
+ """`iter_sse`, with a connection that breaks mid-answer raised as
71
+ `interrupted`. Only the read is covered: what the caller does with an
72
+ event (`on_delta`) raises as itself."""
73
+ try:
74
+ yield from iter_sse(response)
75
+ except TimeoutError:
76
+ raise GatewayError(f"The gateway went silent mid-answer: nothing arrived for {CALL_TIMEOUT} s.",
77
+ code="interrupted", detail={"transport": "TimeoutError"}) from None
78
+ except (OSError, http.client.HTTPException) as exc:
79
+ raise GatewayError(f"The connection to the gateway broke mid-answer ({type(exc).__name__}).",
80
+ code="interrupted", detail={"transport": type(exc).__name__}) from None
81
+
82
+
64
83
  class GatewayClient(_shared.GatewayClient):
65
84
  """The shared transport, plus the harness's streamed model call.
66
85
 
@@ -126,7 +145,7 @@ class GatewayClient(_shared.GatewayClient):
126
145
  usage_event: dict = {}
127
146
  accepted: dict = {}
128
147
  stop = None
129
- for event, data in iter_sse(response):
148
+ for event, data in _events(response):
130
149
  try:
131
150
  payload = json.loads(data)
132
151
  except ValueError:
@@ -4,12 +4,14 @@ hand-written event streams."""
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import contextlib
8
+ import http.client
7
9
  import io
8
10
  import json
9
11
 
10
12
  import pytest
11
13
 
12
- from spatialagents.gateway import PAUSE_CODES, GatewayClient, GatewayError
14
+ from spatialagents.gateway import CALL_TIMEOUT, PAUSE_CODES, GatewayClient, GatewayError
13
15
  from spatialagents.testing import FakeGateway, Reply, fixed_tokens
14
16
  from spatialagents.wire import ModelRequest, task_id
15
17
 
@@ -102,6 +104,56 @@ def test_a_stream_the_gateway_never_settled_is_interrupted():
102
104
  assert caught.value.code == "interrupted"
103
105
 
104
106
 
107
+ HALF = ("content_block_delta", {"index": 0, "delta": {"type": "text_delta", "text": "half"}})
108
+
109
+
110
+ def stalls(error, *events):
111
+ """A body that streams `events`, then fails its next read with `error`."""
112
+ def lines():
113
+ yield from sse(*events)
114
+ raise error
115
+ return lines()
116
+
117
+
118
+ @pytest.mark.parametrize("error, said", [(TimeoutError("The read operation timed out"), "went silent"),
119
+ (ConnectionResetError(10054, "reset"), "ConnectionResetError"),
120
+ (http.client.IncompleteRead(b""), "IncompleteRead")])
121
+ def test_a_connection_that_breaks_mid_answer_is_interrupted(error, said):
122
+ gc = GatewayClient(fixed_tokens(), "http://127.0.0.1:9")
123
+ deltas = []
124
+ with pytest.raises(GatewayError) as caught:
125
+ gc._read_stream(stalls(error, HALF), deltas.append)
126
+ assert caught.value.code == "interrupted" and said in str(caught.value)
127
+ assert deltas == ["half"]
128
+
129
+
130
+ def test_a_call_cut_mid_answer_is_not_retried(monkeypatch):
131
+ gc = GatewayClient(fixed_tokens(), "http://127.0.0.1:9")
132
+ opened, retries = [], []
133
+
134
+ def open_(method, path, body=None, headers=None, timeout=60):
135
+ opened.append(timeout)
136
+ return contextlib.nullcontext(stalls(TimeoutError("The read operation timed out"), HALF))
137
+
138
+ monkeypatch.setattr(gc, "open", open_)
139
+ with pytest.raises(GatewayError) as caught:
140
+ gc.messages(request(), idempotency_key="stall-0001", on_retry=lambda n, exc: retries.append(n))
141
+ assert caught.value.code == "interrupted" and not caught.value.retryable
142
+ assert opened == [CALL_TIMEOUT] and retries == []
143
+
144
+
145
+ def test_an_error_from_on_delta_is_not_taken_for_a_broken_connection():
146
+ class Refused(OSError):
147
+ pass
148
+
149
+ def on_delta(piece):
150
+ raise Refused(piece)
151
+
152
+ gc = GatewayClient(fixed_tokens(), "http://127.0.0.1:9")
153
+ with pytest.raises(Refused):
154
+ gc._read_stream(sse(HALF, USAGE), on_delta)
155
+
156
+
105
157
  def test_the_client_retries_an_outage_with_the_same_key_and_never_retries_credit():
106
158
  with FakeGateway(lambda packet, body: {"kind": "x"}) as gateway:
107
159
  gateway.fail_with = [(503, "provider_unavailable"), (429, "provider_rate_limited")]
@@ -389,7 +389,7 @@ wheels = [
389
389
 
390
390
  [[package]]
391
391
  name = "spatialagents"
392
- version = "0.1.0"
392
+ version = "0.1.1"
393
393
  source = { editable = "." }
394
394
  dependencies = [
395
395
  { name = "biocognia" },
File without changes
File without changes