chore: release api v1.0.0
- Event-based streaming for task agent - Retry logic when LLM responds without calling tools - Hardened prompts to enforce tool use - Working directory context in all agent prompts - Project paused: local LLMs not capable enough for agentic use Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,41 @@ class PermissionMode(str, Enum):
|
||||
auto_accept = "auto_accept"
|
||||
|
||||
|
||||
class StreamEventType(str, Enum):
|
||||
"""Event types for structured agent streaming."""
|
||||
tool_start = "tool_start"
|
||||
tool_done = "tool_done"
|
||||
thinking = "thinking"
|
||||
response = "response"
|
||||
error = "error"
|
||||
done = "done"
|
||||
chunk = "chunk" # Legacy text chunk
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamEvent:
|
||||
"""
|
||||
Structured streaming event from agent execution.
|
||||
|
||||
Different event types carry different data:
|
||||
- tool_start: tool, args
|
||||
- tool_done: tool, result_summary
|
||||
- thinking: message
|
||||
- response: text
|
||||
- error: error_message
|
||||
- done: mode
|
||||
- chunk: text (legacy)
|
||||
"""
|
||||
event: StreamEventType
|
||||
tool: str | None = None
|
||||
args: dict | None = None
|
||||
result_summary: str | None = None
|
||||
message: str | None = None
|
||||
text: str | None = None
|
||||
error_message: str | None = None
|
||||
mode: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentResponse:
|
||||
"""Response from agent execution."""
|
||||
@@ -202,7 +237,7 @@ class WebberClient:
|
||||
prompt: str,
|
||||
working_dir: str = ".",
|
||||
mode: PermissionMode = PermissionMode.default,
|
||||
) -> AsyncIterator[str]:
|
||||
) -> AsyncIterator[StreamEvent]:
|
||||
"""
|
||||
Run an agent with streaming response.
|
||||
|
||||
@@ -213,7 +248,7 @@ class WebberClient:
|
||||
mode: Permission mode controlling tool access
|
||||
|
||||
Yields:
|
||||
Text chunks as they arrive
|
||||
StreamEvent objects as they arrive
|
||||
"""
|
||||
# Use a fresh client for streaming with longer timeout
|
||||
async with httpx.AsyncClient(
|
||||
@@ -235,13 +270,30 @@ class WebberClient:
|
||||
if line.startswith("data: "):
|
||||
try:
|
||||
data = json.loads(line[6:])
|
||||
event = data.get("event")
|
||||
if event == "chunk":
|
||||
yield data.get("data", "")
|
||||
elif event == "error":
|
||||
raise Exception(data.get("data", "Unknown error"))
|
||||
elif event == "done":
|
||||
event_type = data.get("event")
|
||||
|
||||
# Parse event type
|
||||
try:
|
||||
evt_type = StreamEventType(event_type)
|
||||
except ValueError:
|
||||
continue # Unknown event type
|
||||
|
||||
# Build StreamEvent from response data
|
||||
yield StreamEvent(
|
||||
event=evt_type,
|
||||
tool=data.get("tool"),
|
||||
args=data.get("args"),
|
||||
result_summary=data.get("result_summary"),
|
||||
message=data.get("message"),
|
||||
text=data.get("text") or data.get("data"), # 'data' for legacy chunk
|
||||
error_message=data.get("error_message") or data.get("data"),
|
||||
mode=data.get("mode"),
|
||||
)
|
||||
|
||||
# Stop on done or error
|
||||
if evt_type in (StreamEventType.done, StreamEventType.error):
|
||||
break
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from rich.markdown import Markdown
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Confirm
|
||||
|
||||
from webber_cli.client import WebberClient, PermissionMode
|
||||
from webber_cli.client import WebberClient, PermissionMode, StreamEvent, StreamEventType
|
||||
from webber_cli.config import get_config, init_config, get_config_path, CONFIG_FILE
|
||||
from webber_cli.theme import get_console, get_theme
|
||||
|
||||
@@ -471,15 +471,56 @@ async def _chat_loop(
|
||||
console.print()
|
||||
|
||||
if stream:
|
||||
# Stream response in real-time, buffering for persistence
|
||||
# Stream response with structured events
|
||||
response_chunks: list[str] = []
|
||||
current_tool: str | None = None
|
||||
try:
|
||||
async for chunk in client.run_agent_stream(
|
||||
async for event in client.run_agent_stream(
|
||||
agent_type, user_input, working_dir, current_mode
|
||||
):
|
||||
sys.stdout.write(chunk)
|
||||
sys.stdout.flush()
|
||||
response_chunks.append(chunk)
|
||||
if event.event == StreamEventType.thinking:
|
||||
# Show thinking status
|
||||
console.print(f"[dim]{event.message or 'Thinking...'}[/]")
|
||||
|
||||
elif event.event == StreamEventType.tool_start:
|
||||
# Show tool starting
|
||||
current_tool = event.tool
|
||||
args_display = ""
|
||||
if event.args:
|
||||
# Format key args for display
|
||||
key_args = []
|
||||
for k, v in list(event.args.items())[:2]:
|
||||
v_str = str(v)[:40] + "..." if len(str(v)) > 40 else str(v)
|
||||
key_args.append(f"{k}={v_str}")
|
||||
args_display = f" ({', '.join(key_args)})"
|
||||
console.print(f"[info]→ {event.tool}[/]{args_display}", end="")
|
||||
|
||||
elif event.event == StreamEventType.tool_done:
|
||||
# Show tool completed
|
||||
result = event.result_summary or "done"
|
||||
console.print(f" [success]✓[/] [dim]{result}[/]")
|
||||
current_tool = None
|
||||
|
||||
elif event.event == StreamEventType.response:
|
||||
# Stream response text
|
||||
if event.text:
|
||||
sys.stdout.write(event.text)
|
||||
sys.stdout.flush()
|
||||
response_chunks.append(event.text)
|
||||
|
||||
elif event.event == StreamEventType.chunk:
|
||||
# Legacy text chunk (for other agents)
|
||||
if event.text:
|
||||
sys.stdout.write(event.text)
|
||||
sys.stdout.flush()
|
||||
response_chunks.append(event.text)
|
||||
|
||||
elif event.event == StreamEventType.error:
|
||||
console.print(f"\n[error]Error:[/] {event.error_message}")
|
||||
|
||||
elif event.event == StreamEventType.done:
|
||||
pass # Stream complete
|
||||
|
||||
console.print() # Newline after streaming
|
||||
|
||||
# Save messages to conversation if we have a session
|
||||
@@ -672,13 +713,23 @@ async def _explore(
|
||||
console.print()
|
||||
|
||||
if stream:
|
||||
# Stream response in real-time
|
||||
# Stream response with structured events
|
||||
try:
|
||||
async for chunk in client.run_agent_stream(
|
||||
async for event in client.run_agent_stream(
|
||||
"task", query, working_dir, mode
|
||||
):
|
||||
sys.stdout.write(chunk)
|
||||
sys.stdout.flush()
|
||||
if event.event == StreamEventType.thinking:
|
||||
console.print(f"[dim]{event.message or 'Thinking...'}[/]")
|
||||
elif event.event == StreamEventType.tool_start:
|
||||
console.print(f"[info]→ {event.tool}[/]", end="")
|
||||
elif event.event == StreamEventType.tool_done:
|
||||
console.print(f" [success]✓[/] [dim]{event.result_summary or 'done'}[/]")
|
||||
elif event.event in (StreamEventType.response, StreamEventType.chunk):
|
||||
if event.text:
|
||||
sys.stdout.write(event.text)
|
||||
sys.stdout.flush()
|
||||
elif event.event == StreamEventType.error:
|
||||
console.print(f"\n[error]Error:[/] {event.error_message}")
|
||||
console.print() # Newline after streaming
|
||||
except Exception as e:
|
||||
console.print(f"\n[error]Stream error:[/] {e}")
|
||||
|
||||
Reference in New Issue
Block a user