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.
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/PKG-INFO +1 -1
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/pyproject.toml +1 -1
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/voice.py +11 -2
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_voice.py +6 -1
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_voice_delegation.py +63 -3
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/.gitignore +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/README.md +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/examples/__init__.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/examples/runs_app.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/checkpoints.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/deepagents.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/src/harness_sdk/run_manager.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/run_helpers.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_admit_steer.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_applied_visibility.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_branch_anchor.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_checkpoints.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_deepagents_messages.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_dispatching.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_durability.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_edit_reload.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_input_required.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_linear_thread.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_meta.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_mint_concurrency.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_outcomes.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_prepare_hooks.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_restore.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_rewind_during_run.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_run_crash_sweep.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_settle.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_steer.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_stop_continue.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_stopping_window.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/test_voice_frontend_tools.py +0 -0
- {harness_sdk_python-0.30.0 → harness_sdk_python-0.30.1}/tests/voice_helpers.py +0 -0
|
@@ -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 == {
|
|
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``,
|
|
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
|
|
|
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
|