statewire 0.3.3__tar.gz → 0.4.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.
- {statewire-0.3.3 → statewire-0.4.1}/PKG-INFO +1 -1
- {statewire-0.3.3 → statewire-0.4.1}/pyproject.toml +1 -1
- {statewire-0.3.3 → statewire-0.4.1}/src/statewire/__init__.py +2 -0
- {statewire-0.3.3 → statewire-0.4.1}/src/statewire/assistant_transport.py +19 -70
- statewire-0.4.1/src/statewire/legacy.py +114 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_assistant_transport.py +15 -10
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_assistant_transport_client.py +2 -2
- statewire-0.4.1/tests/test_legacy.py +110 -0
- {statewire-0.3.3 → statewire-0.4.1}/.gitignore +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/README.md +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/examples/__init__.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/examples/demo_app.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/src/statewire/api.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/src/statewire/assistant_transport_client.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/src/statewire/client.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/src/statewire/langgraph.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/src/statewire/ops.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/src/statewire/state.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/client_helpers.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/statewire_helpers.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_assistant_transport_facade.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_authorize.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_client.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_client_ws.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_commands.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_context.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_langgraph.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_lifespan.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_meta.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_state_proxy.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_statewire_hostable.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_stream.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_writer_lease.py +0 -0
- {statewire-0.3.3 → statewire-0.4.1}/tests/test_ws.py +0 -0
|
@@ -6,12 +6,14 @@ from .api import (
|
|
|
6
6
|
command,
|
|
7
7
|
)
|
|
8
8
|
from .assistant_transport import AssistantTransport, add_assistant_transport_facade
|
|
9
|
+
from .legacy import upgrade_legacy_commands
|
|
9
10
|
from .state import StateProxy, plain
|
|
10
11
|
|
|
11
12
|
__all__ = [
|
|
12
13
|
"Statewire",
|
|
13
14
|
"AssistantTransport",
|
|
14
15
|
"add_assistant_transport_facade",
|
|
16
|
+
"upgrade_legacy_commands",
|
|
15
17
|
"command",
|
|
16
18
|
"StatewireClientHandle",
|
|
17
19
|
"StatewireCommandContext",
|
|
@@ -17,8 +17,9 @@ the legacy commands, which are normalized to the canonical ``run/*`` dialect
|
|
|
17
17
|
(a host-registered handler of the same legacy name always wins and receives
|
|
18
18
|
the command whole):
|
|
19
19
|
|
|
20
|
-
- ``add-message`` becomes ``run/steer`` with ``{"message": <wire message
|
|
21
|
-
|
|
20
|
+
- ``add-message`` becomes ``run/steer`` with ``{"message": <wire message>,
|
|
21
|
+
"anchorMessageId": <body parentId>}`` (body ``parentId`` null or absent →
|
|
22
|
+
``null``): a fresh ``legacy_`` id is synthesized, image parts become
|
|
22
23
|
``{"type": "file", "mediaType": "image/*", "url"}`` parts, and command-level
|
|
23
24
|
anchors are dropped. A command-level non-null ``sourceId`` makes it
|
|
24
25
|
``run/edit`` with that ``sourceId`` directly; otherwise, when the body
|
|
@@ -31,8 +32,8 @@ the command whole):
|
|
|
31
32
|
unanswered tool-call request in ``state["inputRequests"]`` (None rejects),
|
|
32
33
|
and a ``modelContent`` field rejects (unsupported in ``run/input``).
|
|
33
34
|
- A body with a ``parentId`` and no commands is the legacy reload: it becomes
|
|
34
|
-
``run/reload`` with ``{"sourceId": <child
|
|
35
|
-
(None rejects).
|
|
35
|
+
``run/reload`` with ``{"sourceId": <child>, "messageId": <fresh legacy_
|
|
36
|
+
id>}`` via ``get_message_child_id`` (None rejects).
|
|
36
37
|
|
|
37
38
|
A host without the target ``run/*`` handler rejects the batch via the
|
|
38
39
|
ordinary unknown-command path. The response streams state changes as legacy
|
|
@@ -80,7 +81,6 @@ import json
|
|
|
80
81
|
import secrets
|
|
81
82
|
import time
|
|
82
83
|
from typing import Any, AsyncIterator, Coroutine
|
|
83
|
-
from uuid import uuid4
|
|
84
84
|
|
|
85
85
|
import httpx
|
|
86
86
|
from fastapi import FastAPI, HTTPException, Request
|
|
@@ -89,6 +89,7 @@ from starlette.background import BackgroundTask
|
|
|
89
89
|
from starlette.responses import Response, StreamingResponse
|
|
90
90
|
|
|
91
91
|
from .api import _LAGGARD, Statewire
|
|
92
|
+
from .legacy import upgrade_legacy_commands
|
|
92
93
|
from .state import _frozen, plain
|
|
93
94
|
|
|
94
95
|
_STREAM_HEADERS = {
|
|
@@ -195,28 +196,6 @@ class _RunError(Exception):
|
|
|
195
196
|
self.payload = payload
|
|
196
197
|
|
|
197
198
|
|
|
198
|
-
def _legacy_wire_message(message: Any) -> dict[str, Any]:
|
|
199
|
-
if not isinstance(message, dict):
|
|
200
|
-
raise _RunError("add-message: message must be an object")
|
|
201
|
-
parts = message.get("parts")
|
|
202
|
-
if not isinstance(parts, list):
|
|
203
|
-
raise _RunError("add-message: message.parts must be an array")
|
|
204
|
-
converted = []
|
|
205
|
-
for part in parts:
|
|
206
|
-
if isinstance(part, dict) and part.get("type") == "image":
|
|
207
|
-
image = part.get("image")
|
|
208
|
-
if not isinstance(image, str):
|
|
209
|
-
raise _RunError("add-message: image part requires a string image field")
|
|
210
|
-
converted.append({"type": "file", "mediaType": "image/*", "url": image})
|
|
211
|
-
else:
|
|
212
|
-
converted.append(part)
|
|
213
|
-
return {
|
|
214
|
-
"id": f"legacy_{uuid4().hex}",
|
|
215
|
-
"role": message.get("role"),
|
|
216
|
-
"parts": converted,
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
199
|
_Item = tuple[str, Any]
|
|
221
200
|
|
|
222
201
|
|
|
@@ -429,7 +408,11 @@ class AssistantTransport(Statewire):
|
|
|
429
408
|
if not commands:
|
|
430
409
|
if parent_id is None:
|
|
431
410
|
return
|
|
432
|
-
members =
|
|
411
|
+
members = await upgrade_legacy_commands(
|
|
412
|
+
[],
|
|
413
|
+
parent_id=parent_id,
|
|
414
|
+
get_message_child_id=self.get_message_child_id,
|
|
415
|
+
)
|
|
433
416
|
else:
|
|
434
417
|
members = [
|
|
435
418
|
await self._assistant_transport_member(command, parent_id)
|
|
@@ -452,49 +435,15 @@ class AssistantTransport(Statewire):
|
|
|
452
435
|
) -> dict[str, Any]:
|
|
453
436
|
kind = command["type"]
|
|
454
437
|
registered = type(self)._statewire_commands
|
|
455
|
-
if kind
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
source_id = await self.get_message_child_id(parent_id)
|
|
462
|
-
if source_id is not None:
|
|
463
|
-
return {
|
|
464
|
-
"method": "run/edit",
|
|
465
|
-
"params": [{"sourceId": source_id, "message": message}],
|
|
466
|
-
}
|
|
467
|
-
return {"method": "run/steer", "params": [{"message": message}]}
|
|
468
|
-
if kind == "add-tool-result" and "add-tool-result" not in registered:
|
|
469
|
-
return {
|
|
470
|
-
"method": "run/input",
|
|
471
|
-
"params": [await self._legacy_input(command)],
|
|
472
|
-
}
|
|
473
|
-
return {"method": kind, "params": [command]}
|
|
474
|
-
|
|
475
|
-
async def _legacy_input(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
476
|
-
if command.get("modelContent") is not None:
|
|
477
|
-
raise _RunError("add-tool-result: modelContent is not supported")
|
|
478
|
-
tool_call_id = command.get("toolCallId")
|
|
479
|
-
if not isinstance(tool_call_id, str):
|
|
480
|
-
raise _RunError("add-tool-result: toolCallId must be a string")
|
|
481
|
-
request_id = await self.get_input_request_id(tool_call_id)
|
|
482
|
-
if request_id is None:
|
|
483
|
-
raise _RunError(
|
|
484
|
-
f"add-tool-result: no input request for toolCallId {tool_call_id!r}"
|
|
438
|
+
if kind in ("add-message", "add-tool-result") and kind not in registered:
|
|
439
|
+
[member] = await upgrade_legacy_commands(
|
|
440
|
+
[command],
|
|
441
|
+
parent_id=parent_id,
|
|
442
|
+
get_message_child_id=self.get_message_child_id,
|
|
443
|
+
get_input_request_id=self.get_input_request_id,
|
|
485
444
|
)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
response["isError"] = True
|
|
489
|
-
if "artifact" in command:
|
|
490
|
-
response["artifact"] = command["artifact"]
|
|
491
|
-
return {"requestId": request_id, "response": response}
|
|
492
|
-
|
|
493
|
-
async def _legacy_reload(self, parent_id: str) -> dict[str, Any]:
|
|
494
|
-
source_id = await self.get_message_child_id(parent_id)
|
|
495
|
-
if source_id is None:
|
|
496
|
-
raise _RunError(f"reload: no child message for parentId {parent_id!r}")
|
|
497
|
-
return {"method": "run/reload", "params": [{"sourceId": source_id}]}
|
|
445
|
+
return member
|
|
446
|
+
return {"method": kind, "params": [command]}
|
|
498
447
|
|
|
499
448
|
async def _assistant_transport_dispatch(self, method: str, identity: Any) -> None:
|
|
500
449
|
client_id = "at-" + secrets.token_urlsafe(9)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Upgrade legacy assistant-transport commands to the canonical ``run/*`` dialect."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Awaitable, Callable
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
_Resolver = Callable[[str], Awaitable[str | None]]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def _unresolved(_: str) -> None:
|
|
10
|
+
return None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _wire_message(message: Any) -> dict[str, Any]:
|
|
14
|
+
if not isinstance(message, dict):
|
|
15
|
+
raise ValueError("add-message: message must be an object")
|
|
16
|
+
parts = message.get("parts")
|
|
17
|
+
if not isinstance(parts, list):
|
|
18
|
+
raise ValueError("add-message: message.parts must be an array")
|
|
19
|
+
converted = []
|
|
20
|
+
for part in parts:
|
|
21
|
+
if isinstance(part, dict) and part.get("type") == "image":
|
|
22
|
+
image = part.get("image")
|
|
23
|
+
if not isinstance(image, str):
|
|
24
|
+
raise ValueError("add-message: image part requires a string image field")
|
|
25
|
+
converted.append({"type": "file", "mediaType": "image/*", "url": image})
|
|
26
|
+
else:
|
|
27
|
+
converted.append(part)
|
|
28
|
+
return {
|
|
29
|
+
"id": f"legacy_{uuid4().hex}",
|
|
30
|
+
"role": message.get("role"),
|
|
31
|
+
"parts": converted,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def _add_message(
|
|
36
|
+
command: dict[str, Any], parent_id: str | None, resolve_child: _Resolver
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
message = _wire_message(command.get("message"))
|
|
39
|
+
source_id = command.get("sourceId")
|
|
40
|
+
if source_id is not None and not isinstance(source_id, str):
|
|
41
|
+
raise ValueError("add-message: sourceId must be a string or null")
|
|
42
|
+
if source_id is None and parent_id is not None:
|
|
43
|
+
source_id = await resolve_child(parent_id)
|
|
44
|
+
if source_id is not None:
|
|
45
|
+
return {
|
|
46
|
+
"method": "run/edit",
|
|
47
|
+
"params": [{"sourceId": source_id, "message": message}],
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
"method": "run/steer",
|
|
51
|
+
"params": [{"message": message, "anchorMessageId": parent_id}],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def _add_tool_result(
|
|
56
|
+
command: dict[str, Any], resolve_request: _Resolver
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
if command.get("modelContent") is not None:
|
|
59
|
+
raise ValueError("add-tool-result: modelContent is not supported")
|
|
60
|
+
tool_call_id = command.get("toolCallId")
|
|
61
|
+
if not isinstance(tool_call_id, str):
|
|
62
|
+
raise ValueError("add-tool-result: toolCallId must be a string")
|
|
63
|
+
request_id = await resolve_request(tool_call_id)
|
|
64
|
+
if request_id is None:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"add-tool-result: no input request for toolCallId {tool_call_id!r}"
|
|
67
|
+
)
|
|
68
|
+
response: dict[str, Any] = {"output": command.get("result")}
|
|
69
|
+
if command.get("isError"):
|
|
70
|
+
response["isError"] = True
|
|
71
|
+
if "artifact" in command:
|
|
72
|
+
response["artifact"] = command["artifact"]
|
|
73
|
+
return {"requestId": request_id, "response": response}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def upgrade_legacy_commands(
|
|
77
|
+
commands: list[dict[str, Any]],
|
|
78
|
+
*,
|
|
79
|
+
parent_id: str | None = None,
|
|
80
|
+
get_message_child_id: _Resolver | None = None,
|
|
81
|
+
get_input_request_id: _Resolver | None = None,
|
|
82
|
+
) -> list[dict[str, Any]]:
|
|
83
|
+
"""Upgrade a legacy assistant-transport body (``commands`` + optional ``parentId``) into ``run/*`` command members."""
|
|
84
|
+
resolve_child = get_message_child_id or _unresolved
|
|
85
|
+
resolve_request = get_input_request_id or _unresolved
|
|
86
|
+
if not commands:
|
|
87
|
+
if parent_id is None:
|
|
88
|
+
return []
|
|
89
|
+
source_id = await resolve_child(parent_id)
|
|
90
|
+
if source_id is None:
|
|
91
|
+
raise ValueError(f"reload: no child message for parentId {parent_id!r}")
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
"method": "run/reload",
|
|
95
|
+
"params": [
|
|
96
|
+
{"sourceId": source_id, "messageId": f"legacy_{uuid4().hex}"}
|
|
97
|
+
],
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
members = []
|
|
101
|
+
for command in commands:
|
|
102
|
+
kind = command.get("type")
|
|
103
|
+
if kind == "add-message":
|
|
104
|
+
members.append(await _add_message(command, parent_id, resolve_child))
|
|
105
|
+
elif kind == "add-tool-result":
|
|
106
|
+
members.append(
|
|
107
|
+
{
|
|
108
|
+
"method": "run/input",
|
|
109
|
+
"params": [await _add_tool_result(command, resolve_request)],
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
raise ValueError(f"unknown legacy command type: {kind!r}")
|
|
114
|
+
return members
|
|
@@ -27,7 +27,7 @@ class Harness(AssistantTransport):
|
|
|
27
27
|
self.state = {"messages": [], "text": "", "meta": {"title": None}}
|
|
28
28
|
yield
|
|
29
29
|
|
|
30
|
-
@command("
|
|
30
|
+
@command("demo/echo")
|
|
31
31
|
async def add_message(self, cmd):
|
|
32
32
|
self.seen.append(cmd)
|
|
33
33
|
self.state["messages"].append(cmd["message"])
|
|
@@ -120,7 +120,7 @@ async def test_data_stream_response_streams_snapshot_then_ops_until_idle():
|
|
|
120
120
|
{
|
|
121
121
|
"commands": [
|
|
122
122
|
{
|
|
123
|
-
"type": "
|
|
123
|
+
"type": "demo/echo",
|
|
124
124
|
"message": {
|
|
125
125
|
"role": "user",
|
|
126
126
|
"parts": [{"type": "text", "text": "hi"}],
|
|
@@ -229,7 +229,7 @@ async def test_splice_and_remove_fall_back_to_parent_set():
|
|
|
229
229
|
{
|
|
230
230
|
"commands": [
|
|
231
231
|
{
|
|
232
|
-
"type": "
|
|
232
|
+
"type": "demo/echo",
|
|
233
233
|
"message": {
|
|
234
234
|
"role": "user",
|
|
235
235
|
"parts": [{"type": "text", "text": "hi"}],
|
|
@@ -266,7 +266,7 @@ async def test_assistant_transport_sse_mode():
|
|
|
266
266
|
{
|
|
267
267
|
"commands": [
|
|
268
268
|
{
|
|
269
|
-
"type": "
|
|
269
|
+
"type": "demo/echo",
|
|
270
270
|
"message": {
|
|
271
271
|
"role": "user",
|
|
272
272
|
"parts": [{"type": "text", "text": "hi"}],
|
|
@@ -428,7 +428,7 @@ async def test_resume_after_completed_run_replays_with_completed_header():
|
|
|
428
428
|
{
|
|
429
429
|
"commands": [
|
|
430
430
|
{
|
|
431
|
-
"type": "
|
|
431
|
+
"type": "demo/echo",
|
|
432
432
|
"message": {
|
|
433
433
|
"role": "user",
|
|
434
434
|
"parts": [{"type": "text", "text": "hi"}],
|
|
@@ -453,7 +453,7 @@ async def test_resume_rejects_commands_and_malformed_bodies():
|
|
|
453
453
|
async with statewire_client(Harness) as (app, client):
|
|
454
454
|
response = await client.post(
|
|
455
455
|
"/threads/t1/assistant-transport/api/resume",
|
|
456
|
-
json={"commands": [{"type": "
|
|
456
|
+
json={"commands": [{"type": "demo/echo"}]},
|
|
457
457
|
)
|
|
458
458
|
assert response.status_code == 400
|
|
459
459
|
assert (
|
|
@@ -840,7 +840,8 @@ async def test_add_message_is_normalized_to_run_steer():
|
|
|
840
840
|
assert response.status_code == 200
|
|
841
841
|
instance = (await app.state.pinned_host.directory.get("t1")).instance
|
|
842
842
|
[(params, caller_id)] = instance.steered
|
|
843
|
-
assert set(params) == {"message"}
|
|
843
|
+
assert set(params) == {"message", "anchorMessageId"}
|
|
844
|
+
assert params["anchorMessageId"] is None
|
|
844
845
|
message = params["message"]
|
|
845
846
|
assert message["id"].startswith("legacy_")
|
|
846
847
|
assert message["role"] == "user"
|
|
@@ -1094,7 +1095,9 @@ async def test_parent_id_without_commands_is_normalized_to_run_reload():
|
|
|
1094
1095
|
frames = data_stream_frames(response.text)
|
|
1095
1096
|
assert all(prefix == "aui-state" for prefix, _ in frames)
|
|
1096
1097
|
instance = (await app.state.pinned_host.directory.get("t1")).instance
|
|
1097
|
-
|
|
1098
|
+
[params] = instance.reloaded
|
|
1099
|
+
assert params["sourceId"] == "m2"
|
|
1100
|
+
assert params["messageId"].startswith("legacy_")
|
|
1098
1101
|
|
|
1099
1102
|
|
|
1100
1103
|
async def test_reload_without_a_resolvable_child_is_an_error_frame():
|
|
@@ -1184,7 +1187,8 @@ async def test_add_message_with_a_null_source_id_and_no_parent_is_run_steer():
|
|
|
1184
1187
|
instance = (await app.state.pinned_host.directory.get("t1")).instance
|
|
1185
1188
|
assert instance.edited == []
|
|
1186
1189
|
[(params, _)] = instance.steered
|
|
1187
|
-
assert set(params) == {"message"}
|
|
1190
|
+
assert set(params) == {"message", "anchorMessageId"}
|
|
1191
|
+
assert params["anchorMessageId"] is None
|
|
1188
1192
|
|
|
1189
1193
|
|
|
1190
1194
|
async def test_add_message_with_an_unresolvable_parent_falls_back_to_run_steer():
|
|
@@ -1209,4 +1213,5 @@ async def test_add_message_with_an_unresolvable_parent_falls_back_to_run_steer()
|
|
|
1209
1213
|
instance = (await app.state.pinned_host.directory.get("t1")).instance
|
|
1210
1214
|
assert instance.edited == []
|
|
1211
1215
|
[(params, _)] = instance.steered
|
|
1212
|
-
assert set(params) == {"message"}
|
|
1216
|
+
assert set(params) == {"message", "anchorMessageId"}
|
|
1217
|
+
assert params["anchorMessageId"] == "unknown"
|
|
@@ -124,11 +124,11 @@ async def test_client_posts_the_legacy_body_and_applies_state_ops():
|
|
|
124
124
|
body={"system": "be nice"},
|
|
125
125
|
on_chunk=seen.append,
|
|
126
126
|
)
|
|
127
|
-
assert await client.send([{"type": "
|
|
127
|
+
assert await client.send([{"type": "demo/echo", "message": "hi"}]) is True
|
|
128
128
|
assert client.state == {"text": "hello"}
|
|
129
129
|
assert received == [
|
|
130
130
|
{
|
|
131
|
-
"commands": [{"type": "
|
|
131
|
+
"commands": [{"type": "demo/echo", "message": "hi"}],
|
|
132
132
|
"state": {"text": ""},
|
|
133
133
|
"threadId": "t1",
|
|
134
134
|
"system": "be nice",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from statewire import upgrade_legacy_commands
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def child_of(parent_id: str) -> str | None:
|
|
7
|
+
return {"m1": "m2"}.get(parent_id)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def request_of(tool_call_id: str) -> str | None:
|
|
11
|
+
return {"call_1": "req_1"}.get(tool_call_id)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def test_add_message_without_anchor_is_steer():
|
|
15
|
+
[member] = await upgrade_legacy_commands(
|
|
16
|
+
[{"type": "add-message", "message": {"role": "user", "parts": [{"type": "text", "text": "hi"}]}}]
|
|
17
|
+
)
|
|
18
|
+
assert member["method"] == "run/steer"
|
|
19
|
+
params = member["params"][0]
|
|
20
|
+
assert params["anchorMessageId"] is None
|
|
21
|
+
assert params["message"]["role"] == "user"
|
|
22
|
+
assert params["message"]["parts"] == [{"type": "text", "text": "hi"}]
|
|
23
|
+
assert params["message"]["id"].startswith("legacy_")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def test_add_message_with_resolved_child_is_edit():
|
|
27
|
+
[member] = await upgrade_legacy_commands(
|
|
28
|
+
[{"type": "add-message", "message": {"role": "user", "parts": []}}],
|
|
29
|
+
parent_id="m1",
|
|
30
|
+
get_message_child_id=child_of,
|
|
31
|
+
)
|
|
32
|
+
assert member["method"] == "run/edit"
|
|
33
|
+
assert member["params"][0]["sourceId"] == "m2"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def test_add_message_with_unresolved_child_is_steer_with_anchor():
|
|
37
|
+
[member] = await upgrade_legacy_commands(
|
|
38
|
+
[{"type": "add-message", "message": {"role": "user", "parts": []}}],
|
|
39
|
+
parent_id="unknown",
|
|
40
|
+
get_message_child_id=child_of,
|
|
41
|
+
)
|
|
42
|
+
assert member["method"] == "run/steer"
|
|
43
|
+
assert member["params"][0]["anchorMessageId"] == "unknown"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def test_add_message_explicit_source_id_wins():
|
|
47
|
+
[member] = await upgrade_legacy_commands(
|
|
48
|
+
[{"type": "add-message", "sourceId": "m9", "message": {"role": "user", "parts": []}}],
|
|
49
|
+
parent_id="m1",
|
|
50
|
+
get_message_child_id=child_of,
|
|
51
|
+
)
|
|
52
|
+
assert member["method"] == "run/edit"
|
|
53
|
+
assert member["params"][0]["sourceId"] == "m9"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def test_add_message_converts_image_parts():
|
|
57
|
+
[member] = await upgrade_legacy_commands(
|
|
58
|
+
[{"type": "add-message", "message": {"role": "user", "parts": [{"type": "image", "image": "https://x/i.png"}]}}]
|
|
59
|
+
)
|
|
60
|
+
assert member["params"][0]["message"]["parts"] == [
|
|
61
|
+
{"type": "file", "mediaType": "image/*", "url": "https://x/i.png"}
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def test_add_tool_result_is_input():
|
|
66
|
+
[member] = await upgrade_legacy_commands(
|
|
67
|
+
[{"type": "add-tool-result", "toolCallId": "call_1", "result": {"ok": True}, "isError": True, "artifact": "a"}],
|
|
68
|
+
get_input_request_id=request_of,
|
|
69
|
+
)
|
|
70
|
+
assert member == {
|
|
71
|
+
"method": "run/input",
|
|
72
|
+
"params": [
|
|
73
|
+
{
|
|
74
|
+
"requestId": "req_1",
|
|
75
|
+
"response": {"output": {"ok": True}, "isError": True, "artifact": "a"},
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def test_add_tool_result_without_request_raises():
|
|
82
|
+
with pytest.raises(ValueError, match="no input request"):
|
|
83
|
+
await upgrade_legacy_commands(
|
|
84
|
+
[{"type": "add-tool-result", "toolCallId": "nope", "result": 1}],
|
|
85
|
+
get_input_request_id=request_of,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def test_parent_id_without_commands_is_reload():
|
|
90
|
+
[member] = await upgrade_legacy_commands(
|
|
91
|
+
[], parent_id="m1", get_message_child_id=child_of
|
|
92
|
+
)
|
|
93
|
+
assert member["method"] == "run/reload"
|
|
94
|
+
params = member["params"][0]
|
|
95
|
+
assert params["sourceId"] == "m2"
|
|
96
|
+
assert params["messageId"].startswith("legacy_")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def test_reload_without_child_raises():
|
|
100
|
+
with pytest.raises(ValueError, match="no child message"):
|
|
101
|
+
await upgrade_legacy_commands([], parent_id="unknown", get_message_child_id=child_of)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def test_empty_body_is_empty():
|
|
105
|
+
assert await upgrade_legacy_commands([]) == []
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def test_unknown_type_raises():
|
|
109
|
+
with pytest.raises(ValueError, match="unknown legacy command type"):
|
|
110
|
+
await upgrade_legacy_commands([{"type": "set-title", "title": "x"}])
|
|
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
|