codeoptix 0.1.0__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.
- codeoptix/__init__.py +8 -0
- codeoptix/acp/__init__.py +33 -0
- codeoptix/acp/agent.py +209 -0
- codeoptix/acp/bridge.py +402 -0
- codeoptix/acp/client_adapter.py +312 -0
- codeoptix/acp/code_extractor.py +125 -0
- codeoptix/acp/orchestrator.py +349 -0
- codeoptix/acp/registry.py +294 -0
- codeoptix/adapters/__init__.py +18 -0
- codeoptix/adapters/base.py +50 -0
- codeoptix/adapters/basic.py +195 -0
- codeoptix/adapters/claude_code.py +218 -0
- codeoptix/adapters/codex.py +327 -0
- codeoptix/adapters/factory.py +56 -0
- codeoptix/adapters/gemini_cli.py +370 -0
- codeoptix/artifacts/__init__.py +5 -0
- codeoptix/artifacts/manager.py +193 -0
- codeoptix/behaviors/__init__.py +45 -0
- codeoptix/behaviors/base.py +81 -0
- codeoptix/behaviors/insecure_code.py +129 -0
- codeoptix/behaviors/plan_drift.py +192 -0
- codeoptix/behaviors/vacuous_tests.py +198 -0
- codeoptix/cli.py +1472 -0
- codeoptix/evaluation/__init__.py +23 -0
- codeoptix/evaluation/bloom_integration.py +271 -0
- codeoptix/evaluation/engine.py +274 -0
- codeoptix/evaluation/evaluators.py +308 -0
- codeoptix/evaluation/scenario_generator.py +222 -0
- codeoptix/evolution/__init__.py +7 -0
- codeoptix/evolution/engine.py +206 -0
- codeoptix/evolution/gepa_integration.py +149 -0
- codeoptix/evolution/proposer.py +185 -0
- codeoptix/linters/__init__.py +13 -0
- codeoptix/linters/bandit_linter.py +172 -0
- codeoptix/linters/base.py +105 -0
- codeoptix/linters/coverage_linter.py +156 -0
- codeoptix/linters/flake8_linter.py +156 -0
- codeoptix/linters/html_accessibility_linter.py +374 -0
- codeoptix/linters/language_detector.py +150 -0
- codeoptix/linters/mypy_linter.py +184 -0
- codeoptix/linters/pip_audit_linter.py +152 -0
- codeoptix/linters/pylint_linter.py +198 -0
- codeoptix/linters/ruff_linter.py +206 -0
- codeoptix/linters/runner.py +186 -0
- codeoptix/linters/safety_linter.py +184 -0
- codeoptix/reflection/__init__.py +6 -0
- codeoptix/reflection/engine.py +70 -0
- codeoptix/reflection/generator.py +209 -0
- codeoptix/utils/__init__.py +1 -0
- codeoptix/utils/config.py +91 -0
- codeoptix/utils/llm.py +332 -0
- codeoptix/utils/retry.py +133 -0
- codeoptix/vendor/__init__.py +2 -0
- codeoptix/vendor/bloom/README.md +26 -0
- codeoptix/vendor/bloom/__init__.py +11 -0
- codeoptix/vendor/bloom/globals.py +39 -0
- codeoptix/vendor/bloom/orchestrators/ConversationOrchestrator.py +450 -0
- codeoptix/vendor/bloom/orchestrators/SimEnvOrchestrator.py +839 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/README.md +85 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/default.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/ideation-default.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_animal-welfare.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_contextual-optimism.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defend-objects.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defer-to-users.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_emotional-bond.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_flattery.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_hardcode-test-cases.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_increasing-pep.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_research-sandbagging.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_self-promotion.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/sandbag.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/self-preferential-bias.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/static-prompts.yaml +72 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/web-search.json +18 -0
- codeoptix/vendor/bloom/prompts/step1_understanding.py +63 -0
- codeoptix/vendor/bloom/prompts/step2_ideation.py +254 -0
- codeoptix/vendor/bloom/prompts/step3_rollout.py +120 -0
- codeoptix/vendor/bloom/prompts/step4_judgment.py +183 -0
- codeoptix/vendor/bloom/schemas/behavior.schema.json +160 -0
- codeoptix/vendor/bloom/schemas/conversation.schema.json +51 -0
- codeoptix/vendor/bloom/schemas/transcript_schema.json +2225 -0
- codeoptix/vendor/bloom/scripts/step2_ideation.py +667 -0
- codeoptix/vendor/bloom/scripts/step4_judgment.py +811 -0
- codeoptix/vendor/bloom/transcript_utils.py +440 -0
- codeoptix/vendor/bloom/utils.py +700 -0
- codeoptix-0.1.0.dist-info/METADATA +304 -0
- codeoptix-0.1.0.dist-info/RECORD +91 -0
- codeoptix-0.1.0.dist-info/WHEEL +4 -0
- codeoptix-0.1.0.dist-info/entry_points.txt +2 -0
- codeoptix-0.1.0.dist-info/licenses/LICENSE +203 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Agent Orchestration - Route to best agent for each task."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from acp import text_block
|
|
7
|
+
|
|
8
|
+
from codeoptix.acp.code_extractor import extract_code_from_message, extract_code_from_text
|
|
9
|
+
from codeoptix.acp.registry import ACPAgentRegistry
|
|
10
|
+
from codeoptix.evaluation import EvaluationEngine
|
|
11
|
+
from codeoptix.utils.llm import LLMClient
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentOrchestrator:
|
|
17
|
+
"""Orchestrates multiple ACP agents for task execution."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
registry: ACPAgentRegistry,
|
|
22
|
+
evaluation_engine: EvaluationEngine | None = None,
|
|
23
|
+
llm_client: LLMClient | None = None,
|
|
24
|
+
):
|
|
25
|
+
"""Initialize agent orchestrator.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
registry: ACP agent registry
|
|
29
|
+
evaluation_engine: Optional evaluation engine
|
|
30
|
+
llm_client: Optional LLM client
|
|
31
|
+
"""
|
|
32
|
+
self.registry = registry
|
|
33
|
+
self.evaluation_engine = evaluation_engine
|
|
34
|
+
self.llm_client = llm_client
|
|
35
|
+
|
|
36
|
+
async def route_to_agent(
|
|
37
|
+
self,
|
|
38
|
+
prompt: str,
|
|
39
|
+
agent_name: str | None = None,
|
|
40
|
+
context: dict[str, Any] | None = None,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
"""Route a prompt to the best agent.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
prompt: The prompt to execute
|
|
46
|
+
agent_name: Specific agent name (if None, selects best agent)
|
|
47
|
+
context: Additional context
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Result dictionary with agent response and metadata
|
|
51
|
+
"""
|
|
52
|
+
# Select agent
|
|
53
|
+
if agent_name:
|
|
54
|
+
selected_agent = agent_name
|
|
55
|
+
else:
|
|
56
|
+
selected_agent = await self._select_best_agent(prompt, context)
|
|
57
|
+
|
|
58
|
+
if not selected_agent:
|
|
59
|
+
raise ValueError("No agent available")
|
|
60
|
+
|
|
61
|
+
# Connect to agent
|
|
62
|
+
connection = await self.registry.connect(selected_agent)
|
|
63
|
+
session_id = self.registry.get_session_id(selected_agent)
|
|
64
|
+
|
|
65
|
+
if not session_id:
|
|
66
|
+
raise RuntimeError(f"No session ID for agent {selected_agent}")
|
|
67
|
+
|
|
68
|
+
# Send prompt
|
|
69
|
+
response = await connection.prompt(
|
|
70
|
+
session_id=session_id,
|
|
71
|
+
prompt=[text_block(prompt)],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
"agent": selected_agent,
|
|
76
|
+
"response": response,
|
|
77
|
+
"session_id": session_id,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async def _select_best_agent(
|
|
81
|
+
self,
|
|
82
|
+
prompt: str,
|
|
83
|
+
context: dict[str, Any] | None = None,
|
|
84
|
+
) -> str | None:
|
|
85
|
+
"""Select the best agent for a given prompt.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
prompt: The prompt
|
|
89
|
+
context: Additional context
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Agent name or None if no agents available
|
|
93
|
+
"""
|
|
94
|
+
agents = self.registry.list_agents()
|
|
95
|
+
if not agents:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
# Intelligent agent selection based on:
|
|
99
|
+
# - Agent capabilities (from registry)
|
|
100
|
+
# - Task type (inferred from prompt)
|
|
101
|
+
# - Context requirements
|
|
102
|
+
|
|
103
|
+
# Check if context specifies an agent
|
|
104
|
+
if context and "preferred_agent" in context:
|
|
105
|
+
preferred = context["preferred_agent"]
|
|
106
|
+
if preferred in agents:
|
|
107
|
+
return preferred
|
|
108
|
+
|
|
109
|
+
# Infer task type from prompt
|
|
110
|
+
prompt_lower = prompt.lower()
|
|
111
|
+
|
|
112
|
+
# Security-focused tasks
|
|
113
|
+
if any(
|
|
114
|
+
keyword in prompt_lower
|
|
115
|
+
for keyword in ["security", "secure", "vulnerability", "exploit", "attack"]
|
|
116
|
+
):
|
|
117
|
+
# Prefer agents with security capabilities
|
|
118
|
+
for agent_name in agents:
|
|
119
|
+
agent_config = self.registry.get_agent(agent_name)
|
|
120
|
+
if agent_config and "security" in [c.lower() for c in agent_config.capabilities]:
|
|
121
|
+
return agent_name
|
|
122
|
+
|
|
123
|
+
# Code review tasks
|
|
124
|
+
if any(keyword in prompt_lower for keyword in ["review", "critique", "judge", "evaluate"]):
|
|
125
|
+
# Prefer agents with review capabilities
|
|
126
|
+
for agent_name in agents:
|
|
127
|
+
agent_config = self.registry.get_agent(agent_name)
|
|
128
|
+
if agent_config and "review" in [c.lower() for c in agent_config.capabilities]:
|
|
129
|
+
return agent_name
|
|
130
|
+
|
|
131
|
+
# Default: use first available agent
|
|
132
|
+
return agents[0]
|
|
133
|
+
|
|
134
|
+
async def execute_multi_agent_workflow(
|
|
135
|
+
self,
|
|
136
|
+
workflow: list[dict[str, Any]],
|
|
137
|
+
) -> list[dict[str, Any]]:
|
|
138
|
+
"""Execute a multi-agent workflow.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
workflow: List of workflow steps, each with 'agent', 'prompt', etc.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
List of results from each step
|
|
145
|
+
"""
|
|
146
|
+
results = []
|
|
147
|
+
for step in workflow:
|
|
148
|
+
agent_name = step.get("agent")
|
|
149
|
+
prompt = step.get("prompt", "")
|
|
150
|
+
context = step.get("context", {})
|
|
151
|
+
|
|
152
|
+
result = await self.route_to_agent(
|
|
153
|
+
prompt=prompt,
|
|
154
|
+
agent_name=agent_name,
|
|
155
|
+
context=context,
|
|
156
|
+
)
|
|
157
|
+
results.append(result)
|
|
158
|
+
|
|
159
|
+
return results
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class MultiAgentJudge:
|
|
163
|
+
"""Multi-agent judge - Use different agents for generation vs. judgment."""
|
|
164
|
+
|
|
165
|
+
def __init__(
|
|
166
|
+
self,
|
|
167
|
+
registry: ACPAgentRegistry,
|
|
168
|
+
generate_agent: str,
|
|
169
|
+
judge_agent: str,
|
|
170
|
+
evaluation_engine: EvaluationEngine | None = None,
|
|
171
|
+
llm_client: LLMClient | None = None,
|
|
172
|
+
):
|
|
173
|
+
"""Initialize multi-agent judge.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
registry: ACP agent registry
|
|
177
|
+
generate_agent: Name of agent for code generation
|
|
178
|
+
judge_agent: Name of agent for code judgment/critique
|
|
179
|
+
evaluation_engine: Optional evaluation engine
|
|
180
|
+
llm_client: Optional LLM client
|
|
181
|
+
"""
|
|
182
|
+
self.registry = registry
|
|
183
|
+
self.generate_agent = generate_agent
|
|
184
|
+
self.judge_agent = judge_agent
|
|
185
|
+
self.evaluation_engine = evaluation_engine
|
|
186
|
+
self.llm_client = llm_client
|
|
187
|
+
|
|
188
|
+
async def generate_and_judge(
|
|
189
|
+
self,
|
|
190
|
+
prompt: str,
|
|
191
|
+
context: dict[str, Any] | None = None,
|
|
192
|
+
) -> dict[str, Any]:
|
|
193
|
+
"""Generate code with one agent and judge with another.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
prompt: The prompt for code generation
|
|
197
|
+
context: Additional context
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Dictionary with generated code, judgment, and evaluation results
|
|
201
|
+
"""
|
|
202
|
+
# Step 1: Generate code with generate agent
|
|
203
|
+
generate_conn = await self.registry.connect(self.generate_agent)
|
|
204
|
+
generate_session = self.registry.get_session_id(self.generate_agent)
|
|
205
|
+
|
|
206
|
+
if not generate_session:
|
|
207
|
+
raise RuntimeError(f"No session for generate agent {self.generate_agent}")
|
|
208
|
+
|
|
209
|
+
generate_response = await generate_conn.prompt(
|
|
210
|
+
session_id=generate_session,
|
|
211
|
+
prompt=[text_block(prompt)],
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Extract generated code (simplified - would need proper extraction)
|
|
215
|
+
generated_code = self._extract_code_from_response(generate_response)
|
|
216
|
+
|
|
217
|
+
# Step 2: Judge code with judge agent
|
|
218
|
+
judge_prompt = f"""Please review and critique the following code:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
{generated_code}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Provide a detailed critique focusing on:
|
|
225
|
+
- Code quality and best practices
|
|
226
|
+
- Potential bugs or issues
|
|
227
|
+
- Security concerns
|
|
228
|
+
- Performance considerations
|
|
229
|
+
- Suggestions for improvement
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
judge_conn = await self.registry.connect(self.judge_agent)
|
|
233
|
+
judge_session = self.registry.get_session_id(self.judge_agent)
|
|
234
|
+
|
|
235
|
+
if not judge_session:
|
|
236
|
+
raise RuntimeError(f"No session for judge agent {self.judge_agent}")
|
|
237
|
+
|
|
238
|
+
judge_response = await judge_conn.prompt(
|
|
239
|
+
session_id=judge_session,
|
|
240
|
+
prompt=[text_block(judge_prompt)],
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Extract judgment
|
|
244
|
+
judgment = self._extract_text_from_response(judge_response)
|
|
245
|
+
|
|
246
|
+
# Step 3: Evaluate both with CodeOptiX
|
|
247
|
+
evaluation_results = None
|
|
248
|
+
if self.evaluation_engine:
|
|
249
|
+
from codeoptix.adapters.base import AgentOutput
|
|
250
|
+
|
|
251
|
+
AgentOutput(
|
|
252
|
+
code=generated_code,
|
|
253
|
+
tests="",
|
|
254
|
+
messages=[],
|
|
255
|
+
metadata={"source": "multi_agent_judge", "judgment": judgment},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Evaluate behaviors
|
|
259
|
+
evaluation_results = await self.evaluation_engine.evaluate_behaviors(
|
|
260
|
+
behavior_names=["insecure-code", "vacuous-tests", "plan-drift"],
|
|
261
|
+
context={"code": generated_code, "judgment": judgment},
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
"generated_code": generated_code,
|
|
266
|
+
"judgment": judgment,
|
|
267
|
+
"evaluation_results": evaluation_results,
|
|
268
|
+
"generate_agent": self.generate_agent,
|
|
269
|
+
"judge_agent": self.judge_agent,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
def _extract_code_from_response(self, response: Any) -> str:
|
|
273
|
+
"""Extract code from agent response.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
response: ACP prompt response
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Extracted code as string
|
|
280
|
+
"""
|
|
281
|
+
if not response:
|
|
282
|
+
return ""
|
|
283
|
+
|
|
284
|
+
# Extract from response messages
|
|
285
|
+
code_blocks = []
|
|
286
|
+
|
|
287
|
+
# Check if response has messages
|
|
288
|
+
if hasattr(response, "messages"):
|
|
289
|
+
for message in response.messages:
|
|
290
|
+
if hasattr(message, "content"):
|
|
291
|
+
content = message.content
|
|
292
|
+
if isinstance(content, str):
|
|
293
|
+
code_blocks.extend(extract_code_from_text(content))
|
|
294
|
+
elif hasattr(content, "text"):
|
|
295
|
+
code_blocks.extend(extract_code_from_text(getattr(content, "text", "")))
|
|
296
|
+
|
|
297
|
+
# Check if response has updates
|
|
298
|
+
if hasattr(response, "updates"):
|
|
299
|
+
for update in response.updates:
|
|
300
|
+
code_blocks.extend(extract_code_from_message(update))
|
|
301
|
+
|
|
302
|
+
# Combine all code blocks
|
|
303
|
+
if code_blocks:
|
|
304
|
+
# Prefer code blocks over inline code
|
|
305
|
+
block_codes = [cb["content"] for cb in code_blocks if cb.get("type") == "block"]
|
|
306
|
+
if block_codes:
|
|
307
|
+
return "\n\n".join(block_codes)
|
|
308
|
+
# Fallback to inline code
|
|
309
|
+
inline_codes = [cb["content"] for cb in code_blocks if cb.get("type") == "inline"]
|
|
310
|
+
if inline_codes:
|
|
311
|
+
return "\n".join(inline_codes)
|
|
312
|
+
|
|
313
|
+
return ""
|
|
314
|
+
|
|
315
|
+
def _extract_text_from_response(self, response: Any) -> str:
|
|
316
|
+
"""Extract text from agent response.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
response: ACP prompt response
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Extracted text as string
|
|
323
|
+
"""
|
|
324
|
+
if not response:
|
|
325
|
+
return ""
|
|
326
|
+
|
|
327
|
+
text_parts = []
|
|
328
|
+
|
|
329
|
+
# Check if response has messages
|
|
330
|
+
if hasattr(response, "messages"):
|
|
331
|
+
for message in response.messages:
|
|
332
|
+
if hasattr(message, "content"):
|
|
333
|
+
content = message.content
|
|
334
|
+
if isinstance(content, str):
|
|
335
|
+
text_parts.append(content)
|
|
336
|
+
elif hasattr(content, "text"):
|
|
337
|
+
text_parts.append(getattr(content, "text", ""))
|
|
338
|
+
|
|
339
|
+
# Check if response has updates
|
|
340
|
+
if hasattr(response, "updates"):
|
|
341
|
+
for update in response.updates:
|
|
342
|
+
if hasattr(update, "content"):
|
|
343
|
+
content = update.content
|
|
344
|
+
if isinstance(content, str):
|
|
345
|
+
text_parts.append(content)
|
|
346
|
+
elif hasattr(content, "text"):
|
|
347
|
+
text_parts.append(getattr(content, "text", ""))
|
|
348
|
+
|
|
349
|
+
return "\n".join(text_parts)
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""ACP Agent Registry - Manage and connect to multiple ACP-compatible agents."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from acp import PROTOCOL_VERSION, Client, connect_to_agent
|
|
9
|
+
from acp.core import ClientSideConnection
|
|
10
|
+
from acp.schema import (
|
|
11
|
+
ClientCapabilities,
|
|
12
|
+
CreateTerminalResponse,
|
|
13
|
+
Implementation,
|
|
14
|
+
ReadTextFileResponse,
|
|
15
|
+
RequestPermissionResponse,
|
|
16
|
+
WriteTextFileResponse,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ACPAgentConfig:
|
|
24
|
+
"""Configuration for an ACP agent."""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
command: list[str]
|
|
28
|
+
cwd: str | None = None
|
|
29
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
30
|
+
description: str = ""
|
|
31
|
+
capabilities: list[str] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ACPAgentRegistry:
|
|
35
|
+
"""Registry for managing ACP-compatible agents."""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
"""Initialize the agent registry."""
|
|
39
|
+
self._agents: dict[str, ACPAgentConfig] = {}
|
|
40
|
+
self._connections: dict[str, ClientSideConnection] = {}
|
|
41
|
+
self._sessions: dict[str, str] = {} # agent_name -> session_id
|
|
42
|
+
|
|
43
|
+
def register(
|
|
44
|
+
self,
|
|
45
|
+
name: str,
|
|
46
|
+
command: list[str],
|
|
47
|
+
cwd: str | None = None,
|
|
48
|
+
env: dict[str, str] | None = None,
|
|
49
|
+
description: str = "",
|
|
50
|
+
capabilities: list[str] | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Register an ACP agent.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
name: Unique name for the agent
|
|
56
|
+
command: Command to spawn the agent (e.g., ["python", "agent.py"])
|
|
57
|
+
cwd: Working directory for the agent
|
|
58
|
+
env: Environment variables
|
|
59
|
+
description: Description of the agent
|
|
60
|
+
capabilities: List of agent capabilities
|
|
61
|
+
"""
|
|
62
|
+
config = ACPAgentConfig(
|
|
63
|
+
name=name,
|
|
64
|
+
command=command,
|
|
65
|
+
cwd=cwd,
|
|
66
|
+
env=env or {},
|
|
67
|
+
description=description,
|
|
68
|
+
capabilities=capabilities or [],
|
|
69
|
+
)
|
|
70
|
+
self._agents[name] = config
|
|
71
|
+
logger.info(f"Registered ACP agent: {name}")
|
|
72
|
+
|
|
73
|
+
def unregister(self, name: str) -> None:
|
|
74
|
+
"""Unregister an ACP agent.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
name: Name of the agent to unregister
|
|
78
|
+
"""
|
|
79
|
+
if name in self._agents:
|
|
80
|
+
# Close connection if open
|
|
81
|
+
if name in self._connections:
|
|
82
|
+
self._connections.pop(name)
|
|
83
|
+
if name in self._sessions:
|
|
84
|
+
self._sessions.pop(name)
|
|
85
|
+
del self._agents[name]
|
|
86
|
+
logger.info(f"Unregistered ACP agent: {name}")
|
|
87
|
+
|
|
88
|
+
def list_agents(self) -> list[str]:
|
|
89
|
+
"""List all registered agent names."""
|
|
90
|
+
return list(self._agents.keys())
|
|
91
|
+
|
|
92
|
+
def get_agent(self, name: str) -> ACPAgentConfig | None:
|
|
93
|
+
"""Get agent configuration.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
name: Agent name
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Agent configuration or None if not found
|
|
100
|
+
"""
|
|
101
|
+
return self._agents.get(name)
|
|
102
|
+
|
|
103
|
+
async def connect(self, name: str) -> ClientSideConnection:
|
|
104
|
+
"""Connect to a registered ACP agent.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
name: Agent name
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
ClientSideConnection to the agent
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
ValueError: If agent not found
|
|
114
|
+
RuntimeError: If connection fails
|
|
115
|
+
"""
|
|
116
|
+
if name not in self._agents:
|
|
117
|
+
raise ValueError(f"Agent '{name}' not found in registry")
|
|
118
|
+
|
|
119
|
+
# Return existing connection if available
|
|
120
|
+
if name in self._connections:
|
|
121
|
+
return self._connections[name]
|
|
122
|
+
|
|
123
|
+
config = self._agents[name]
|
|
124
|
+
|
|
125
|
+
# Spawn agent process
|
|
126
|
+
process = await asyncio.create_subprocess_exec(
|
|
127
|
+
*config.command,
|
|
128
|
+
stdin=asyncio.subprocess.PIPE,
|
|
129
|
+
stdout=asyncio.subprocess.PIPE,
|
|
130
|
+
cwd=config.cwd,
|
|
131
|
+
env={**config.env, **dict(asyncio.get_event_loop().get_environ())},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if process.stdin is None or process.stdout is None:
|
|
135
|
+
raise RuntimeError("Agent process does not expose stdio pipes")
|
|
136
|
+
|
|
137
|
+
# Create client implementation
|
|
138
|
+
client_impl = _RegistryClientImpl()
|
|
139
|
+
connection = connect_to_agent(client_impl, process.stdin, process.stdout)
|
|
140
|
+
|
|
141
|
+
# Initialize connection
|
|
142
|
+
await connection.initialize(
|
|
143
|
+
protocol_version=PROTOCOL_VERSION,
|
|
144
|
+
client_capabilities=ClientCapabilities(),
|
|
145
|
+
client_info=Implementation(
|
|
146
|
+
name="codeoptix-registry",
|
|
147
|
+
title="CodeOptiX Agent Registry",
|
|
148
|
+
version="0.1.0",
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Create new session
|
|
153
|
+
session = await connection.new_session(mcp_servers=[], cwd=config.cwd or ".")
|
|
154
|
+
self._sessions[name] = session.session_id
|
|
155
|
+
|
|
156
|
+
# Store connection
|
|
157
|
+
self._connections[name] = connection
|
|
158
|
+
|
|
159
|
+
logger.info(f"Connected to ACP agent: {name}")
|
|
160
|
+
return connection
|
|
161
|
+
|
|
162
|
+
async def disconnect(self, name: str) -> None:
|
|
163
|
+
"""Disconnect from an agent.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
name: Agent name
|
|
167
|
+
"""
|
|
168
|
+
if name in self._connections:
|
|
169
|
+
connection = self._connections[name]
|
|
170
|
+
try:
|
|
171
|
+
# Try to gracefully close the connection
|
|
172
|
+
if hasattr(connection, "close"):
|
|
173
|
+
await connection.close()
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.warning(f"Error closing connection to agent {name}: {e}")
|
|
176
|
+
finally:
|
|
177
|
+
self._connections.pop(name)
|
|
178
|
+
if name in self._sessions:
|
|
179
|
+
self._sessions.pop(name)
|
|
180
|
+
logger.info(f"Disconnected from ACP agent: {name}")
|
|
181
|
+
|
|
182
|
+
def get_session_id(self, name: str) -> str | None:
|
|
183
|
+
"""Get session ID for an agent.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
name: Agent name
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Session ID or None if not connected
|
|
190
|
+
"""
|
|
191
|
+
return self._sessions.get(name)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class _RegistryClientImpl(Client):
|
|
195
|
+
"""Internal client implementation for registry connections."""
|
|
196
|
+
|
|
197
|
+
async def request_permission(
|
|
198
|
+
self,
|
|
199
|
+
options: list,
|
|
200
|
+
session_id: str,
|
|
201
|
+
tool_call: Any,
|
|
202
|
+
**kwargs: Any,
|
|
203
|
+
) -> RequestPermissionResponse:
|
|
204
|
+
"""Handle permission requests."""
|
|
205
|
+
return RequestPermissionResponse(granted=True)
|
|
206
|
+
|
|
207
|
+
async def write_text_file(
|
|
208
|
+
self,
|
|
209
|
+
content: str,
|
|
210
|
+
path: str,
|
|
211
|
+
session_id: str,
|
|
212
|
+
**kwargs: Any,
|
|
213
|
+
) -> WriteTextFileResponse | None:
|
|
214
|
+
"""Handle file write requests."""
|
|
215
|
+
return WriteTextFileResponse()
|
|
216
|
+
|
|
217
|
+
async def read_text_file(
|
|
218
|
+
self,
|
|
219
|
+
path: str,
|
|
220
|
+
session_id: str,
|
|
221
|
+
limit: int | None = None,
|
|
222
|
+
line: int | None = None,
|
|
223
|
+
**kwargs: Any,
|
|
224
|
+
) -> ReadTextFileResponse:
|
|
225
|
+
"""Handle file read requests."""
|
|
226
|
+
try:
|
|
227
|
+
with open(path, encoding="utf-8") as f:
|
|
228
|
+
if line is not None:
|
|
229
|
+
lines = f.readlines()
|
|
230
|
+
if 0 <= line < len(lines):
|
|
231
|
+
content = lines[line]
|
|
232
|
+
else:
|
|
233
|
+
content = ""
|
|
234
|
+
elif limit is not None:
|
|
235
|
+
content = f.read(limit)
|
|
236
|
+
else:
|
|
237
|
+
content = f.read()
|
|
238
|
+
return ReadTextFileResponse(content=content)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"Error reading file {path}: {e}")
|
|
241
|
+
raise
|
|
242
|
+
|
|
243
|
+
async def create_terminal(self, *args: Any, **kwargs: Any) -> CreateTerminalResponse:
|
|
244
|
+
"""Handle terminal creation."""
|
|
245
|
+
from acp.exceptions import RequestError
|
|
246
|
+
|
|
247
|
+
raise RequestError.method_not_found("terminal/create")
|
|
248
|
+
|
|
249
|
+
async def terminal_output(self, *args: Any, **kwargs: Any) -> Any:
|
|
250
|
+
"""Handle terminal output."""
|
|
251
|
+
from acp.exceptions import RequestError
|
|
252
|
+
|
|
253
|
+
raise RequestError.method_not_found("terminal/output")
|
|
254
|
+
|
|
255
|
+
async def release_terminal(self, *args: Any, **kwargs: Any) -> Any:
|
|
256
|
+
"""Handle terminal release."""
|
|
257
|
+
from acp.exceptions import RequestError
|
|
258
|
+
|
|
259
|
+
raise RequestError.method_not_found("terminal/release")
|
|
260
|
+
|
|
261
|
+
async def wait_for_terminal_exit(self, *args: Any, **kwargs: Any) -> Any:
|
|
262
|
+
"""Handle terminal exit wait."""
|
|
263
|
+
from acp.exceptions import RequestError
|
|
264
|
+
|
|
265
|
+
raise RequestError.method_not_found("terminal/wait_for_exit")
|
|
266
|
+
|
|
267
|
+
async def kill_terminal(self, *args: Any, **kwargs: Any) -> Any:
|
|
268
|
+
"""Handle terminal kill."""
|
|
269
|
+
from acp.exceptions import RequestError
|
|
270
|
+
|
|
271
|
+
raise RequestError.method_not_found("terminal/kill")
|
|
272
|
+
|
|
273
|
+
async def session_update(
|
|
274
|
+
self,
|
|
275
|
+
session_id: str,
|
|
276
|
+
update: Any,
|
|
277
|
+
**kwargs: Any,
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Handle session updates from agent."""
|
|
280
|
+
# Registry doesn't need to handle updates
|
|
281
|
+
|
|
282
|
+
async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
283
|
+
"""Handle extension methods."""
|
|
284
|
+
from acp.exceptions import RequestError
|
|
285
|
+
|
|
286
|
+
raise RequestError.method_not_found(method)
|
|
287
|
+
|
|
288
|
+
async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
|
|
289
|
+
"""Handle extension notifications."""
|
|
290
|
+
logger.debug(f"Extension notification: {method}")
|
|
291
|
+
|
|
292
|
+
def on_connect(self, conn: Any) -> None:
|
|
293
|
+
"""Called when client connects to agent."""
|
|
294
|
+
logger.debug("Registry connected to ACP agent")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Agent adapters for CodeOptix."""
|
|
2
|
+
|
|
3
|
+
from codeoptix.adapters.base import AgentAdapter, AgentOutput
|
|
4
|
+
from codeoptix.adapters.basic import BasicAdapter
|
|
5
|
+
from codeoptix.adapters.claude_code import ClaudeCodeAdapter
|
|
6
|
+
from codeoptix.adapters.codex import CodexAdapter
|
|
7
|
+
from codeoptix.adapters.factory import create_adapter
|
|
8
|
+
from codeoptix.adapters.gemini_cli import GeminiCLIAdapter
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AgentAdapter",
|
|
12
|
+
"AgentOutput",
|
|
13
|
+
"BasicAdapter",
|
|
14
|
+
"ClaudeCodeAdapter",
|
|
15
|
+
"CodexAdapter",
|
|
16
|
+
"GeminiCLIAdapter",
|
|
17
|
+
"create_adapter",
|
|
18
|
+
]
|