harness-sdk-python 0.10.1__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.
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/PKG-INFO +1 -1
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/examples/runs_app.py +3 -5
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/pyproject.toml +1 -1
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/src/harness_sdk/run_manager.py +7 -13
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_ack_visibility.py +4 -4
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_dispatching.py +5 -5
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_meta.py +0 -17
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_settle.py +23 -27
- harness_sdk_python-0.10.1/tests/test_facade.py +0 -100
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/.gitignore +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/README.md +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/examples/__init__.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/run_helpers.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_branch_anchor.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_edit_reload.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_input_required.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_linear_thread.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_outcomes.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_prepare_hooks.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_rewind_during_run.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_steer.py +0 -0
- {harness_sdk_python-0.10.1 → harness_sdk_python-0.10.2}/tests/test_stop_continue.py +0 -0
|
@@ -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
|
|
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
|
|
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(
|
|
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": []}
|
|
@@ -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.
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
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
|
|
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
|
|
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") == [
|
|
72
|
+
assert queue_ids(drv.replica, "queue") == []
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
async def
|
|
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") == [
|
|
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
|
|
7
|
-
|
|
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
|
|
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") == ["
|
|
91
|
+
assert queue_ids(drv.replica, "queue") == ["m2"]
|
|
92
92
|
assert dispatching_ids(drv.replica) == []
|
|
93
|
-
assert residence(drv, "
|
|
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:
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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") == [
|
|
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(
|
|
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") == [
|
|
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
|
|
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") == [
|
|
98
|
+
assert queue_ids(drv.replica, "queue") == []
|
|
101
99
|
assert drv.run["error"] == {"message": "boom"}
|
|
102
|
-
await drv.command("run/steer",
|
|
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()] == ["
|
|
104
|
+
assert [m["id"] for m in continued.ctx.steering.take()] == ["s1"]
|
|
107
105
|
|
|
108
106
|
|
|
109
|
-
async def
|
|
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") == [
|
|
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
|
|
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
|
|
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") == [
|
|
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
|
|
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") == [
|
|
207
|
+
assert queue_ids(drv.replica, "queue") == []
|
|
212
208
|
assert task.cancelled()
|
|
213
209
|
|
|
214
210
|
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
"""Contract: run lifetime composed with the assistant-transport chat
|
|
2
|
-
endpoint. The ``/chat`` 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": "run/steer",
|
|
48
|
-
"runId": "run-1",
|
|
49
|
-
"runAnchorMessageId": None,
|
|
50
|
-
"message": {
|
|
51
|
-
"id": "m1",
|
|
52
|
-
"role": "user",
|
|
53
|
-
"parts": [{"type": "text", "text": "hi"}],
|
|
54
|
-
},
|
|
55
|
-
}
|
|
56
|
-
],
|
|
57
|
-
"state": {},
|
|
58
|
-
"threadId": "t1",
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
async def test_chat_holds_until_the_entity_run_settles_then_eofs():
|
|
63
|
-
script = Script()
|
|
64
|
-
async with statewire_client(make_facade_host(script)) as (app, client):
|
|
65
|
-
async with _PostStream(
|
|
66
|
-
app, "/threads/t1/assistant-transport/api/chat", chat_body()
|
|
67
|
-
) as stream:
|
|
68
|
-
assert stream.status == 200
|
|
69
|
-
call = await script.next_call()
|
|
70
|
-
assert call.ctx.trigger == "message-send"
|
|
71
|
-
instance = (await app.state.pinned_host.directory.get("t1")).instance
|
|
72
|
-
instance.state["messages"] = [dict(call.ctx.messages[0])]
|
|
73
|
-
call.ack()
|
|
74
|
-
await asyncio.sleep(0.05)
|
|
75
|
-
assert stream._task is not None and not stream._task.done()
|
|
76
|
-
call.finish(RunManager.Complete())
|
|
77
|
-
tail = await stream.until_closed()
|
|
78
|
-
frames = data_stream_frames(tail.decode())
|
|
79
|
-
assert frames and all(prefix == "aui-state" for prefix, _ in frames)
|
|
80
|
-
ops = state_ops(frames)
|
|
81
|
-
assert ops[0]["type"] == "set" and ops[0]["path"] == []
|
|
82
|
-
record = ops[0]["value"]["runs"][0]["dispatching"]
|
|
83
|
-
assert [m["id"] for m in record["messages"]] == [call.ctx.messages[0]["id"]]
|
|
84
|
-
assert any(
|
|
85
|
-
op["type"] == "set" and op["path"] == ["messages"] for op in ops[1:]
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
async def test_chat_run_reject_is_one_error_frame_then_eof():
|
|
90
|
-
script = Script()
|
|
91
|
-
async with statewire_client(make_facade_host(script)) as (app, client):
|
|
92
|
-
async with _PostStream(
|
|
93
|
-
app, "/threads/t1/assistant-transport/api/chat", chat_body()
|
|
94
|
-
) as stream:
|
|
95
|
-
call = await script.next_call()
|
|
96
|
-
call.fail(StatewireReject("payment required", payload={"code": 402}))
|
|
97
|
-
tail = await stream.until_closed()
|
|
98
|
-
frames = data_stream_frames(tail.decode())
|
|
99
|
-
errors = [frame for frame in frames if frame[0] == "3"]
|
|
100
|
-
assert errors == [("3", "payment required")]
|
|
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
|