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

@@ -52,6 +52,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 a tool-enabled task using an OpenAI-compatible API.
@@ -117,6 +118,7 @@ async def run(
confirm_allow=_confirm_allow,
confirm_deny=_confirm_deny,
starting_round=0,
on_progress=on_progress,
)
if checkpoint:
@@ -307,6 +309,7 @@ async def _run_from_messages(
confirm_deny: frozenset,
starting_round: int = 0,
tool_list: list[str] | None = None,
on_progress=None,
) -> tuple[str, OrchestrateCheckpoint | None]:
"""
Run the OpenAI ReAct loop from the current messages state.
@@ -323,6 +326,8 @@ async def _run_from_messages(
est = _estimate_tokens(messages)
logger.info("OpenAI orchestrator round %d / %d model=%s ~%d tokens",
round_num + 1, effective_limit, model_name, est)
if on_progress:
await on_progress(f"Round {round_num + 1} — thinking…")
call_kwargs: dict = {"model": model_name, "messages": messages}
if active_tools:
@@ -373,6 +378,8 @@ async def _run_from_messages(
pending_tools.append({"name": name, "args": args_parsed, "tool_call_id": tc.id})
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, tc.function.arguments, user_role, tool_list)
logger.info("Tool %s%d chars", name, len(result_str))
executed_results.append({"name": name, "args": args_parsed, "result": result_str, "tool_call_id": tc.id})
@@ -415,6 +422,8 @@ async def _run_from_messages(
return final_response, checkpoint
else:
if on_progress:
await on_progress("Generating response…")
final_response = msg.content or ""
logger.info(
"OpenAI orchestrator done after %d round(s). Tools used: %d",