netra-zen 1.0.8__py3-none-any.whl → 1.0.9__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.
- agent_interface/__init__.py +25 -25
- agent_interface/base_agent.py +350 -350
- {netra_zen-1.0.8.dist-info → netra_zen-1.0.9.dist-info}/METADATA +971 -971
- {netra_zen-1.0.8.dist-info → netra_zen-1.0.9.dist-info}/RECORD +15 -15
- scripts/agent_cli.py +643 -10
- scripts/bump_version.py +137 -137
- token_budget/budget_manager.py +199 -199
- token_budget/models.py +73 -73
- token_budget/visualization.py +21 -21
- token_transparency/__init__.py +19 -19
- token_transparency/claude_pricing_engine.py +326 -326
- {netra_zen-1.0.8.dist-info → netra_zen-1.0.9.dist-info}/WHEEL +0 -0
- {netra_zen-1.0.8.dist-info → netra_zen-1.0.9.dist-info}/entry_points.txt +0 -0
- {netra_zen-1.0.8.dist-info → netra_zen-1.0.9.dist-info}/licenses/LICENSE.md +0 -0
- {netra_zen-1.0.8.dist-info → netra_zen-1.0.9.dist-info}/top_level.txt +0 -0
agent_interface/base_agent.py
CHANGED
@@ -1,351 +1,351 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
Base Agent Interface for Extensible Coding Agent Support
|
4
|
-
|
5
|
-
This module provides the foundation for supporting multiple coding agents
|
6
|
-
beyond Claude Code, enabling zen to orchestrate various AI coding assistants.
|
7
|
-
|
8
|
-
Design Goals:
|
9
|
-
- Simple interface for easy integration
|
10
|
-
- Consistent token/cost tracking across agents
|
11
|
-
- Minimal overhead for basic agents
|
12
|
-
- Extensible for complex agent features
|
13
|
-
"""
|
14
|
-
|
15
|
-
from abc import ABC, abstractmethod
|
16
|
-
from dataclasses import dataclass
|
17
|
-
from typing import Dict, List, Optional, Any, AsyncIterator
|
18
|
-
from pathlib import Path
|
19
|
-
import subprocess
|
20
|
-
import asyncio
|
21
|
-
|
22
|
-
|
23
|
-
@dataclass
|
24
|
-
class AgentConfig:
|
25
|
-
"""Configuration for any coding agent"""
|
26
|
-
name: str
|
27
|
-
command: str
|
28
|
-
description: Optional[str] = None
|
29
|
-
allowed_tools: List[str] = None
|
30
|
-
output_format: str = "text"
|
31
|
-
workspace_dir: Optional[Path] = None
|
32
|
-
session_id: Optional[str] = None
|
33
|
-
environment_vars: Dict[str, str] = None
|
34
|
-
timeout: int = 300
|
35
|
-
|
36
|
-
def __post_init__(self):
|
37
|
-
if self.description is None:
|
38
|
-
self.description = f"Execute {self.command}"
|
39
|
-
if self.environment_vars is None:
|
40
|
-
self.environment_vars = {}
|
41
|
-
|
42
|
-
|
43
|
-
@dataclass
|
44
|
-
class AgentUsageMetrics:
|
45
|
-
"""Standardized usage metrics across all agents"""
|
46
|
-
total_tokens: int = 0
|
47
|
-
input_tokens: int = 0
|
48
|
-
output_tokens: int = 0
|
49
|
-
cached_tokens: int = 0
|
50
|
-
tool_calls: int = 0
|
51
|
-
api_calls: int = 0
|
52
|
-
total_cost_usd: Optional[float] = None
|
53
|
-
model_used: str = "unknown"
|
54
|
-
|
55
|
-
# Agent-specific metrics (extensible)
|
56
|
-
agent_specific: Dict[str, Any] = None
|
57
|
-
|
58
|
-
def __post_init__(self):
|
59
|
-
if self.agent_specific is None:
|
60
|
-
self.agent_specific = {}
|
61
|
-
|
62
|
-
|
63
|
-
@dataclass
|
64
|
-
class AgentStatus:
|
65
|
-
"""Execution status for any coding agent"""
|
66
|
-
name: str
|
67
|
-
status: str = "pending" # pending, running, completed, failed
|
68
|
-
start_time: Optional[float] = None
|
69
|
-
end_time: Optional[float] = None
|
70
|
-
pid: Optional[int] = None
|
71
|
-
output: str = ""
|
72
|
-
error: str = ""
|
73
|
-
metrics: AgentUsageMetrics = None
|
74
|
-
|
75
|
-
def __post_init__(self):
|
76
|
-
if self.metrics is None:
|
77
|
-
self.metrics = AgentUsageMetrics()
|
78
|
-
|
79
|
-
|
80
|
-
class BaseCodingAgent(ABC):
|
81
|
-
"""
|
82
|
-
Abstract base class for all coding agents.
|
83
|
-
|
84
|
-
Provides the interface that all coding agents must implement
|
85
|
-
to be compatible with the zen orchestrator.
|
86
|
-
"""
|
87
|
-
|
88
|
-
def __init__(self, config: AgentConfig):
|
89
|
-
self.config = config
|
90
|
-
self.status = AgentStatus(name=config.name)
|
91
|
-
|
92
|
-
@abstractmethod
|
93
|
-
def build_command(self) -> List[str]:
|
94
|
-
"""
|
95
|
-
Build the command line arguments for executing this agent.
|
96
|
-
|
97
|
-
Returns:
|
98
|
-
List of command line arguments
|
99
|
-
"""
|
100
|
-
pass
|
101
|
-
|
102
|
-
@abstractmethod
|
103
|
-
async def execute(self) -> bool:
|
104
|
-
"""
|
105
|
-
Execute the agent and return success status.
|
106
|
-
|
107
|
-
Returns:
|
108
|
-
True if execution was successful, False otherwise
|
109
|
-
"""
|
110
|
-
pass
|
111
|
-
|
112
|
-
@abstractmethod
|
113
|
-
def parse_output_line(self, line: str) -> bool:
|
114
|
-
"""
|
115
|
-
Parse a single line of output to extract metrics.
|
116
|
-
|
117
|
-
Args:
|
118
|
-
line: Single line from agent output
|
119
|
-
|
120
|
-
Returns:
|
121
|
-
True if line was parsed successfully, False otherwise
|
122
|
-
"""
|
123
|
-
pass
|
124
|
-
|
125
|
-
@abstractmethod
|
126
|
-
def calculate_cost(self) -> float:
|
127
|
-
"""
|
128
|
-
Calculate the total cost for this agent's execution.
|
129
|
-
|
130
|
-
Returns:
|
131
|
-
Total cost in USD
|
132
|
-
"""
|
133
|
-
pass
|
134
|
-
|
135
|
-
def get_agent_type(self) -> str:
|
136
|
-
"""Return the type identifier for this agent"""
|
137
|
-
return self.__class__.__name__.lower().replace('agent', '')
|
138
|
-
|
139
|
-
def get_metrics(self) -> AgentUsageMetrics:
|
140
|
-
"""Get current usage metrics"""
|
141
|
-
return self.status.metrics
|
142
|
-
|
143
|
-
def get_status(self) -> AgentStatus:
|
144
|
-
"""Get current execution status"""
|
145
|
-
return self.status
|
146
|
-
|
147
|
-
def supports_feature(self, feature: str) -> bool:
|
148
|
-
"""Check if agent supports a specific feature"""
|
149
|
-
# Override in subclasses to declare supported features
|
150
|
-
return False
|
151
|
-
|
152
|
-
|
153
|
-
class ClaudeCodeAgent(BaseCodingAgent):
|
154
|
-
"""
|
155
|
-
Claude Code agent implementation.
|
156
|
-
|
157
|
-
Provides compatibility with the existing Claude Code orchestrator
|
158
|
-
through the new agent interface.
|
159
|
-
"""
|
160
|
-
|
161
|
-
def build_command(self) -> List[str]:
|
162
|
-
"""Build Claude Code command"""
|
163
|
-
# Use existing logic from ClaudeInstanceOrchestrator.build_claude_command
|
164
|
-
cmd = ["claude", "-p", self.config.command]
|
165
|
-
|
166
|
-
if self.config.output_format and self.config.output_format != "text":
|
167
|
-
cmd.append(f"--output-format={self.config.output_format}")
|
168
|
-
|
169
|
-
if self.config.allowed_tools:
|
170
|
-
cmd.append(f"--allowedTools={','.join(self.config.allowed_tools)}")
|
171
|
-
|
172
|
-
if self.config.session_id:
|
173
|
-
cmd.extend(["--session-id", self.config.session_id])
|
174
|
-
|
175
|
-
return cmd
|
176
|
-
|
177
|
-
async def execute(self) -> bool:
|
178
|
-
"""Execute Claude Code with async process handling"""
|
179
|
-
try:
|
180
|
-
cmd = self.build_command()
|
181
|
-
self.status.start_time = asyncio.get_event_loop().time()
|
182
|
-
self.status.status = "running"
|
183
|
-
|
184
|
-
# Create async process
|
185
|
-
process = await asyncio.create_subprocess_exec(
|
186
|
-
*cmd,
|
187
|
-
stdout=asyncio.subprocess.PIPE,
|
188
|
-
stderr=asyncio.subprocess.PIPE,
|
189
|
-
cwd=self.config.workspace_dir
|
190
|
-
)
|
191
|
-
|
192
|
-
self.status.pid = process.pid
|
193
|
-
|
194
|
-
# Stream output and parse in real-time
|
195
|
-
async def read_stream(stream, is_stdout=True):
|
196
|
-
while True:
|
197
|
-
line = await stream.readline()
|
198
|
-
if not line:
|
199
|
-
break
|
200
|
-
line_str = line.decode().strip()
|
201
|
-
|
202
|
-
if is_stdout:
|
203
|
-
self.status.output += line_str + "\n"
|
204
|
-
self.parse_output_line(line_str)
|
205
|
-
else:
|
206
|
-
self.status.error += line_str + "\n"
|
207
|
-
|
208
|
-
# Read both streams concurrently
|
209
|
-
await asyncio.gather(
|
210
|
-
read_stream(process.stdout, True),
|
211
|
-
read_stream(process.stderr, False),
|
212
|
-
return_exceptions=True
|
213
|
-
)
|
214
|
-
|
215
|
-
# Wait for process completion
|
216
|
-
returncode = await process.wait()
|
217
|
-
self.status.end_time = asyncio.get_event_loop().time()
|
218
|
-
|
219
|
-
if returncode == 0:
|
220
|
-
self.status.status = "completed"
|
221
|
-
return True
|
222
|
-
else:
|
223
|
-
self.status.status = "failed"
|
224
|
-
return False
|
225
|
-
|
226
|
-
except Exception as e:
|
227
|
-
self.status.status = "failed"
|
228
|
-
self.status.error = str(e)
|
229
|
-
return False
|
230
|
-
|
231
|
-
def parse_output_line(self, line: str) -> bool:
|
232
|
-
"""Parse Claude Code JSON output"""
|
233
|
-
# Use existing logic from ClaudeInstanceOrchestrator._parse_token_usage
|
234
|
-
try:
|
235
|
-
import json
|
236
|
-
if line.startswith('{'):
|
237
|
-
data = json.loads(line)
|
238
|
-
if 'usage' in data:
|
239
|
-
usage = data['usage']
|
240
|
-
self.status.metrics.input_tokens = max(
|
241
|
-
self.status.metrics.input_tokens,
|
242
|
-
int(usage.get('input_tokens', 0))
|
243
|
-
)
|
244
|
-
self.status.metrics.output_tokens = max(
|
245
|
-
self.status.metrics.output_tokens,
|
246
|
-
int(usage.get('output_tokens', 0))
|
247
|
-
)
|
248
|
-
self.status.metrics.total_tokens = max(
|
249
|
-
self.status.metrics.total_tokens,
|
250
|
-
int(usage.get('total_tokens', 0))
|
251
|
-
)
|
252
|
-
return True
|
253
|
-
except (json.JSONDecodeError, ValueError, KeyError):
|
254
|
-
pass
|
255
|
-
return False
|
256
|
-
|
257
|
-
def calculate_cost(self) -> float:
|
258
|
-
"""Calculate cost using Claude pricing"""
|
259
|
-
# Use existing logic or new pricing engine
|
260
|
-
if self.status.metrics.total_cost_usd is not None:
|
261
|
-
return self.status.metrics.total_cost_usd
|
262
|
-
|
263
|
-
# Fallback calculation
|
264
|
-
input_cost = (self.status.metrics.input_tokens / 1_000_000) * 3.00
|
265
|
-
output_cost = (self.status.metrics.output_tokens / 1_000_000) * 15.00
|
266
|
-
return input_cost + output_cost
|
267
|
-
|
268
|
-
def supports_feature(self, feature: str) -> bool:
|
269
|
-
"""Claude Code supported features"""
|
270
|
-
supported = ['streaming', 'json_output', 'tools', 'sessions', 'real_time_metrics']
|
271
|
-
return feature in supported
|
272
|
-
|
273
|
-
|
274
|
-
# Example of another agent implementation
|
275
|
-
class ContinueAgent(BaseCodingAgent):
|
276
|
-
"""
|
277
|
-
Example implementation for Continue.dev agent.
|
278
|
-
|
279
|
-
This demonstrates how to add support for other coding agents.
|
280
|
-
"""
|
281
|
-
|
282
|
-
def build_command(self) -> List[str]:
|
283
|
-
"""Build Continue.dev command"""
|
284
|
-
return ["continue", self.config.command]
|
285
|
-
|
286
|
-
async def execute(self) -> bool:
|
287
|
-
"""Execute Continue.dev agent"""
|
288
|
-
# Placeholder implementation
|
289
|
-
# Real implementation would integrate with Continue.dev API
|
290
|
-
self.status.status = "completed"
|
291
|
-
return True
|
292
|
-
|
293
|
-
def parse_output_line(self, line: str) -> bool:
|
294
|
-
"""Parse Continue.dev output"""
|
295
|
-
# Placeholder - implement Continue.dev specific parsing
|
296
|
-
return False
|
297
|
-
|
298
|
-
def calculate_cost(self) -> float:
|
299
|
-
"""Calculate Continue.dev costs"""
|
300
|
-
# Continue.dev may have different pricing model
|
301
|
-
return 0.0
|
302
|
-
|
303
|
-
def supports_feature(self, feature: str) -> bool:
|
304
|
-
"""Continue.dev supported features"""
|
305
|
-
supported = ['autocomplete', 'chat', 'refactoring']
|
306
|
-
return feature in supported
|
307
|
-
|
308
|
-
|
309
|
-
# Agent factory for easy creation
|
310
|
-
class AgentFactory:
|
311
|
-
"""Factory for creating appropriate agent instances"""
|
312
|
-
|
313
|
-
_agent_types = {
|
314
|
-
'claude': ClaudeCodeAgent,
|
315
|
-
'continue': ContinueAgent,
|
316
|
-
# Add more agents here as they're implemented
|
317
|
-
}
|
318
|
-
|
319
|
-
@classmethod
|
320
|
-
def create_agent(cls, agent_type: str, config: AgentConfig) -> BaseCodingAgent:
|
321
|
-
"""
|
322
|
-
Create an agent instance of the specified type.
|
323
|
-
|
324
|
-
Args:
|
325
|
-
agent_type: Type of agent to create (e.g., 'claude', 'continue')
|
326
|
-
config: Agent configuration
|
327
|
-
|
328
|
-
Returns:
|
329
|
-
Agent instance
|
330
|
-
|
331
|
-
Raises:
|
332
|
-
ValueError: If agent type is not supported
|
333
|
-
"""
|
334
|
-
if agent_type not in cls._agent_types:
|
335
|
-
raise ValueError(f"Unsupported agent type: {agent_type}. "
|
336
|
-
f"Supported types: {list(cls._agent_types.keys())}")
|
337
|
-
|
338
|
-
agent_class = cls._agent_types[agent_type]
|
339
|
-
return agent_class(config)
|
340
|
-
|
341
|
-
@classmethod
|
342
|
-
def get_supported_agents(cls) -> List[str]:
|
343
|
-
"""Get list of supported agent types"""
|
344
|
-
return list(cls._agent_types.keys())
|
345
|
-
|
346
|
-
@classmethod
|
347
|
-
def register_agent(cls, agent_type: str, agent_class: type):
|
348
|
-
"""Register a new agent type"""
|
349
|
-
if not issubclass(agent_class, BaseCodingAgent):
|
350
|
-
raise ValueError("Agent class must inherit from BaseCodingAgent")
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Base Agent Interface for Extensible Coding Agent Support
|
4
|
+
|
5
|
+
This module provides the foundation for supporting multiple coding agents
|
6
|
+
beyond Claude Code, enabling zen to orchestrate various AI coding assistants.
|
7
|
+
|
8
|
+
Design Goals:
|
9
|
+
- Simple interface for easy integration
|
10
|
+
- Consistent token/cost tracking across agents
|
11
|
+
- Minimal overhead for basic agents
|
12
|
+
- Extensible for complex agent features
|
13
|
+
"""
|
14
|
+
|
15
|
+
from abc import ABC, abstractmethod
|
16
|
+
from dataclasses import dataclass
|
17
|
+
from typing import Dict, List, Optional, Any, AsyncIterator
|
18
|
+
from pathlib import Path
|
19
|
+
import subprocess
|
20
|
+
import asyncio
|
21
|
+
|
22
|
+
|
23
|
+
@dataclass
|
24
|
+
class AgentConfig:
|
25
|
+
"""Configuration for any coding agent"""
|
26
|
+
name: str
|
27
|
+
command: str
|
28
|
+
description: Optional[str] = None
|
29
|
+
allowed_tools: List[str] = None
|
30
|
+
output_format: str = "text"
|
31
|
+
workspace_dir: Optional[Path] = None
|
32
|
+
session_id: Optional[str] = None
|
33
|
+
environment_vars: Dict[str, str] = None
|
34
|
+
timeout: int = 300
|
35
|
+
|
36
|
+
def __post_init__(self):
|
37
|
+
if self.description is None:
|
38
|
+
self.description = f"Execute {self.command}"
|
39
|
+
if self.environment_vars is None:
|
40
|
+
self.environment_vars = {}
|
41
|
+
|
42
|
+
|
43
|
+
@dataclass
|
44
|
+
class AgentUsageMetrics:
|
45
|
+
"""Standardized usage metrics across all agents"""
|
46
|
+
total_tokens: int = 0
|
47
|
+
input_tokens: int = 0
|
48
|
+
output_tokens: int = 0
|
49
|
+
cached_tokens: int = 0
|
50
|
+
tool_calls: int = 0
|
51
|
+
api_calls: int = 0
|
52
|
+
total_cost_usd: Optional[float] = None
|
53
|
+
model_used: str = "unknown"
|
54
|
+
|
55
|
+
# Agent-specific metrics (extensible)
|
56
|
+
agent_specific: Dict[str, Any] = None
|
57
|
+
|
58
|
+
def __post_init__(self):
|
59
|
+
if self.agent_specific is None:
|
60
|
+
self.agent_specific = {}
|
61
|
+
|
62
|
+
|
63
|
+
@dataclass
|
64
|
+
class AgentStatus:
|
65
|
+
"""Execution status for any coding agent"""
|
66
|
+
name: str
|
67
|
+
status: str = "pending" # pending, running, completed, failed
|
68
|
+
start_time: Optional[float] = None
|
69
|
+
end_time: Optional[float] = None
|
70
|
+
pid: Optional[int] = None
|
71
|
+
output: str = ""
|
72
|
+
error: str = ""
|
73
|
+
metrics: AgentUsageMetrics = None
|
74
|
+
|
75
|
+
def __post_init__(self):
|
76
|
+
if self.metrics is None:
|
77
|
+
self.metrics = AgentUsageMetrics()
|
78
|
+
|
79
|
+
|
80
|
+
class BaseCodingAgent(ABC):
|
81
|
+
"""
|
82
|
+
Abstract base class for all coding agents.
|
83
|
+
|
84
|
+
Provides the interface that all coding agents must implement
|
85
|
+
to be compatible with the zen orchestrator.
|
86
|
+
"""
|
87
|
+
|
88
|
+
def __init__(self, config: AgentConfig):
|
89
|
+
self.config = config
|
90
|
+
self.status = AgentStatus(name=config.name)
|
91
|
+
|
92
|
+
@abstractmethod
|
93
|
+
def build_command(self) -> List[str]:
|
94
|
+
"""
|
95
|
+
Build the command line arguments for executing this agent.
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
List of command line arguments
|
99
|
+
"""
|
100
|
+
pass
|
101
|
+
|
102
|
+
@abstractmethod
|
103
|
+
async def execute(self) -> bool:
|
104
|
+
"""
|
105
|
+
Execute the agent and return success status.
|
106
|
+
|
107
|
+
Returns:
|
108
|
+
True if execution was successful, False otherwise
|
109
|
+
"""
|
110
|
+
pass
|
111
|
+
|
112
|
+
@abstractmethod
|
113
|
+
def parse_output_line(self, line: str) -> bool:
|
114
|
+
"""
|
115
|
+
Parse a single line of output to extract metrics.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
line: Single line from agent output
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
True if line was parsed successfully, False otherwise
|
122
|
+
"""
|
123
|
+
pass
|
124
|
+
|
125
|
+
@abstractmethod
|
126
|
+
def calculate_cost(self) -> float:
|
127
|
+
"""
|
128
|
+
Calculate the total cost for this agent's execution.
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
Total cost in USD
|
132
|
+
"""
|
133
|
+
pass
|
134
|
+
|
135
|
+
def get_agent_type(self) -> str:
|
136
|
+
"""Return the type identifier for this agent"""
|
137
|
+
return self.__class__.__name__.lower().replace('agent', '')
|
138
|
+
|
139
|
+
def get_metrics(self) -> AgentUsageMetrics:
|
140
|
+
"""Get current usage metrics"""
|
141
|
+
return self.status.metrics
|
142
|
+
|
143
|
+
def get_status(self) -> AgentStatus:
|
144
|
+
"""Get current execution status"""
|
145
|
+
return self.status
|
146
|
+
|
147
|
+
def supports_feature(self, feature: str) -> bool:
|
148
|
+
"""Check if agent supports a specific feature"""
|
149
|
+
# Override in subclasses to declare supported features
|
150
|
+
return False
|
151
|
+
|
152
|
+
|
153
|
+
class ClaudeCodeAgent(BaseCodingAgent):
|
154
|
+
"""
|
155
|
+
Claude Code agent implementation.
|
156
|
+
|
157
|
+
Provides compatibility with the existing Claude Code orchestrator
|
158
|
+
through the new agent interface.
|
159
|
+
"""
|
160
|
+
|
161
|
+
def build_command(self) -> List[str]:
|
162
|
+
"""Build Claude Code command"""
|
163
|
+
# Use existing logic from ClaudeInstanceOrchestrator.build_claude_command
|
164
|
+
cmd = ["claude", "-p", self.config.command]
|
165
|
+
|
166
|
+
if self.config.output_format and self.config.output_format != "text":
|
167
|
+
cmd.append(f"--output-format={self.config.output_format}")
|
168
|
+
|
169
|
+
if self.config.allowed_tools:
|
170
|
+
cmd.append(f"--allowedTools={','.join(self.config.allowed_tools)}")
|
171
|
+
|
172
|
+
if self.config.session_id:
|
173
|
+
cmd.extend(["--session-id", self.config.session_id])
|
174
|
+
|
175
|
+
return cmd
|
176
|
+
|
177
|
+
async def execute(self) -> bool:
|
178
|
+
"""Execute Claude Code with async process handling"""
|
179
|
+
try:
|
180
|
+
cmd = self.build_command()
|
181
|
+
self.status.start_time = asyncio.get_event_loop().time()
|
182
|
+
self.status.status = "running"
|
183
|
+
|
184
|
+
# Create async process
|
185
|
+
process = await asyncio.create_subprocess_exec(
|
186
|
+
*cmd,
|
187
|
+
stdout=asyncio.subprocess.PIPE,
|
188
|
+
stderr=asyncio.subprocess.PIPE,
|
189
|
+
cwd=self.config.workspace_dir
|
190
|
+
)
|
191
|
+
|
192
|
+
self.status.pid = process.pid
|
193
|
+
|
194
|
+
# Stream output and parse in real-time
|
195
|
+
async def read_stream(stream, is_stdout=True):
|
196
|
+
while True:
|
197
|
+
line = await stream.readline()
|
198
|
+
if not line:
|
199
|
+
break
|
200
|
+
line_str = line.decode().strip()
|
201
|
+
|
202
|
+
if is_stdout:
|
203
|
+
self.status.output += line_str + "\n"
|
204
|
+
self.parse_output_line(line_str)
|
205
|
+
else:
|
206
|
+
self.status.error += line_str + "\n"
|
207
|
+
|
208
|
+
# Read both streams concurrently
|
209
|
+
await asyncio.gather(
|
210
|
+
read_stream(process.stdout, True),
|
211
|
+
read_stream(process.stderr, False),
|
212
|
+
return_exceptions=True
|
213
|
+
)
|
214
|
+
|
215
|
+
# Wait for process completion
|
216
|
+
returncode = await process.wait()
|
217
|
+
self.status.end_time = asyncio.get_event_loop().time()
|
218
|
+
|
219
|
+
if returncode == 0:
|
220
|
+
self.status.status = "completed"
|
221
|
+
return True
|
222
|
+
else:
|
223
|
+
self.status.status = "failed"
|
224
|
+
return False
|
225
|
+
|
226
|
+
except Exception as e:
|
227
|
+
self.status.status = "failed"
|
228
|
+
self.status.error = str(e)
|
229
|
+
return False
|
230
|
+
|
231
|
+
def parse_output_line(self, line: str) -> bool:
|
232
|
+
"""Parse Claude Code JSON output"""
|
233
|
+
# Use existing logic from ClaudeInstanceOrchestrator._parse_token_usage
|
234
|
+
try:
|
235
|
+
import json
|
236
|
+
if line.startswith('{'):
|
237
|
+
data = json.loads(line)
|
238
|
+
if 'usage' in data:
|
239
|
+
usage = data['usage']
|
240
|
+
self.status.metrics.input_tokens = max(
|
241
|
+
self.status.metrics.input_tokens,
|
242
|
+
int(usage.get('input_tokens', 0))
|
243
|
+
)
|
244
|
+
self.status.metrics.output_tokens = max(
|
245
|
+
self.status.metrics.output_tokens,
|
246
|
+
int(usage.get('output_tokens', 0))
|
247
|
+
)
|
248
|
+
self.status.metrics.total_tokens = max(
|
249
|
+
self.status.metrics.total_tokens,
|
250
|
+
int(usage.get('total_tokens', 0))
|
251
|
+
)
|
252
|
+
return True
|
253
|
+
except (json.JSONDecodeError, ValueError, KeyError):
|
254
|
+
pass
|
255
|
+
return False
|
256
|
+
|
257
|
+
def calculate_cost(self) -> float:
|
258
|
+
"""Calculate cost using Claude pricing"""
|
259
|
+
# Use existing logic or new pricing engine
|
260
|
+
if self.status.metrics.total_cost_usd is not None:
|
261
|
+
return self.status.metrics.total_cost_usd
|
262
|
+
|
263
|
+
# Fallback calculation
|
264
|
+
input_cost = (self.status.metrics.input_tokens / 1_000_000) * 3.00
|
265
|
+
output_cost = (self.status.metrics.output_tokens / 1_000_000) * 15.00
|
266
|
+
return input_cost + output_cost
|
267
|
+
|
268
|
+
def supports_feature(self, feature: str) -> bool:
|
269
|
+
"""Claude Code supported features"""
|
270
|
+
supported = ['streaming', 'json_output', 'tools', 'sessions', 'real_time_metrics']
|
271
|
+
return feature in supported
|
272
|
+
|
273
|
+
|
274
|
+
# Example of another agent implementation
|
275
|
+
class ContinueAgent(BaseCodingAgent):
|
276
|
+
"""
|
277
|
+
Example implementation for Continue.dev agent.
|
278
|
+
|
279
|
+
This demonstrates how to add support for other coding agents.
|
280
|
+
"""
|
281
|
+
|
282
|
+
def build_command(self) -> List[str]:
|
283
|
+
"""Build Continue.dev command"""
|
284
|
+
return ["continue", self.config.command]
|
285
|
+
|
286
|
+
async def execute(self) -> bool:
|
287
|
+
"""Execute Continue.dev agent"""
|
288
|
+
# Placeholder implementation
|
289
|
+
# Real implementation would integrate with Continue.dev API
|
290
|
+
self.status.status = "completed"
|
291
|
+
return True
|
292
|
+
|
293
|
+
def parse_output_line(self, line: str) -> bool:
|
294
|
+
"""Parse Continue.dev output"""
|
295
|
+
# Placeholder - implement Continue.dev specific parsing
|
296
|
+
return False
|
297
|
+
|
298
|
+
def calculate_cost(self) -> float:
|
299
|
+
"""Calculate Continue.dev costs"""
|
300
|
+
# Continue.dev may have different pricing model
|
301
|
+
return 0.0
|
302
|
+
|
303
|
+
def supports_feature(self, feature: str) -> bool:
|
304
|
+
"""Continue.dev supported features"""
|
305
|
+
supported = ['autocomplete', 'chat', 'refactoring']
|
306
|
+
return feature in supported
|
307
|
+
|
308
|
+
|
309
|
+
# Agent factory for easy creation
|
310
|
+
class AgentFactory:
|
311
|
+
"""Factory for creating appropriate agent instances"""
|
312
|
+
|
313
|
+
_agent_types = {
|
314
|
+
'claude': ClaudeCodeAgent,
|
315
|
+
'continue': ContinueAgent,
|
316
|
+
# Add more agents here as they're implemented
|
317
|
+
}
|
318
|
+
|
319
|
+
@classmethod
|
320
|
+
def create_agent(cls, agent_type: str, config: AgentConfig) -> BaseCodingAgent:
|
321
|
+
"""
|
322
|
+
Create an agent instance of the specified type.
|
323
|
+
|
324
|
+
Args:
|
325
|
+
agent_type: Type of agent to create (e.g., 'claude', 'continue')
|
326
|
+
config: Agent configuration
|
327
|
+
|
328
|
+
Returns:
|
329
|
+
Agent instance
|
330
|
+
|
331
|
+
Raises:
|
332
|
+
ValueError: If agent type is not supported
|
333
|
+
"""
|
334
|
+
if agent_type not in cls._agent_types:
|
335
|
+
raise ValueError(f"Unsupported agent type: {agent_type}. "
|
336
|
+
f"Supported types: {list(cls._agent_types.keys())}")
|
337
|
+
|
338
|
+
agent_class = cls._agent_types[agent_type]
|
339
|
+
return agent_class(config)
|
340
|
+
|
341
|
+
@classmethod
|
342
|
+
def get_supported_agents(cls) -> List[str]:
|
343
|
+
"""Get list of supported agent types"""
|
344
|
+
return list(cls._agent_types.keys())
|
345
|
+
|
346
|
+
@classmethod
|
347
|
+
def register_agent(cls, agent_type: str, agent_class: type):
|
348
|
+
"""Register a new agent type"""
|
349
|
+
if not issubclass(agent_class, BaseCodingAgent):
|
350
|
+
raise ValueError("Agent class must inherit from BaseCodingAgent")
|
351
351
|
cls._agent_types[agent_type] = agent_class
|