harness-sdk-python 0.4.4__tar.gz → 0.4.5__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.4.4 → harness_sdk_python-0.4.5}/PKG-INFO +1 -1
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/pyproject.toml +1 -1
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/src/harness_sdk/run_manager.py +27 -18
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_input_required.py +88 -12
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_stop_continue.py +3 -3
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/.gitignore +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/README.md +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/run_helpers.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_branch_anchor.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_edit_reload.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_facade.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_meta.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_outcomes.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_rewind_during_run.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_run_leaf.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_settle.py +0 -0
- {harness_sdk_python-0.4.4 → harness_sdk_python-0.4.5}/tests/test_steer.py +0 -0
|
@@ -326,17 +326,20 @@ class RunManager:
|
|
|
326
326
|
fx.continues.clear()
|
|
327
327
|
elif fx.new_added and pre_empty:
|
|
328
328
|
self._pop_dispatchable()
|
|
329
|
-
elif
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
329
|
+
elif status == "input-required":
|
|
330
|
+
if fx.steer_added and self._lane_items("steerQueue"):
|
|
331
|
+
abandoned = tuple(self._input_requests)
|
|
332
|
+
self._clear_input()
|
|
333
|
+
self._pop_dispatchable(abandoned_input=abandoned)
|
|
334
|
+
elif self._input_requests and len(self._input_answers) == len(
|
|
335
|
+
self._input_requests
|
|
336
|
+
):
|
|
337
|
+
responses = tuple(
|
|
338
|
+
(request, *self._input_answers[request["id"]])
|
|
339
|
+
for request in self._input_requests
|
|
340
|
+
)
|
|
341
|
+
self._clear_input()
|
|
342
|
+
self._dispatch("input-resume", [], responses=responses)
|
|
340
343
|
|
|
341
344
|
def _settle_outcome(self, outcome: Any) -> None:
|
|
342
345
|
if isinstance(outcome, RunManager.Complete):
|
|
@@ -372,6 +375,7 @@ class RunManager:
|
|
|
372
375
|
rollback_to: Any = _ABSENT,
|
|
373
376
|
responses: Any = _ABSENT,
|
|
374
377
|
root_meta: Any = None,
|
|
378
|
+
abandoned_input: tuple[dict[str, Any], ...] = (),
|
|
375
379
|
) -> None:
|
|
376
380
|
if type not in _ENTRY_TYPES:
|
|
377
381
|
raise ValueError(f"invalid entry type: {type!r}")
|
|
@@ -407,6 +411,7 @@ class RunManager:
|
|
|
407
411
|
_responses=responses,
|
|
408
412
|
_message_meta=message_meta,
|
|
409
413
|
_root_meta=root_meta,
|
|
414
|
+
_abandoned_input=abandoned_input,
|
|
410
415
|
)
|
|
411
416
|
self._ctx = ctx
|
|
412
417
|
self._sync_steering()
|
|
@@ -509,18 +514,20 @@ class RunManager:
|
|
|
509
514
|
if plain(self._state["runLeafMessageId"]) in ids:
|
|
510
515
|
self._state["runLeafMessageId"] = self._leaf_confirmed
|
|
511
516
|
|
|
512
|
-
def _pop_dispatchable(
|
|
517
|
+
def _pop_dispatchable(
|
|
518
|
+
self, *, abandoned_input: tuple[dict[str, Any], ...] = ()
|
|
519
|
+
) -> bool:
|
|
513
520
|
steer = self._lane_items("steerQueue")
|
|
514
521
|
if steer:
|
|
515
522
|
self._state["steerQueue"] = []
|
|
516
523
|
self._dispatching = [("steerQueue", item) for item in steer]
|
|
517
|
-
self._dispatch("message-send", steer)
|
|
524
|
+
self._dispatch("message-send", steer, abandoned_input=abandoned_input)
|
|
518
525
|
return True
|
|
519
526
|
queue = self._lane_items("queue")
|
|
520
527
|
if queue:
|
|
521
528
|
self._state["queue"].pop(0)
|
|
522
529
|
self._dispatching = [("queue", queue[0])]
|
|
523
|
-
self._dispatch("message-send", [queue[0]])
|
|
530
|
+
self._dispatch("message-send", [queue[0]], abandoned_input=abandoned_input)
|
|
524
531
|
return True
|
|
525
532
|
return False
|
|
526
533
|
|
|
@@ -990,10 +997,6 @@ class RunManager:
|
|
|
990
997
|
ack()
|
|
991
998
|
self._schedule(self._drain)
|
|
992
999
|
return await entry.future
|
|
993
|
-
if status == "input-required":
|
|
994
|
-
self._clear_input()
|
|
995
|
-
self._state["status"] = "stopped"
|
|
996
|
-
return None
|
|
997
1000
|
raise _reject("wrong-state", f"run/stop is rejected in {status}")
|
|
998
1001
|
|
|
999
1002
|
def _validated_response(self, request_type: str, response: Any) -> dict[str, Any]:
|
|
@@ -1133,6 +1136,7 @@ class RunManager:
|
|
|
1133
1136
|
_responses: Any
|
|
1134
1137
|
_message_meta: dict[str, Any]
|
|
1135
1138
|
_root_meta: Any
|
|
1139
|
+
_abandoned_input: tuple[dict[str, Any], ...]
|
|
1136
1140
|
steering: "RunManager.Steering" = field(init=False)
|
|
1137
1141
|
|
|
1138
1142
|
def __post_init__(self) -> None:
|
|
@@ -1148,6 +1152,11 @@ class RunManager:
|
|
|
1148
1152
|
raise ValueError(f"message {message_id} was not dispatched to this run")
|
|
1149
1153
|
return self._message_meta[message_id]
|
|
1150
1154
|
|
|
1155
|
+
@property
|
|
1156
|
+
def abandoned_input(self) -> tuple[dict[str, Any], ...]:
|
|
1157
|
+
"""Input requests pending when a dispatching send abandoned them; empty on every other start."""
|
|
1158
|
+
return self._abandoned_input
|
|
1159
|
+
|
|
1151
1160
|
@property
|
|
1152
1161
|
def stop_reason(self) -> str | None:
|
|
1153
1162
|
return self._manager._stop_reason
|
|
@@ -207,20 +207,21 @@ async def test_custom_request_type_passes_through():
|
|
|
207
207
|
await drv.wait_status("ready")
|
|
208
208
|
|
|
209
209
|
|
|
210
|
-
async def
|
|
210
|
+
async def test_stop_while_parked_rejects_and_keeps_the_pending_set():
|
|
211
211
|
script = Script()
|
|
212
212
|
async with run_host(script) as (drv, host):
|
|
213
213
|
await park(drv, script, [tool_call("r1")])
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
assert "inputRequests"
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
"run/input", {"requestId": "r1", "response": {"output": "x"}}
|
|
220
|
-
),
|
|
221
|
-
"wrong-state",
|
|
214
|
+
assert_rejected(await drv.command("run/stop"), "wrong-state")
|
|
215
|
+
assert drv.replica["status"] == "input-required"
|
|
216
|
+
assert drv.replica["inputRequests"] == [{**tool_call("r1"), "response": None}]
|
|
217
|
+
await drv.command(
|
|
218
|
+
"run/input", {"requestId": "r1", "response": {"output": "x"}}
|
|
222
219
|
)
|
|
223
|
-
script.
|
|
220
|
+
resumed = await script.next_call()
|
|
221
|
+
assert resumed.ctx.type == "input-resume"
|
|
222
|
+
resumed.ack()
|
|
223
|
+
resumed.finish(RunManager.Complete())
|
|
224
|
+
await drv.wait_status("ready")
|
|
224
225
|
|
|
225
226
|
|
|
226
227
|
async def test_invalid_requests_raise_at_construction():
|
|
@@ -307,10 +308,85 @@ async def test_reload_while_parked_clears_requests_and_reruns():
|
|
|
307
308
|
await drv.wait_status("ready")
|
|
308
309
|
|
|
309
310
|
|
|
310
|
-
async def
|
|
311
|
+
async def test_steer_add_while_parked_abandons_input_and_dispatches():
|
|
311
312
|
script = Script()
|
|
312
313
|
async with run_host(script) as (drv, host):
|
|
313
|
-
await park(drv, script, [tool_call("r1")])
|
|
314
|
+
await park(drv, script, [tool_call("r1"), approval("r2")])
|
|
315
|
+
res = await drv.command("run/steer", add("s1"), terminal=False)
|
|
316
|
+
assert res["type"] == "pending"
|
|
317
|
+
run = await script.next_call()
|
|
318
|
+
assert run.ctx.type == "message-send"
|
|
319
|
+
assert [m["id"] for m in run.ctx.messages] == ["s1"]
|
|
320
|
+
assert run.ctx.abandoned_input == (tool_call("r1"), approval("r2"))
|
|
321
|
+
assert "inputRequests" not in drv.replica
|
|
322
|
+
run.ack()
|
|
323
|
+
run.finish(RunManager.Complete())
|
|
324
|
+
await drv.wait_status("ready")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
async def test_steer_add_onto_nonempty_lane_while_parked_dispatches_whole_lane():
|
|
328
|
+
script = Script()
|
|
329
|
+
async with run_host(script) as (drv, host):
|
|
330
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
331
|
+
call = await script.next_call()
|
|
314
332
|
await drv.command("run/steer", add("s1"), terminal=False)
|
|
333
|
+
call.ack()
|
|
334
|
+
call.finish(RunManager.InputRequired([tool_call("r1")]))
|
|
335
|
+
await drv.wait_status("input-required")
|
|
315
336
|
assert queue_ids(drv.replica, "steerQueue") == ["s1"]
|
|
337
|
+
await drv.command("run/steer", add("s2"), terminal=False)
|
|
338
|
+
run = await script.next_call()
|
|
339
|
+
assert [m["id"] for m in run.ctx.messages] == ["s1", "s2"]
|
|
340
|
+
assert run.ctx.abandoned_input == (tool_call("r1"),)
|
|
341
|
+
assert "inputRequests" not in drv.replica
|
|
342
|
+
run.ack()
|
|
343
|
+
run.finish(RunManager.Complete())
|
|
344
|
+
await drv.wait_status("ready")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
async def test_promote_to_steer_while_parked_abandons_input_and_dispatches():
|
|
348
|
+
script = Script()
|
|
349
|
+
async with run_host(script) as (drv, host):
|
|
350
|
+
await park(drv, script, [tool_call("r1")])
|
|
351
|
+
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
352
|
+
assert queue_ids(drv.replica, "queue") == ["m2"]
|
|
353
|
+
script.no_call()
|
|
354
|
+
await drv.command("run/steer", {"messageId": "m2"})
|
|
355
|
+
run = await script.next_call()
|
|
356
|
+
assert run.ctx.type == "message-send"
|
|
357
|
+
assert [m["id"] for m in run.ctx.messages] == ["m2"]
|
|
358
|
+
assert run.ctx.abandoned_input == (tool_call("r1"),)
|
|
359
|
+
assert "inputRequests" not in drv.replica
|
|
360
|
+
run.ack()
|
|
361
|
+
run.finish(RunManager.Complete())
|
|
362
|
+
await drv.wait_status("ready")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
async def test_enqueue_while_parked_parks_and_keeps_the_pending_set():
|
|
366
|
+
script = Script()
|
|
367
|
+
async with run_host(script) as (drv, host):
|
|
368
|
+
await park(drv, script, [tool_call("r1")])
|
|
369
|
+
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
370
|
+
assert queue_ids(drv.replica, "queue") == ["m2"]
|
|
371
|
+
assert drv.replica["status"] == "input-required"
|
|
372
|
+
assert drv.replica["inputRequests"] == [{**tool_call("r1"), "response": None}]
|
|
316
373
|
script.no_call()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
async def test_abandoned_input_is_empty_on_ordinary_starts():
|
|
377
|
+
script = Script()
|
|
378
|
+
async with run_host(script) as (drv, host):
|
|
379
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
380
|
+
call = await script.next_call()
|
|
381
|
+
assert call.ctx.abandoned_input == ()
|
|
382
|
+
call.ack()
|
|
383
|
+
call.finish(RunManager.InputRequired([tool_call("r1")]))
|
|
384
|
+
await drv.wait_status("input-required")
|
|
385
|
+
await drv.command(
|
|
386
|
+
"run/input", {"requestId": "r1", "response": {"output": "ok"}}
|
|
387
|
+
)
|
|
388
|
+
resumed = await script.next_call()
|
|
389
|
+
assert resumed.ctx.abandoned_input == ()
|
|
390
|
+
resumed.ack()
|
|
391
|
+
resumed.finish(RunManager.Complete())
|
|
392
|
+
await drv.wait_status("ready")
|
|
@@ -87,7 +87,7 @@ async def test_stop_rejected_outside_active_runs(state):
|
|
|
87
87
|
assert_rejected(await drv.command("run/stop"), "wrong-state")
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
async def
|
|
90
|
+
async def test_stop_in_input_required_rejects_wrong_state():
|
|
91
91
|
script = Script()
|
|
92
92
|
async with run_host(script) as (drv, host):
|
|
93
93
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
@@ -95,8 +95,8 @@ async def test_stop_in_input_required_cancels_the_parked_run():
|
|
|
95
95
|
call.ack()
|
|
96
96
|
call.finish(RunManager.InputRequired([{"type": "free-form", "id": "r1"}]))
|
|
97
97
|
await drv.wait_status("input-required")
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
assert_rejected(await drv.command("run/stop"), "wrong-state")
|
|
99
|
+
assert drv.replica["status"] == "input-required"
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
async def test_stop_settles_even_when_the_run_completes_normally():
|
|
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
|