harness-sdk-python 0.10.0__tar.gz → 0.10.2__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 (30) hide show
  1. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/.gitignore +2 -0
  2. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/PKG-INFO +2 -2
  3. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/examples/runs_app.py +3 -5
  4. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/pyproject.toml +2 -2
  5. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/src/harness_sdk/run_manager.py +7 -13
  6. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_ack_visibility.py +4 -4
  7. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_dispatching.py +5 -5
  8. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_meta.py +0 -17
  9. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_settle.py +23 -27
  10. harness_sdk_python-0.10.0/tests/test_facade.py +0 -96
  11. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/README.md +0 -0
  12. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/examples/__init__.py +0 -0
  13. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/src/harness_sdk/__init__.py +0 -0
  14. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/src/harness_sdk/fenced_postgres.py +0 -0
  15. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/src/harness_sdk/linear_thread.py +0 -0
  16. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/run_helpers.py +0 -0
  17. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_batches.py +0 -0
  18. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_branch_anchor.py +0 -0
  19. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_edit_dispatched.py +0 -0
  20. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_edit_reload.py +0 -0
  21. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_enqueue.py +0 -0
  22. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_fenced_postgres.py +0 -0
  23. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_input_required.py +0 -0
  24. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_linear_thread.py +0 -0
  25. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_outcomes.py +0 -0
  26. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_placement.py +0 -0
  27. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_prepare_hooks.py +0 -0
  28. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_rewind_during_run.py +0 -0
  29. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_steer.py +0 -0
  30. {harness_sdk_python-0.10.0 → harness_sdk_python-0.10.2}/tests/test_stop_continue.py +0 -0
@@ -22,3 +22,5 @@ __pycache__
22
22
  /doc_*.md
23
23
  apps/docs/.docs
24
24
  /.agentdoc/
25
+ .vercel
26
+ .env*
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: harness-sdk-python
3
- Version: 0.10.0
3
+ Version: 0.10.2
4
4
  Summary: RunManager: the harness-sdk runs subsystem for Python Statewire hosts
5
5
  Project-URL: Repository, https://github.com/assistant-ui/harness-sdk
6
6
  License-Expression: MIT
7
7
  Requires-Python: <4.0,>=3.11
8
- Requires-Dist: statewire<0.5,>=0.4.0
8
+ Requires-Dist: statewire<0.6,>=0.5.0
9
9
  Provides-Extra: deepagents
10
10
  Requires-Dist: deepagents>=0.6.12; extra == 'deepagents'
11
11
  Requires-Dist: langchain-core>=0.3; extra == 'deepagents'
@@ -6,7 +6,7 @@ Run from python/harness-sdk-python:
6
6
 
7
7
  State is ``{"messages": [...]}`` in langchain format; rewind truncates the
8
8
  list. Threads live at /threads/{id}: ``GET .../stream`` (SSE), ``.../ws``,
9
- ``POST .../commands``, and ``POST .../assistant-transport/api/chat``.
9
+ ``POST .../commands``.
10
10
 
11
11
  Set PINBOARD_URL, PINBOARD_TOKEN, and ADVERTISE_URL to register with a
12
12
  pinboard control plane instead of local placement.
@@ -20,7 +20,7 @@ from typing import Any
20
20
  from fastapi import FastAPI
21
21
  from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
22
22
  from pinned import PinnedHost
23
- from statewire import AssistantTransport, command, plain
23
+ from statewire import Statewire, command, plain
24
24
  from statewire.langgraph import append_langgraph_event
25
25
 
26
26
  from harness_sdk import RunManager, linear_thread
@@ -72,9 +72,7 @@ def user_text(message: dict[str, Any]) -> str:
72
72
  )
73
73
 
74
74
 
75
- class RunsHost(AssistantTransport):
76
- assistant_transport_protocol = "assistant-transport"
77
-
75
+ class RunsHost(Statewire):
78
76
  async def lifespan(self):
79
77
  self.agent = build_agent()
80
78
  self.state = {"messages": []}
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "harness-sdk-python"
3
- version = "0.10.0"
3
+ version = "0.10.2"
4
4
  description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
7
  requires-python = ">=3.11,<4.0"
8
- dependencies = ["statewire>=0.4.0,<0.5"]
8
+ dependencies = ["statewire>=0.5.0,<0.6"]
9
9
 
10
10
  [project.optional-dependencies]
11
11
  postgres = ["langgraph-checkpoint-postgres>=2.0.0"]
@@ -15,10 +15,9 @@ in-flight run has ended.
15
15
 
16
16
  ``entry["dispatching"]`` is the dispatch record (or None): the payload the
17
17
  run callback receives. ``ctx.ack()`` clears it; ``steering.take()`` may
18
- refill it — at most one unacked batch ever exists. A run that ends with an
19
- unacked record reverts its items
20
- by provenance: lane items to the front of their lane, direct dispatches to
21
- the front of the queue, rewind replacements dropped.
18
+ refill it — at most one unacked batch ever exists. Settle consumes the
19
+ delivered batch, acked or not; steer entries still undelivered in the lane
20
+ stay queued for the next run.
22
21
  """
23
22
 
24
23
  import asyncio
@@ -308,7 +307,7 @@ class RunManager:
308
307
  rewind = self._staged_rewinds.pop(0)
309
308
  self._clear_input()
310
309
  if self._dispatching:
311
- self._revert_dispatching()
310
+ self._unstage_dispatching()
312
311
  entry = self._ensure_entry()
313
312
  if entry["runId"] is None:
314
313
  entry["runId"] = rewind.run_id
@@ -492,7 +491,6 @@ class RunManager:
492
491
  except asyncio.CancelledError:
493
492
  self._settle(ctx)
494
493
  self._set_status("stopped")
495
- self._revert_dispatching()
496
494
  self._idle.set()
497
495
  raise # no drain, no freeze
498
496
  except Exception as exc:
@@ -502,11 +500,9 @@ class RunManager:
502
500
  self._freeze(exc.message, exc.payload)
503
501
  else:
504
502
  self._freeze(message)
505
- self._revert_dispatching()
506
503
  self._drain()
507
504
  return
508
505
  self._settle(ctx)
509
- self._revert_dispatching()
510
506
  self._outcome = outcome
511
507
  self._drain()
512
508
 
@@ -515,6 +511,8 @@ class RunManager:
515
511
  self._ctx = None
516
512
  self._task = None
517
513
  self._dispatch_record = None
514
+ self._dispatching = []
515
+ self._ensure_entry()["dispatching"] = None
518
516
 
519
517
  def _ack(self) -> None:
520
518
  entry = self._entry()
@@ -524,18 +522,14 @@ class RunManager:
524
522
  self._dispatching = []
525
523
  entry["dispatching"] = None
526
524
 
527
- def _revert_dispatching(self) -> None:
525
+ def _unstage_dispatching(self) -> None:
528
526
  taken, self._dispatching = self._dispatching, []
529
527
  entry = self._ensure_entry()
530
528
  entry["dispatching"] = None
531
- if not taken:
532
- return
533
529
  for lane in ("steerQueue", "queue"):
534
530
  front = [item for taken_lane, item in taken if taken_lane == lane]
535
531
  if front:
536
532
  entry[lane] = front + self._lane_items(lane)
537
- ids = {item["id"] for _, item in taken}
538
- self._dispatched_ids = tuple(id for id in self._dispatched_ids if id not in ids)
539
533
 
540
534
  def _dispatchable(self, lane: str) -> bool:
541
535
  if self._task is not None or self._staged_rewinds:
@@ -60,7 +60,7 @@ async def test_parked_send_acks_with_its_queue_insert():
60
60
  call.finish(RunManager.Complete())
61
61
 
62
62
 
63
- async def test_unacked_run_end_reverts_the_direct_dispatch_to_the_queue():
63
+ async def test_unacked_run_end_consumes_the_direct_dispatch():
64
64
  script = Script()
65
65
  async with run_host(script) as (drv, host):
66
66
  seq = drv.post("run/enqueue", add("m1"))
@@ -69,10 +69,10 @@ async def test_unacked_run_end_reverts_the_direct_dispatch_to_the_queue():
69
69
  assert dispatching_ids(covered_state(drv, seq)) == ["m1"]
70
70
  call.fail(RuntimeError("boom"))
71
71
  await drv.wait_status("error")
72
- assert queue_ids(drv.replica, "queue") == ["m1"]
72
+ assert queue_ids(drv.replica, "queue") == []
73
73
 
74
74
 
75
- async def test_unacked_run_end_reverts_the_direct_steer_send_to_the_steer_lane():
75
+ async def test_unacked_run_end_consumes_the_direct_steer_send():
76
76
  script = Script()
77
77
  async with run_host(script) as (drv, host):
78
78
  seq = drv.post("run/steer", add("s1"))
@@ -81,7 +81,7 @@ async def test_unacked_run_end_reverts_the_direct_steer_send_to_the_steer_lane()
81
81
  assert dispatching_ids(covered_state(drv, seq)) == ["s1"]
82
82
  call.fail(RuntimeError("boom"))
83
83
  await drv.wait_status("error")
84
- assert queue_ids(drv.replica, "steerQueue") == ["s1"]
84
+ assert queue_ids(drv.replica, "steerQueue") == []
85
85
  assert queue_ids(drv.replica, "queue") == []
86
86
 
87
87
 
@@ -3,8 +3,8 @@ is the current unacked batch — a record ``{trigger, messages, rollbackTo?,
3
3
  inputOutcomes?}`` or None. A message item lives in exactly one of the lanes,
4
4
  the record's ``messages``, or the thread, and every transition is
5
5
  single-envelope atomic: dispatch fills the record, ``ctx.ack()`` clears it as
6
- the canonical message appears, a steer take refills it, and an unacked run
7
- end returns lane items to the front of their lane."""
6
+ the canonical message appears, a steer take refills it, and settle consumes
7
+ whatever is still unacked."""
8
8
 
9
9
  import copy
10
10
 
@@ -78,7 +78,7 @@ async def test_ack_clears_dispatching_with_the_message_append():
78
78
  call.finish(RunManager.Complete())
79
79
 
80
80
 
81
- async def test_unacked_run_end_returns_the_item_to_the_lane_front():
81
+ async def test_unacked_run_end_consumes_the_delivered_item():
82
82
  script = Script()
83
83
  async with run_host(script) as (drv, host):
84
84
  drv.post("run/enqueue", add("m1"))
@@ -88,9 +88,9 @@ async def test_unacked_run_end_returns_the_item_to_the_lane_front():
88
88
  await drv.side("run/stop", {"runId": await drv.run_id()})
89
89
  call.finish(RunManager.Stop(dispatch_queue=False))
90
90
  await drv.wait_status("stopped")
91
- assert queue_ids(drv.replica, "queue") == ["m1", "m2"]
91
+ assert queue_ids(drv.replica, "queue") == ["m2"]
92
92
  assert dispatching_ids(drv.replica) == []
93
- assert residence(drv, "m1") == ["dispatching", "queue"]
93
+ assert residence(drv, "m2") == ["queue"]
94
94
 
95
95
 
96
96
  async def test_steering_take_moves_the_lane_into_dispatching():
@@ -128,23 +128,6 @@ async def test_meta_dies_with_a_dequeued_entry():
128
128
  dispatched.finish(RunManager.Complete())
129
129
 
130
130
 
131
- async def test_unacked_run_end_reprojects_entries_with_meta():
132
- script = Script()
133
- async with run_host(script) as (drv, _):
134
- drv.batch(
135
- [
136
- ("run/enqueue", add("m1", meta={"clientId": "c1"})),
137
- ("run/steer", add("s1", meta={"clientId": "c2"})),
138
- ]
139
- )
140
- call = await script.next_call()
141
- assert [m["id"] for m in call.ctx.messages] == ["m1", "s1"]
142
- call.fail(RuntimeError("boom"))
143
- await drv.wait_status("error")
144
- assert drv.run["queue"][0]["meta"] == {"clientId": "c1"}
145
- assert drv.run["steerQueue"][0]["meta"] == {"clientId": "c2"}
146
-
147
-
148
131
  async def test_edit_meta_rides_the_replacement_message():
149
132
  script = Script()
150
133
  thread_with_turn(script)
@@ -1,14 +1,13 @@
1
1
  """Contract: command settlement. Every initiator settles accepted with its
2
2
  state-visible application — sends with their insert (a lane entry or a direct
3
3
  ``dispatching`` entry), edit/reload/continue at their dispatch. The run's
4
- outcome never travels through settlement: a run that ends with unacked
5
- entries returns lane-origin items to the front of their lane, direct
6
- dispatches to the front of the queue, and lands any failure on
7
- ``state.error``."""
4
+ outcome never travels through settlement: settle consumes every delivered
5
+ item, acked or not; steer entries still undelivered in the lane stay queued,
6
+ and any failure lands on ``state.error``."""
8
7
 
9
8
  import asyncio
10
9
 
11
- from run_helpers import Script, add, edit_params, msg, queue_ids, run_host, target
10
+ from run_helpers import Script, add, edit_params, msg, queue_ids, run_host
12
11
 
13
12
  from statewire import StatewireReject
14
13
 
@@ -51,7 +50,7 @@ async def test_run_reject_lands_the_payload_on_state_error():
51
50
  )
52
51
  assert (await drv.res(seq))["type"] == "accepted"
53
52
  await drv.wait_status("error")
54
- assert queue_ids(drv.replica, "queue") == ["m1"]
53
+ assert queue_ids(drv.replica, "queue") == []
55
54
  assert drv.run["error"] == {
56
55
  "message": "payment required",
57
56
  "reason": "payment-required",
@@ -62,16 +61,15 @@ async def test_run_reject_lands_the_payload_on_state_error():
62
61
  async def test_unacked_run_end_settles_the_send_accepted():
63
62
  script = Script()
64
63
  async with run_host(script) as (drv, host):
65
- for outcome in [
66
- lambda call: call.fail(RuntimeError("boom")),
67
- lambda call: call.finish(RunManager.Error(dispatch_queue=False)),
64
+ for name, outcome in [
65
+ ("m1", lambda call: call.fail(RuntimeError("boom"))),
66
+ ("m2", lambda call: call.finish(RunManager.Error(dispatch_queue=False))),
68
67
  ]:
69
- seq = drv.post("run/enqueue", add("m1"))
68
+ seq = drv.post("run/enqueue", add(name))
70
69
  outcome(await script.next_call())
71
70
  assert (await drv.res(seq))["type"] == "accepted"
72
71
  await drv.wait_status("error")
73
- assert queue_ids(drv.replica, "queue") == ["m1"]
74
- await drv.command("run/dequeue", {"messageId": "m1"})
72
+ assert queue_ids(drv.replica, "queue") == []
75
73
 
76
74
 
77
75
  async def test_rewind_settles_before_the_reruns_end():
@@ -90,23 +88,23 @@ async def test_rewind_settles_before_the_reruns_end():
90
88
  await drv.wait_status("stopped")
91
89
 
92
90
 
93
- async def test_unacked_run_end_reverts_entries_to_the_lane_front():
91
+ async def test_unacked_run_end_consumes_the_delivered_batch():
94
92
  script = Script()
95
93
  async with run_host(script) as (drv, host):
96
94
  drv.batch([("run/enqueue", add("m1")), ("run/enqueue", add("m2"))])
97
95
  call = await script.next_call()
98
96
  call.fail(RuntimeError("boom"))
99
97
  await drv.wait_status("error")
100
- assert queue_ids(drv.replica, "queue") == ["m1", "m2"]
98
+ assert queue_ids(drv.replica, "queue") == []
101
99
  assert drv.run["error"] == {"message": "boom"}
102
- await drv.command("run/steer", target(messageId="m1"), terminal=False)
100
+ await drv.command("run/steer", add("s1"), terminal=False)
103
101
  continued = await script.next_call()
104
102
  assert continued.ctx.trigger == "error-continue"
105
103
  continued.ack()
106
- assert [m["id"] for m in continued.ctx.steering.take()] == ["m1"]
104
+ assert [m["id"] for m in continued.ctx.steering.take()] == ["s1"]
107
105
 
108
106
 
109
- async def test_unacked_run_end_reverts_taken_steered_items():
107
+ async def test_unacked_run_end_consumes_taken_steered_items():
110
108
  script = Script()
111
109
  async with run_host(script) as (drv, host):
112
110
  drv.post("run/enqueue", add("m1"))
@@ -117,7 +115,7 @@ async def test_unacked_run_end_reverts_taken_steered_items():
117
115
  call.ctx.steering.take()
118
116
  call.fail(RuntimeError("boom"))
119
117
  await drv.wait_status("error")
120
- assert queue_ids(drv.replica, "steerQueue") == ["s1"]
118
+ assert queue_ids(drv.replica, "steerQueue") == []
121
119
  assert queue_ids(drv.replica, "queue") == []
122
120
 
123
121
 
@@ -132,17 +130,15 @@ async def test_ack_removes_the_entries_for_good():
132
130
  assert queue_ids(drv.replica, "queue") == []
133
131
 
134
132
 
135
- async def test_complete_without_ack_reverts_and_redispatches():
133
+ async def test_complete_without_ack_consumes_the_dispatch():
136
134
  script = Script()
137
135
  async with run_host(script) as (drv, host):
138
136
  seq = drv.post("run/enqueue", add("m1"))
139
137
  (await script.next_call()).finish(RunManager.Complete())
140
- redo = await script.next_call()
141
- assert [m["id"] for m in redo.ctx.messages] == ["m1"]
142
138
  assert (await drv.res(seq))["type"] == "accepted"
143
- redo.ack()
144
- redo.finish(RunManager.Complete())
145
139
  await drv.wait_status("ready")
140
+ assert drv.replica["runs"] == []
141
+ script.no_call()
146
142
 
147
143
 
148
144
  async def test_dequeue_removes_a_parked_entry():
@@ -169,9 +165,9 @@ async def test_stop_lands_while_a_steer_is_parked():
169
165
  await asyncio.wait_for(call.ctx.stop_requested.wait(), 5)
170
166
  call.finish(RunManager.Stop(dispatch_queue=False))
171
167
  await drv.wait_status("stopped")
172
- # The unconsumed steer stays parked; the unacked dispatch reverts.
168
+ # The undelivered steer stays parked; the delivered dispatch is consumed.
173
169
  assert queue_ids(drv.replica, "steerQueue") == ["s1"]
174
- assert queue_ids(drv.replica, "queue") == ["m1"]
170
+ assert queue_ids(drv.replica, "queue") == []
175
171
 
176
172
 
177
173
  async def test_rewind_edit_settles_at_its_dispatch():
@@ -199,7 +195,7 @@ async def test_rewind_rerun_failure_lands_on_state_error():
199
195
  assert drv.run["error"] == {"message": "payment required", "code": 402}
200
196
 
201
197
 
202
- async def test_task_cancellation_reverts_entries_and_settles_sends():
198
+ async def test_task_cancellation_consumes_entries_and_settles_sends():
203
199
  script = Script()
204
200
  async with run_host(script) as (drv, host):
205
201
  seq = drv.batch([("run/enqueue", add("m1")), ("run/enqueue", add("m2"))])
@@ -208,7 +204,7 @@ async def test_task_cancellation_reverts_entries_and_settles_sends():
208
204
  task.cancel()
209
205
  assert (await drv.res(seq))["type"] == "accepted"
210
206
  await drv.wait_status("stopped")
211
- assert queue_ids(drv.replica, "queue") == ["m1", "m2"]
207
+ assert queue_ids(drv.replica, "queue") == []
212
208
  assert task.cancelled()
213
209
 
214
210
 
@@ -1,96 +0,0 @@
1
- """Contract: run lifetime composed with the legacy facade. The ``/chat``
2
- hold releases once the send settles — the opening ``set`` carries the sent
3
- message as the ``dispatching`` record — then the response stays open through
4
- the run and EOFs once the run task ends; a run rejection is a single error
5
- frame then EOF.
6
- """
7
-
8
- import asyncio
9
-
10
- from run_helpers import Script
11
- from statewire_helpers import statewire_client
12
- from test_assistant_transport import _PostStream, data_stream_frames, state_ops
13
-
14
- from statewire import AssistantTransport, StatewireReject, command
15
-
16
- from harness_sdk import RunManager
17
-
18
-
19
- def make_facade_host(script: Script):
20
- class Host(AssistantTransport):
21
- async def lifespan(self):
22
- self.state = {}
23
- self.runs = RunManager(
24
- state=self.state,
25
- run=script.run,
26
- thread=script,
27
- create_task=self.create_task,
28
- schedule=self.schedule,
29
- )
30
- yield
31
-
32
- @command("run/steer")
33
- async def run_steer(self, params, *, ctx):
34
- return await self.runs.steer(params, ack=ctx.ack)
35
-
36
- @command("run/stop")
37
- async def run_stop(self, params=None, *, ctx):
38
- return await self.runs.stop(params, ack=ctx.ack)
39
-
40
- return Host
41
-
42
-
43
- def chat_body():
44
- return {
45
- "commands": [
46
- {
47
- "type": "add-message",
48
- "message": {"role": "user", "parts": [{"type": "text", "text": "hi"}]},
49
- "parentId": None,
50
- "sourceId": None,
51
- }
52
- ],
53
- "state": {},
54
- "threadId": "t1",
55
- }
56
-
57
-
58
- async def test_chat_holds_until_the_entity_run_settles_then_eofs():
59
- script = Script()
60
- async with statewire_client(make_facade_host(script)) as (app, client):
61
- async with _PostStream(
62
- app, "/threads/t1/assistant-transport/api/chat", chat_body()
63
- ) as stream:
64
- assert stream.status == 200
65
- call = await script.next_call()
66
- assert call.ctx.trigger == "message-send"
67
- instance = (await app.state.pinned_host.directory.get("t1")).instance
68
- instance.state["messages"] = [dict(call.ctx.messages[0])]
69
- call.ack()
70
- await asyncio.sleep(0.05)
71
- assert stream._task is not None and not stream._task.done()
72
- call.finish(RunManager.Complete())
73
- tail = await stream.until_closed()
74
- frames = data_stream_frames(tail.decode())
75
- assert frames and all(prefix == "aui-state" for prefix, _ in frames)
76
- ops = state_ops(frames)
77
- assert ops[0]["type"] == "set" and ops[0]["path"] == []
78
- record = ops[0]["value"]["runs"][0]["dispatching"]
79
- assert [m["id"] for m in record["messages"]] == [call.ctx.messages[0]["id"]]
80
- assert any(
81
- op["type"] == "set" and op["path"] == ["messages"] for op in ops[1:]
82
- )
83
-
84
-
85
- async def test_chat_run_reject_is_one_error_frame_then_eof():
86
- script = Script()
87
- async with statewire_client(make_facade_host(script)) as (app, client):
88
- async with _PostStream(
89
- app, "/threads/t1/assistant-transport/api/chat", chat_body()
90
- ) as stream:
91
- call = await script.next_call()
92
- call.fail(StatewireReject("payment required", payload={"code": 402}))
93
- tail = await stream.until_closed()
94
- frames = data_stream_frames(tail.decode())
95
- errors = [frame for frame in frames if frame[0] == "3"]
96
- assert errors == [("3", "payment required")]