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.
- {spatialagents-0.1.0 → spatialagents-0.1.1}/PKG-INFO +1 -1
- {spatialagents-0.1.0 → spatialagents-0.1.1}/pyproject.toml +1 -1
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/__init__.py +1 -1
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/gateway.py +21 -2
- {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_gateway.py +53 -1
- {spatialagents-0.1.0 → spatialagents-0.1.1}/uv.lock +1 -1
- {spatialagents-0.1.0 → spatialagents-0.1.1}/.gitignore +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/.python-version +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/README.md +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/approvals.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/backend.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/cache_plan.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/__init__.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/cli.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/http.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/server.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/static/chat.css +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/static/chat.js +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/chat/static/index.html +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/conversations.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/errors.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/offload.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/orchestrator.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/plans.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/policy.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/prefix.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/py.typed +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/runner.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/schema.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/serialize.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/sessions.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/testing.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/toolcache.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/tools.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/trace.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/src/spatialagents/wire.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/conftest.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/js/page_probe.mjs +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_approvals.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_chat_http.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_chat_page.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_compaction.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_conversations.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_import_light.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_pieces.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_plans.py +0 -0
- {spatialagents-0.1.0 → spatialagents-0.1.1}/tests/test_runner.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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")]
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|