harness-sdk-python 0.32.1__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.1 → harness_sdk_python-0.32.2}/PKG-INFO +1 -1
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/pyproject.toml +1 -1
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/voice.py +18 -7
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_voice.py +43 -2
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/.gitignore +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/README.md +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/examples/__init__.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/examples/runs_app.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/checkpoints.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/deepagents.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/run_manager.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/run_helpers.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_admit_steer.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_applied_visibility.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_branch_anchor.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_checkpoints.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_deepagents_messages.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_dispatching.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_durability.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_edit_reload.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_input_required.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_linear_thread.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_meta.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_mint_concurrency.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_outcomes.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_prepare_hooks.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_restore.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_rewind_during_run.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_run_crash_sweep.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_settle.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_steer.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_stop_continue.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_stopping_window.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_voice_delegation.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_voice_frontend_tools.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/voice_helpers.py +0 -0
|
@@ -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)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|