Added streaming

This commit is contained in:
vh
2026-04-02 22:47:30 -07:00
parent f8fb8aebdb
commit a282d853a6
6 changed files with 400 additions and 33 deletions
+89 -30
View File
@@ -44,6 +44,13 @@ class ProviderClient:
temperature: float, max_tokens: int) -> str:
return self.call(system, messages, model, temperature, max_tokens)
async def call_async_streaming(self, system: str, messages: list[dict], model: str,
temperature: float, max_tokens: int):
"""Async generator yielding ('thinking'|'content', text) chunks.
Default: emits the full response as a single 'content' chunk."""
response = await self.call_async(system, messages, model, temperature, max_tokens)
yield ('content', response)
# --- OpenAI and any OpenAI-compatible endpoint ---
# Covers AIPA_PROVIDER / {AGENT}_PROVIDER = openai or openai_compatible.
@@ -71,6 +78,29 @@ class OpenAIClient(ProviderClient):
self.call_async(system, messages, model, temperature, max_tokens)
)
async def call_async_streaming(self, system, messages, model, temperature, max_tokens):
"""Stream via the OpenAI-compatible API, yielding thinking and content chunks.
Thinking tokens arrive in delta.reasoning_content (Qwen3, DeepSeek R1 style);
regular tokens arrive in delta.content."""
full_messages = [{"role": "system", "content": system}] + messages
stream = await self.client.chat.completions.create(
model=model,
temperature=temperature,
max_tokens=max_tokens,
messages=full_messages,
extra_body=self.extra_body or None,
stream=True,
)
async for chunk in stream:
if not chunk.choices:
continue
delta = chunk.choices[0].delta
thinking = getattr(delta, 'reasoning_content', None)
if thinking:
yield ('thinking', thinking)
if delta.content:
yield ('content', delta.content)
# --- Anthropic ---
# Uncomment when ready to use. Install: pip install anthropic
@@ -283,6 +313,8 @@ async def call_agent_async(
agent: AgentState,
user_message: str,
dry_run: bool = False,
stream: bool = True,
stream_mode: str = "deliverable",
) -> str:
if dry_run:
return f"[DRY RUN] Would call {agent.name} ({config.agent_provider_name(agent.name)}) with: {user_message[:100]}..."
@@ -290,19 +322,32 @@ async def call_agent_async(
agent.add_user(user_message)
try:
response = await agent.client.call_async(
system=agent.system_prompt,
messages=agent.messages_for_call(),
model=agent.model,
temperature=agent.temperature,
max_tokens=agent.max_tokens,
)
if stream:
response = await ui.stream_agent_output(
agent.name,
agent.client.call_async_streaming(
system=agent.system_prompt,
messages=agent.messages_for_call(),
model=agent.model,
temperature=agent.temperature,
max_tokens=agent.max_tokens,
),
mode=stream_mode,
)
else:
response = await agent.client.call_async(
system=agent.system_prompt,
messages=agent.messages_for_call(),
model=agent.model,
temperature=agent.temperature,
max_tokens=agent.max_tokens,
)
except NotImplementedError as e:
raise RuntimeError(str(e)) from e
agent.add_assistant(response)
if config.DEBUG_PRINT_AGENT_OUTPUTS:
if config.DEBUG_PRINT_AGENT_OUTPUTS and not stream:
ui.print_agent_panel(agent.name, response)
return response
@@ -357,7 +402,7 @@ async def dispatch_to_lead(
if session_id:
task_store.update_task(task, session_id)
try:
output = await call_agent_async(lead, task.brief, dry_run=dry_run)
output = await call_agent_async(lead, task.brief, dry_run=dry_run, stream=False)
task.output = output
task.status = "complete"
except Exception as e:
@@ -424,7 +469,7 @@ async def run_auditor(
f"{deliverable}"
)
auditor.history = [] # Auditor is stateless — fresh call each time
return await call_agent_async(auditor, audit_request, dry_run=dry_run)
return await call_agent_async(auditor, audit_request, dry_run=dry_run, stream_mode="audit")
# ---------------------------------------------------------------------------
@@ -446,7 +491,7 @@ async def update_standing_brief(
f"log entry at the top of Section 9. Archive anything older than "
f"{config.SESSION_LOG_RETENTION} sessions."
)
return await call_agent_async(orchestrator, update_request, dry_run=dry_run)
return await call_agent_async(orchestrator, update_request, dry_run=dry_run, stream_mode="background")
# ---------------------------------------------------------------------------
@@ -502,7 +547,7 @@ class Session:
f"Directive: {directive}"
)
cos_response = await call_agent_async(
self.orchestrator, cos_prompt, dry_run=self.dry_run
self.orchestrator, cos_prompt, dry_run=self.dry_run, stream=True
)
# ── Step 2: Parse any task briefs the orchestrator issued ───────────
@@ -532,10 +577,9 @@ class Session:
f"Please synthesize these into a final deliverable for the Principal.\n\n"
f"{lead_outputs_text}"
)
with ui.synthesis_progress():
deliverable = await call_agent_async(
self.orchestrator, synthesis_prompt, dry_run=self.dry_run
)
deliverable = await call_agent_async(
self.orchestrator, synthesis_prompt, dry_run=self.dry_run, stream=True
)
dispatched_leads = sorted({t.assigned_to for t in completed_tasks})
self.session_log.append(
@@ -549,10 +593,9 @@ class Session:
audit_memo = ""
if self.auto_audit:
try:
with ui.audit_progress():
audit_memo = await run_auditor(
self.auditor, deliverable, task_id, dry_run=self.dry_run
)
audit_memo = await run_auditor(
self.auditor, deliverable, task_id, dry_run=self.dry_run
)
self.session_log.append(f"Audit complete for {task_id}.")
except RuntimeError as e:
ui.print_warning(f"Audit skipped — {e}")
@@ -561,10 +604,9 @@ class Session:
async def close(self):
summary = "\n".join(self.session_log)
with ui.brief_update_progress():
updated_brief = await update_standing_brief(
self.orchestrator, summary, dry_run=self.dry_run
)
updated_brief = await update_standing_brief(
self.orchestrator, summary, dry_run=self.dry_run
)
if not self.dry_run:
save_standing_brief(updated_brief)
ui.print_session_footer(self.directive_count)
@@ -645,17 +687,26 @@ async def cmd_history(session: Session, args: str) -> bool:
return True
@command(["/thinking"], "Show the thinking output from the last response")
async def cmd_thinking(session: Session, args: str) -> bool:
last = ui.get_last_thinking()
if not last:
ui.print_system("No thinking output recorded — thinking mode may not be active.")
else:
ui.print_thinking_expansion(last)
return True
@command(["/audit"], "Re-run Vera's audit on the last deliverable")
async def cmd_audit(session: Session, args: str) -> bool:
if not session.last_deliverable:
ui.print_warning("No deliverable yet — issue a directive first.")
return True
ui.print_system("Running audit on last deliverable…")
with ui.audit_progress():
memo = await run_auditor(
session.auditor, session.last_deliverable,
session.last_task_id, dry_run=session.dry_run,
)
memo = await run_auditor(
session.auditor, session.last_deliverable,
session.last_task_id, dry_run=session.dry_run,
)
ui.print_audit_memo(memo, task_id=session.last_task_id)
return True
@@ -731,7 +782,15 @@ async def direct_agent_session(agent_name: str, dry_run: bool = False):
if not user_input or user_input.lower() in ("/quit", "/exit", "/q"):
break
response = await call_agent_async(agent, user_input, dry_run=dry_run)
if user_input.lower() == "/thinking":
last = ui.get_last_thinking()
if not last:
ui.print_system("No thinking output recorded — thinking mode may not be active.")
else:
ui.print_thinking_expansion(last)
continue
response = await call_agent_async(agent, user_input, dry_run=dry_run, stream=True)
ui.print_agent_panel(agent_name, response)
ui.print_session_footer(0)