superqode 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SuperQode Agent Streaming - Real-time Agent Communication
|
|
3
|
+
|
|
4
|
+
Implements ACP (Agent Client Protocol) for streaming agent output in real-time.
|
|
5
|
+
Supports OpenCode and other ACP-compatible agents.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Real-time message streaming (agent responses, thoughts, tool calls)
|
|
9
|
+
- Interactive permission requests
|
|
10
|
+
- Plan tracking with live updates
|
|
11
|
+
- Colorful SuperQode styling
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Callable, Dict, List, Optional, Literal
|
|
23
|
+
from time import monotonic
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# THEME & COLORS (SuperQode style)
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
STREAM_COLORS = {
|
|
30
|
+
"message": "#a855f7", # Purple - agent messages
|
|
31
|
+
"thought": "#ec4899", # Pink - thinking
|
|
32
|
+
"tool": "#f97316", # Orange - tool calls
|
|
33
|
+
"plan": "#06b6d4", # Cyan - plan updates
|
|
34
|
+
"success": "#22c55e", # Green - completed
|
|
35
|
+
"error": "#ef4444", # Red - errors
|
|
36
|
+
"warning": "#f59e0b", # Amber - warnings
|
|
37
|
+
"pending": "#71717a", # Gray - pending
|
|
38
|
+
"progress": "#3b82f6", # Blue - in progress
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
STREAM_ICONS = {
|
|
42
|
+
"message": "💬",
|
|
43
|
+
"thought": "💭",
|
|
44
|
+
"tool_read": "📖",
|
|
45
|
+
"tool_edit": "✏️",
|
|
46
|
+
"tool_delete": "🗑️",
|
|
47
|
+
"tool_execute": "⚡",
|
|
48
|
+
"tool_search": "🔍",
|
|
49
|
+
"tool_think": "🧠",
|
|
50
|
+
"tool_fetch": "🌐",
|
|
51
|
+
"tool_other": "🔧",
|
|
52
|
+
"plan": "📋",
|
|
53
|
+
"permission": "🔐",
|
|
54
|
+
"success": "✅",
|
|
55
|
+
"error": "❌",
|
|
56
|
+
"pending": "⏳",
|
|
57
|
+
"progress": "🔄",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ============================================================================
|
|
62
|
+
# MESSAGE TYPES
|
|
63
|
+
# ============================================================================
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class StreamEventType(Enum):
|
|
67
|
+
"""Types of streaming events from agents."""
|
|
68
|
+
|
|
69
|
+
MESSAGE_CHUNK = "message_chunk" # Agent text response
|
|
70
|
+
THOUGHT_CHUNK = "thought_chunk" # Agent thinking
|
|
71
|
+
TOOL_CALL = "tool_call" # Tool invocation started
|
|
72
|
+
TOOL_UPDATE = "tool_update" # Tool status update
|
|
73
|
+
PLAN = "plan" # Plan with tasks
|
|
74
|
+
PERMISSION = "permission" # Permission request
|
|
75
|
+
MODE_UPDATE = "mode_update" # Mode change
|
|
76
|
+
STATUS = "status" # Status line update
|
|
77
|
+
ERROR = "error" # Error occurred
|
|
78
|
+
COMPLETE = "complete" # Agent finished
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ToolKind(Enum):
|
|
82
|
+
"""Types of tool operations."""
|
|
83
|
+
|
|
84
|
+
READ = "read"
|
|
85
|
+
EDIT = "edit"
|
|
86
|
+
DELETE = "delete"
|
|
87
|
+
MOVE = "move"
|
|
88
|
+
SEARCH = "search"
|
|
89
|
+
EXECUTE = "execute"
|
|
90
|
+
THINK = "think"
|
|
91
|
+
FETCH = "fetch"
|
|
92
|
+
SWITCH_MODE = "switch_mode"
|
|
93
|
+
OTHER = "other"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ToolStatus(Enum):
|
|
97
|
+
"""Status of a tool call."""
|
|
98
|
+
|
|
99
|
+
PENDING = "pending"
|
|
100
|
+
IN_PROGRESS = "in_progress"
|
|
101
|
+
COMPLETED = "completed"
|
|
102
|
+
FAILED = "failed"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TaskStatus(Enum):
|
|
106
|
+
"""Status of a plan task."""
|
|
107
|
+
|
|
108
|
+
PENDING = "pending"
|
|
109
|
+
IN_PROGRESS = "in_progress"
|
|
110
|
+
COMPLETED = "completed"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TaskPriority(Enum):
|
|
114
|
+
"""Priority of a plan task."""
|
|
115
|
+
|
|
116
|
+
HIGH = "high"
|
|
117
|
+
MEDIUM = "medium"
|
|
118
|
+
LOW = "low"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ============================================================================
|
|
122
|
+
# DATA CLASSES
|
|
123
|
+
# ============================================================================
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class StreamMessage:
|
|
128
|
+
"""A chunk of agent message text."""
|
|
129
|
+
|
|
130
|
+
text: str
|
|
131
|
+
is_complete: bool = False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class StreamThought:
|
|
136
|
+
"""Agent's thinking/reasoning."""
|
|
137
|
+
|
|
138
|
+
text: str
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class ToolCallContent:
|
|
143
|
+
"""Content within a tool call (diff, terminal, etc.)."""
|
|
144
|
+
|
|
145
|
+
type: str # "content", "diff", "terminal"
|
|
146
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class StreamToolCall:
|
|
151
|
+
"""A tool call from the agent."""
|
|
152
|
+
|
|
153
|
+
tool_id: str
|
|
154
|
+
title: str
|
|
155
|
+
kind: ToolKind = ToolKind.OTHER
|
|
156
|
+
status: ToolStatus = ToolStatus.PENDING
|
|
157
|
+
content: List[ToolCallContent] = field(default_factory=list)
|
|
158
|
+
locations: List[Dict[str, Any]] = field(default_factory=list)
|
|
159
|
+
raw_input: Dict[str, Any] = field(default_factory=dict)
|
|
160
|
+
raw_output: Dict[str, Any] = field(default_factory=dict)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class PlanTask:
|
|
165
|
+
"""A task in the agent's plan."""
|
|
166
|
+
|
|
167
|
+
content: str
|
|
168
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
169
|
+
priority: TaskPriority = TaskPriority.MEDIUM
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class StreamPlan:
|
|
174
|
+
"""Agent's plan with tasks."""
|
|
175
|
+
|
|
176
|
+
tasks: List[PlanTask] = field(default_factory=list)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class PermissionOption:
|
|
181
|
+
"""An option for permission request."""
|
|
182
|
+
|
|
183
|
+
option_id: str
|
|
184
|
+
name: str
|
|
185
|
+
kind: str # allow_once, allow_always, reject_once, reject_always
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass
|
|
189
|
+
class StreamPermission:
|
|
190
|
+
"""Permission request from agent."""
|
|
191
|
+
|
|
192
|
+
tool_call: StreamToolCall
|
|
193
|
+
options: List[PermissionOption] = field(default_factory=list)
|
|
194
|
+
result_future: Optional[asyncio.Future] = None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass
|
|
198
|
+
class StreamEvent:
|
|
199
|
+
"""A streaming event from the agent."""
|
|
200
|
+
|
|
201
|
+
event_type: StreamEventType
|
|
202
|
+
data: Any # StreamMessage, StreamThought, StreamToolCall, etc.
|
|
203
|
+
timestamp: float = field(default_factory=monotonic)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ============================================================================
|
|
207
|
+
# JSON-RPC HELPERS
|
|
208
|
+
# ============================================================================
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class JSONRPCError(Exception):
|
|
212
|
+
"""JSON-RPC error."""
|
|
213
|
+
|
|
214
|
+
def __init__(self, code: int, message: str, data: Any = None):
|
|
215
|
+
self.code = code
|
|
216
|
+
self.message = message
|
|
217
|
+
self.data = data
|
|
218
|
+
super().__init__(f"JSON-RPC Error {code}: {message}")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def make_request(method: str, params: Dict[str, Any], request_id: int) -> bytes:
|
|
222
|
+
"""Create a JSON-RPC request."""
|
|
223
|
+
request = {
|
|
224
|
+
"jsonrpc": "2.0",
|
|
225
|
+
"id": request_id,
|
|
226
|
+
"method": method,
|
|
227
|
+
"params": params,
|
|
228
|
+
}
|
|
229
|
+
return json.dumps(request).encode("utf-8") + b"\n"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def make_response(request_id: int, result: Any) -> bytes:
|
|
233
|
+
"""Create a JSON-RPC response."""
|
|
234
|
+
response = {
|
|
235
|
+
"jsonrpc": "2.0",
|
|
236
|
+
"id": request_id,
|
|
237
|
+
"result": result,
|
|
238
|
+
}
|
|
239
|
+
return json.dumps(response).encode("utf-8") + b"\n"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def parse_message(line: bytes) -> Optional[Dict[str, Any]]:
|
|
243
|
+
"""Parse a JSON-RPC message from a line."""
|
|
244
|
+
try:
|
|
245
|
+
text = line.decode("utf-8").strip()
|
|
246
|
+
if not text:
|
|
247
|
+
return None
|
|
248
|
+
return json.loads(text)
|
|
249
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ============================================================================
|
|
254
|
+
# AGENT STREAM CLIENT
|
|
255
|
+
# ============================================================================
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class AgentStreamClient:
|
|
259
|
+
"""
|
|
260
|
+
Real-time streaming client for ACP-compatible agents.
|
|
261
|
+
|
|
262
|
+
Spawns agent subprocess and streams JSON-RPC messages for live updates.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
PROTOCOL_VERSION = 1
|
|
266
|
+
|
|
267
|
+
def __init__(
|
|
268
|
+
self,
|
|
269
|
+
project_root: Path,
|
|
270
|
+
agent_command: str,
|
|
271
|
+
on_event: Optional[Callable[[StreamEvent], None]] = None,
|
|
272
|
+
):
|
|
273
|
+
self.project_root = project_root
|
|
274
|
+
self.agent_command = agent_command
|
|
275
|
+
self.on_event = on_event
|
|
276
|
+
|
|
277
|
+
self._process: Optional[asyncio.subprocess.Process] = None
|
|
278
|
+
self._request_id = 0
|
|
279
|
+
self._pending_requests: Dict[int, asyncio.Future] = {}
|
|
280
|
+
self._session_id: str = ""
|
|
281
|
+
self._tool_calls: Dict[str, StreamToolCall] = {}
|
|
282
|
+
self._current_message: str = ""
|
|
283
|
+
self._running = False
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def is_running(self) -> bool:
|
|
287
|
+
return self._running and self._process is not None
|
|
288
|
+
|
|
289
|
+
def _next_request_id(self) -> int:
|
|
290
|
+
self._request_id += 1
|
|
291
|
+
return self._request_id
|
|
292
|
+
|
|
293
|
+
def _emit(self, event_type: StreamEventType, data: Any):
|
|
294
|
+
"""Emit a streaming event."""
|
|
295
|
+
event = StreamEvent(event_type=event_type, data=data)
|
|
296
|
+
if self.on_event:
|
|
297
|
+
self.on_event(event)
|
|
298
|
+
|
|
299
|
+
async def start(self) -> bool:
|
|
300
|
+
"""Start the agent subprocess."""
|
|
301
|
+
if self._process is not None:
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
env = os.environ.copy()
|
|
305
|
+
env["SUPERQODE_CWD"] = str(self.project_root.absolute())
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
self._process = await asyncio.create_subprocess_shell(
|
|
309
|
+
self.agent_command,
|
|
310
|
+
stdin=asyncio.subprocess.PIPE,
|
|
311
|
+
stdout=asyncio.subprocess.PIPE,
|
|
312
|
+
stderr=asyncio.subprocess.PIPE,
|
|
313
|
+
env=env,
|
|
314
|
+
cwd=str(self.project_root),
|
|
315
|
+
limit=10 * 1024 * 1024, # 10MB buffer
|
|
316
|
+
)
|
|
317
|
+
self._running = True
|
|
318
|
+
|
|
319
|
+
# Start reading stdout in background
|
|
320
|
+
asyncio.create_task(self._read_loop())
|
|
321
|
+
|
|
322
|
+
# Initialize ACP
|
|
323
|
+
await self._initialize()
|
|
324
|
+
await self._new_session()
|
|
325
|
+
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
except Exception as e:
|
|
329
|
+
self._emit(StreamEventType.ERROR, str(e))
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
async def stop(self):
|
|
333
|
+
"""Stop the agent subprocess."""
|
|
334
|
+
self._running = False
|
|
335
|
+
if self._process:
|
|
336
|
+
self._process.terminate()
|
|
337
|
+
try:
|
|
338
|
+
await asyncio.wait_for(self._process.wait(), timeout=5.0)
|
|
339
|
+
except asyncio.TimeoutError:
|
|
340
|
+
self._process.kill()
|
|
341
|
+
self._process = None
|
|
342
|
+
|
|
343
|
+
async def _send(self, method: str, params: Dict[str, Any]) -> asyncio.Future:
|
|
344
|
+
"""Send a JSON-RPC request and return a future for the response."""
|
|
345
|
+
if not self._process or not self._process.stdin:
|
|
346
|
+
raise RuntimeError("Agent not started")
|
|
347
|
+
|
|
348
|
+
request_id = self._next_request_id()
|
|
349
|
+
future: asyncio.Future = asyncio.Future()
|
|
350
|
+
self._pending_requests[request_id] = future
|
|
351
|
+
|
|
352
|
+
request = make_request(method, params, request_id)
|
|
353
|
+
self._process.stdin.write(request)
|
|
354
|
+
await self._process.stdin.drain()
|
|
355
|
+
|
|
356
|
+
return future
|
|
357
|
+
|
|
358
|
+
async def _respond(self, request_id: int, result: Any):
|
|
359
|
+
"""Send a JSON-RPC response."""
|
|
360
|
+
if not self._process or not self._process.stdin:
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
response = make_response(request_id, result)
|
|
364
|
+
self._process.stdin.write(response)
|
|
365
|
+
await self._process.stdin.drain()
|
|
366
|
+
|
|
367
|
+
async def _read_loop(self):
|
|
368
|
+
"""Read and process messages from agent stdout."""
|
|
369
|
+
if not self._process or not self._process.stdout:
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
while self._running:
|
|
373
|
+
try:
|
|
374
|
+
line = await self._process.stdout.readline()
|
|
375
|
+
if not line:
|
|
376
|
+
break
|
|
377
|
+
|
|
378
|
+
msg = parse_message(line)
|
|
379
|
+
if msg:
|
|
380
|
+
await self._handle_message(msg)
|
|
381
|
+
|
|
382
|
+
except Exception as e:
|
|
383
|
+
self._emit(StreamEventType.ERROR, f"Read error: {e}")
|
|
384
|
+
break
|
|
385
|
+
|
|
386
|
+
self._running = False
|
|
387
|
+
self._emit(StreamEventType.COMPLETE, None)
|
|
388
|
+
|
|
389
|
+
async def _handle_message(self, msg: Dict[str, Any]):
|
|
390
|
+
"""Handle an incoming JSON-RPC message."""
|
|
391
|
+
# Check if it's a response to a pending request
|
|
392
|
+
if "result" in msg or "error" in msg:
|
|
393
|
+
request_id = msg.get("id")
|
|
394
|
+
if request_id and request_id in self._pending_requests:
|
|
395
|
+
future = self._pending_requests.pop(request_id)
|
|
396
|
+
if "error" in msg:
|
|
397
|
+
err = msg["error"]
|
|
398
|
+
future.set_exception(
|
|
399
|
+
JSONRPCError(
|
|
400
|
+
err.get("code", -1),
|
|
401
|
+
err.get("message", "Unknown error"),
|
|
402
|
+
err.get("data"),
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
else:
|
|
406
|
+
future.set_result(msg.get("result"))
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
# It's a notification or request from the agent
|
|
410
|
+
method = msg.get("method", "")
|
|
411
|
+
params = msg.get("params", {})
|
|
412
|
+
request_id = msg.get("id")
|
|
413
|
+
|
|
414
|
+
if method == "session/update":
|
|
415
|
+
await self._handle_session_update(params, request_id)
|
|
416
|
+
elif method == "session/request_permission":
|
|
417
|
+
await self._handle_permission_request(params, request_id)
|
|
418
|
+
elif method == "fs/read_text_file":
|
|
419
|
+
await self._handle_read_file(params, request_id)
|
|
420
|
+
elif method == "fs/write_text_file":
|
|
421
|
+
await self._handle_write_file(params, request_id)
|
|
422
|
+
elif method == "terminal/create":
|
|
423
|
+
await self._handle_terminal_create(params, request_id)
|
|
424
|
+
elif method == "terminal/output":
|
|
425
|
+
await self._handle_terminal_output(params, request_id)
|
|
426
|
+
elif method == "terminal/kill":
|
|
427
|
+
await self._handle_terminal_kill(params, request_id)
|
|
428
|
+
|
|
429
|
+
async def _handle_session_update(self, params: Dict[str, Any], request_id: Optional[int]):
|
|
430
|
+
"""Handle session/update notifications."""
|
|
431
|
+
update = params.get("update", {})
|
|
432
|
+
update_type = update.get("sessionUpdate", "")
|
|
433
|
+
|
|
434
|
+
if update_type == "agent_message_chunk":
|
|
435
|
+
content = update.get("content", {})
|
|
436
|
+
text = content.get("text", "")
|
|
437
|
+
self._current_message += text
|
|
438
|
+
self._emit(StreamEventType.MESSAGE_CHUNK, StreamMessage(text=text))
|
|
439
|
+
|
|
440
|
+
elif update_type == "agent_thought_chunk":
|
|
441
|
+
content = update.get("content", {})
|
|
442
|
+
text = content.get("text", "")
|
|
443
|
+
self._emit(StreamEventType.THOUGHT_CHUNK, StreamThought(text=text))
|
|
444
|
+
|
|
445
|
+
elif update_type == "tool_call":
|
|
446
|
+
tool_call = self._parse_tool_call(update)
|
|
447
|
+
self._tool_calls[tool_call.tool_id] = tool_call
|
|
448
|
+
self._emit(StreamEventType.TOOL_CALL, tool_call)
|
|
449
|
+
|
|
450
|
+
elif update_type == "tool_call_update":
|
|
451
|
+
tool_id = update.get("toolCallId", "")
|
|
452
|
+
if tool_id in self._tool_calls:
|
|
453
|
+
tool_call = self._tool_calls[tool_id]
|
|
454
|
+
self._update_tool_call(tool_call, update)
|
|
455
|
+
self._emit(StreamEventType.TOOL_UPDATE, tool_call)
|
|
456
|
+
else:
|
|
457
|
+
# Create new tool call from update
|
|
458
|
+
tool_call = self._parse_tool_call(update)
|
|
459
|
+
self._tool_calls[tool_id] = tool_call
|
|
460
|
+
self._emit(StreamEventType.TOOL_CALL, tool_call)
|
|
461
|
+
|
|
462
|
+
elif update_type == "plan":
|
|
463
|
+
entries = update.get("entries", [])
|
|
464
|
+
plan = StreamPlan(
|
|
465
|
+
tasks=[
|
|
466
|
+
PlanTask(
|
|
467
|
+
content=e.get("content", ""),
|
|
468
|
+
status=TaskStatus(e.get("status", "pending")),
|
|
469
|
+
priority=TaskPriority(e.get("priority", "medium")),
|
|
470
|
+
)
|
|
471
|
+
for e in entries
|
|
472
|
+
]
|
|
473
|
+
)
|
|
474
|
+
self._emit(StreamEventType.PLAN, plan)
|
|
475
|
+
|
|
476
|
+
def _parse_tool_call(self, data: Dict[str, Any]) -> StreamToolCall:
|
|
477
|
+
"""Parse a tool call from JSON data."""
|
|
478
|
+
kind_str = data.get("kind", "other")
|
|
479
|
+
try:
|
|
480
|
+
kind = ToolKind(kind_str)
|
|
481
|
+
except ValueError:
|
|
482
|
+
kind = ToolKind.OTHER
|
|
483
|
+
|
|
484
|
+
status_str = data.get("status", "pending")
|
|
485
|
+
try:
|
|
486
|
+
status = ToolStatus(status_str)
|
|
487
|
+
except ValueError:
|
|
488
|
+
status = ToolStatus.PENDING
|
|
489
|
+
|
|
490
|
+
content = []
|
|
491
|
+
for c in data.get("content", []):
|
|
492
|
+
content.append(ToolCallContent(type=c.get("type", "content"), data=c))
|
|
493
|
+
|
|
494
|
+
return StreamToolCall(
|
|
495
|
+
tool_id=data.get("toolCallId", ""),
|
|
496
|
+
title=data.get("title", "Tool Call"),
|
|
497
|
+
kind=kind,
|
|
498
|
+
status=status,
|
|
499
|
+
content=content,
|
|
500
|
+
locations=data.get("locations", []),
|
|
501
|
+
raw_input=data.get("rawInput", {}),
|
|
502
|
+
raw_output=data.get("rawOutput", {}),
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
def _update_tool_call(self, tool_call: StreamToolCall, update: Dict[str, Any]):
|
|
506
|
+
"""Update a tool call with new data."""
|
|
507
|
+
if "title" in update and update["title"]:
|
|
508
|
+
tool_call.title = update["title"]
|
|
509
|
+
if "kind" in update and update["kind"]:
|
|
510
|
+
try:
|
|
511
|
+
tool_call.kind = ToolKind(update["kind"])
|
|
512
|
+
except ValueError:
|
|
513
|
+
pass
|
|
514
|
+
if "status" in update and update["status"]:
|
|
515
|
+
try:
|
|
516
|
+
tool_call.status = ToolStatus(update["status"])
|
|
517
|
+
except ValueError:
|
|
518
|
+
pass
|
|
519
|
+
if "content" in update and update["content"]:
|
|
520
|
+
tool_call.content = [
|
|
521
|
+
ToolCallContent(type=c.get("type", "content"), data=c) for c in update["content"]
|
|
522
|
+
]
|
|
523
|
+
if "locations" in update:
|
|
524
|
+
tool_call.locations = update["locations"]
|
|
525
|
+
if "rawInput" in update:
|
|
526
|
+
tool_call.raw_input = update["rawInput"]
|
|
527
|
+
if "rawOutput" in update:
|
|
528
|
+
tool_call.raw_output = update["rawOutput"]
|
|
529
|
+
|
|
530
|
+
async def _handle_permission_request(self, params: Dict[str, Any], request_id: Optional[int]):
|
|
531
|
+
"""Handle permission request from agent."""
|
|
532
|
+
options_data = params.get("options", [])
|
|
533
|
+
tool_call_data = params.get("toolCall", {})
|
|
534
|
+
|
|
535
|
+
options = [
|
|
536
|
+
PermissionOption(
|
|
537
|
+
option_id=o.get("optionId", ""),
|
|
538
|
+
name=o.get("name", ""),
|
|
539
|
+
kind=o.get("kind", "allow_once"),
|
|
540
|
+
)
|
|
541
|
+
for o in options_data
|
|
542
|
+
]
|
|
543
|
+
|
|
544
|
+
tool_call = self._parse_tool_call(tool_call_data)
|
|
545
|
+
|
|
546
|
+
# Create future for response
|
|
547
|
+
result_future: asyncio.Future = asyncio.Future()
|
|
548
|
+
|
|
549
|
+
permission = StreamPermission(
|
|
550
|
+
tool_call=tool_call,
|
|
551
|
+
options=options,
|
|
552
|
+
result_future=result_future,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
self._emit(StreamEventType.PERMISSION, permission)
|
|
556
|
+
|
|
557
|
+
# Wait for user response
|
|
558
|
+
try:
|
|
559
|
+
selected_option_id = await asyncio.wait_for(result_future, timeout=300)
|
|
560
|
+
|
|
561
|
+
if request_id is not None:
|
|
562
|
+
await self._respond(
|
|
563
|
+
request_id,
|
|
564
|
+
{
|
|
565
|
+
"outcome": {
|
|
566
|
+
"outcome": "selected",
|
|
567
|
+
"optionId": selected_option_id,
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
)
|
|
571
|
+
except asyncio.TimeoutError:
|
|
572
|
+
if request_id is not None:
|
|
573
|
+
await self._respond(request_id, {"outcome": {"outcome": "cancelled"}})
|
|
574
|
+
|
|
575
|
+
async def _handle_read_file(self, params: Dict[str, Any], request_id: Optional[int]):
|
|
576
|
+
"""Handle file read request from agent."""
|
|
577
|
+
path = params.get("path", "")
|
|
578
|
+
line = params.get("line")
|
|
579
|
+
limit = params.get("limit")
|
|
580
|
+
|
|
581
|
+
read_path = self.project_root / path
|
|
582
|
+
try:
|
|
583
|
+
text = read_path.read_text(encoding="utf-8", errors="ignore")
|
|
584
|
+
if line is not None:
|
|
585
|
+
line = max(0, line - 1)
|
|
586
|
+
lines = text.splitlines()
|
|
587
|
+
if limit is None:
|
|
588
|
+
text = "\n".join(lines[line:])
|
|
589
|
+
else:
|
|
590
|
+
text = "\n".join(lines[line : line + limit])
|
|
591
|
+
except IOError:
|
|
592
|
+
text = ""
|
|
593
|
+
|
|
594
|
+
if request_id is not None:
|
|
595
|
+
await self._respond(request_id, {"content": text})
|
|
596
|
+
|
|
597
|
+
async def _handle_write_file(self, params: Dict[str, Any], request_id: Optional[int]):
|
|
598
|
+
"""Handle file write request from agent."""
|
|
599
|
+
path = params.get("path", "")
|
|
600
|
+
content = params.get("content", "")
|
|
601
|
+
|
|
602
|
+
write_path = self.project_root / path
|
|
603
|
+
write_path.parent.mkdir(parents=True, exist_ok=True)
|
|
604
|
+
write_path.write_text(content, encoding="utf-8")
|
|
605
|
+
|
|
606
|
+
if request_id is not None:
|
|
607
|
+
await self._respond(request_id, {})
|
|
608
|
+
|
|
609
|
+
async def _handle_terminal_create(self, params: Dict[str, Any], request_id: Optional[int]):
|
|
610
|
+
"""Handle terminal create request."""
|
|
611
|
+
# For now, just acknowledge - full terminal support can be added later
|
|
612
|
+
terminal_id = f"terminal-{self._next_request_id()}"
|
|
613
|
+
if request_id is not None:
|
|
614
|
+
await self._respond(request_id, {"terminalId": terminal_id})
|
|
615
|
+
|
|
616
|
+
async def _handle_terminal_output(self, params: Dict[str, Any], request_id: Optional[int]):
|
|
617
|
+
"""Handle terminal output request."""
|
|
618
|
+
if request_id is not None:
|
|
619
|
+
await self._respond(
|
|
620
|
+
request_id,
|
|
621
|
+
{
|
|
622
|
+
"output": "",
|
|
623
|
+
"truncated": False,
|
|
624
|
+
},
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
async def _handle_terminal_kill(self, params: Dict[str, Any], request_id: Optional[int]):
|
|
628
|
+
"""Handle terminal kill request."""
|
|
629
|
+
if request_id is not None:
|
|
630
|
+
await self._respond(request_id, {})
|
|
631
|
+
|
|
632
|
+
async def _initialize(self):
|
|
633
|
+
"""Initialize ACP protocol."""
|
|
634
|
+
future = await self._send(
|
|
635
|
+
"initialize",
|
|
636
|
+
{
|
|
637
|
+
"protocolVersion": self.PROTOCOL_VERSION,
|
|
638
|
+
"clientCapabilities": {
|
|
639
|
+
"fs": {"readTextFile": True, "writeTextFile": True},
|
|
640
|
+
"terminal": True,
|
|
641
|
+
},
|
|
642
|
+
"clientInfo": {
|
|
643
|
+
"name": "SuperQode",
|
|
644
|
+
"title": "SuperQode - Multi-Agent Coding Team",
|
|
645
|
+
"version": "1.0.0",
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
try:
|
|
651
|
+
result = await asyncio.wait_for(future, timeout=30)
|
|
652
|
+
return result
|
|
653
|
+
except asyncio.TimeoutError:
|
|
654
|
+
raise RuntimeError("Agent initialization timed out")
|
|
655
|
+
|
|
656
|
+
async def _new_session(self):
|
|
657
|
+
"""Create a new ACP session."""
|
|
658
|
+
future = await self._send(
|
|
659
|
+
"session/new",
|
|
660
|
+
{
|
|
661
|
+
"projectRoot": str(self.project_root),
|
|
662
|
+
"mcpServers": [],
|
|
663
|
+
},
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
try:
|
|
667
|
+
result = await asyncio.wait_for(future, timeout=30)
|
|
668
|
+
self._session_id = result.get("sessionId", "")
|
|
669
|
+
return result
|
|
670
|
+
except asyncio.TimeoutError:
|
|
671
|
+
raise RuntimeError("Session creation timed out")
|
|
672
|
+
|
|
673
|
+
async def send_prompt(self, prompt: str) -> Optional[str]:
|
|
674
|
+
"""Send a prompt to the agent and stream the response."""
|
|
675
|
+
self._current_message = ""
|
|
676
|
+
|
|
677
|
+
future = await self._send(
|
|
678
|
+
"session/prompt",
|
|
679
|
+
{
|
|
680
|
+
"sessionId": self._session_id,
|
|
681
|
+
"content": [{"type": "text", "text": prompt}],
|
|
682
|
+
},
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
try:
|
|
686
|
+
result = await future
|
|
687
|
+
# Mark message as complete
|
|
688
|
+
self._emit(StreamEventType.MESSAGE_CHUNK, StreamMessage(text="", is_complete=True))
|
|
689
|
+
return result.get("stopReason")
|
|
690
|
+
except JSONRPCError as e:
|
|
691
|
+
self._emit(StreamEventType.ERROR, str(e))
|
|
692
|
+
return None
|
|
693
|
+
|
|
694
|
+
async def cancel(self) -> bool:
|
|
695
|
+
"""Cancel the current operation."""
|
|
696
|
+
try:
|
|
697
|
+
future = await self._send(
|
|
698
|
+
"session/cancel",
|
|
699
|
+
{
|
|
700
|
+
"sessionId": self._session_id,
|
|
701
|
+
"options": {},
|
|
702
|
+
},
|
|
703
|
+
)
|
|
704
|
+
await asyncio.wait_for(future, timeout=5)
|
|
705
|
+
return True
|
|
706
|
+
except Exception:
|
|
707
|
+
return False
|
|
708
|
+
|
|
709
|
+
async def reset_session(self) -> bool:
|
|
710
|
+
"""
|
|
711
|
+
Reset the session (e.g., after model change).
|
|
712
|
+
|
|
713
|
+
Creates a new session without restarting the agent process.
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
True if reset was successful, False otherwise.
|
|
717
|
+
"""
|
|
718
|
+
try:
|
|
719
|
+
# Cancel any pending operations
|
|
720
|
+
await self.cancel()
|
|
721
|
+
|
|
722
|
+
# Clear internal state
|
|
723
|
+
self._tool_calls.clear()
|
|
724
|
+
self._current_message = ""
|
|
725
|
+
self._pending_requests.clear()
|
|
726
|
+
|
|
727
|
+
# Create new session
|
|
728
|
+
await self._new_session()
|
|
729
|
+
|
|
730
|
+
return True
|
|
731
|
+
except Exception as e:
|
|
732
|
+
self._emit(StreamEventType.ERROR, f"Session reset failed: {e}")
|
|
733
|
+
return False
|
|
734
|
+
|
|
735
|
+
async def switch_agent(self, new_command: str) -> bool:
|
|
736
|
+
"""
|
|
737
|
+
Switch to a different agent command.
|
|
738
|
+
|
|
739
|
+
Stops the current agent and starts a new one with the given command.
|
|
740
|
+
|
|
741
|
+
Args:
|
|
742
|
+
new_command: The new agent command to run.
|
|
743
|
+
|
|
744
|
+
Returns:
|
|
745
|
+
True if switch was successful, False otherwise.
|
|
746
|
+
"""
|
|
747
|
+
try:
|
|
748
|
+
# Stop current agent
|
|
749
|
+
await self.stop()
|
|
750
|
+
|
|
751
|
+
# Update command
|
|
752
|
+
self.agent_command = new_command
|
|
753
|
+
|
|
754
|
+
# Clear state
|
|
755
|
+
self._tool_calls.clear()
|
|
756
|
+
self._current_message = ""
|
|
757
|
+
self._pending_requests.clear()
|
|
758
|
+
self._session_id = ""
|
|
759
|
+
self._request_id = 0
|
|
760
|
+
|
|
761
|
+
# Start fresh
|
|
762
|
+
return await self.start()
|
|
763
|
+
|
|
764
|
+
except Exception as e:
|
|
765
|
+
self._emit(StreamEventType.ERROR, f"Agent switch failed: {e}")
|
|
766
|
+
return False
|
|
767
|
+
|
|
768
|
+
def get_session_id(self) -> str:
|
|
769
|
+
"""Get the current session ID."""
|
|
770
|
+
return self._session_id
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
# ============================================================================
|
|
774
|
+
# SIMPLE STREAMING CLIENT (for non-ACP agents like basic OpenCode)
|
|
775
|
+
# ============================================================================
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
class SimpleStreamClient:
|
|
779
|
+
"""
|
|
780
|
+
Simple streaming client for agents that output plain text.
|
|
781
|
+
|
|
782
|
+
Reads stdout line by line and emits message events.
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
def __init__(
|
|
786
|
+
self,
|
|
787
|
+
project_root: Path,
|
|
788
|
+
command: List[str],
|
|
789
|
+
on_event: Optional[Callable[[StreamEvent], None]] = None,
|
|
790
|
+
):
|
|
791
|
+
self.project_root = project_root
|
|
792
|
+
self.command = command
|
|
793
|
+
self.on_event = on_event
|
|
794
|
+
|
|
795
|
+
self._process: Optional[asyncio.subprocess.Process] = None
|
|
796
|
+
self._running = False
|
|
797
|
+
|
|
798
|
+
@property
|
|
799
|
+
def is_running(self) -> bool:
|
|
800
|
+
return self._running and self._process is not None
|
|
801
|
+
|
|
802
|
+
def _emit(self, event_type: StreamEventType, data: Any):
|
|
803
|
+
"""Emit a streaming event."""
|
|
804
|
+
event = StreamEvent(event_type=event_type, data=data)
|
|
805
|
+
if self.on_event:
|
|
806
|
+
self.on_event(event)
|
|
807
|
+
|
|
808
|
+
async def start(self) -> bool:
|
|
809
|
+
"""Start the subprocess."""
|
|
810
|
+
if self._process is not None:
|
|
811
|
+
return True
|
|
812
|
+
|
|
813
|
+
try:
|
|
814
|
+
self._process = await asyncio.create_subprocess_exec(
|
|
815
|
+
*self.command,
|
|
816
|
+
stdin=asyncio.subprocess.PIPE,
|
|
817
|
+
stdout=asyncio.subprocess.PIPE,
|
|
818
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
819
|
+
cwd=str(self.project_root),
|
|
820
|
+
)
|
|
821
|
+
self._running = True
|
|
822
|
+
return True
|
|
823
|
+
except Exception as e:
|
|
824
|
+
self._emit(StreamEventType.ERROR, str(e))
|
|
825
|
+
return False
|
|
826
|
+
|
|
827
|
+
async def stop(self):
|
|
828
|
+
"""Stop the subprocess."""
|
|
829
|
+
self._running = False
|
|
830
|
+
if self._process:
|
|
831
|
+
self._process.terminate()
|
|
832
|
+
try:
|
|
833
|
+
await asyncio.wait_for(self._process.wait(), timeout=5.0)
|
|
834
|
+
except asyncio.TimeoutError:
|
|
835
|
+
self._process.kill()
|
|
836
|
+
self._process = None
|
|
837
|
+
|
|
838
|
+
async def run_and_stream(self) -> int:
|
|
839
|
+
"""Run the command and stream output."""
|
|
840
|
+
if not self._process or not self._process.stdout:
|
|
841
|
+
return -1
|
|
842
|
+
|
|
843
|
+
buffer = ""
|
|
844
|
+
while self._running:
|
|
845
|
+
try:
|
|
846
|
+
chunk = await self._process.stdout.read(256)
|
|
847
|
+
if not chunk:
|
|
848
|
+
break
|
|
849
|
+
|
|
850
|
+
text = chunk.decode("utf-8", errors="replace")
|
|
851
|
+
buffer += text
|
|
852
|
+
|
|
853
|
+
# Emit line by line for cleaner output
|
|
854
|
+
while "\n" in buffer:
|
|
855
|
+
line, buffer = buffer.split("\n", 1)
|
|
856
|
+
self._emit(StreamEventType.MESSAGE_CHUNK, StreamMessage(text=line + "\n"))
|
|
857
|
+
|
|
858
|
+
except Exception as e:
|
|
859
|
+
self._emit(StreamEventType.ERROR, str(e))
|
|
860
|
+
break
|
|
861
|
+
|
|
862
|
+
# Emit remaining buffer
|
|
863
|
+
if buffer:
|
|
864
|
+
self._emit(StreamEventType.MESSAGE_CHUNK, StreamMessage(text=buffer))
|
|
865
|
+
|
|
866
|
+
# Wait for process to complete
|
|
867
|
+
if self._process:
|
|
868
|
+
await self._process.wait()
|
|
869
|
+
return_code = self._process.returncode or 0
|
|
870
|
+
else:
|
|
871
|
+
return_code = -1
|
|
872
|
+
|
|
873
|
+
self._emit(StreamEventType.MESSAGE_CHUNK, StreamMessage(text="", is_complete=True))
|
|
874
|
+
self._emit(StreamEventType.COMPLETE, return_code)
|
|
875
|
+
|
|
876
|
+
return return_code
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
# ============================================================================
|
|
880
|
+
# RENDERING HELPERS (SuperQode colorful style)
|
|
881
|
+
# ============================================================================
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def get_tool_icon(kind: ToolKind) -> str:
|
|
885
|
+
"""Get icon for tool kind."""
|
|
886
|
+
icons = {
|
|
887
|
+
ToolKind.READ: "📖",
|
|
888
|
+
ToolKind.EDIT: "✏️",
|
|
889
|
+
ToolKind.DELETE: "🗑️",
|
|
890
|
+
ToolKind.MOVE: "📦",
|
|
891
|
+
ToolKind.SEARCH: "🔍",
|
|
892
|
+
ToolKind.EXECUTE: "⚡",
|
|
893
|
+
ToolKind.THINK: "🧠",
|
|
894
|
+
ToolKind.FETCH: "🌐",
|
|
895
|
+
ToolKind.SWITCH_MODE: "🔄",
|
|
896
|
+
ToolKind.OTHER: "🔧",
|
|
897
|
+
}
|
|
898
|
+
return icons.get(kind, "🔧")
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def get_status_icon(status: ToolStatus) -> str:
|
|
902
|
+
"""Get icon for tool status."""
|
|
903
|
+
icons = {
|
|
904
|
+
ToolStatus.PENDING: "⏳",
|
|
905
|
+
ToolStatus.IN_PROGRESS: "🔄",
|
|
906
|
+
ToolStatus.COMPLETED: "✅",
|
|
907
|
+
ToolStatus.FAILED: "❌",
|
|
908
|
+
}
|
|
909
|
+
return icons.get(status, "○")
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def get_status_color(status: ToolStatus) -> str:
|
|
913
|
+
"""Get color for tool status."""
|
|
914
|
+
colors = {
|
|
915
|
+
ToolStatus.PENDING: STREAM_COLORS["pending"],
|
|
916
|
+
ToolStatus.IN_PROGRESS: STREAM_COLORS["progress"],
|
|
917
|
+
ToolStatus.COMPLETED: STREAM_COLORS["success"],
|
|
918
|
+
ToolStatus.FAILED: STREAM_COLORS["error"],
|
|
919
|
+
}
|
|
920
|
+
return colors.get(status, STREAM_COLORS["pending"])
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def get_task_icon(status: TaskStatus) -> str:
|
|
924
|
+
"""Get icon for task status."""
|
|
925
|
+
icons = {
|
|
926
|
+
TaskStatus.PENDING: "○",
|
|
927
|
+
TaskStatus.IN_PROGRESS: "●",
|
|
928
|
+
TaskStatus.COMPLETED: "✓",
|
|
929
|
+
}
|
|
930
|
+
return icons.get(status, "○")
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def get_task_color(status: TaskStatus) -> str:
|
|
934
|
+
"""Get color for task status."""
|
|
935
|
+
colors = {
|
|
936
|
+
TaskStatus.PENDING: STREAM_COLORS["pending"],
|
|
937
|
+
TaskStatus.IN_PROGRESS: STREAM_COLORS["progress"],
|
|
938
|
+
TaskStatus.COMPLETED: STREAM_COLORS["success"],
|
|
939
|
+
}
|
|
940
|
+
return colors.get(status, STREAM_COLORS["pending"])
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def format_tool_call_title(tool_call: StreamToolCall) -> str:
|
|
944
|
+
"""Format tool call title with icon."""
|
|
945
|
+
icon = get_tool_icon(tool_call.kind)
|
|
946
|
+
status_icon = get_status_icon(tool_call.status)
|
|
947
|
+
return f"{status_icon} {icon} {tool_call.title}"
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def format_plan_task(task: PlanTask, index: int) -> str:
|
|
951
|
+
"""Format a plan task."""
|
|
952
|
+
icon = get_task_icon(task.status)
|
|
953
|
+
return f" {icon} {index}. {task.content}"
|