harness-sdk-python 0.31.2__tar.gz → 0.32.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.31.2 → harness_sdk_python-0.32.1}/PKG-INFO +1 -1
  2. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/pyproject.toml +1 -1
  3. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/src/harness_sdk/run_manager.py +15 -3
  4. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/src/harness_sdk/voice.py +25 -20
  5. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/run_helpers.py +15 -1
  6. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_input_required.py +10 -10
  7. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_restore.py +1 -1
  8. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_stopping_window.py +4 -3
  9. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_voice_frontend_tools.py +40 -22
  10. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/.gitignore +0 -0
  11. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/README.md +0 -0
  12. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/examples/__init__.py +0 -0
  13. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/examples/runs_app.py +0 -0
  14. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/src/harness_sdk/__init__.py +0 -0
  15. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/src/harness_sdk/checkpoints.py +0 -0
  16. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/src/harness_sdk/deepagents.py +0 -0
  17. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/src/harness_sdk/fenced_postgres.py +0 -0
  18. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/src/harness_sdk/linear_thread.py +0 -0
  19. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_admit_steer.py +0 -0
  20. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_applied_visibility.py +0 -0
  21. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_batches.py +0 -0
  22. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_branch_anchor.py +0 -0
  23. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_checkpoints.py +0 -0
  24. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_deepagents_messages.py +0 -0
  25. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_dispatching.py +0 -0
  26. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_durability.py +0 -0
  27. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_edit_dispatched.py +0 -0
  28. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_edit_reload.py +0 -0
  29. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_enqueue.py +0 -0
  30. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_fenced_postgres.py +0 -0
  31. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_linear_thread.py +0 -0
  32. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_meta.py +0 -0
  33. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_mint_concurrency.py +0 -0
  34. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_outcomes.py +0 -0
  35. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_placement.py +0 -0
  36. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_prepare_hooks.py +0 -0
  37. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_rewind_during_run.py +0 -0
  38. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_run_crash_sweep.py +0 -0
  39. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_settle.py +0 -0
  40. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_steer.py +0 -0
  41. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_stop_continue.py +0 -0
  42. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_voice.py +0 -0
  43. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.1}/tests/test_voice_delegation.py +0 -0
  44. {harness_sdk_python-0.31.2 → harness_sdk_python-0.32.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.31.2
3
+ Version: 0.32.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.31.2"
3
+ version = "0.32.1"
4
4
  description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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
- unsubscribe = self.steerQueue.subscribe(lambda _op: self._mirror_steer())
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:
@@ -7,9 +7,10 @@ lives in its own ephemeral tray, minted by ``voice/start`` and deleted when
7
7
  the session ends. The host implements ``VoiceManager.Host``: the seed read
8
8
  and the turn commit both stay host-owned.
9
9
 
10
- ``voice/start {offer, tools?}`` seeds the provider from the thread
11
- transcript (at most 128 messages, tail taken), hands the client's frontend
12
- function tool definitions to the provider, and projects ``state["voice"] =
10
+ ``voice/start {offer}`` seeds the provider from the thread
11
+ transcript (at most 128 messages, tail taken), hands the caller's frontend
12
+ function tool definitions (the ``tools`` map the client's synced model
13
+ context stores at ``ctx.caller.context``) to the provider, and projects ``state["voice"] =
13
14
  {clientId, status, toolCalls, activeTask}`` — the SDP answer rides only the command's
14
15
  result payload ``{answer}``. A named call matching a client-declared tool
15
16
  projects into ``toolCalls`` as ``{callId, name, arguments}``; the owning
@@ -74,28 +75,32 @@ def _reject(reason: str, message: str) -> StatewireReject:
74
75
  return StatewireReject(message, payload={"reason": reason})
75
76
 
76
77
 
77
- def _validated_tools(tools: Any) -> "list[VoiceManager.ToolDef]":
78
+ def _context_tools(context: Any) -> "list[VoiceManager.ToolDef]":
79
+ tools = context.get("tools") if isinstance(context, dict) else None
78
80
  if tools is None:
79
81
  return []
80
- if not isinstance(tools, list):
81
- raise _reject("invalid-message", "tools must be a list")
82
- names: set[str] = set()
83
- for tool in tools:
82
+ if not isinstance(tools, dict):
83
+ raise _reject("invalid-message", "context tools must be a name-keyed object")
84
+ defs: "list[VoiceManager.ToolDef]" = []
85
+ for name, spec in tools.items():
84
86
  if (
85
- not isinstance(tool, dict)
86
- or not isinstance(tool.get("name"), str)
87
- or tool["name"] == ""
88
- or not isinstance(tool.get("description"), str)
89
- or not isinstance(tool.get("parameters"), dict)
87
+ name == ""
88
+ or not isinstance(spec, dict)
89
+ or not isinstance(spec.get("description", ""), str)
90
+ or not isinstance(spec.get("parameters"), dict)
90
91
  ):
91
92
  raise _reject(
92
93
  "invalid-message",
93
- f"tools entries must be {{name, description, parameters}}, got {tool!r}",
94
+ f"context tools entries must be name -> {{description?, parameters}}, got {name!r}: {spec!r}",
94
95
  )
95
- if tool["name"] in names:
96
- raise _reject("invalid-message", f"duplicate tool name {tool['name']!r}")
97
- names.add(tool["name"])
98
- return tools
96
+ defs.append(
97
+ {
98
+ "name": name,
99
+ "description": spec.get("description", ""),
100
+ "parameters": spec["parameters"],
101
+ }
102
+ )
103
+ return defs
99
104
 
100
105
 
101
106
  def _capped_seed(seed: Any) -> "list[VoiceManager.SeedMessage]":
@@ -156,7 +161,7 @@ class VoiceManager(Tray):
156
161
  text: str
157
162
 
158
163
  class ToolDef(TypedDict):
159
- """One frontend function tool the client declares at ``voice/start``."""
164
+ """One frontend function tool from the caller's synced model context."""
160
165
 
161
166
  name: str
162
167
  description: str
@@ -593,7 +598,7 @@ class VoiceManagerNamespace(TrayMapNamespace):
593
598
  offer = params.get("offer")
594
599
  if not isinstance(offer, str) or offer == "":
595
600
  raise _reject("invalid-message", "offer must be a non-empty SDP string")
596
- tools = _validated_tools(params.get("tools"))
601
+ tools = _context_tools(ctx.caller.context)
597
602
  if self._starting:
598
603
  raise _reject("voice-active", "a voice session is already live")
599
604
  session = self.live()
@@ -162,6 +162,7 @@ class RunDriver:
162
162
  self.client_id = client_id
163
163
  self._side_seqs: dict[str, int] = {}
164
164
  self._seq = 0
165
+ self._ctx_seq = 0
165
166
  self._res: dict[int, list[dict[str, Any]]] = {}
166
167
  self._posts: list["asyncio.Task[None]"] = []
167
168
  self.replica: dict[str, Any] = {}
@@ -180,6 +181,20 @@ class RunDriver:
180
181
  def _spawn(self, frame: dict[str, Any]) -> None:
181
182
  self._posts.append(asyncio.create_task(self._submit(frame)))
182
183
 
184
+ async def set_context(self, value: dict[str, Any]) -> None:
185
+ """Sync this client's stored context (one root replace), as contextSync does."""
186
+ self._ctx_seq += 1
187
+ await self._submit(
188
+ {
189
+ "ctx": [
190
+ {
191
+ "ops": [{"op": "replace", "path": [], "value": value}],
192
+ "seq": self._ctx_seq,
193
+ }
194
+ ]
195
+ }
196
+ )
197
+
183
198
  def post(self, method: str, params: Any = None) -> int:
184
199
  """Background frame POST; settle via ``res(seq)``."""
185
200
  self._seq += 1
@@ -393,7 +408,6 @@ def run_of(replica: dict[str, Any]) -> dict[str, Any]:
393
408
  "dispatching": None,
394
409
  "stopping": None,
395
410
  "nextDispatch": None,
396
- "inputRequests": None,
397
411
  "error": None,
398
412
  "stopReason": None,
399
413
  }
@@ -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)["inputRequests"][0]
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 drv.run["inputRequests"] is None
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 drv.run["inputRequests"] is None
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: run_of(s)["inputRequests"] is None)
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: run_of(s)["inputRequests"] is None)
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 drv.run["inputRequests"] is None
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 drv.run["inputRequests"] is None
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 drv.run["inputRequests"] is None
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 drv.run["inputRequests"] is None
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)["inputRequests"][0])
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 drv2.run["inputRequests"] is None
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 (drv.run["inputRequests"], drv.run["error"]) == (None, None)
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 (drv.run["nextDispatch"], drv.run["inputRequests"]) == (None, None)
400
+ assert drv.run["nextDispatch"] is None
400
401
  assert (drv.run["queue"], drv.run["steerQueue"]) == ([], [])
@@ -1,5 +1,5 @@
1
- """Frontend function tools contract: ``voice/start {tools}`` reaches the
2
- provider, matching calls project into ``state["voice"]["toolCalls"]``,
1
+ """Frontend function tools contract: the caller's synced context ``tools``
2
+ map reaches the provider at ``voice/start``, matching calls project into ``state["voice"]["toolCalls"]``,
3
3
  ``voice/temp-frontend-tool-result`` answers them, and the delegated-task
4
4
  handler's ``call_frontend_tool`` rides the same projection."""
5
5
 
@@ -23,11 +23,13 @@ async def until(predicate, timeout: float = 5) -> None:
23
23
  assert asyncio.get_running_loop().time() < deadline, "condition never held"
24
24
  await asyncio.sleep(0.01)
25
25
 
26
- TOOL = {
27
- "name": "get_local_time",
28
- "description": "Read the user's local date and time.",
29
- "parameters": {"type": "object", "properties": {}},
26
+ TOOLS = {
27
+ "get_local_time": {
28
+ "description": "Read the user's local date and time.",
29
+ "parameters": {"type": "object", "properties": {}},
30
+ },
30
31
  }
32
+ TOOL = {"name": "get_local_time", **TOOLS["get_local_time"]}
31
33
 
32
34
 
33
35
  def call_of(events, call_id: str = "call-1") -> None:
@@ -38,25 +40,34 @@ def call_of(events, call_id: str = "call-1") -> None:
38
40
  )
39
41
 
40
42
 
41
- async def test_start_passes_tools_to_the_provider():
43
+ async def test_start_reads_tools_from_the_caller_context():
42
44
  bench = Bench()
43
45
  async with voice_host(bench) as (drv, _app):
44
- rsp = await drv.command("voice/start", {"offer": OFFER, "tools": [TOOL]})
46
+ await drv.set_context(
47
+ {"tools": {**TOOLS, "no_desc": {"parameters": {"type": "object"}}}}
48
+ )
49
+ rsp = await drv.command("voice/start", {"offer": OFFER})
45
50
  assert rsp["type"] == "accepted", rsp
46
- assert bench.provider.calls[0] == ("start", OFFER, [], [TOOL])
51
+ assert bench.provider.calls[0] == (
52
+ "start",
53
+ OFFER,
54
+ [],
55
+ [TOOL, {"name": "no_desc", "description": "", "parameters": {"type": "object"}}],
56
+ )
47
57
 
48
58
 
49
- async def test_malformed_tools_reject_invalid_message():
59
+ async def test_malformed_context_tools_reject_invalid_message():
50
60
  bench = Bench()
51
61
  async with voice_host(bench) as (drv, _app):
52
62
  for tools in (
53
- "not-a-list",
54
- [{"name": "", "description": "", "parameters": {}}],
55
- [{"name": "a", "description": "d"}],
56
- [TOOL, TOOL],
63
+ "not-a-map",
64
+ {"": {"description": "", "parameters": {}}},
65
+ {"a": {"description": "d"}},
66
+ {"a": "not-a-spec"},
57
67
  ):
68
+ await drv.set_context({"tools": tools})
58
69
  assert_rejected(
59
- await drv.command("voice/start", {"offer": OFFER, "tools": tools}),
70
+ await drv.command("voice/start", {"offer": OFFER}),
60
71
  "invalid-message",
61
72
  )
62
73
  assert bench.provider.calls == []
@@ -65,7 +76,8 @@ async def test_malformed_tools_reject_invalid_message():
65
76
  async def test_client_tool_call_projects_into_state():
66
77
  bench = Bench()
67
78
  async with voice_host(bench) as (drv, _app):
68
- await drv.command("voice/start", {"offer": OFFER, "tools": [TOOL]})
79
+ await drv.set_context({"tools": TOOLS})
80
+ await drv.command("voice/start", {"offer": OFFER})
69
81
  call_of(bench.events())
70
82
  await drv.wait(
71
83
  lambda s: (voice_of(s) or {}).get("toolCalls")
@@ -78,7 +90,8 @@ async def test_client_tool_call_projects_into_state():
78
90
  async def test_tool_result_forwards_and_clears_the_pending_call():
79
91
  bench = Bench()
80
92
  async with voice_host(bench) as (drv, _app):
81
- await drv.command("voice/start", {"offer": OFFER, "tools": [TOOL]})
93
+ await drv.set_context({"tools": TOOLS})
94
+ await drv.command("voice/start", {"offer": OFFER})
82
95
  call_of(bench.events())
83
96
  await drv.wait(lambda s: (voice_of(s) or {}).get("toolCalls") != [])
84
97
  rsp = await drv.command(
@@ -93,7 +106,8 @@ async def test_tool_result_forwards_and_clears_the_pending_call():
93
106
  async def test_unknown_call_id_rejects_unknown_tool_call():
94
107
  bench = Bench()
95
108
  async with voice_host(bench) as (drv, _app):
96
- await drv.command("voice/start", {"offer": OFFER, "tools": [TOOL]})
109
+ await drv.set_context({"tools": TOOLS})
110
+ await drv.command("voice/start", {"offer": OFFER})
97
111
  assert_rejected(
98
112
  await drv.command(
99
113
  "voice/temp-frontend-tool-result",
@@ -113,7 +127,8 @@ async def test_executor_call_projects_and_the_result_resolves_the_await():
113
127
 
114
128
  bench.task_handler = handler
115
129
  async with voice_host(bench) as (drv, _app):
116
- await drv.command("voice/start", {"offer": OFFER, "tools": [TOOL]})
130
+ await drv.set_context({"tools": TOOLS})
131
+ await drv.command("voice/start", {"offer": OFFER})
117
132
  bench.events().task_requested("d1", "what time is it locally?")
118
133
  await drv.wait(lambda s: (voice_of(s) or {}).get("toolCalls") != [])
119
134
  [call] = drv.replica["voice"]["toolCalls"]
@@ -142,7 +157,8 @@ async def test_executor_call_with_undeclared_tool_raises_in_the_handler():
142
157
 
143
158
  bench.task_handler = handler
144
159
  async with voice_host(bench) as (drv, _app):
145
- await drv.command("voice/start", {"offer": OFFER, "tools": [TOOL]})
160
+ await drv.set_context({"tools": TOOLS})
161
+ await drv.command("voice/start", {"offer": OFFER})
146
162
  bench.events().task_requested("d1", "open up")
147
163
  await until(
148
164
  lambda: any(
@@ -170,7 +186,8 @@ async def test_session_end_fails_a_pending_executor_call():
170
186
 
171
187
  bench.task_handler = handler
172
188
  async with voice_host(bench) as (drv, _app):
173
- await drv.command("voice/start", {"offer": OFFER, "tools": [TOOL]})
189
+ await drv.set_context({"tools": TOOLS})
190
+ await drv.command("voice/start", {"offer": OFFER})
174
191
  bench.events().task_requested("d1", "what time is it?")
175
192
  await drv.wait(lambda s: (voice_of(s) or {}).get("toolCalls") != [])
176
193
  rsp = await drv.command("voice/end")
@@ -183,7 +200,8 @@ async def test_session_end_fails_a_pending_executor_call():
183
200
  async def test_tool_result_from_other_client_rejects_not_voice_owner():
184
201
  bench = Bench()
185
202
  async with voice_host(bench) as (drv, app):
186
- await drv.command("voice/start", {"offer": OFFER, "tools": [TOOL]})
203
+ await drv.set_context({"tools": TOOLS})
204
+ await drv.command("voice/start", {"offer": OFFER})
187
205
  call_of(bench.events())
188
206
  await drv.wait(lambda s: (voice_of(s) or {}).get("toolCalls") != [])
189
207
  rsp = await side_command(