gobby 0.2.5__py3-none-any.whl → 0.2.7__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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +111 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
gobby/tasks/research.py
DELETED
|
@@ -1,421 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Agentic codebase research for task expansion.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import ast
|
|
6
|
-
import logging
|
|
7
|
-
import os
|
|
8
|
-
import re
|
|
9
|
-
import shlex
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
from gobby.config.app import TaskExpansionConfig
|
|
14
|
-
from gobby.llm import LLMService
|
|
15
|
-
from gobby.storage.tasks import Task
|
|
16
|
-
from gobby.utils.project_context import find_project_root
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class TaskResearchAgent:
|
|
22
|
-
"""
|
|
23
|
-
Agent that autonomously researches the codebase to gather context for a task.
|
|
24
|
-
|
|
25
|
-
Implements a simple ReAct loop:
|
|
26
|
-
1. Think: Analyze current context and decide next action
|
|
27
|
-
2. Act: Execute a tool (glob, grep, read_file)
|
|
28
|
-
3. Observe: Add tool output to context
|
|
29
|
-
4. Repeat until done or timeout
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
def __init__(
|
|
33
|
-
self,
|
|
34
|
-
config: TaskExpansionConfig,
|
|
35
|
-
llm_service: LLMService,
|
|
36
|
-
mcp_manager: Any | None = None,
|
|
37
|
-
):
|
|
38
|
-
self.config = config
|
|
39
|
-
self.llm_service = llm_service
|
|
40
|
-
self.mcp_manager = mcp_manager
|
|
41
|
-
self.max_steps = 10
|
|
42
|
-
self.root = find_project_root()
|
|
43
|
-
# Search tool discovery happens effectively at runtime now via _build_prompt
|
|
44
|
-
# but we keep the helper method if we want to pre-check.
|
|
45
|
-
|
|
46
|
-
async def run(
|
|
47
|
-
self,
|
|
48
|
-
task: Task,
|
|
49
|
-
enable_web_search: bool = False,
|
|
50
|
-
) -> dict[str, Any]:
|
|
51
|
-
"""
|
|
52
|
-
Run the research loop.
|
|
53
|
-
|
|
54
|
-
Args:
|
|
55
|
-
task: The task to research.
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
Dictionary containing gathered context (files, snippets, findings).
|
|
59
|
-
"""
|
|
60
|
-
if not self.root:
|
|
61
|
-
logger.warning("No project root found, skipping research")
|
|
62
|
-
return {"relevant_files": [], "findings": "No project root found"}
|
|
63
|
-
|
|
64
|
-
logger.info(f"Starting research for task {task.id}: {task.title}")
|
|
65
|
-
|
|
66
|
-
# Initialize context
|
|
67
|
-
context: dict[str, Any] = {
|
|
68
|
-
"task": task,
|
|
69
|
-
"history": [],
|
|
70
|
-
"found_files": set(),
|
|
71
|
-
"snippets": {},
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
# Select provider (use research_model if configured, else default)
|
|
75
|
-
model = self.config.research_model or self.config.model
|
|
76
|
-
provider = self.llm_service.get_provider(self.config.provider)
|
|
77
|
-
|
|
78
|
-
for step in range(self.max_steps):
|
|
79
|
-
# 1. Generate Thought/Action
|
|
80
|
-
prompt = await self._build_step_prompt(context, step, enable_web_search) # Made async
|
|
81
|
-
response = await provider.generate_text(
|
|
82
|
-
prompt=prompt,
|
|
83
|
-
system_prompt=self.config.research_system_prompt,
|
|
84
|
-
model=model,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# Parse action
|
|
88
|
-
action = self._parse_action(response)
|
|
89
|
-
context["history"].append(
|
|
90
|
-
{"role": "model", "content": response, "parsed_action": action}
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
if not action or action["tool"] == "done":
|
|
94
|
-
reason = action.get("reason", "No action") if action else "Failed to parse action"
|
|
95
|
-
logger.info(f"Research finished: {reason}")
|
|
96
|
-
break
|
|
97
|
-
|
|
98
|
-
# 2. Execute Action
|
|
99
|
-
tool_output = await self._execute_tool(action)
|
|
100
|
-
|
|
101
|
-
# 3. Observe
|
|
102
|
-
context["history"].append({"role": "tool", "content": tool_output})
|
|
103
|
-
logger.debug(f"Step {step} tool {action['tool']} output len: {len(tool_output)}")
|
|
104
|
-
|
|
105
|
-
return self._summarize_results(context)
|
|
106
|
-
|
|
107
|
-
async def _build_step_prompt(
|
|
108
|
-
self,
|
|
109
|
-
context: dict[str, Any],
|
|
110
|
-
step: int,
|
|
111
|
-
enable_web_search: bool = False,
|
|
112
|
-
) -> str:
|
|
113
|
-
task = context["task"]
|
|
114
|
-
history = context["history"]
|
|
115
|
-
|
|
116
|
-
prompt = f"""Task: {task.title}
|
|
117
|
-
Description: {task.description}
|
|
118
|
-
|
|
119
|
-
You are researching this task to identify relevant files and implementation details.
|
|
120
|
-
You have access to the following tools:
|
|
121
|
-
|
|
122
|
-
1. glob(pattern): Find files matching a pattern (e.g. "src/**/*.py")
|
|
123
|
-
2. grep(pattern, path): Search for text in files (e.g. "def login", "src/")
|
|
124
|
-
3. read_file(path): Read the content of a file
|
|
125
|
-
4. done(reason): Finish research
|
|
126
|
-
"""
|
|
127
|
-
# Add search tool if available and enabled
|
|
128
|
-
# Check both config global enable AND request-specific enable
|
|
129
|
-
# Note: config.web_research_enabled is the global "allowed" switch.
|
|
130
|
-
# enable_web_search is the per-request "requested" switch.
|
|
131
|
-
# We need BOTH to be true.
|
|
132
|
-
can_use_search = self.config.web_research_enabled and enable_web_search
|
|
133
|
-
|
|
134
|
-
if self.mcp_manager and can_use_search:
|
|
135
|
-
# Dynamically check for search tool
|
|
136
|
-
# We prefer 'search_web' if available, else others
|
|
137
|
-
tools = await self.mcp_manager.list_tools() # Assuming this API
|
|
138
|
-
# Flatten tools list
|
|
139
|
-
all_tools = []
|
|
140
|
-
for _server, server_tools in tools.items():
|
|
141
|
-
all_tools.extend(server_tools)
|
|
142
|
-
|
|
143
|
-
for t in all_tools:
|
|
144
|
-
if t.name in ("search_web", "google_search", "brave_search"):
|
|
145
|
-
prompt += f"5. {t.name}(query): {t.description[:100]}...\n"
|
|
146
|
-
break
|
|
147
|
-
|
|
148
|
-
prompt += f"""
|
|
149
|
-
Current Context:
|
|
150
|
-
Found Files: {list(context["found_files"])}
|
|
151
|
-
Snippets: {list(context["snippets"].keys())}
|
|
152
|
-
|
|
153
|
-
History:
|
|
154
|
-
"""
|
|
155
|
-
# Add limited history (last 5 turns to save context)
|
|
156
|
-
recent_history = history[-5:]
|
|
157
|
-
for item in recent_history:
|
|
158
|
-
if item["role"] == "model":
|
|
159
|
-
prompt += f"Agent: {item['content']}\n"
|
|
160
|
-
elif item["role"] == "tool":
|
|
161
|
-
# Truncate tool output
|
|
162
|
-
content = item["content"]
|
|
163
|
-
if len(content) > 500:
|
|
164
|
-
content = content[:500] + "... (truncated)"
|
|
165
|
-
prompt += f"Tool: {content}\n"
|
|
166
|
-
|
|
167
|
-
prompt += f"\nStep {step + 1}/{self.max_steps}. What is your next move? Respond with THOUGHT followed by ACTION."
|
|
168
|
-
return prompt
|
|
169
|
-
|
|
170
|
-
def _parse_action(self, response: str) -> dict[str, Any] | None:
|
|
171
|
-
"""
|
|
172
|
-
Parse LLM response for ACTION: tool_name(args).
|
|
173
|
-
|
|
174
|
-
Uses multiple parsing strategies in order of robustness:
|
|
175
|
-
1. ast.literal_eval for Python-style tuple syntax
|
|
176
|
-
2. shlex for shell-like quoting (handles commas in quotes)
|
|
177
|
-
3. Simple comma split as last resort
|
|
178
|
-
"""
|
|
179
|
-
# Check for explicit "ACTION: done" first (tighter than substring match)
|
|
180
|
-
# Matches: "ACTION: done", "ACTION: done(reason)", "ACTION: done("reason")"
|
|
181
|
-
done_match = re.search(
|
|
182
|
-
r"^ACTION:\s*done(?:\s*\(([^)]*)\))?",
|
|
183
|
-
response,
|
|
184
|
-
re.IGNORECASE | re.MULTILINE,
|
|
185
|
-
)
|
|
186
|
-
if done_match:
|
|
187
|
-
reason = done_match.group(1)
|
|
188
|
-
if reason:
|
|
189
|
-
reason = reason.strip().strip("'\"")
|
|
190
|
-
return {"tool": "done", "reason": reason or response}
|
|
191
|
-
|
|
192
|
-
# Parse ACTION: tool_name(args) pattern
|
|
193
|
-
# Use DOTALL to handle args spanning multiple lines
|
|
194
|
-
match = re.search(r"ACTION:\s*(\w+)\((.*)\)", response, re.IGNORECASE | re.DOTALL)
|
|
195
|
-
if not match:
|
|
196
|
-
return None
|
|
197
|
-
|
|
198
|
-
tool = match.group(1).lower()
|
|
199
|
-
args_str = match.group(2).strip()
|
|
200
|
-
|
|
201
|
-
# Handle done tool explicitly (in case it matched the general pattern)
|
|
202
|
-
if tool == "done":
|
|
203
|
-
return {"tool": "done", "reason": args_str.strip("'\"") or response}
|
|
204
|
-
|
|
205
|
-
# If no args, return empty args list
|
|
206
|
-
if not args_str:
|
|
207
|
-
return {"tool": tool, "args": []}
|
|
208
|
-
|
|
209
|
-
# Try multiple parsing strategies in order of robustness
|
|
210
|
-
args = None
|
|
211
|
-
parse_errors = []
|
|
212
|
-
|
|
213
|
-
# Strategy 1: ast.literal_eval as tuple
|
|
214
|
-
# Handles: "arg1", "arg2" → ('arg1', 'arg2')
|
|
215
|
-
# Handles escaped quotes, nested structures, etc.
|
|
216
|
-
try:
|
|
217
|
-
# Wrap in parens with trailing comma to make it a tuple
|
|
218
|
-
parsed = ast.literal_eval(f"({args_str},)")
|
|
219
|
-
args = [str(a) for a in parsed]
|
|
220
|
-
except (ValueError, SyntaxError) as e:
|
|
221
|
-
parse_errors.append(f"ast.literal_eval: {e}")
|
|
222
|
-
|
|
223
|
-
# Strategy 2: shlex-based parsing (handles shell-like quoting)
|
|
224
|
-
# Handles: "arg with spaces", 'single quotes', arg\ with\ escapes
|
|
225
|
-
if args is None:
|
|
226
|
-
try:
|
|
227
|
-
lexer = shlex.shlex(args_str, posix=True)
|
|
228
|
-
lexer.whitespace = ","
|
|
229
|
-
lexer.whitespace_split = True
|
|
230
|
-
args = [token.strip() for token in lexer]
|
|
231
|
-
except ValueError as e:
|
|
232
|
-
parse_errors.append(f"shlex: {e}")
|
|
233
|
-
|
|
234
|
-
# Strategy 3: Simple comma split as last resort
|
|
235
|
-
if args is None:
|
|
236
|
-
args = [a.strip().strip("'\"") for a in args_str.split(",")]
|
|
237
|
-
if not args or all(not a for a in args):
|
|
238
|
-
logger.error(
|
|
239
|
-
f"All parsing strategies failed for args: {args_str!r}. Errors: {parse_errors}"
|
|
240
|
-
)
|
|
241
|
-
return None
|
|
242
|
-
|
|
243
|
-
if parse_errors:
|
|
244
|
-
logger.debug(
|
|
245
|
-
f"Argument parsing recovered after failures: {parse_errors}. Final args: {args}"
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
return {"tool": tool, "args": args}
|
|
249
|
-
|
|
250
|
-
async def _execute_tool(self, action: dict[str, Any]) -> str:
|
|
251
|
-
tool = action["tool"]
|
|
252
|
-
args = action.get("args", [])
|
|
253
|
-
|
|
254
|
-
try:
|
|
255
|
-
if tool == "glob":
|
|
256
|
-
if not args:
|
|
257
|
-
return "Error: Missing pattern"
|
|
258
|
-
return self._glob(args[0])
|
|
259
|
-
elif tool == "grep":
|
|
260
|
-
if len(args) < 2:
|
|
261
|
-
return "Error: Missing pattern or path"
|
|
262
|
-
return self._grep(args[0], args[1])
|
|
263
|
-
elif tool == "read_file":
|
|
264
|
-
if not args:
|
|
265
|
-
return "Error: Missing path"
|
|
266
|
-
return self._read_file(args[0])
|
|
267
|
-
elif tool == "done":
|
|
268
|
-
return "Done"
|
|
269
|
-
|
|
270
|
-
# Check for MCP search tools
|
|
271
|
-
# We strictly check if the tool is one of the search tools we support
|
|
272
|
-
# The enable_web_search check was done at prompt time, but good to enforce here too
|
|
273
|
-
# However, execute_tool doesn't receive the flag currently.
|
|
274
|
-
# We rely on the model only calling it if presented in prompt.
|
|
275
|
-
if self.mcp_manager:
|
|
276
|
-
if tool in ("search_web", "google_search", "brave_search"):
|
|
277
|
-
if not args:
|
|
278
|
-
return "Error: Missing query"
|
|
279
|
-
# Call via MCP manager
|
|
280
|
-
# self.mcp_manager.call_tool returns Result object or dict
|
|
281
|
-
result = await self.mcp_manager.call_tool(tool, {"query": args[0]})
|
|
282
|
-
# Format result - assume it returns text or structured content
|
|
283
|
-
return str(result)
|
|
284
|
-
|
|
285
|
-
return f"Error: Unknown tool {tool}"
|
|
286
|
-
except Exception as e:
|
|
287
|
-
return f"Error executing {tool}: {e}"
|
|
288
|
-
|
|
289
|
-
def _glob(self, pattern: str) -> str:
|
|
290
|
-
if not self.root:
|
|
291
|
-
return "No root"
|
|
292
|
-
# Security: ensure pattern doesn't traverse up
|
|
293
|
-
if ".." in pattern:
|
|
294
|
-
return "Error: .. not allowed"
|
|
295
|
-
|
|
296
|
-
matches = []
|
|
297
|
-
try:
|
|
298
|
-
# Use rglob if ** in pattern, else glob
|
|
299
|
-
# Simplified: Use fnmatch on all files walking from root (safer but slower)
|
|
300
|
-
# Or use pathlib.glob
|
|
301
|
-
# Let's use pathlib glob
|
|
302
|
-
for path in self.root.glob(pattern):
|
|
303
|
-
if path.is_file():
|
|
304
|
-
matches.append(str(path.relative_to(self.root)))
|
|
305
|
-
if len(matches) > 50: # Limit results
|
|
306
|
-
break
|
|
307
|
-
except Exception as e:
|
|
308
|
-
return f"Glob error: {e}"
|
|
309
|
-
|
|
310
|
-
return "\n".join(matches) or "No matches found"
|
|
311
|
-
|
|
312
|
-
def _grep(self, pattern: str, path_str: str) -> str:
|
|
313
|
-
if not self.root:
|
|
314
|
-
return "No root"
|
|
315
|
-
search_path = (self.root / path_str).resolve()
|
|
316
|
-
if self.root not in search_path.parents and search_path != self.root:
|
|
317
|
-
return "Error: Path outside root"
|
|
318
|
-
|
|
319
|
-
# Simple recursive grep
|
|
320
|
-
# Limit to text files
|
|
321
|
-
results = []
|
|
322
|
-
|
|
323
|
-
is_dir = search_path.is_dir()
|
|
324
|
-
|
|
325
|
-
# If dir, walk. If file, search.
|
|
326
|
-
files_to_search = []
|
|
327
|
-
if is_dir:
|
|
328
|
-
for root, _, files in os.walk(search_path):
|
|
329
|
-
for f in files:
|
|
330
|
-
# Skip hidden and non-text (basic heuristic)
|
|
331
|
-
if f.startswith("."):
|
|
332
|
-
continue
|
|
333
|
-
if f.endswith((".pyc", ".png", ".jpg")):
|
|
334
|
-
continue
|
|
335
|
-
files_to_search.append(Path(root) / f)
|
|
336
|
-
else:
|
|
337
|
-
if search_path.exists():
|
|
338
|
-
files_to_search.append(search_path)
|
|
339
|
-
|
|
340
|
-
count = 0
|
|
341
|
-
for fpath in files_to_search:
|
|
342
|
-
if count > 20:
|
|
343
|
-
break # Limit files matched
|
|
344
|
-
try:
|
|
345
|
-
rel_path = fpath.relative_to(self.root)
|
|
346
|
-
with open(fpath, encoding="utf-8", errors="ignore") as fp:
|
|
347
|
-
content = fp.read()
|
|
348
|
-
if pattern in content:
|
|
349
|
-
# Extract snippet (one line context)
|
|
350
|
-
lines = content.splitlines()
|
|
351
|
-
for i, line in enumerate(lines):
|
|
352
|
-
if pattern in line:
|
|
353
|
-
results.append(f"{rel_path}:{i + 1}: {line.strip()}")
|
|
354
|
-
break # One match per file for brevity in overview
|
|
355
|
-
count += 1
|
|
356
|
-
except Exception:
|
|
357
|
-
continue # nosec B112 - skip files we can't read
|
|
358
|
-
|
|
359
|
-
return "\n".join(results) or "No matches found"
|
|
360
|
-
|
|
361
|
-
def _read_file(self, path_str: str) -> str:
|
|
362
|
-
if not self.root:
|
|
363
|
-
return "No root"
|
|
364
|
-
path = (self.root / path_str).resolve()
|
|
365
|
-
if self.root not in path.parents and path != self.root:
|
|
366
|
-
return "Error: Path outside root"
|
|
367
|
-
|
|
368
|
-
if not path.exists():
|
|
369
|
-
return "Error: File not found"
|
|
370
|
-
|
|
371
|
-
try:
|
|
372
|
-
with open(path, encoding="utf-8") as f:
|
|
373
|
-
content = f.read()
|
|
374
|
-
# Limit size
|
|
375
|
-
if len(content) > 5000:
|
|
376
|
-
return content[:5000] + "\n...(truncated)"
|
|
377
|
-
return content
|
|
378
|
-
except Exception as e:
|
|
379
|
-
return f"Read error: {e}"
|
|
380
|
-
|
|
381
|
-
def _summarize_results(self, context: dict[str, Any]) -> dict[str, Any]:
|
|
382
|
-
"""Convert agent history into structured context."""
|
|
383
|
-
# Extract files that were read or found relevant
|
|
384
|
-
found_files = set()
|
|
385
|
-
web_search_results: list[dict[str, Any]] = []
|
|
386
|
-
|
|
387
|
-
# Process history to extract files and web search results
|
|
388
|
-
history = context["history"]
|
|
389
|
-
i = 0
|
|
390
|
-
while i < len(history):
|
|
391
|
-
item = history[i]
|
|
392
|
-
if item["role"] == "model":
|
|
393
|
-
action = item.get("parsed_action")
|
|
394
|
-
if action:
|
|
395
|
-
tool = action["tool"]
|
|
396
|
-
args = action.get("args", [])
|
|
397
|
-
|
|
398
|
-
if tool == "read_file" and args:
|
|
399
|
-
found_files.add(args[0])
|
|
400
|
-
|
|
401
|
-
# Capture web search results (action followed by tool output)
|
|
402
|
-
if tool in ("search_web", "google_search", "brave_search") and args:
|
|
403
|
-
query = args[0]
|
|
404
|
-
# Look for the tool output in the next item
|
|
405
|
-
if i + 1 < len(history) and history[i + 1]["role"] == "tool":
|
|
406
|
-
result = history[i + 1]["content"]
|
|
407
|
-
web_search_results.append(
|
|
408
|
-
{
|
|
409
|
-
"tool": tool,
|
|
410
|
-
"query": query,
|
|
411
|
-
"result": result[:2000] if len(result) > 2000 else result,
|
|
412
|
-
}
|
|
413
|
-
)
|
|
414
|
-
i += 1
|
|
415
|
-
|
|
416
|
-
return {
|
|
417
|
-
"relevant_files": list(found_files),
|
|
418
|
-
"findings": "Agent research completed.",
|
|
419
|
-
"web_research": web_search_results,
|
|
420
|
-
"raw_history": history,
|
|
421
|
-
}
|