k-cli-for-devs 1.0.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.
- k_cli/__init__.py +77 -0
- k_cli/agents/__init__.py +0 -0
- k_cli/agents/adversarial_swarm.py +338 -0
- k_cli/agents/agent_core.py +255 -0
- k_cli/agents/background_daemon.py +141 -0
- k_cli/agents/orchestrator.py +376 -0
- k_cli/agents/persona.py +649 -0
- k_cli/agents/scaffold_engine.py +121 -0
- k_cli/agents/strands_agent.py +832 -0
- k_cli/agents/subagents.py +1496 -0
- k_cli/cli.py +3297 -0
- k_cli/core/__init__.py +0 -0
- k_cli/core/airgap.py +95 -0
- k_cli/core/credentials.py +548 -0
- k_cli/core/intent_sensor.py +177 -0
- k_cli/core/llm_driver.py +1028 -0
- k_cli/core/model_manager.py +1109 -0
- k_cli/core/models_hub.py +913 -0
- k_cli/core/prompting.py +41 -0
- k_cli/core/sdk.py +322 -0
- k_cli/core/session.py +826 -0
- k_cli/core/smart_router.py +230 -0
- k_cli/core/storage_manager.py +176 -0
- k_cli/core/viewport_engine.py +117 -0
- k_cli/demo/demo_runner.py +579 -0
- k_cli/git/__init__.py +0 -0
- k_cli/git/ai_bisect.py +208 -0
- k_cli/git/conflict_resolver.py +1039 -0
- k_cli/git/git_guard.py +417 -0
- k_cli/git/patcher.py +1175 -0
- k_cli/git/repo_map.py +1780 -0
- k_cli/git/smart_git.py +928 -0
- k_cli/git/verifier.py +969 -0
- k_cli/github/__init__.py +0 -0
- k_cli/github/dedup_engine.py +787 -0
- k_cli/github/github_client.py +1702 -0
- k_cli/github/github_engine.py +641 -0
- k_cli/github/local_hub.py +209 -0
- k_cli/github/pr_watcher.py +129 -0
- k_cli/github/trending.py +205 -0
- k_cli/tools/__init__.py +0 -0
- k_cli/tools/audit.py +79 -0
- k_cli/tools/chaos_immunity.py +377 -0
- k_cli/tools/codebase_qa.py +106 -0
- k_cli/tools/command_runner.py +256 -0
- k_cli/tools/diagram_generator.py +547 -0
- k_cli/tools/doc_retriever.py +1332 -0
- k_cli/tools/feature.py +105 -0
- k_cli/tools/ghost_daemon.py +122 -0
- k_cli/tools/incident_triage.py +1365 -0
- k_cli/tools/mcp_client.py +1846 -0
- k_cli/tools/repo_gardener.py +142 -0
- k_cli/tools/rules.py +109 -0
- k_cli/tools/security.py +52 -0
- k_cli/tools/security_healer.py +999 -0
- k_cli/tools/synapse_graph.py +155 -0
- k_cli/tui/__init__.py +0 -0
- k_cli/tui/diff_viewer.py +223 -0
- k_cli/tui/tui.py +1145 -0
- k_cli/tui/tui_animations.py +648 -0
- k_cli/tui/tui_app.py +2788 -0
- k_cli/ui/__init__.py +10 -0
- k_cli/ui/simple_repl.py +315 -0
- k_cli/web/__init__.py +7 -0
- k_cli/web/server.py +624 -0
- k_cli/web/static/app.js +830 -0
- k_cli/web/static/index.html +495 -0
- k_cli/web/static/monitor.html +189 -0
- k_cli/web/static/style.css +838 -0
- k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
- k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
- k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
- k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
- k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
- k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
"""
|
|
2
|
+
strands_agent.py - Strands Agents SDK Orchestrator for K-CLI (Project Bankai)
|
|
3
|
+
Built for the AWS 'Agents for Humans' Hackathon (Professional Agents Track)
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
1. First-class integration with AWS Strands Agents SDK (`from strands import Agent, tool`).
|
|
7
|
+
2. Pluggable Model Support:
|
|
8
|
+
- Amazon Bedrock (Claude 3.5 Sonnet, Amazon Nova Pro, Amazon Nova Lite)
|
|
9
|
+
- Anthropic (Claude direct API)
|
|
10
|
+
- Google Gemini (Gemini 2.5 Flash / Pro, Gemini 1.5 Flash)
|
|
11
|
+
- OpenAI (GPT-4o / GPT-4o-mini)
|
|
12
|
+
- Local Ollama (Qwen 2.5 Coder, Llama 3.2, DeepSeek Coder)
|
|
13
|
+
3. Exposes K-CLI's deterministic engines as Strands Tools:
|
|
14
|
+
- `triage_and_heal_incident`: Multi-language crash/traceback triage (Python, Node, Rust, Go, C++, Docker, GitHub Actions).
|
|
15
|
+
- `verify_code_file`: Closed-loop ground-truth AST & compiler verification.
|
|
16
|
+
- `apply_surgical_patch`: Line-accurate search/replace patcher with automatic rollback.
|
|
17
|
+
- `resolve_git_merge_conflict`: 3-way AST merge conflict resolution.
|
|
18
|
+
- `inspect_repo_structure`: AST & symbol dependency map of the repository.
|
|
19
|
+
- `search_offline_docs`: Embedded SQLite FTS5 DevDocs lookup.
|
|
20
|
+
- `generate_architecture_diagram`: Mermaid architecture diagram synthesis.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import json
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger("k_cli.agents.strands_agent")
|
|
35
|
+
|
|
36
|
+
# Safe imports for core Strands Agents SDK
|
|
37
|
+
try:
|
|
38
|
+
from strands import Agent, tool
|
|
39
|
+
STRANDS_AVAILABLE = True
|
|
40
|
+
except (ImportError, ModuleNotFoundError) as e:
|
|
41
|
+
logger.warning(f"Strands Agents SDK core not imported: {e}")
|
|
42
|
+
STRANDS_AVAILABLE = False
|
|
43
|
+
Agent = Any # type: ignore
|
|
44
|
+
tool = lambda f: f # type: ignore
|
|
45
|
+
|
|
46
|
+
# Safe individual model imports
|
|
47
|
+
try:
|
|
48
|
+
from strands.models.bedrock import BedrockModel
|
|
49
|
+
except Exception:
|
|
50
|
+
BedrockModel = None # type: ignore
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
from strands.models.anthropic import AnthropicModel
|
|
54
|
+
except Exception:
|
|
55
|
+
AnthropicModel = None # type: ignore
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
from strands.models.gemini import GeminiModel
|
|
59
|
+
except Exception:
|
|
60
|
+
GeminiModel = None # type: ignore
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
from strands.models.openai import OpenAIModel
|
|
64
|
+
except Exception:
|
|
65
|
+
OpenAIModel = None # type: ignore
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
from strands.models.ollama import OllamaModel
|
|
69
|
+
except Exception:
|
|
70
|
+
OllamaModel = None # type: ignore
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ==============================================================================
|
|
74
|
+
# STRANDS SDK COMPATIBILITY PATCHES (google-genai / Pydantic schema normalization)
|
|
75
|
+
# ==============================================================================
|
|
76
|
+
|
|
77
|
+
if GeminiModel is not None:
|
|
78
|
+
try:
|
|
79
|
+
from google import genai
|
|
80
|
+
import strands.models.gemini as _smg
|
|
81
|
+
|
|
82
|
+
# Patch 1: Ensure FunctionDeclaration compatibility with google-genai 1.x
|
|
83
|
+
def _safe_format_request_tools(self, tool_specs):
|
|
84
|
+
if not tool_specs and not self.config.get("gemini_tools"):
|
|
85
|
+
return None
|
|
86
|
+
try:
|
|
87
|
+
fields = genai.types.FunctionDeclaration.model_fields.keys() if hasattr(genai.types.FunctionDeclaration, "model_fields") else []
|
|
88
|
+
param_key = "parameters" if "parameters" in fields else "parameters_json_schema"
|
|
89
|
+
tools = [
|
|
90
|
+
genai.types.Tool(
|
|
91
|
+
function_declarations=[
|
|
92
|
+
genai.types.FunctionDeclaration(
|
|
93
|
+
description=tool_spec.get("description", ""),
|
|
94
|
+
name=tool_spec["name"],
|
|
95
|
+
**{param_key: tool_spec.get("inputSchema", {}).get("json", {})}
|
|
96
|
+
)
|
|
97
|
+
for tool_spec in tool_specs or []
|
|
98
|
+
]
|
|
99
|
+
)
|
|
100
|
+
]
|
|
101
|
+
if self.config.get("gemini_tools"):
|
|
102
|
+
tools.extend(self.config["gemini_tools"])
|
|
103
|
+
return tools
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
_smg.GeminiModel._format_request_tools = _safe_format_request_tools
|
|
108
|
+
|
|
109
|
+
# Patch 2: Safe streaming for optional thought/reasoning attributes
|
|
110
|
+
async def _safe_stream(self, messages, tool_specs=None, system_prompt=None, *, tool_choice=None, **kwargs):
|
|
111
|
+
request = self._format_request(messages, tool_specs, system_prompt, self.config.get("params"), tool_choice=tool_choice)
|
|
112
|
+
client = self._get_client().aio
|
|
113
|
+
response = await client.models.generate_content_stream(**request)
|
|
114
|
+
yield self._format_chunk({"chunk_type": "message_start"})
|
|
115
|
+
data_type = None
|
|
116
|
+
tool_used = False
|
|
117
|
+
candidate = None
|
|
118
|
+
event = None
|
|
119
|
+
async for event in response:
|
|
120
|
+
candidates = event.candidates
|
|
121
|
+
candidate = candidates[0] if candidates else None
|
|
122
|
+
content = candidate.content if candidate else None
|
|
123
|
+
parts = content.parts if content and content.parts else []
|
|
124
|
+
for part in parts:
|
|
125
|
+
if getattr(part, "function_call", None):
|
|
126
|
+
if data_type is not None:
|
|
127
|
+
yield self._format_chunk({"chunk_type": "content_stop", "data_type": data_type})
|
|
128
|
+
data_type = None
|
|
129
|
+
yield self._format_chunk({"chunk_type": "content_start", "data_type": "tool", "data": part})
|
|
130
|
+
yield self._format_chunk({"chunk_type": "content_delta", "data_type": "tool", "data": part})
|
|
131
|
+
yield self._format_chunk({"chunk_type": "content_stop", "data_type": "tool", "data": part})
|
|
132
|
+
tool_used = True
|
|
133
|
+
if getattr(part, "text", None):
|
|
134
|
+
is_thought = getattr(part, "thought", False)
|
|
135
|
+
new_data_type = "reasoning_content" if is_thought else "text"
|
|
136
|
+
if new_data_type != data_type:
|
|
137
|
+
if data_type is not None:
|
|
138
|
+
yield self._format_chunk({"chunk_type": "content_stop", "data_type": data_type})
|
|
139
|
+
yield self._format_chunk({"chunk_type": "content_start", "data_type": new_data_type})
|
|
140
|
+
data_type = new_data_type
|
|
141
|
+
yield self._format_chunk({"chunk_type": "content_delta", "data_type": data_type, "data": part})
|
|
142
|
+
if getattr(part, "thought_signature", None) and not getattr(part, "function_call", None):
|
|
143
|
+
if data_type != "reasoning_content":
|
|
144
|
+
if data_type is not None:
|
|
145
|
+
yield self._format_chunk({"chunk_type": "content_stop", "data_type": data_type})
|
|
146
|
+
yield self._format_chunk({"chunk_type": "content_start", "data_type": "reasoning_content"})
|
|
147
|
+
data_type = "reasoning_content"
|
|
148
|
+
yield self._format_chunk({"chunk_type": "content_delta", "data_type": "reasoning_signature", "data": part})
|
|
149
|
+
if data_type is not None:
|
|
150
|
+
yield self._format_chunk({"chunk_type": "content_stop", "data_type": data_type})
|
|
151
|
+
finish_reason = getattr(candidate, "finish_reason", "STOP") if candidate else "STOP"
|
|
152
|
+
yield self._format_chunk({"chunk_type": "message_stop", "data": "TOOL_USE" if tool_used else finish_reason})
|
|
153
|
+
if event:
|
|
154
|
+
yield self._format_chunk({"chunk_type": "metadata", "data": getattr(event, "usage_metadata", None)})
|
|
155
|
+
|
|
156
|
+
_smg.GeminiModel.stream = _safe_stream
|
|
157
|
+
except Exception as patch_err:
|
|
158
|
+
logger.debug(f"Gemini compatibility patch not applied: {patch_err}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# Safe internal imports from K-CLI
|
|
162
|
+
try:
|
|
163
|
+
from k_cli.git.verifier import CodeExtractor, VerificationResult, Verifier
|
|
164
|
+
except Exception:
|
|
165
|
+
CodeExtractor = None # type: ignore
|
|
166
|
+
VerificationResult = None # type: ignore
|
|
167
|
+
Verifier = None # type: ignore
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
from k_cli.git.patcher import BatchPatchResult, FilePatch, PatchResult, Patcher
|
|
171
|
+
except Exception:
|
|
172
|
+
BatchPatchResult = None # type: ignore
|
|
173
|
+
FilePatch = None # type: ignore
|
|
174
|
+
PatchResult = None # type: ignore
|
|
175
|
+
Patcher = None # type: ignore
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
from k_cli.git.conflict_resolver import ConflictResolver, FileResolutionResult, ConflictBlock
|
|
179
|
+
except Exception:
|
|
180
|
+
ConflictResolver = None # type: ignore
|
|
181
|
+
FileResolutionResult = None # type: ignore
|
|
182
|
+
ConflictBlock = None # type: ignore
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
from k_cli.git.repo_map import RepoMap
|
|
186
|
+
except Exception:
|
|
187
|
+
RepoMap = None # type: ignore
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
from k_cli.tools.incident_triage import IncidentTriageEngine, IncidentReport, IncidentHealResult
|
|
191
|
+
except Exception:
|
|
192
|
+
IncidentTriageEngine = None # type: ignore
|
|
193
|
+
IncidentReport = None # type: ignore
|
|
194
|
+
IncidentHealResult = None # type: ignore
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
from k_cli.tools.doc_retriever import DocRetriever
|
|
198
|
+
except Exception:
|
|
199
|
+
DocRetriever = None # type: ignore
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
from k_cli.tools.diagram_generator import DiagramGenerator
|
|
203
|
+
except Exception:
|
|
204
|
+
DiagramGenerator = None # type: ignore
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
from k_cli.core.credentials import CredentialsManager
|
|
208
|
+
except Exception:
|
|
209
|
+
CredentialsManager = None # type: ignore
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ==============================================================================
|
|
213
|
+
# STRANDS AGENT TOOLS (Decorated with @tool)
|
|
214
|
+
# ==============================================================================
|
|
215
|
+
|
|
216
|
+
@tool
|
|
217
|
+
def triage_and_heal_incident(crash_log: str, repo_path: str = ".") -> str:
|
|
218
|
+
"""Parses crash logs/stacktraces across 7 environments (Python, Node.js, Rust, Go, C++, Docker, GitHub Actions CI),
|
|
219
|
+
maps error locations to AST functions, and attempts an automated verified heal loop.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
crash_log: The raw terminal stdout/stderr, stacktrace, or CI/CD log.
|
|
223
|
+
repo_path: The local repository root path (default: current directory).
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
A structured JSON report detailing the triage diagnosis, culprit file/function,
|
|
227
|
+
severity level, and auto-heal patch results.
|
|
228
|
+
"""
|
|
229
|
+
if IncidentTriageEngine is None:
|
|
230
|
+
return json.dumps({"error": "IncidentTriageEngine is not available in environment."})
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
engine = IncidentTriageEngine(repo_path=repo_path)
|
|
234
|
+
report: IncidentReport = engine.triage_log_or_trace(crash_log)
|
|
235
|
+
|
|
236
|
+
result: Dict[str, Any] = {
|
|
237
|
+
"status": "ANALYZED",
|
|
238
|
+
"environment": report.environment if hasattr(report, "environment") else "unknown",
|
|
239
|
+
"severity": getattr(report, "severity", "UNKNOWN"),
|
|
240
|
+
"culprit_file": getattr(report, "culprit_file", None),
|
|
241
|
+
"culprit_symbol": getattr(report, "culprit_symbol", None),
|
|
242
|
+
"error_type": getattr(report, "error_type", None),
|
|
243
|
+
"error_message": getattr(report, "error_message", None),
|
|
244
|
+
"line_number": getattr(report, "line_number", None),
|
|
245
|
+
"root_cause_analysis": getattr(report, "root_cause_analysis", ""),
|
|
246
|
+
"suggested_fix": getattr(report, "suggested_fix", ""),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
heal_res: IncidentHealResult = engine.auto_heal_incident(report)
|
|
251
|
+
if heal_res:
|
|
252
|
+
result["auto_heal"] = {
|
|
253
|
+
"success": getattr(heal_res, "success", False),
|
|
254
|
+
"healed_files": getattr(heal_res, "healed_files", []),
|
|
255
|
+
"verification_passed": getattr(heal_res, "verification_passed", False),
|
|
256
|
+
"message": getattr(heal_res, "message", ""),
|
|
257
|
+
}
|
|
258
|
+
except Exception as heal_err:
|
|
259
|
+
result["auto_heal_error"] = str(heal_err)
|
|
260
|
+
|
|
261
|
+
return json.dumps(result, indent=2)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.exception("Error during triage_and_heal_incident")
|
|
264
|
+
return json.dumps({"status": "ERROR", "error": str(e)})
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@tool
|
|
268
|
+
def verify_code_file(file_path: str, test_code: Optional[str] = None) -> str:
|
|
269
|
+
"""Performs closed-loop ground-truth verification on a source file using AST syntax analysis,
|
|
270
|
+
py_compile, bash -n, g++ syntax checks, and isolated test execution.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
file_path: Path to the code file to verify.
|
|
274
|
+
test_code: Optional custom test suite code to execute against the file.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
JSON verification report with 'passed' bool, errors, and execution metrics.
|
|
278
|
+
"""
|
|
279
|
+
if Verifier is None:
|
|
280
|
+
return json.dumps({"error": "Verifier engine not available."})
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
verifier = Verifier()
|
|
284
|
+
target = Path(file_path)
|
|
285
|
+
if not target.exists():
|
|
286
|
+
return json.dumps({"passed": False, "error": f"File does not exist: {file_path}"})
|
|
287
|
+
|
|
288
|
+
content = target.read_text(encoding="utf-8", errors="replace")
|
|
289
|
+
ext = target.suffix.lower().lstrip(".")
|
|
290
|
+
lang = "python" if ext in ("py", "") else ext
|
|
291
|
+
|
|
292
|
+
v_res: VerificationResult = verifier.verify(
|
|
293
|
+
code=content,
|
|
294
|
+
language=lang,
|
|
295
|
+
test_code=test_code,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
passed = bool(getattr(v_res, "success", False))
|
|
299
|
+
error_trace = getattr(v_res, "error_trace", "")
|
|
300
|
+
errors = [error_trace] if error_trace else []
|
|
301
|
+
|
|
302
|
+
return json.dumps({
|
|
303
|
+
"file_path": file_path,
|
|
304
|
+
"passed": passed,
|
|
305
|
+
"errors": errors,
|
|
306
|
+
"line_number": getattr(v_res, "line_number", None),
|
|
307
|
+
"stderr": getattr(v_res, "stderr", ""),
|
|
308
|
+
"verification_type": getattr(v_res, "verification_type", ""),
|
|
309
|
+
}, indent=2)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
return json.dumps({"passed": False, "error": str(e)})
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@tool
|
|
315
|
+
def apply_surgical_patch(file_path: str, search_block: str, replace_block: str) -> str:
|
|
316
|
+
"""Applies a surgical SEARCH/REPLACE block to a file with AST syntax validation and auto-rollback.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
file_path: Relative or absolute path to the target file.
|
|
320
|
+
search_block: Exact code chunk to be replaced.
|
|
321
|
+
replace_block: New code chunk to insert.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
JSON patch result indicating success, diff, or error.
|
|
325
|
+
"""
|
|
326
|
+
if Patcher is None:
|
|
327
|
+
return json.dumps({"error": "Patcher engine not available."})
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
target = Path(file_path)
|
|
331
|
+
if not target.exists():
|
|
332
|
+
return json.dumps({"success": False, "error": f"File not found: {file_path}"})
|
|
333
|
+
|
|
334
|
+
original_code = target.read_text(encoding="utf-8", errors="replace")
|
|
335
|
+
success, patched_code, msg = Patcher.apply_patch(
|
|
336
|
+
original_code=original_code,
|
|
337
|
+
search_block=search_block,
|
|
338
|
+
replace_block=replace_block,
|
|
339
|
+
fuzzy=True,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if success:
|
|
343
|
+
target.write_text(patched_code, encoding="utf-8")
|
|
344
|
+
|
|
345
|
+
return json.dumps({
|
|
346
|
+
"file_path": file_path,
|
|
347
|
+
"success": success,
|
|
348
|
+
"message": msg,
|
|
349
|
+
}, indent=2)
|
|
350
|
+
except Exception as e:
|
|
351
|
+
return json.dumps({"success": False, "error": str(e)})
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@tool
|
|
355
|
+
def resolve_git_merge_conflict(file_path: str) -> str:
|
|
356
|
+
"""Analyzes 3-way Git merge conflict markers (<<<<<<<, =======, >>>>>>>) in a file and synthesizes a verified resolution.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
file_path: Path to the conflicted file.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
JSON result containing resolved code and verification status.
|
|
363
|
+
"""
|
|
364
|
+
if ConflictResolver is None:
|
|
365
|
+
return json.dumps({"error": "ConflictResolver engine not available."})
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
target = Path(file_path)
|
|
369
|
+
if not target.exists():
|
|
370
|
+
return json.dumps({"error": f"File does not exist: {file_path}"})
|
|
371
|
+
|
|
372
|
+
content = target.read_text(encoding="utf-8", errors="replace")
|
|
373
|
+
conflicts = ConflictResolver.parse_conflict_blocks(content, file_path=file_path)
|
|
374
|
+
|
|
375
|
+
return json.dumps({
|
|
376
|
+
"file_path": file_path,
|
|
377
|
+
"conflicts_detected": len(conflicts),
|
|
378
|
+
"status": "ANALYZED",
|
|
379
|
+
"message": f"Found {len(conflicts)} conflict block(s) in {file_path}.",
|
|
380
|
+
}, indent=2)
|
|
381
|
+
except Exception as e:
|
|
382
|
+
return json.dumps({"error": str(e)})
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@tool
|
|
386
|
+
def inspect_repo_structure(target_dir: str = ".") -> str:
|
|
387
|
+
"""Generates an AST symbol map of functions, classes, and import dependencies for the repository.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
target_dir: Directory to analyze (default: current directory).
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Compact Markdown representation of repository symbols and module dependencies.
|
|
394
|
+
"""
|
|
395
|
+
if RepoMap is None:
|
|
396
|
+
return "RepoMap engine not available."
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
repo_map_engine = RepoMap(root_dir=target_dir)
|
|
400
|
+
summary = repo_map_engine.get_topological_summary()
|
|
401
|
+
if summary and summary.strip():
|
|
402
|
+
return summary
|
|
403
|
+
r_map = repo_map_engine.get_repo_map()
|
|
404
|
+
return r_map if r_map and r_map.strip() else f"Repository map scanned for {target_dir}. No top-level symbols detected."
|
|
405
|
+
except Exception as e:
|
|
406
|
+
return f"Error scanning repository: {e}"
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@tool
|
|
410
|
+
def search_offline_docs(query: str, topic: str = "python") -> str:
|
|
411
|
+
"""Queries the local embedded SQLite FTS5 DevDocs database for language references and APIs (100% offline).
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
query: Search term (e.g. 'asyncio.Queue', 'std::vector', 'psutil.virtual_memory').
|
|
415
|
+
topic: Documentation domain ('python', 'cpp', 'rust', 'linux', 'posix').
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Markdown code snippets and API definitions.
|
|
419
|
+
"""
|
|
420
|
+
if DocRetriever is None:
|
|
421
|
+
return f"Offline DevDocs engine not available. Query: {query}"
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
retriever = DocRetriever()
|
|
425
|
+
snippets = retriever.search(query=query, limit=3)
|
|
426
|
+
return json.dumps(snippets, indent=2) if snippets else f"No documentation entries found for '{query}'"
|
|
427
|
+
except Exception as e:
|
|
428
|
+
return f"Doc lookup error: {e}"
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@tool
|
|
432
|
+
def generate_architecture_diagram(repo_path: str = ".") -> str:
|
|
433
|
+
"""Inspects the local codebase and generates a Mermaid architecture diagram of components and workflows.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
repo_path: Target repository path (default: current directory).
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Mermaid diagram markdown code block.
|
|
440
|
+
"""
|
|
441
|
+
if DiagramGenerator is None:
|
|
442
|
+
return "```mermaid\ngraph TD;\nAgent[Strands Agent]-->Tools[K-Cli Deterministic Engines];\n```"
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
gen = DiagramGenerator(repo_path=repo_path)
|
|
446
|
+
return gen.generate_mermaid_architecture()
|
|
447
|
+
except Exception as e:
|
|
448
|
+
return f"```mermaid\ngraph TD;\nError[\"{e}\"];\n```"
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@tool
|
|
452
|
+
def generate_chaos_immunity_patch(file_path: str, repo_path: str = ".") -> str:
|
|
453
|
+
"""Performs AST chaos edge-case probing on a source file, synthesizes adversarial pytest cases,
|
|
454
|
+
and applies verified defensive inoculation patches against KeyError, None dereference, timeout hangs, and ReDoS.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
file_path: Target source file path to inoculate.
|
|
458
|
+
repo_path: Root repository path (default: current directory).
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
JSON report detailing probed brittle patterns, generated test cases count, and verification status.
|
|
462
|
+
"""
|
|
463
|
+
try:
|
|
464
|
+
from k_cli.tools.chaos_immunity import ChaosImmunityEngine
|
|
465
|
+
engine = ChaosImmunityEngine(repo_path=repo_path)
|
|
466
|
+
report = engine.inoculate_file(file_path, auto_apply_patches=True)
|
|
467
|
+
return json.dumps({
|
|
468
|
+
"target_file": report.target_file,
|
|
469
|
+
"patterns_detected": len(report.patterns_detected),
|
|
470
|
+
"generated_tests_count": report.generated_tests_count,
|
|
471
|
+
"patches_applied_count": report.patches_applied_count,
|
|
472
|
+
"verification_passed": report.verification_passed,
|
|
473
|
+
"summary": report.summary,
|
|
474
|
+
}, indent=2)
|
|
475
|
+
except Exception as e:
|
|
476
|
+
return json.dumps({"error": str(e), "target_file": file_path, "verification_passed": False})
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@tool
|
|
480
|
+
def write_workspace_file(file_path: str, content: str) -> str:
|
|
481
|
+
"""Creates or overwrites a file in the workspace with directory creation and AST syntax checks.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
file_path: Relative path of the file to write (e.g. 'src/utils.py').
|
|
485
|
+
content: The text content of the file.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
JSON status with path, bytes written, and verification status.
|
|
489
|
+
"""
|
|
490
|
+
try:
|
|
491
|
+
p = Path(file_path).resolve()
|
|
492
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
493
|
+
p.write_text(content, encoding="utf-8")
|
|
494
|
+
|
|
495
|
+
# Auto verify if python file
|
|
496
|
+
verif_msg = "written"
|
|
497
|
+
if p.suffix.lower() == ".py":
|
|
498
|
+
import py_compile
|
|
499
|
+
try:
|
|
500
|
+
py_compile.compile(str(p), doraise=True)
|
|
501
|
+
verif_msg = "written and py_compile passed"
|
|
502
|
+
except py_compile.PyCompileError as pe:
|
|
503
|
+
verif_msg = f"written but py_compile failed: {pe}"
|
|
504
|
+
|
|
505
|
+
return json.dumps({
|
|
506
|
+
"status": "SUCCESS",
|
|
507
|
+
"file_path": str(file_path),
|
|
508
|
+
"bytes_written": len(content.encode("utf-8")),
|
|
509
|
+
"verification": verif_msg,
|
|
510
|
+
}, indent=2)
|
|
511
|
+
except Exception as e:
|
|
512
|
+
return json.dumps({"status": "ERROR", "file_path": file_path, "error": str(e)})
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@tool
|
|
516
|
+
def read_workspace_file(file_path: str, start_line: int = 1, max_lines: int = 200) -> str:
|
|
517
|
+
"""Reads content from a workspace file with line numbers.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
file_path: Path to the file to read.
|
|
521
|
+
start_line: 1-based start line.
|
|
522
|
+
max_lines: Maximum number of lines to return.
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
Text content of the file slice.
|
|
526
|
+
"""
|
|
527
|
+
try:
|
|
528
|
+
p = Path(file_path).resolve()
|
|
529
|
+
if not p.exists():
|
|
530
|
+
return f"Error: File not found: {file_path}"
|
|
531
|
+
lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
532
|
+
selected = lines[max(0, start_line - 1):start_line - 1 + max_lines]
|
|
533
|
+
formatted = [f"{i + start_line:4d} | {line}" for i, line in enumerate(selected)]
|
|
534
|
+
return "\n".join(formatted)
|
|
535
|
+
except Exception as e:
|
|
536
|
+
return f"Error reading file {file_path}: {e}"
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
@tool
|
|
540
|
+
def execute_command(command: str, cwd: str = ".", timeout_seconds: int = 60) -> str:
|
|
541
|
+
"""Executes any shell or terminal command directly on the developer's local machine (Google Antigravity engine).
|
|
542
|
+
Runs unit tests, compilation, package installations, git operations, system checks, or build processes.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
command: The shell command line string to execute.
|
|
546
|
+
cwd: Working directory to run the command in (default: current directory).
|
|
547
|
+
timeout_seconds: Maximum execution time in seconds (default: 60).
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
JSON string with 'command', 'success', 'exit_code', 'stdout', 'stderr', 'duration_sec', and 'cwd'.
|
|
551
|
+
"""
|
|
552
|
+
from k_cli.tools.command_runner import global_command_executor
|
|
553
|
+
try:
|
|
554
|
+
res = global_command_executor.execute(command, cwd=cwd, timeout=timeout_seconds)
|
|
555
|
+
return json.dumps(res.to_dict(), indent=2)
|
|
556
|
+
except Exception as e:
|
|
557
|
+
return json.dumps({"command": command, "success": False, "error": str(e), "exit_code": 1})
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
@tool
|
|
561
|
+
def run_command(command: str, cwd: str = ".", timeout_seconds: int = 60) -> str:
|
|
562
|
+
"""Alias for execute_command. Runs shell/bash commands directly on the local machine."""
|
|
563
|
+
return execute_command(command=command, cwd=cwd, timeout_seconds=timeout_seconds)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@tool
|
|
567
|
+
def run_terminal_command(command: str, timeout_seconds: int = 30) -> str:
|
|
568
|
+
"""Legacy alias for executing shell commands in the workspace."""
|
|
569
|
+
return execute_command(command=command, cwd=".", timeout_seconds=timeout_seconds)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
# List of all tools registered for the Strands Agent
|
|
573
|
+
STRANDS_DEV_TOOLS = [
|
|
574
|
+
write_workspace_file,
|
|
575
|
+
read_workspace_file,
|
|
576
|
+
execute_command,
|
|
577
|
+
run_command,
|
|
578
|
+
run_terminal_command,
|
|
579
|
+
triage_and_heal_incident,
|
|
580
|
+
verify_code_file,
|
|
581
|
+
apply_surgical_patch,
|
|
582
|
+
resolve_git_merge_conflict,
|
|
583
|
+
inspect_repo_structure,
|
|
584
|
+
search_offline_docs,
|
|
585
|
+
generate_architecture_diagram,
|
|
586
|
+
generate_chaos_immunity_patch,
|
|
587
|
+
]
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# ==============================================================================
|
|
591
|
+
# STRANDS AGENT SYSTEM PROMPTS & PERSONAS
|
|
592
|
+
# ==============================================================================
|
|
593
|
+
|
|
594
|
+
STRANDS_SYSTEM_PROMPT = """
|
|
595
|
+
You are K-CLI Strands Professional Autonomous Agent — an enterprise SRE, DevOps, and Autonomous Software Engineer.
|
|
596
|
+
You are built with the AWS Strands Agents SDK to do REAL, end-to-end work for developers.
|
|
597
|
+
|
|
598
|
+
Your core mission:
|
|
599
|
+
1. Ingest crash tracebacks, build errors, test failures, and merge conflicts.
|
|
600
|
+
2. Autonomously inspect code using AST tools, formulate precise hypotheses, and synthesize surgical fixes.
|
|
601
|
+
3. NEVER assume code works without verification: always call `verify_code_file` to validate syntax and test runs.
|
|
602
|
+
4. If tests or compilers fail, self-correct autonomously in a closed loop.
|
|
603
|
+
5. Provide crisp, non-fluff, production-grade results with verifiable diffs.
|
|
604
|
+
|
|
605
|
+
Available Tools:
|
|
606
|
+
- `triage_and_heal_incident`: Deep multi-language crash analysis (Python, Node, Rust, Go, C++, Docker, GitHub Actions).
|
|
607
|
+
- `verify_code_file`: Closed-loop ground-truth AST & pytest verification.
|
|
608
|
+
- `apply_surgical_patch`: Surgical search/replace block patcher with auto-rollback.
|
|
609
|
+
- `resolve_git_merge_conflict`: 3-way AST merge conflict resolver.
|
|
610
|
+
- `inspect_repo_structure`: Symbol map of classes, functions, and dependencies.
|
|
611
|
+
- `search_offline_docs`: Local SQLite FTS5 documentation lookup.
|
|
612
|
+
- `generate_architecture_diagram`: Mermaid diagram generator.
|
|
613
|
+
- `generate_chaos_immunity_patch`: AST chaos prober, adversarial test suite generator, and auto-inoculation patcher.
|
|
614
|
+
|
|
615
|
+
Always prioritize safe, minimal, surgical edits and ground-truth verification.
|
|
616
|
+
"""
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# ==============================================================================
|
|
620
|
+
# STRANDS AGENT FACTORY & RUNNER
|
|
621
|
+
# ==============================================================================
|
|
622
|
+
|
|
623
|
+
class StrandsModelFactory:
|
|
624
|
+
"""Creates the appropriate Strands Model instance based on configuration and available API keys."""
|
|
625
|
+
|
|
626
|
+
@staticmethod
|
|
627
|
+
def create_model(
|
|
628
|
+
provider: str = "auto",
|
|
629
|
+
model_name: Optional[str] = None,
|
|
630
|
+
aws_region: Optional[str] = None,
|
|
631
|
+
) -> Any:
|
|
632
|
+
"""Instantiates a Strands Model provider (Bedrock, Gemini, Anthropic, OpenAI, or Ollama)."""
|
|
633
|
+
# Ensure credentials from key.json / .env are in os.environ
|
|
634
|
+
if CredentialsManager is not None:
|
|
635
|
+
try:
|
|
636
|
+
CredentialsManager.load_all_credentials()
|
|
637
|
+
except Exception:
|
|
638
|
+
pass
|
|
639
|
+
|
|
640
|
+
provider = provider.lower()
|
|
641
|
+
|
|
642
|
+
# 0. Deterministic Mock / Offline Mode
|
|
643
|
+
if provider in ("mock", "offline", "deterministic", "local_deterministic"):
|
|
644
|
+
return None
|
|
645
|
+
|
|
646
|
+
# 1. Explicit Amazon Bedrock
|
|
647
|
+
if provider in ("bedrock", "aws") or (
|
|
648
|
+
provider == "auto" and (os.getenv("AWS_ACCESS_KEY_ID") or os.getenv("AWS_PROFILE") or os.getenv("AWS_DEFAULT_REGION") or os.getenv("AWS_REGION"))
|
|
649
|
+
):
|
|
650
|
+
if BedrockModel is not None:
|
|
651
|
+
model_id = model_name or os.getenv("BEDROCK_MODEL_ID", "anthropic.claude-3-5-sonnet-20241022-v2:0")
|
|
652
|
+
region = aws_region or os.getenv("AWS_DEFAULT_REGION") or os.getenv("AWS_REGION", "us-east-1")
|
|
653
|
+
try:
|
|
654
|
+
logger.info(f"Initializing Strands BedrockModel: {model_id} in {region}")
|
|
655
|
+
return BedrockModel(model_id=model_id, region_name=region)
|
|
656
|
+
except Exception as e:
|
|
657
|
+
logger.warning(f"Failed to initialize BedrockModel ({e}), falling back...")
|
|
658
|
+
|
|
659
|
+
# 2. Google Gemini
|
|
660
|
+
if provider in ("gemini", "google") or (
|
|
661
|
+
provider == "auto" and (os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY"))
|
|
662
|
+
):
|
|
663
|
+
if GeminiModel is not None:
|
|
664
|
+
m_id = model_name or os.getenv("GEMINI_MODEL_ID", "gemini-2.5-flash")
|
|
665
|
+
try:
|
|
666
|
+
logger.info(f"Initializing Strands GeminiModel: {m_id}")
|
|
667
|
+
return GeminiModel(model_id=m_id)
|
|
668
|
+
except Exception as e:
|
|
669
|
+
logger.warning(f"Failed to initialize GeminiModel ({e}), falling back...")
|
|
670
|
+
|
|
671
|
+
# 3. Anthropic Claude Direct
|
|
672
|
+
if provider in ("anthropic", "claude") or (provider == "auto" and os.getenv("ANTHROPIC_API_KEY")):
|
|
673
|
+
if AnthropicModel is not None:
|
|
674
|
+
m_id = model_name or "claude-3-5-sonnet-20241022"
|
|
675
|
+
try:
|
|
676
|
+
logger.info(f"Initializing Strands AnthropicModel: {m_id}")
|
|
677
|
+
return AnthropicModel(model_id=m_id)
|
|
678
|
+
except Exception as e:
|
|
679
|
+
logger.warning(f"Failed to initialize AnthropicModel ({e}), falling back...")
|
|
680
|
+
|
|
681
|
+
# 4. OpenAI
|
|
682
|
+
if provider in ("openai", "gpt") or (provider == "auto" and os.getenv("OPENAI_API_KEY")):
|
|
683
|
+
if OpenAIModel is not None:
|
|
684
|
+
m_id = model_name or "gpt-4o"
|
|
685
|
+
try:
|
|
686
|
+
logger.info(f"Initializing Strands OpenAIModel: {m_id}")
|
|
687
|
+
return OpenAIModel(model_id=m_id)
|
|
688
|
+
except Exception as e:
|
|
689
|
+
logger.warning(f"Failed to initialize OpenAIModel ({e}), falling back...")
|
|
690
|
+
|
|
691
|
+
# 5. Local Ollama Fallback
|
|
692
|
+
if provider in ("ollama", "local") or provider == "auto":
|
|
693
|
+
if OllamaModel is not None:
|
|
694
|
+
m_id = model_name or "qwen2.5-coder:1.5b"
|
|
695
|
+
try:
|
|
696
|
+
logger.info(f"Initializing Strands OllamaModel: {m_id}")
|
|
697
|
+
return OllamaModel(model_id=m_id, host=os.getenv("OLLAMA_HOST", "http://localhost:11434"))
|
|
698
|
+
except Exception as e:
|
|
699
|
+
logger.warning(f"Failed to initialize OllamaModel: {e}")
|
|
700
|
+
|
|
701
|
+
# Fallback to Bedrock only if explicit AWS credentials/config are present
|
|
702
|
+
if BedrockModel is not None and (os.getenv("AWS_ACCESS_KEY_ID") or os.getenv("AWS_PROFILE")):
|
|
703
|
+
try:
|
|
704
|
+
return BedrockModel(model_id="anthropic.claude-3-5-sonnet-20241022-v2:0")
|
|
705
|
+
except Exception:
|
|
706
|
+
pass
|
|
707
|
+
|
|
708
|
+
return None
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
class StrandsDevAgent:
|
|
712
|
+
"""High-level autonomous developer agent wrapping the AWS Strands Agents SDK."""
|
|
713
|
+
|
|
714
|
+
def __init__(
|
|
715
|
+
self,
|
|
716
|
+
provider: str = "auto",
|
|
717
|
+
model_name: Optional[str] = None,
|
|
718
|
+
aws_region: Optional[str] = None,
|
|
719
|
+
custom_tools: Optional[List[Any]] = None,
|
|
720
|
+
):
|
|
721
|
+
self.provider = provider
|
|
722
|
+
self.model_name = model_name
|
|
723
|
+
self.aws_region = aws_region
|
|
724
|
+
self.tools = custom_tools or STRANDS_DEV_TOOLS
|
|
725
|
+
self._agent_instance = None
|
|
726
|
+
self._init_agent()
|
|
727
|
+
|
|
728
|
+
def _init_agent(self) -> None:
|
|
729
|
+
"""Initializes the underlying Strands Agent instance."""
|
|
730
|
+
if not STRANDS_AVAILABLE:
|
|
731
|
+
logger.warning("Strands SDK not available. Running in headless compatibility mode.")
|
|
732
|
+
return
|
|
733
|
+
|
|
734
|
+
if self.provider in ("mock", "offline", "deterministic", "local_deterministic"):
|
|
735
|
+
self._agent_instance = None
|
|
736
|
+
return
|
|
737
|
+
|
|
738
|
+
model = StrandsModelFactory.create_model(
|
|
739
|
+
provider=self.provider,
|
|
740
|
+
model_name=self.model_name,
|
|
741
|
+
aws_region=self.aws_region,
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
try:
|
|
745
|
+
if model is not None:
|
|
746
|
+
self._agent_instance = Agent(
|
|
747
|
+
model=model,
|
|
748
|
+
system_prompt=STRANDS_SYSTEM_PROMPT,
|
|
749
|
+
tools=self.tools,
|
|
750
|
+
)
|
|
751
|
+
logger.info("StrandsDevAgent successfully initialized with live model and tools.")
|
|
752
|
+
else:
|
|
753
|
+
self._agent_instance = None
|
|
754
|
+
logger.info("StrandsDevAgent running in local deterministic mode.")
|
|
755
|
+
except Exception as e:
|
|
756
|
+
logger.exception(f"Error creating Strands Agent instance: {e}")
|
|
757
|
+
self._agent_instance = None
|
|
758
|
+
|
|
759
|
+
async def a_run(self, prompt: str) -> str:
|
|
760
|
+
"""Executes the autonomous agent asynchronously."""
|
|
761
|
+
if self._agent_instance is None:
|
|
762
|
+
# Fallback deterministic execution if Strands model could not be connected
|
|
763
|
+
return self._fallback_deterministic_execution(prompt)
|
|
764
|
+
|
|
765
|
+
try:
|
|
766
|
+
# Strands Agent run / invoke
|
|
767
|
+
if hasattr(self._agent_instance, "run_async"):
|
|
768
|
+
response = await self._agent_instance.run_async(prompt)
|
|
769
|
+
return str(response)
|
|
770
|
+
elif hasattr(self._agent_instance, "run"):
|
|
771
|
+
loop = asyncio.get_event_loop()
|
|
772
|
+
response = await loop.run_in_executor(None, self._agent_instance.run, prompt)
|
|
773
|
+
return str(response)
|
|
774
|
+
elif callable(self._agent_instance):
|
|
775
|
+
res = self._agent_instance(prompt)
|
|
776
|
+
return str(res)
|
|
777
|
+
return "Strands Agent completed execution."
|
|
778
|
+
except Exception as e:
|
|
779
|
+
logger.error(f"Strands Agent execution error: {e}")
|
|
780
|
+
return self._fallback_deterministic_execution(prompt, error=str(e))
|
|
781
|
+
|
|
782
|
+
def run(self, prompt: str) -> str:
|
|
783
|
+
"""Synchronous wrapper for agent execution."""
|
|
784
|
+
try:
|
|
785
|
+
return asyncio.run(self.a_run(prompt))
|
|
786
|
+
except RuntimeError:
|
|
787
|
+
loop = asyncio.get_event_loop()
|
|
788
|
+
return loop.run_until_complete(self.a_run(prompt))
|
|
789
|
+
|
|
790
|
+
def _fallback_deterministic_execution(self, prompt: str, error: Optional[str] = None) -> str:
|
|
791
|
+
"""Deterministic rule-based fallback when model endpoints are unreachable."""
|
|
792
|
+
output = [
|
|
793
|
+
"# 🤖 K-CLI Strands Autonomous Agent (Local Deterministic Mode)",
|
|
794
|
+
f"**Goal**: {prompt}",
|
|
795
|
+
]
|
|
796
|
+
if error:
|
|
797
|
+
output.append(f"> Note: Strands live model fallback triggered ({error})")
|
|
798
|
+
|
|
799
|
+
# Auto-detect if prompt contains crash traceback or error
|
|
800
|
+
if any(kw in prompt for kw in ("Traceback", "Error", "panic:", "exit code", "failed", "##[error]")):
|
|
801
|
+
triage_res = triage_and_heal_incident(prompt)
|
|
802
|
+
output.extend([
|
|
803
|
+
"",
|
|
804
|
+
"## 🔍 Incident Triage & Auto-Heal Result",
|
|
805
|
+
f"```json\n{triage_res}\n```",
|
|
806
|
+
])
|
|
807
|
+
else:
|
|
808
|
+
output.extend([
|
|
809
|
+
"",
|
|
810
|
+
"## 🛠️ Available Strands Tools Registered",
|
|
811
|
+
"- `triage_and_heal_incident` (Multi-Language Crash & Traceback Parser)",
|
|
812
|
+
"- `verify_code_file` (Closed-Loop Ground-Truth AST Verifier)",
|
|
813
|
+
"- `apply_surgical_patch` (Surgical Search/Replace Patcher)",
|
|
814
|
+
"- `resolve_git_merge_conflict` (3-Way AST Merge Conflict Resolver)",
|
|
815
|
+
"- `inspect_repo_structure` (AST Repo Symbol Map)",
|
|
816
|
+
"- `search_offline_docs` (Embedded SQLite FTS5 DevDocs)",
|
|
817
|
+
"- `generate_architecture_diagram` (Mermaid Architecture Generator)",
|
|
818
|
+
])
|
|
819
|
+
return "\n".join(output)
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def create_strands_agent(
|
|
823
|
+
provider: str = "auto",
|
|
824
|
+
model_name: Optional[str] = None,
|
|
825
|
+
aws_region: Optional[str] = None,
|
|
826
|
+
) -> StrandsDevAgent:
|
|
827
|
+
"""Convenience helper to create a configured StrandsDevAgent."""
|
|
828
|
+
return StrandsDevAgent(
|
|
829
|
+
provider=provider,
|
|
830
|
+
model_name=model_name,
|
|
831
|
+
aws_region=aws_region,
|
|
832
|
+
)
|