harness-sdk-python 0.32.0__tar.gz → 0.32.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.32.0 → harness_sdk_python-0.32.2}/PKG-INFO +1 -1
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/pyproject.toml +1 -1
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/src/harness_sdk/run_manager.py +15 -3
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/src/harness_sdk/voice.py +18 -7
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/run_helpers.py +0 -1
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_input_required.py +10 -10
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_restore.py +1 -1
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_stopping_window.py +4 -3
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_voice.py +43 -2
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/.gitignore +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/README.md +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/examples/__init__.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/examples/runs_app.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/src/harness_sdk/checkpoints.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/src/harness_sdk/deepagents.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_admit_steer.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_applied_visibility.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_branch_anchor.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_checkpoints.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_deepagents_messages.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_dispatching.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_durability.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_edit_reload.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_linear_thread.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_meta.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_mint_concurrency.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_outcomes.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_prepare_hooks.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_rewind_during_run.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_run_crash_sweep.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_settle.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_steer.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_stop_continue.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_voice_delegation.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/test_voice_frontend_tools.py +0 -0
- {harness_sdk_python-0.32.0 → harness_sdk_python-0.32.2}/tests/voice_helpers.py +0 -0
|
@@ -342,7 +342,6 @@ class RunManager(Tray):
|
|
|
342
342
|
"dispatching": dispatching,
|
|
343
343
|
"stopping": stopping,
|
|
344
344
|
"nextDispatch": nextDispatch,
|
|
345
|
-
"inputRequests": inputRequests,
|
|
346
345
|
"error": error,
|
|
347
346
|
"stopReason": stopReason,
|
|
348
347
|
}
|
|
@@ -370,9 +369,14 @@ class RunManager(Tray):
|
|
|
370
369
|
self.state["runId"] = self.id
|
|
371
370
|
self._hydrate_machine()
|
|
372
371
|
self._mirror_steer()
|
|
373
|
-
|
|
372
|
+
self._mirror_input()
|
|
373
|
+
unsubscribes = [
|
|
374
|
+
self.steerQueue.subscribe(lambda _op: self._mirror_steer()),
|
|
375
|
+
self.inputRequests.subscribe(lambda _op: self._mirror_input()),
|
|
376
|
+
]
|
|
374
377
|
yield
|
|
375
|
-
unsubscribe
|
|
378
|
+
for unsubscribe in unsubscribes:
|
|
379
|
+
unsubscribe()
|
|
376
380
|
|
|
377
381
|
class Thread(Protocol):
|
|
378
382
|
"""The thread projection RunManager reads; the host owns the tree."""
|
|
@@ -500,6 +504,14 @@ class RunManager(Tray):
|
|
|
500
504
|
def _mirror_steer(self) -> None:
|
|
501
505
|
self.steerView.value = [_public(item) for item in self.steerQueue.value]
|
|
502
506
|
|
|
507
|
+
def _mirror_input(self) -> None:
|
|
508
|
+
# None publishes as an absent key, matching the TS harness shape
|
|
509
|
+
requests = self.inputRequests.value
|
|
510
|
+
if requests is None:
|
|
511
|
+
self.state.pop("inputRequests", None)
|
|
512
|
+
else:
|
|
513
|
+
self.state["inputRequests"] = requests
|
|
514
|
+
|
|
503
515
|
def _sync_steering(self) -> None:
|
|
504
516
|
ctx = self._run_live
|
|
505
517
|
if ctx is None:
|
|
@@ -20,7 +20,10 @@ clears the entry. The session is bound to the issuing client's connection: ``voi
|
|
|
20
20
|
provider ``closed`` event, or the owner staying detached past the
|
|
21
21
|
``departure_grace`` window (the host's ``client_departed`` hook starts the
|
|
22
22
|
timer, the re-check runs at expiry) ends it; ``voice/start`` while the live
|
|
23
|
-
session's owner is detached ends the orphan and starts the new session
|
|
23
|
+
session's owner is detached ends the orphan and starts the new session, and
|
|
24
|
+
``voice/start`` from the owner itself supersedes its own session — the owner
|
|
25
|
+
holds one media leg, so a fresh start is the definitive signal the old leg
|
|
26
|
+
died.
|
|
24
27
|
Provider turn
|
|
25
28
|
events render live in ``state["messages"]`` through
|
|
26
29
|
``Host.upsert_ephemeral_message`` (message id = turn id), tagged
|
|
@@ -45,8 +48,8 @@ same ``toolCalls`` projection: the client's ``voice/temp-frontend-tool-result``
|
|
|
45
48
|
resolves the await instead of forwarding to the provider, and session end
|
|
46
49
|
fails outstanding awaits.
|
|
47
50
|
|
|
48
|
-
Rejections: ``voice-active`` (
|
|
49
|
-
attached), ``no-voice``
|
|
51
|
+
Rejections: ``voice-active`` (another client's session is live and its
|
|
52
|
+
owner attached), ``no-voice``
|
|
50
53
|
(no voice manager declared, or ``voice/end`` with no session),
|
|
51
54
|
``not-voice-owner`` (``voice/end`` from another client),
|
|
52
55
|
``voice-unavailable`` (the factory or ``provider.start`` raised).
|
|
@@ -602,12 +605,20 @@ class VoiceManagerNamespace(TrayMapNamespace):
|
|
|
602
605
|
if self._starting:
|
|
603
606
|
raise _reject("voice-active", "a voice session is already live")
|
|
604
607
|
session = self.live()
|
|
605
|
-
if
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
608
|
+
if (
|
|
609
|
+
session is not None
|
|
610
|
+
and session.clientId.value != ctx.caller.client_id
|
|
611
|
+
and self._api.has_subscriber(session.clientId.value)
|
|
612
|
+
):
|
|
613
|
+
raise _reject("voice-active", "a voice session is already live")
|
|
609
614
|
self._starting = True
|
|
610
615
|
try:
|
|
616
|
+
if session is not None:
|
|
617
|
+
await session._end(
|
|
618
|
+
"superseded"
|
|
619
|
+
if session.clientId.value == ctx.caller.client_id
|
|
620
|
+
else "connection-drop"
|
|
621
|
+
)
|
|
611
622
|
seed = _capped_seed(await self._api.get_voice_seed())
|
|
612
623
|
tray = await self.get_or_create(uuid4().hex)
|
|
613
624
|
assert isinstance(tray, VoiceManager)
|
|
@@ -54,7 +54,7 @@ async def test_partial_answer_stays_parked_and_echoes_response():
|
|
|
54
54
|
)
|
|
55
55
|
assert res["type"] == "accepted"
|
|
56
56
|
await drv.wait(
|
|
57
|
-
lambda s: "response" in run_of(s)
|
|
57
|
+
lambda s: "response" in run_of(s).get("inputRequests", [{}])[0]
|
|
58
58
|
)
|
|
59
59
|
assert drv.replica["status"] == "input-required"
|
|
60
60
|
assert drv.run["inputRequests"][0]["response"] == {"output": "ok"}
|
|
@@ -82,7 +82,7 @@ async def test_completing_answer_resumes_with_ordered_responses():
|
|
|
82
82
|
(tool_call("r1"), {"output": "ok"}, None),
|
|
83
83
|
(approval("r2"), {"decision": "approve"}, None),
|
|
84
84
|
)
|
|
85
|
-
assert
|
|
85
|
+
assert "inputRequests" not in drv.run
|
|
86
86
|
resumed.applied()
|
|
87
87
|
resumed.finish(RunManager.Complete())
|
|
88
88
|
await drv.wait_status("ready")
|
|
@@ -133,7 +133,7 @@ async def test_completing_batch_resumes_once_in_the_same_envelope():
|
|
|
133
133
|
# the resume dispatched in the batch's own flush: by the time the accepts
|
|
134
134
|
# settle the replica already shows the run in flight
|
|
135
135
|
assert drv.replica["status"] == "running"
|
|
136
|
-
assert
|
|
136
|
+
assert "inputRequests" not in drv.run
|
|
137
137
|
resumed = await script.next_call()
|
|
138
138
|
assert resumed.ctx.trigger == "input-resume"
|
|
139
139
|
assert [r["id"] for r, _, _ in resumed.ctx.input_outcomes] == ["r1", "r2"]
|
|
@@ -324,7 +324,7 @@ async def test_edit_while_parked_clears_requests_and_reruns():
|
|
|
324
324
|
rerun = await script.next_call()
|
|
325
325
|
assert rerun.ctx.trigger == "message-edit"
|
|
326
326
|
assert rerun.ctx.rollback_to is None
|
|
327
|
-
await drv.wait(lambda s:
|
|
327
|
+
await drv.wait(lambda s: "inputRequests" not in run_of(s))
|
|
328
328
|
rerun.applied()
|
|
329
329
|
rerun.finish(RunManager.Complete())
|
|
330
330
|
await drv.wait_status("ready")
|
|
@@ -339,7 +339,7 @@ async def test_reload_while_parked_clears_requests_and_reruns():
|
|
|
339
339
|
rerun = await script.next_call()
|
|
340
340
|
assert rerun.ctx.trigger == "message-reload"
|
|
341
341
|
assert rerun.ctx.rollback_to == "u1"
|
|
342
|
-
await drv.wait(lambda s:
|
|
342
|
+
await drv.wait(lambda s: "inputRequests" not in run_of(s))
|
|
343
343
|
rerun.applied()
|
|
344
344
|
rerun.finish(RunManager.Complete())
|
|
345
345
|
await drv.wait_status("ready")
|
|
@@ -358,7 +358,7 @@ async def test_steer_add_while_parked_abandons_input_and_dispatches():
|
|
|
358
358
|
(approval("r2"), None, None),
|
|
359
359
|
)
|
|
360
360
|
await drv.wait_status("running")
|
|
361
|
-
assert
|
|
361
|
+
assert "inputRequests" not in drv.run
|
|
362
362
|
run.applied()
|
|
363
363
|
run.finish(RunManager.Complete())
|
|
364
364
|
await drv.wait_status("ready")
|
|
@@ -379,7 +379,7 @@ async def test_steer_add_onto_nonempty_lane_while_parked_dispatches_whole_lane()
|
|
|
379
379
|
assert [m["id"] for m in run.ctx.messages] == ["s1", "s2"]
|
|
380
380
|
assert run.ctx.input_outcomes == ((tool_call("r1"), None, None),)
|
|
381
381
|
await drv.wait_status("running")
|
|
382
|
-
assert
|
|
382
|
+
assert "inputRequests" not in drv.run
|
|
383
383
|
run.applied()
|
|
384
384
|
run.finish(RunManager.Complete())
|
|
385
385
|
await drv.wait_status("ready")
|
|
@@ -397,7 +397,7 @@ async def test_promote_to_steer_while_parked_abandons_input_and_dispatches():
|
|
|
397
397
|
assert run.ctx.trigger == "message-send"
|
|
398
398
|
assert [m["id"] for m in run.ctx.messages] == ["m2"]
|
|
399
399
|
assert run.ctx.input_outcomes == ((tool_call("r1"), None, None),)
|
|
400
|
-
assert
|
|
400
|
+
assert "inputRequests" not in drv.run
|
|
401
401
|
run.applied()
|
|
402
402
|
run.finish(RunManager.Complete())
|
|
403
403
|
await drv.wait_status("ready")
|
|
@@ -430,7 +430,7 @@ async def test_steer_add_with_partial_answers_hands_over_the_answered_subset():
|
|
|
430
430
|
(approval("r2"), None, None),
|
|
431
431
|
)
|
|
432
432
|
await drv.wait_status("running")
|
|
433
|
-
assert
|
|
433
|
+
assert "inputRequests" not in drv.run
|
|
434
434
|
run.applied()
|
|
435
435
|
run.finish(RunManager.Complete())
|
|
436
436
|
await drv.wait_status("ready")
|
|
@@ -444,7 +444,7 @@ async def test_answer_meta_replicates_only_when_carried():
|
|
|
444
444
|
"run/input",
|
|
445
445
|
{"requestId": "r1", "response": {"output": "ok"}, "meta": {"src": "c1"}},
|
|
446
446
|
)
|
|
447
|
-
await drv.wait(lambda s: "response" in run_of(s)
|
|
447
|
+
await drv.wait(lambda s: "response" in run_of(s).get("inputRequests", [{}])[0])
|
|
448
448
|
assert drv.run["inputRequests"][0] == {
|
|
449
449
|
**tool_call("r1"),
|
|
450
450
|
"response": {"output": "ok"},
|
|
@@ -107,7 +107,7 @@ async def test_steer_after_a_restart_abandons_input_and_dispatches():
|
|
|
107
107
|
assert [m["id"] for m in run.ctx.messages] == ["s1"]
|
|
108
108
|
assert run.ctx.input_outcomes == ((tool_call("r1"), None, None),)
|
|
109
109
|
await drv2.wait_status("running")
|
|
110
|
-
assert
|
|
110
|
+
assert "inputRequests" not in drv2.run
|
|
111
111
|
run.applied()
|
|
112
112
|
run.finish(RunManager.Complete())
|
|
113
113
|
await drv2.wait_status("ready")
|
|
@@ -373,13 +373,14 @@ ENTRY_KEYS = {
|
|
|
373
373
|
"dispatching",
|
|
374
374
|
"stopping",
|
|
375
375
|
"nextDispatch",
|
|
376
|
-
"inputRequests",
|
|
377
376
|
"error",
|
|
378
377
|
"stopReason",
|
|
379
378
|
}
|
|
380
379
|
|
|
381
380
|
|
|
382
381
|
async def test_entry_keys_are_always_present_and_null_when_empty():
|
|
382
|
+
"""inputRequests is the one absent-when-empty key: present only while a
|
|
383
|
+
request set is open, matching the TS harness shape."""
|
|
383
384
|
script = Script()
|
|
384
385
|
async with run_host(script) as (drv, host):
|
|
385
386
|
drv.post("run/enqueue", add("m1"))
|
|
@@ -388,7 +389,7 @@ async def test_entry_keys_are_always_present_and_null_when_empty():
|
|
|
388
389
|
assert set(drv.run.keys()) == ENTRY_KEYS
|
|
389
390
|
assert drv.run["dispatching"] is not None
|
|
390
391
|
assert (drv.run["stopping"], drv.run["nextDispatch"]) == (None, None)
|
|
391
|
-
assert
|
|
392
|
+
assert drv.run["error"] is None
|
|
392
393
|
call.applied()
|
|
393
394
|
await drv.wait(lambda s: run_of(s)["dispatching"] is None)
|
|
394
395
|
call.fail(RuntimeError("boom"))
|
|
@@ -396,5 +397,5 @@ async def test_entry_keys_are_always_present_and_null_when_empty():
|
|
|
396
397
|
assert set(drv.run.keys()) == ENTRY_KEYS
|
|
397
398
|
assert drv.run["error"] == {"message": "boom"}
|
|
398
399
|
assert (drv.run["dispatching"], drv.run["stopping"]) == (None, None)
|
|
399
|
-
assert
|
|
400
|
+
assert drv.run["nextDispatch"] is None
|
|
400
401
|
assert (drv.run["queue"], drv.run["steerQueue"]) == ([], [])
|
|
@@ -48,12 +48,53 @@ async def test_start_seeds_provider_and_projects_session():
|
|
|
48
48
|
assert bench.contexts[0].client_id == drv.client_id
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
async def
|
|
51
|
+
async def test_start_from_the_owner_supersedes_its_own_session():
|
|
52
52
|
bench = Bench()
|
|
53
53
|
async with voice_host(bench) as (drv, _app):
|
|
54
54
|
await drv.command("voice/start", {"offer": OFFER})
|
|
55
|
+
superseded = bench.provider
|
|
56
|
+
bench.provider = FakeVoiceProvider()
|
|
57
|
+
rsp = await drv.command("voice/start", {"offer": OFFER})
|
|
58
|
+
assert rsp["type"] == "accepted", rsp
|
|
59
|
+
assert superseded.closed
|
|
60
|
+
await drv.wait(
|
|
61
|
+
lambda s: (voice_of(s) or {}).get("clientId") == drv.client_id
|
|
62
|
+
and (voice_of(s) or {}).get("status") == "live"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def test_overlapping_start_during_supersede_rejects_voice_active():
|
|
67
|
+
class SuspendedCloseProvider(FakeVoiceProvider):
|
|
68
|
+
def __init__(self):
|
|
69
|
+
super().__init__()
|
|
70
|
+
self.closing = asyncio.Event()
|
|
71
|
+
self.release = asyncio.Event()
|
|
72
|
+
|
|
73
|
+
async def close(self):
|
|
74
|
+
self.closing.set()
|
|
75
|
+
await self.release.wait()
|
|
76
|
+
await super().close()
|
|
77
|
+
|
|
78
|
+
bench = Bench(provider=SuspendedCloseProvider())
|
|
79
|
+
async with voice_host(bench) as (drv, app):
|
|
80
|
+
await drv.command("voice/start", {"offer": OFFER})
|
|
81
|
+
old = bench.provider
|
|
82
|
+
bench.provider = FakeVoiceProvider()
|
|
83
|
+
supersede = asyncio.ensure_future(
|
|
84
|
+
drv.command("voice/start", {"offer": OFFER})
|
|
85
|
+
)
|
|
86
|
+
await old.closing.wait()
|
|
55
87
|
assert_rejected(
|
|
56
|
-
await drv.
|
|
88
|
+
await side_command(app, drv._client, "voice/start", {"offer": OFFER}),
|
|
89
|
+
"voice-active",
|
|
90
|
+
)
|
|
91
|
+
old.release.set()
|
|
92
|
+
rsp = await supersede
|
|
93
|
+
assert rsp["type"] == "accepted", rsp
|
|
94
|
+
assert [call[0] for call in bench.provider.calls] == ["start"]
|
|
95
|
+
await drv.wait(
|
|
96
|
+
lambda s: (voice_of(s) or {}).get("clientId") == drv.client_id
|
|
97
|
+
and (voice_of(s) or {}).get("status") == "live"
|
|
57
98
|
)
|
|
58
99
|
|
|
59
100
|
|
|
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
|