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.
Files changed (44) hide show
  1. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/PKG-INFO +1 -1
  2. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/pyproject.toml +1 -1
  3. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/voice.py +18 -7
  4. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_voice.py +43 -2
  5. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/.gitignore +0 -0
  6. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/README.md +0 -0
  7. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/examples/__init__.py +0 -0
  8. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/examples/runs_app.py +0 -0
  9. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/__init__.py +0 -0
  10. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/checkpoints.py +0 -0
  11. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/deepagents.py +0 -0
  12. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/fenced_postgres.py +0 -0
  13. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/linear_thread.py +0 -0
  14. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/src/harness_sdk/run_manager.py +0 -0
  15. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/run_helpers.py +0 -0
  16. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_admit_steer.py +0 -0
  17. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_applied_visibility.py +0 -0
  18. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_batches.py +0 -0
  19. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_branch_anchor.py +0 -0
  20. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_checkpoints.py +0 -0
  21. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_deepagents_messages.py +0 -0
  22. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_dispatching.py +0 -0
  23. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_durability.py +0 -0
  24. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_edit_dispatched.py +0 -0
  25. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_edit_reload.py +0 -0
  26. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_enqueue.py +0 -0
  27. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_fenced_postgres.py +0 -0
  28. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_input_required.py +0 -0
  29. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_linear_thread.py +0 -0
  30. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_meta.py +0 -0
  31. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_mint_concurrency.py +0 -0
  32. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_outcomes.py +0 -0
  33. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_placement.py +0 -0
  34. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_prepare_hooks.py +0 -0
  35. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_restore.py +0 -0
  36. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_rewind_during_run.py +0 -0
  37. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_run_crash_sweep.py +0 -0
  38. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_settle.py +0 -0
  39. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_steer.py +0 -0
  40. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_stop_continue.py +0 -0
  41. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_stopping_window.py +0 -0
  42. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_voice_delegation.py +0 -0
  43. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/test_voice_frontend_tools.py +0 -0
  44. {harness_sdk_python-0.32.1 → harness_sdk_python-0.32.2}/tests/voice_helpers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: harness-sdk-python
3
- Version: 0.32.1
3
+ Version: 0.32.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "harness-sdk-python"
3
- version = "0.32.1"
3
+ version = "0.32.2"
4
4
  description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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`` (a session is already live and its owner
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 session is not None:
606
- if self._api.has_subscriber(session.clientId.value):
607
- raise _reject("voice-active", "a voice session is already live")
608
- await session._end("connection-drop")
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 test_start_while_active_rejects_voice_active():
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.command("voice/start", {"offer": OFFER}), "voice-active"
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