harness-sdk-python 0.30.0__tar.gz → 0.30.1__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.30.0 → harness_sdk_python-0.30.1}/PKG-INFO +1 -1
  2. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/pyproject.toml +1 -1
  3. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/voice.py +11 -2
  4. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_voice.py +6 -1
  5. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_voice_delegation.py +63 -3
  6. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/.gitignore +0 -0
  7. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/README.md +0 -0
  8. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/examples/__init__.py +0 -0
  9. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/examples/runs_app.py +0 -0
  10. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/__init__.py +0 -0
  11. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/checkpoints.py +0 -0
  12. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/deepagents.py +0 -0
  13. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/fenced_postgres.py +0 -0
  14. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/linear_thread.py +0 -0
  15. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/run_manager.py +0 -0
  16. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/run_helpers.py +0 -0
  17. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_admit_steer.py +0 -0
  18. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_applied_visibility.py +0 -0
  19. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_batches.py +0 -0
  20. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_branch_anchor.py +0 -0
  21. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_checkpoints.py +0 -0
  22. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_deepagents_messages.py +0 -0
  23. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_dispatching.py +0 -0
  24. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_durability.py +0 -0
  25. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_edit_dispatched.py +0 -0
  26. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_edit_reload.py +0 -0
  27. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_enqueue.py +0 -0
  28. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_fenced_postgres.py +0 -0
  29. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_input_required.py +0 -0
  30. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_linear_thread.py +0 -0
  31. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_meta.py +0 -0
  32. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_mint_concurrency.py +0 -0
  33. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_outcomes.py +0 -0
  34. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_placement.py +0 -0
  35. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_prepare_hooks.py +0 -0
  36. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_restore.py +0 -0
  37. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_rewind_during_run.py +0 -0
  38. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_run_crash_sweep.py +0 -0
  39. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_settle.py +0 -0
  40. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_steer.py +0 -0
  41. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_stop_continue.py +0 -0
  42. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_stopping_window.py +0 -0
  43. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_voice_frontend_tools.py +0 -0
  44. {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/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.30.0
3
+ Version: 0.30.1
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.30.0"
3
+ version = "0.30.1"
4
4
  description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -10,7 +10,7 @@ and the turn commit both stay host-owned.
10
10
  ``voice/start {offer, tools?}`` seeds the provider from the thread
11
11
  transcript (at most 128 messages, tail taken), hands the client's frontend
12
12
  function tool definitions to the provider, and projects ``state["voice"] =
13
- {clientId, status, toolCalls}`` — the SDP answer rides only the command's
13
+ {clientId, status, toolCalls, activeTask}`` — the SDP answer rides only the command's
14
14
  result payload ``{answer}``. A named call matching a client-declared tool
15
15
  projects into ``toolCalls`` as ``{callId, name, arguments}``; the owning
16
16
  client answers it with ``voice/temp-frontend-tool-result {call_id,
@@ -33,7 +33,8 @@ transcript handoff — the first task the conversation so far, follow-ups the
33
33
  turns since the previous task. Interim ``speak`` text and the returned
34
34
  result flow back as speakable context appends; a named call's result
35
35
  returns via ``provider.return_tool_result``; a failed handler surfaces to
36
- the model as commentary (task) or an error output (call).
36
+ the model as commentary (task) or an error output (call). The running unit
37
+ projects as ``activeTask`` = ``{taskId, text}``, ``None`` at rest.
37
38
 
38
39
  Rejections: ``voice-active`` (a session is already live), ``no-voice``
39
40
  (no voice manager declared, or ``voice/end`` with no session),
@@ -111,12 +112,14 @@ class VoiceManager(Tray):
111
112
  clientId = observable(None)
112
113
  status = observable("connecting")
113
114
  toolCalls = observable([])
115
+ activeTask = observable(None)
114
116
 
115
117
  state = observable(
116
118
  {
117
119
  "clientId": clientId,
118
120
  "status": status,
119
121
  "toolCalls": toolCalls,
122
+ "activeTask": activeTask,
120
123
  }
121
124
  )
122
125
 
@@ -382,6 +385,7 @@ class VoiceManager(Tray):
382
385
  follow_up=self._tasks_run > 0,
383
386
  )
384
387
  self._tasks_run += 1
388
+ self.activeTask.value = {"taskId": task_id, "text": text}
385
389
  try:
386
390
  answer = await self._host.handle_voice_task(task, speak=self._speak)
387
391
  if not isinstance(answer, str) or answer == "":
@@ -395,6 +399,8 @@ class VoiceManager(Tray):
395
399
  channel="commentary",
396
400
  )
397
401
  return
402
+ finally:
403
+ self.activeTask.value = None
398
404
  await self._append(answer, channel="speakable")
399
405
 
400
406
  def tool_requested(self, request: "VoiceManager.ToolRequest") -> None:
@@ -427,6 +433,7 @@ class VoiceManager(Tray):
427
433
  ]
428
434
 
429
435
  async def _run_tool_call(self, request: "VoiceManager.ToolRequest") -> None:
436
+ self.activeTask.value = {"taskId": request.call_id, "text": request.name}
430
437
  try:
431
438
  output = await self._host.handle_voice_task(request, speak=self._speak)
432
439
  if not isinstance(output, str):
@@ -438,6 +445,8 @@ class VoiceManager(Tray):
438
445
  "voice session %r tool call %r failed", self.id, request.call_id
439
446
  )
440
447
  output = json.dumps({"error": f"{type(exc).__name__}: {exc}"})
448
+ finally:
449
+ self.activeTask.value = None
441
450
  provider = self._provider
442
451
  if provider is not None:
443
452
  await provider.return_tool_result(request.call_id, output)
@@ -39,7 +39,12 @@ async def test_start_seeds_provider_and_projects_session():
39
39
  assert bench.provider.calls == [("start", OFFER, bench.transcript, [])]
40
40
  await drv.wait(lambda s: (voice_of(s) or {}).get("status") == "live")
41
41
  voice = voice_of(drv.replica)
42
- assert voice == {"clientId": drv.client_id, "status": "live", "toolCalls": []}
42
+ assert voice == {
43
+ "clientId": drv.client_id,
44
+ "status": "live",
45
+ "toolCalls": [],
46
+ "activeTask": None,
47
+ }
43
48
  assert bench.contexts[0].client_id == drv.client_id
44
49
 
45
50
 
@@ -1,12 +1,12 @@
1
1
  """Voice delegation contract: work units route to ``Host.handle_voice_task``,
2
2
  handoffs drain the transcript incrementally, results flow back as speakable
3
- context appends, named calls answer via ``return_tool_result``, and repeated
4
- request ids run once."""
3
+ context appends, named calls answer via ``return_tool_result``, repeated
4
+ request ids run once, and the running unit projects as ``activeTask``."""
5
5
 
6
6
  import asyncio
7
7
  import json
8
8
 
9
- from voice_helpers import OFFER, Bench, voice_host
9
+ from voice_helpers import OFFER, Bench, voice_host, voice_of
10
10
 
11
11
  from harness_sdk import VoiceManager
12
12
 
@@ -142,6 +142,66 @@ async def test_task_failure_surfaces_as_commentary():
142
142
  ]
143
143
 
144
144
 
145
+ async def test_active_task_projects_while_a_task_runs_and_clears_after():
146
+ bench = Bench()
147
+ gate = asyncio.Event()
148
+
149
+ async def handler(task, speak):
150
+ await gate.wait()
151
+ return "all done"
152
+
153
+ bench.task_handler = handler
154
+ async with voice_host(bench) as (drv, _app):
155
+ await drv.command("voice/start", {"offer": OFFER})
156
+ bench.events().task_requested("d1", "check the weather")
157
+ await drv.wait(
158
+ lambda s: (voice_of(s) or {}).get("activeTask")
159
+ == {"taskId": "d1", "text": "check the weather"}
160
+ )
161
+ gate.set()
162
+ await drv.wait(lambda s: (voice_of(s) or {}).get("activeTask") is None)
163
+
164
+
165
+ async def test_active_task_projects_a_named_call_and_clears_after():
166
+ bench = Bench()
167
+ gate = asyncio.Event()
168
+
169
+ async def handler(task, speak):
170
+ await gate.wait()
171
+ return "{}"
172
+
173
+ bench.task_handler = handler
174
+ async with voice_host(bench) as (drv, _app):
175
+ await drv.command("voice/start", {"offer": OFFER})
176
+ bench.events().tool_requested(
177
+ VoiceManager.ToolRequest(call_id="c1", name="get_time", arguments="{}")
178
+ )
179
+ await drv.wait(
180
+ lambda s: (voice_of(s) or {}).get("activeTask")
181
+ == {"taskId": "c1", "text": "get_time"}
182
+ )
183
+ gate.set()
184
+ await drv.wait(lambda s: (voice_of(s) or {}).get("activeTask") is None)
185
+
186
+
187
+ async def test_active_task_clears_when_the_handler_fails():
188
+ bench = Bench()
189
+ gate = asyncio.Event()
190
+
191
+ async def handler(task, speak):
192
+ await gate.wait()
193
+ raise RuntimeError("boom")
194
+
195
+ bench.task_handler = handler
196
+ async with voice_host(bench) as (drv, _app):
197
+ await drv.command("voice/start", {"offer": OFFER})
198
+ bench.events().task_requested("d1", "dig in")
199
+ await drv.wait(lambda s: (voice_of(s) or {}).get("activeTask") is not None)
200
+ gate.set()
201
+ await drv.wait(lambda s: (voice_of(s) or {}).get("activeTask") is None)
202
+ assert (voice_of(drv.replica) or {}).get("status") == "live"
203
+
204
+
145
205
  async def test_tool_failure_returns_an_error_output():
146
206
  bench = Bench()
147
207