feat: live progress updates during orchestrator tool loop

The thinking bubble now shows real-time status instead of a static spinner:
   Round 1 — thinking…
   Round 1 — web_search
   Round 2 — thinking…
   Generating response…

Implementation: async on_progress callback passed from _run_job into both
orchestrators (_run_from_messages / _run_from_contents). Callback writes to
job["progress"] under the jobs lock; poll responses include the field;
app.js displays it in the thinking bubble when present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-16 22:55:14 -04:00
parent 4fdb9fd0c7
commit c31eba111f
4 changed files with 37 additions and 4 deletions

View File

@@ -120,6 +120,7 @@ async def run(
max_risk: str | None = None,
risk_whitelist: list[str] | None = None,
risk_blacklist: list[str] | None = None,
on_progress=None, # async (str) -> None; called with live status updates
) -> OrchestratorResult:
"""
Run the full orchestration loop for a task.
@@ -183,6 +184,7 @@ async def run(
starting_round=0,
gemini_api_key=api_key,
max_rounds=max_rounds,
on_progress=on_progress,
)
if checkpoint:
@@ -194,6 +196,9 @@ async def run(
checkpoint=checkpoint,
)
if on_progress:
await on_progress("Generating response…")
return await _claude_handoff(
task=task,
tool_call_log=tool_call_log,
@@ -306,6 +311,7 @@ async def _run_from_contents(
gemini_api_key: str | None = None,
tool_list: list[str] | None = None,
max_rounds: int | None = None,
on_progress=None,
) -> tuple[str, OrchestrateCheckpoint | None]:
"""
Run the ReAct loop from the current contents state.
@@ -317,6 +323,8 @@ async def _run_from_contents(
for round_num in range(starting_round, effective_limit):
logger.info("Orchestrator round %d for task: %.80s", round_num + 1, task)
if on_progress:
await on_progress(f"Round {round_num + 1} — thinking…")
response = await asyncio.to_thread(
client.models.generate_content,
@@ -363,6 +371,8 @@ async def _run_from_contents(
pending_tools.append({"name": name, "args": args})
logger.info("Tool %s blocked — confirmation required", name)
else:
if on_progress:
await on_progress(f"Round {round_num + 1}{name}")
result_str = await _execute_tool(name, args, tool_callables)
logger.info("Tool %s%d chars", name, len(result_str))
executed_results.append({"name": name, "args": args, "result": result_str})