Inherit model overrides in parallel tool workers

Alessandro committed Aug 25, 2026 at 20:19 UTC cf29923cedcfa73a826cb580b4c45979b237c2c3
3 files changed +45
helpers/parallel_tools.py
+4
@@ -477,6 +477,10 @@ async def _run_direct_tool_job(parent_context_id: str, job: ParallelJob) -> str:
477 worker_context.set_data(PARALLEL_WORKER_PARENT_CONTEXT_KEY, parent_context_id)
478 worker_context.set_data(PARALLEL_WORKER_JOB_KEY, job.id)
479 worker_context.set_data(PARALLEL_WORKER_KIND_KEY, job.kind)
480 + worker_context.set_data(
481 + "chat_model_override",
482 + parent_context.get_data("chat_model_override"),
483 + )
484 job.worker_context_id = worker_context.id
485 _copy_project(parent_context, worker_context)
486
helpers/parallel_tools.py.dox.md
+1
@@ -32,6 +32,7 @@
32 - Subordinate child chats are tagged with job metadata, remain outside the scheduler task list, and may use normal child-chat tools including `parallel`.
33 - Nested parallel jobs started by a parallel subordinate are registered as child `DeferredTask` instances so stopping the ancestor also stops its descendants.
34 - Direct tool jobs run in isolated background contexts and are blocked from recursively invoking `parallel`.
35 +- Direct tool jobs inherit the parent's active per-chat model override.
36 - Direct tool background context cleanup removes both the in-memory context and any transient chat folder left on disk.
37 - Parent-visible child log items are created for each wrapped call so the WebUI can inspect concurrent children separately while the wrapper result remains model-history-only.
38 - Child tool logs mirror normal tool-call visible args; job ids remain available through wrapper results and prompt extras rather than visible process-step args.
tests/test_parallel_tool.py
+40
@@ -347,6 +347,46 @@ async def test_parallel_remove_context_deletes_persisted_worker_chat(monkeypatch
347 assert removed == ["missing-worker"]
348
349
350 +@pytest.mark.asyncio
351 +async def test_direct_parallel_worker_inherits_chat_model_override(monkeypatch) -> None:
352 + from agent import AgentConfig, AgentContext
353 +
354 + parent_id = "ctx-parallel-model-override"
355 + AgentContext.remove(parent_id)
356 + parent = AgentContext(
357 + AgentConfig(mcp_servers="", profile="agent0"),
358 + id=parent_id,
359 + set_current=False,
360 + )
361 + override = {"preset_name": "Text only"}
362 + parent.set_data("chat_model_override", override)
363 + observed = {}
364 +
365 + async def fake_execute_tool_call(agent, *_args, **_kwargs):
366 + observed["override"] = agent.context.get_data("chat_model_override")
367 + return "done"
368 +
369 + async def remove_context(context_id):
370 + AgentContext.remove(context_id)
371 +
372 + monkeypatch.setattr(parallel_tools, "execute_tool_call", fake_execute_tool_call)
373 + monkeypatch.setattr(parallel_tools, "_remove_context", remove_context)
374 + job = parallel_tools.ParallelJob(
375 + id="vision-load-override",
376 + parent_context_id=parent_id,
377 + index=0,
378 + tool_name="vision_load",
379 + tool_args={"paths": ["/tmp/example.png"]},
380 + kind="tool",
381 + )
382 +
383 + try:
384 + assert await parallel_tools._run_direct_tool_job(parent_id, job) == "done"
385 + assert observed["override"] == override
386 + finally:
387 + AgentContext.remove(parent_id)
388 +
389 +
390 @pytest.mark.asyncio
391 async def test_parallel_recursion_guard_allows_subordinate_children_but_blocks_tool_workers() -> None:
392 from extensions.python.tool_execute_before._20_block_parallel_recursion import (