Revert "merge: tier2/phase2_4_5_call_site_completion_20260621 (parent + follow-up + Phase 6e analysis)"
This reverts commitf914b2bcd4, reversing changes made to7fef95cc87.
This commit is contained in:
+11
-19
@@ -39,8 +39,6 @@ from typing import Optional, Callable, Any, List, Union, cast, Iterable
|
||||
from src import project_manager
|
||||
from src import file_cache
|
||||
from src import mcp_client
|
||||
from src import mcp_tool_specs
|
||||
from src.openai_schemas import UsageStats
|
||||
from src import mma_prompts
|
||||
from src import performance_monitor
|
||||
from src import project_manager
|
||||
@@ -559,7 +557,7 @@ def _set_tool_preset_result(preset_name: Optional[str]) -> Result[None]:
|
||||
if preset_name in presets:
|
||||
preset = presets[preset_name]
|
||||
_active_tool_preset = preset
|
||||
new_tools = {name: False for name in mcp_tool_specs.tool_names()}
|
||||
new_tools = {name: False for name in mcp_client.TOOL_NAMES}
|
||||
new_tools[TOOL_NAME] = False
|
||||
for cat in preset.categories.values():
|
||||
for tool in cat:
|
||||
@@ -581,7 +579,7 @@ def set_tool_preset(preset_name: Optional[str]) -> None:
|
||||
_tool_approval_modes = {}
|
||||
if not preset_name or preset_name == "None":
|
||||
# Enable all tools if no preset
|
||||
_agent_tools = {name: True for name in mcp_tool_specs.tool_names()}
|
||||
_agent_tools = {name: True for name in mcp_client.TOOL_NAMES}
|
||||
_agent_tools[TOOL_NAME] = True
|
||||
_active_tool_preset = None
|
||||
else:
|
||||
@@ -1011,7 +1009,7 @@ async def _execute_single_tool_call_async(
|
||||
tool_executed = True
|
||||
|
||||
if not tool_executed:
|
||||
is_native = name in mcp_tool_specs.tool_names()
|
||||
is_native = name in mcp_client.TOOL_NAMES
|
||||
ext_tools = mcp_client.get_external_mcp_manager().get_all_tools()
|
||||
is_external = name in ext_tools
|
||||
if name and (is_native or is_external):
|
||||
@@ -2052,7 +2050,7 @@ def _send_gemini_cli(md_content: str, user_message: str, base_dir: str,
|
||||
|
||||
def _send(r_idx: int) -> NormalizedResponse:
|
||||
if adapter is None:
|
||||
return NormalizedResponse(text="(adapter unavailable)", tool_calls=(), usage=UsageStats(input_tokens=0, output_tokens=0), raw_response=None)
|
||||
return NormalizedResponse(text="(adapter unavailable)", tool_calls=[], usage_input_tokens=0, usage_output_tokens=0, usage_cache_read_tokens=0, usage_cache_creation_tokens=0, raw_response=None)
|
||||
send_result = _send_cli_round_result(r_idx, adapter, payload, safety_settings, sys_instr, stream_callback)
|
||||
if not send_result.ok:
|
||||
raise cast(Exception, send_result.errors[0].original) from None
|
||||
@@ -2086,7 +2084,7 @@ def _send_gemini_cli(md_content: str, user_message: str, base_dir: str,
|
||||
"kind": "history_add",
|
||||
"payload": {"role": "AI", "content": txt}
|
||||
})
|
||||
return NormalizedResponse(text=txt, tool_calls=(), usage=UsageStats(input_tokens=usage.get("prompt_tokens", 0), output_tokens=usage.get("completion_tokens", 0)), raw_response=resp_data)
|
||||
return NormalizedResponse(text=txt, tool_calls=calls, usage_input_tokens=usage.get("prompt_tokens", 0), usage_output_tokens=usage.get("completion_tokens", 0), usage_cache_read_tokens=0, usage_cache_creation_tokens=0, raw_response=resp_data)
|
||||
|
||||
def _pre_dispatch(r_idx: int, calls: list[Metadata]) -> list[Metadata]:
|
||||
nonlocal payload, cumulative_tool_bytes, file_items
|
||||
@@ -2570,7 +2568,6 @@ def _send_grok(md_content: str, user_message: str, base_dir: str,
|
||||
Runs synchronously in the caller thread; synchronizes Grok history using _grok_history_lock.
|
||||
"""
|
||||
from src.openai_compatible import OpenAICompatibleRequest, _classify_openai_compatible_error
|
||||
from src.openai_schemas import ChatMessage
|
||||
try:
|
||||
client = _ensure_grok_client()
|
||||
tools: list[Metadata] | None = _get_deepseek_tools() or None
|
||||
@@ -2587,9 +2584,8 @@ def _send_grok(md_content: str, user_message: str, base_dir: str,
|
||||
_grok_history.append({"role": "user", "content": user_content})
|
||||
def _build_grok_request(_round_idx: int) -> OpenAICompatibleRequest:
|
||||
with _grok_history_lock:
|
||||
history_msgs: list[ChatMessage] = [ChatMessage(role=m["role"], content=m["content"]) for m in _grok_history]
|
||||
messages: list[ChatMessage] = [ChatMessage(role="system", content=f"{_get_combined_system_prompt()}\n\n<context>\n{md_content}\n</context>")]
|
||||
messages.extend(history_msgs)
|
||||
messages: list[Metadata] = [{"role": "system", "content": f"{_get_combined_system_prompt()}\n\n<context>\n{md_content}\n</context>"}]
|
||||
messages.extend(_grok_history)
|
||||
extra_body: Metadata = {}
|
||||
if caps.web_search:
|
||||
extra_body["search_parameters"] = {"mode": "auto"}
|
||||
@@ -2657,7 +2653,6 @@ def _send_minimax(md_content: str, user_message: str, base_dir: str,
|
||||
Runs synchronously in the caller thread; synchronizes MiniMax history using _minimax_history_lock.
|
||||
"""
|
||||
from src.openai_compatible import OpenAICompatibleRequest
|
||||
from src.openai_schemas import ChatMessage
|
||||
try:
|
||||
_ensure_minimax_client()
|
||||
tools: list[Metadata] | None = _get_deepseek_tools() or None
|
||||
@@ -2668,9 +2663,8 @@ def _send_minimax(md_content: str, user_message: str, base_dir: str,
|
||||
_minimax_history.append({"role": "user", "content": user_message})
|
||||
def _build_minimax_request(_round_idx: int) -> OpenAICompatibleRequest:
|
||||
with _minimax_history_lock:
|
||||
history_msgs: list[ChatMessage] = [ChatMessage(role=m["role"], content=m["content"]) for m in _minimax_history]
|
||||
messages: list[ChatMessage] = [ChatMessage(role="system", content=f"{_get_combined_system_prompt()}\n\n<context>\n{md_content}\n</context>")]
|
||||
messages.extend(history_msgs)
|
||||
messages: list[Metadata] = [{"role": "system", "content": f"{_get_combined_system_prompt()}\n\n<context>\n{md_content}\n</context>"}]
|
||||
messages.extend(_minimax_history)
|
||||
return OpenAICompatibleRequest(
|
||||
messages=messages, model=_model, temperature=_temperature, top_p=_top_p,
|
||||
max_tokens=min(_max_tokens, 8192), stream=stream, stream_callback=stream_callback,
|
||||
@@ -2899,7 +2893,6 @@ def _send_llama(md_content: str, user_message: str, base_dir: str,
|
||||
Runs synchronously in the caller thread; synchronizes history using _llama_history_lock.
|
||||
"""
|
||||
from src.openai_compatible import OpenAICompatibleRequest, _classify_openai_compatible_error
|
||||
from src.openai_schemas import ChatMessage
|
||||
try:
|
||||
if "localhost" in _llama_base_url or "127.0.0.1" in _llama_base_url:
|
||||
return _send_llama_native(md_content, user_message, base_dir, file_items, discussion_history, stream, pre_tool_callback, qa_callback, stream_callback, patch_callback)
|
||||
@@ -2917,9 +2910,8 @@ def _send_llama(md_content: str, user_message: str, base_dir: str,
|
||||
_llama_history.append({"role": "user", "content": user_content})
|
||||
def _build_llama_request(_round_idx: int) -> OpenAICompatibleRequest:
|
||||
with _llama_history_lock:
|
||||
history_msgs: list[ChatMessage] = [ChatMessage(role=m["role"], content=m["content"]) for m in _llama_history]
|
||||
messages: list[ChatMessage] = [ChatMessage(role="system", content=f"{_get_combined_system_prompt()}\n\n<context>\n{md_content}\n</context>")]
|
||||
messages.extend(history_msgs)
|
||||
messages: list[Metadata] = [{"role": "system", "content": f"{_get_combined_system_prompt()}\n\n<context>\n{md_content}\n</context>"}]
|
||||
messages.extend(_llama_history)
|
||||
return OpenAICompatibleRequest(
|
||||
messages=messages, model=_model, temperature=_temperature, top_p=_top_p,
|
||||
max_tokens=_max_tokens, stream=stream, stream_callback=stream_callback,
|
||||
|
||||
+6
-14
@@ -10,17 +10,9 @@ import uuid
|
||||
# TODO(Ed): Eliminate these?
|
||||
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||
from typing import Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
from src.module_loader import _require_warmed
|
||||
from src.result_types import ErrorInfo, ErrorKind, Result
|
||||
from src.type_aliases import JsonValue
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WebSocketMessage:
|
||||
channel: str
|
||||
payload: JsonValue
|
||||
|
||||
|
||||
"""
|
||||
@@ -139,7 +131,7 @@ class HookServerInstance(ThreadingHTTPServer):
|
||||
super().__init__(server_address, RequestHandlerClass)
|
||||
self.app = app
|
||||
|
||||
def _serialize_for_api(obj: Any) -> JsonValue:
|
||||
def _serialize_for_api(obj: Any) -> Any:
|
||||
"""Serializes complex objects into API-friendly formats (dicts/lists)."""
|
||||
if hasattr(obj, "to_dict"):
|
||||
return obj.to_dict()
|
||||
@@ -980,12 +972,12 @@ class WebSocketServer:
|
||||
if self.thread:
|
||||
self.thread.join(timeout=2.0)
|
||||
|
||||
def broadcast(self, message: WebSocketMessage) -> None:
|
||||
def broadcast(self, channel: str, payload: dict[str, Any]) -> None:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._process_pending_gui_tasks, src/events.py:AsyncEventQueue.put, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
if not self.loop or message.channel not in self.clients:
|
||||
if not self.loop or channel not in self.clients:
|
||||
return
|
||||
wire = json.dumps({"channel": message.channel, "payload": message.payload})
|
||||
for ws in list(self.clients[message.channel]):
|
||||
asyncio.run_coroutine_threadsafe(ws.send(wire), self.loop)
|
||||
message = json.dumps({"channel": channel, "payload": payload})
|
||||
for ws in list(self.clients[channel]):
|
||||
asyncio.run_coroutine_threadsafe(ws.send(message), self.loop)
|
||||
|
||||
@@ -1841,13 +1841,12 @@ class AppController:
|
||||
|
||||
def _process_pending_gui_tasks(self) -> None:
|
||||
"""Processes pending GUI tasks from the queue on the main render thread."""
|
||||
from src.api_hooks import WebSocketMessage
|
||||
now = time.time()
|
||||
if hasattr(self, 'event_queue') and hasattr(self.event_queue, 'websocket_server') and self.event_queue.websocket_server:
|
||||
if now - self._last_telemetry_time >= 1.0:
|
||||
self._last_telemetry_time = now
|
||||
metrics = self.perf_monitor.get_metrics()
|
||||
self.event_queue.websocket_server.broadcast(WebSocketMessage(channel="telemetry", payload=metrics))
|
||||
self.event_queue.websocket_server.broadcast("telemetry", metrics)
|
||||
|
||||
if not self._pending_gui_tasks: return
|
||||
|
||||
|
||||
+1
-3
@@ -34,8 +34,6 @@ import queue
|
||||
from pathlib import Path
|
||||
from typing import Callable, Any, Dict, List, Tuple, Optional
|
||||
|
||||
from src.api_hooks import WebSocketMessage
|
||||
|
||||
|
||||
class EventEmitter:
|
||||
"""
|
||||
@@ -114,7 +112,7 @@ class AsyncEventQueue:
|
||||
elif hasattr(payload, '__dict__'):
|
||||
serializable_payload = vars(payload)
|
||||
|
||||
self.websocket_server.broadcast(WebSocketMessage(channel="events", payload={"event": event_name, "payload": serializable_payload}))
|
||||
self.websocket_server.broadcast("events", {"event": event_name, "payload": serializable_payload})
|
||||
|
||||
def get(self) -> Tuple[str, Any]:
|
||||
"""
|
||||
|
||||
+46
-130
@@ -43,96 +43,12 @@ import os
|
||||
import tomli_w
|
||||
import tomllib
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from src.result_types import Result, ErrorInfo, ErrorKind
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SessionMetadata:
|
||||
message_count: int = 0
|
||||
errors: int = 0
|
||||
size_kb: int = 0
|
||||
whitelisted: bool = False
|
||||
reason: str = ''
|
||||
timestamp: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"message_count": self.message_count,
|
||||
"errors": self.errors,
|
||||
"size_kb": self.size_kb,
|
||||
"whitelisted": self.whitelisted,
|
||||
"reason": self.reason,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Session:
|
||||
session_id: str
|
||||
path: str
|
||||
start_time: str
|
||||
whitelisted: bool = False
|
||||
metadata: Optional[SessionMetadata] = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
d: dict[str, Any] = {
|
||||
"path": self.path,
|
||||
"start_time": self.start_time,
|
||||
"whitelisted": self.whitelisted,
|
||||
}
|
||||
if self.metadata is not None:
|
||||
d["metadata"] = self.metadata.to_dict()
|
||||
else:
|
||||
d["metadata"] = None
|
||||
return d
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
"""Backward-compat: dict-like access (e.g., session['path'])."""
|
||||
if key == "path":
|
||||
return self.path
|
||||
if key == "start_time":
|
||||
return self.start_time
|
||||
if key == "whitelisted":
|
||||
return self.whitelisted
|
||||
if key == "metadata":
|
||||
return self.metadata.to_dict() if self.metadata is not None else None
|
||||
raise KeyError(key)
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""Backward-compat: dict.get."""
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, session_id: str, d: dict[str, Any]) -> Session:
|
||||
metadata_raw = d.get("metadata")
|
||||
metadata: Optional[SessionMetadata] = None
|
||||
if isinstance(metadata_raw, dict):
|
||||
metadata = SessionMetadata(
|
||||
message_count=int(metadata_raw.get("message_count", 0)),
|
||||
errors=int(metadata_raw.get("errors", 0)),
|
||||
size_kb=int(metadata_raw.get("size_kb", 0)),
|
||||
whitelisted=bool(metadata_raw.get("whitelisted", False)),
|
||||
reason=str(metadata_raw.get("reason", "")),
|
||||
timestamp=metadata_raw.get("timestamp"),
|
||||
)
|
||||
elif metadata_raw is not None:
|
||||
metadata = metadata_raw
|
||||
return cls(
|
||||
session_id=session_id,
|
||||
path=str(d.get("path", "")),
|
||||
start_time=str(d.get("start_time", "")),
|
||||
whitelisted=bool(d.get("whitelisted", False)),
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
||||
class LogRegistry:
|
||||
"""
|
||||
Manages a persistent registry of session logs using a TOML file.
|
||||
@@ -142,13 +58,13 @@ class LogRegistry:
|
||||
def __init__(self, registry_path: str) -> None:
|
||||
"""
|
||||
Initializes the LogRegistry with a path to the registry file.
|
||||
|
||||
|
||||
Args:
|
||||
registry_path (str): The file path to the TOML registry.
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.registry_path = registry_path
|
||||
self.data: dict[str, Session] = {}
|
||||
self.data: dict[str, dict[str, Any]] = {}
|
||||
self.load_registry()
|
||||
|
||||
@property
|
||||
@@ -177,7 +93,7 @@ class LogRegistry:
|
||||
m = new_session_data['metadata']
|
||||
if 'timestamp' in m and isinstance(m['timestamp'], datetime):
|
||||
m['timestamp'] = m['timestamp'].isoformat()
|
||||
self.data[session_id] = Session.from_dict(session_id, new_session_data)
|
||||
self.data[session_id] = new_session_data
|
||||
except Exception as e:
|
||||
print(f"Error loading registry from {self.registry_path}: {e}")
|
||||
self.data = {}
|
||||
@@ -193,14 +109,13 @@ class LogRegistry:
|
||||
try:
|
||||
# Convert datetime objects to ISO format strings for TOML serialization
|
||||
data_to_save: dict[str, Any] = {}
|
||||
for session_id, session in self.data.items():
|
||||
session_dict = session.to_dict()
|
||||
filtered: dict[str, Any] = {}
|
||||
for k, v in session_dict.items():
|
||||
for session_id, session_data in self.data.items():
|
||||
session_data_copy: dict[str, Any] = {}
|
||||
for k, v in session_data.items():
|
||||
if v is None:
|
||||
continue
|
||||
if k == 'start_time' and isinstance(v, datetime):
|
||||
filtered[k] = v.isoformat()
|
||||
session_data_copy[k] = v.isoformat()
|
||||
elif k == 'metadata' and isinstance(v, dict):
|
||||
metadata_copy: dict[str, Any] = {}
|
||||
for mk, mv in v.items():
|
||||
@@ -210,10 +125,10 @@ class LogRegistry:
|
||||
metadata_copy[mk] = mv.isoformat()
|
||||
else:
|
||||
metadata_copy[mk] = mv
|
||||
filtered[k] = metadata_copy
|
||||
session_data_copy[k] = metadata_copy
|
||||
else:
|
||||
filtered[k] = v
|
||||
data_to_save[session_id] = filtered
|
||||
session_data_copy[k] = v
|
||||
data_to_save[session_id] = session_data_copy
|
||||
with open(self.registry_path, 'wb') as f:
|
||||
tomli_w.dump(data_to_save, f)
|
||||
return Result(data=True)
|
||||
@@ -237,13 +152,12 @@ class LogRegistry:
|
||||
start_time_str = start_time.isoformat()
|
||||
else:
|
||||
start_time_str = start_time
|
||||
self.data[session_id] = Session(
|
||||
session_id=session_id,
|
||||
path=path,
|
||||
start_time=start_time_str,
|
||||
whitelisted=False,
|
||||
metadata=None,
|
||||
)
|
||||
self.data[session_id] = {
|
||||
'path': path,
|
||||
'start_time': start_time_str,
|
||||
'whitelisted': False,
|
||||
'metadata': None
|
||||
}
|
||||
self.save_registry()
|
||||
|
||||
def update_session_metadata(self, session_id: str, message_count: int, errors: int, size_kb: int, whitelisted: bool, reason: str) -> None:
|
||||
@@ -262,22 +176,21 @@ class LogRegistry:
|
||||
if session_id not in self.data:
|
||||
print(f"Error: Session ID '{session_id}' not found for metadata update.")
|
||||
return
|
||||
existing = self.data[session_id]
|
||||
new_metadata = SessionMetadata(
|
||||
message_count=message_count,
|
||||
errors=errors,
|
||||
size_kb=size_kb,
|
||||
whitelisted=whitelisted,
|
||||
reason=reason,
|
||||
timestamp=existing.metadata.timestamp if existing.metadata else None,
|
||||
)
|
||||
self.data[session_id] = Session(
|
||||
session_id=existing.session_id,
|
||||
path=existing.path,
|
||||
start_time=existing.start_time,
|
||||
whitelisted=whitelisted,
|
||||
metadata=new_metadata,
|
||||
)
|
||||
# Ensure metadata exists
|
||||
if self.data[session_id].get('metadata') is None:
|
||||
self.data[session_id]['metadata'] = {}
|
||||
# Update fields
|
||||
metadata = self.data[session_id].get('metadata')
|
||||
if isinstance(metadata, dict):
|
||||
metadata['message_count'] = message_count
|
||||
metadata['errors'] = errors
|
||||
metadata['size_kb'] = size_kb
|
||||
metadata['whitelisted'] = whitelisted
|
||||
metadata['reason'] = reason
|
||||
# self.data[session_id]['metadata']['timestamp'] = datetime.utcnow() # Optionally add a timestamp
|
||||
# Also update the top-level whitelisted flag if provided
|
||||
if whitelisted is not None:
|
||||
self.data[session_id]['whitelisted'] = whitelisted
|
||||
self.save_registry() # Save after update
|
||||
|
||||
def is_session_whitelisted(self, session_id: str) -> bool:
|
||||
@@ -289,12 +202,13 @@ class LogRegistry:
|
||||
|
||||
Returns:
|
||||
bool: True if whitelisted, False otherwise.
|
||||
[C: tests/test_auto_whitelist.py:test_auto_whitelist_keywords, tests/test_auto_whitelist.py:test_auto_whitelist_large_size, tests/test_auto_whitelist.py:test_auto_whitelist_message_count, tests/test_no_auto_whitelist_insignificant, tests/test_log_registry.py:TestLogRegistry.test_is_session_whitelisted, tests/test_logging_e2e.py:test_logging_e2e]
|
||||
[C: tests/test_auto_whitelist.py:test_auto_whitelist_keywords, tests/test_auto_whitelist.py:test_auto_whitelist_large_size, tests/test_auto_whitelist.py:test_auto_whitelist_message_count, tests/test_auto_whitelist.py:test_no_auto_whitelist_insignificant, tests/test_log_registry.py:TestLogRegistry.test_is_session_whitelisted, tests/test_logging_e2e.py:test_logging_e2e]
|
||||
"""
|
||||
session = self.data.get(session_id)
|
||||
if session is None:
|
||||
session_data = self.data.get(session_id)
|
||||
if session_data is None:
|
||||
return False # Non-existent sessions are not whitelisted
|
||||
return session.whitelisted
|
||||
# Check the top-level 'whitelisted' flag. If it's not set or False, it's not whitelisted.
|
||||
return bool(session_data.get('whitelisted', False))
|
||||
|
||||
def update_auto_whitelist_status(self, session_id: str) -> None:
|
||||
"""
|
||||
@@ -309,7 +223,7 @@ class LogRegistry:
|
||||
if session_id not in self.data:
|
||||
return
|
||||
session_data = self.data[session_id]
|
||||
session_path = session_data.path
|
||||
session_path = session_data.get('path')
|
||||
if not session_path or not os.path.isdir(str(session_path)):
|
||||
return
|
||||
total_size_bytes = 0
|
||||
@@ -371,9 +285,9 @@ class LogRegistry:
|
||||
[C: tests/test_log_pruner.py:test_prune_old_insignificant_logs, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_get_old_non_whitelisted_sessions_includes_empty_sessions, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_get_old_non_whitelisted_sessions_includes_sessions_without_metadata, tests/test_log_registry.py:TestLogRegistry.test_get_old_non_whitelisted_sessions]
|
||||
"""
|
||||
old_sessions = []
|
||||
for session_id, session in self.data.items():
|
||||
for session_id, session_data in self.data.items():
|
||||
# Check if session is older than cutoff and not whitelisted
|
||||
start_time_raw = session.start_time
|
||||
start_time_raw = session_data.get('start_time')
|
||||
if isinstance(start_time_raw, str):
|
||||
try:
|
||||
start_time = datetime.fromisoformat(start_time_raw)
|
||||
@@ -381,20 +295,22 @@ class LogRegistry:
|
||||
start_time = None
|
||||
else:
|
||||
start_time = start_time_raw
|
||||
is_whitelisted = session.whitelisted
|
||||
is_whitelisted = session_data.get('whitelisted', False)
|
||||
|
||||
# Heuristic: also include non-whitelisted sessions that have 0 messages or 0 KB size, or missing metadata
|
||||
metadata = session.metadata
|
||||
metadata = session_data.get('metadata')
|
||||
if metadata is None:
|
||||
is_empty = True
|
||||
else:
|
||||
is_empty = (metadata.message_count == 0 or metadata.size_kb == 0)
|
||||
message_count = metadata.get('message_count', -1)
|
||||
size_kb = metadata.get('size_kb', -1)
|
||||
is_empty = (message_count == 0 or size_kb == 0)
|
||||
|
||||
if not is_whitelisted:
|
||||
if is_empty or (start_time is not None and start_time < cutoff_datetime):
|
||||
old_sessions.append({
|
||||
'session_id': session_id,
|
||||
'path': session.path,
|
||||
'path': session_data.get('path'),
|
||||
'start_time': start_time_raw
|
||||
})
|
||||
return old_sessions
|
||||
|
||||
+780
-7
@@ -69,7 +69,6 @@ from typing import Optional, Callable, Any, cast
|
||||
from scripts import py_struct_tools
|
||||
|
||||
from src import beads_client
|
||||
from src import mcp_tool_specs
|
||||
from src import models
|
||||
from src import outline_tool
|
||||
from src import summarize
|
||||
@@ -1010,10 +1009,10 @@ def get_tree_result(path: str, max_depth: int = 2) -> Result[str]:
|
||||
entries = [e for e in entries if not e.name.startswith('.') and e.name not in ('__pycache__', 'venv', 'env') and e.name != "history.toml" and not e.name.endswith("_history.toml")]
|
||||
for i, entry in enumerate(entries):
|
||||
is_last = (i == len(entries) - 1)
|
||||
connector = "└── " if is_last else "├── "
|
||||
connector = "└── " if is_last else "├── "
|
||||
if entry.is_dir():
|
||||
lines.append(f"{prefix}{connector}{entry.name}/")
|
||||
extension = " " if is_last else "│ "
|
||||
extension = " " if is_last else "│ "
|
||||
lines.extend(_build_tree(entry, current_depth + 1, prefix + extension))
|
||||
else:
|
||||
lines.append(f"{prefix}{connector}{entry.name}")
|
||||
@@ -1942,7 +1941,7 @@ async def async_dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
"""
|
||||
[C: src/rag_engine.py:RAGEngine._async_search_mcp, tests/test_external_mcp.py:test_external_mcp_real_process]
|
||||
"""
|
||||
native_names = mcp_tool_specs.tool_names()
|
||||
native_names = {t['name'] for t in MCP_TOOL_SPECS}
|
||||
if tool_name in native_names:
|
||||
return await asyncio.to_thread(dispatch, tool_name, tool_input)
|
||||
|
||||
@@ -1954,9 +1953,9 @@ async def async_dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
|
||||
def get_tool_schemas() -> list[dict[str, Any]]:
|
||||
"""
|
||||
[C: tests/test_arch_boundary_phase2.py:TestArchBoundaryPhase2.test_mcp_client_dispatch_completeness, tests/test_external_mcp.py:test_get_tool_schemas_includes_external, tests/test_mcp_client.py:test_bd_mcp_tools]
|
||||
[C: tests/test_arch_boundary_phase2.py:TestArchBoundaryPhase2.test_mcp_client_dispatch_completeness, tests/test_external_mcp.py:test_get_tool_schemas_includes_external, tests/test_mcp_client_beads.py:test_bd_mcp_tools]
|
||||
"""
|
||||
res = [s.to_dict() for s in mcp_tool_specs.get_tool_schemas()]
|
||||
res = list(MCP_TOOL_SPECS)
|
||||
manager = get_external_mcp_manager()
|
||||
for tname, tinfo in manager.get_all_tools().items():
|
||||
res.append({
|
||||
@@ -1970,5 +1969,779 @@ def get_tool_schemas() -> list[dict[str, Any]]:
|
||||
# ------------------------------------------------------------------ tool schema helpers
|
||||
# These are imported by ai_client.py to build provider-specific declarations.
|
||||
|
||||
MCP_TOOL_SPECS: list[dict[str, Any]] = [
|
||||
{
|
||||
"name": "py_remove_def",
|
||||
"description": "Excises a specific class or function definition from a Python file using AST-derived line ranges, preserving surrounding formatting and comments.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path to the .py file." },
|
||||
"name": { "type": "string", "description": "The name of the class or function to remove. Use 'ClassName.method_name' for methods." }
|
||||
},
|
||||
"required": ["path", "name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_add_def",
|
||||
"description": "Inserts a new definition into a specific context (module level or within a specific class).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path to the .py file." },
|
||||
"name": { "type": "string", "description": "Context path (e.g. 'ClassName' or empty for module level)." },
|
||||
"new_content": { "type": "string", "description": "The code to insert." },
|
||||
"anchor_type": { "type": "string", "enum": ["before", "after", "top", "bottom"], "description": "Where to insert relative to the anchor." },
|
||||
"anchor_symbol": { "type": "string", "description": "Symbol name to anchor to if anchor_type is 'before' or 'after'." }
|
||||
},
|
||||
"required": ["path", "name", "new_content", "anchor_type"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_move_def",
|
||||
"description": "Relocates a definition within a file or across different Python files.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"src_path": { "type": "string", "description": "Path to the source .py file." },
|
||||
"dest_path": { "type": "string", "description": "Path to the destination .py file." },
|
||||
"name": { "type": "string", "description": "The name of the class or function to move." },
|
||||
"dest_name": { "type": "string", "description": "Context path in destination file (e.g. 'ClassName' or empty)." },
|
||||
"anchor_type": { "type": "string", "enum": ["before", "after", "top", "bottom"], "description": "Where to insert in destination." },
|
||||
"anchor_symbol": { "type": "string", "description": "Anchor symbol in destination." }
|
||||
},
|
||||
"required": ["src_path", "dest_path", "name", "dest_name", "anchor_type"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_region_wrap",
|
||||
"description": "Wraps a specified block of code (e.g., a set of methods) in #region: Name and #endregion: Name tags.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path to the .py file." },
|
||||
"start_line": { "type": "integer", "description": "1-based start line number." },
|
||||
"end_line": { "type": "integer", "description": "1-based end line number (inclusive)." },
|
||||
"region_name": { "type": "string", "description": "The name of the region." }
|
||||
},
|
||||
"required": ["path", "start_line", "end_line", "region_name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": (
|
||||
"Read the full UTF-8 content of a file within the allowed project paths. "
|
||||
"Use get_file_summary first to decide whether you need the full content."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute or relative path to the file to read.",
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "list_directory",
|
||||
"description": (
|
||||
"List files and subdirectories within an allowed directory. "
|
||||
"Shows name, type (file/dir), and size. Use this to explore the project structure."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the directory to list.",
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "search_files",
|
||||
"description": (
|
||||
"Search for files matching a glob pattern within an allowed directory. "
|
||||
"Supports recursive patterns like '**/*.py'. "
|
||||
"Use this to find files by extension or name pattern."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the directory to search within.",
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Glob pattern, e.g. '*.py', '**/*.toml', 'src/**/*.rs'.",
|
||||
},
|
||||
},
|
||||
"required": ["path", "pattern"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "get_file_summary",
|
||||
"description": (
|
||||
"Get a compact heuristic summary of a file without reading its full content. "
|
||||
"For Python: imports, classes, methods, functions, constants. "
|
||||
"For TOML: table keys. For Markdown: headings. Others: line count + preview. "
|
||||
"Use this before read_file to decide if you need the full content."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute or relative path to the file to summarise.",
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "py_get_skeleton",
|
||||
"description": (
|
||||
"Get a skeleton view of a Python file. "
|
||||
"This returns all classes and function signatures with their docstrings, "
|
||||
"but replaces function bodies with '...'. "
|
||||
"Use this to understand module interfaces without reading the full implementation."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the .py file.",
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "py_get_code_outline",
|
||||
"description": (
|
||||
"Get a hierarchical outline of a code file. "
|
||||
"This returns classes, functions, and methods with their line ranges and brief docstrings. "
|
||||
"Use this to quickly map out a file's structure before reading specific sections."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the code file (currently supports .py).",
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "ts_c_get_skeleton",
|
||||
"description": (
|
||||
"Get a skeleton view of a C file. "
|
||||
"This returns all function signatures and structs, "
|
||||
"but replaces function bodies with '...'. "
|
||||
"Use this to understand C interfaces without reading the full implementation."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the C file.",
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "ts_cpp_get_skeleton",
|
||||
"description": (
|
||||
"Get a skeleton view of a C++ file. "
|
||||
"This returns all classes, structs and function signatures, "
|
||||
"but replaces function bodies with '...'. "
|
||||
"Use this to understand C++ interfaces without reading the full implementation."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the C++ file.",
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "ts_c_get_code_outline",
|
||||
"description": (
|
||||
"Get a hierarchical outline of a C file. "
|
||||
"This returns structs and functions with their line ranges. "
|
||||
"Use this to quickly map out a file's structure before reading specific sections."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the C file.",
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "ts_cpp_get_code_outline",
|
||||
"description": (
|
||||
"Get a hierarchical outline of a C++ file. "
|
||||
"This returns classes, structs and functions with their line ranges. "
|
||||
"Use this to quickly map out a file's structure before reading specific sections."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the C++ file.",
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "ts_c_get_definition",
|
||||
"description": (
|
||||
"Get the full source code of a specific function or struct definition in a C file. "
|
||||
"This is more efficient than reading the whole file if you know what you're looking for."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the C file.",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the function or struct to retrieve.",
|
||||
}
|
||||
},
|
||||
"required": ["path", "name"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "ts_cpp_get_definition",
|
||||
"description": (
|
||||
"Get the full source code of a specific class, function, or method definition in a C++ file. "
|
||||
"This is more efficient than reading the whole file if you know what you're looking for."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the C++ file.",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the class or function to retrieve. Use 'ClassName::method_name' for methods.",
|
||||
}
|
||||
},
|
||||
"required": ["path", "name"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "ts_c_get_signature",
|
||||
"description": "Get only the signature part of a C function.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the C file."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the function."
|
||||
}
|
||||
},
|
||||
"required": ["path", "name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ts_cpp_get_signature",
|
||||
"description": "Get only the signature part of a C++ function or method.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the C++ file."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the function/method (e.g. 'ClassName::method_name')."
|
||||
}
|
||||
},
|
||||
"required": ["path", "name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ts_c_update_definition",
|
||||
"description": "Surgically replace the definition of a function in a C file using AST to find line ranges.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the C file."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of function."
|
||||
},
|
||||
"new_content": {
|
||||
"type": "string",
|
||||
"description": "Complete new source for the definition."
|
||||
}
|
||||
},
|
||||
"required": ["path", "name", "new_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ts_cpp_update_definition",
|
||||
"description": "Surgically replace the definition of a class or function in a C++ file using AST to find line ranges.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the C++ file."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of class/function/method."
|
||||
},
|
||||
"new_content": {
|
||||
"type": "string",
|
||||
"description": "Complete new source for the definition."
|
||||
}
|
||||
},
|
||||
"required": ["path", "name", "new_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_file_slice",
|
||||
"description": "Read a specific line range from a file. Useful for reading parts of very large files.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the file."
|
||||
},
|
||||
"start_line": {
|
||||
"type": "integer",
|
||||
"description": "1-based start line number."
|
||||
},
|
||||
"end_line": {
|
||||
"type": "integer",
|
||||
"description": "1-based end line number (inclusive)."
|
||||
}
|
||||
},
|
||||
"required": ["path", "start_line", "end_line"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "set_file_slice",
|
||||
"description": "Replace a specific line range in a file with new content. Surgical edit tool.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the file."
|
||||
},
|
||||
"start_line": {
|
||||
"type": "integer",
|
||||
"description": "1-based start line number."
|
||||
},
|
||||
"end_line": {
|
||||
"type": "integer",
|
||||
"description": "1-based end line number (inclusive)."
|
||||
},
|
||||
"new_content": {
|
||||
"type": "string",
|
||||
"description": "New content to insert."
|
||||
}
|
||||
},
|
||||
"required": ["path", "start_line", "end_line", "new_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "edit_file",
|
||||
"description": "Replace exact string match in a file. Preserves indentation and line endings. Drop-in replacement for native edit tool.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the file."
|
||||
},
|
||||
"old_string": {
|
||||
"type": "string",
|
||||
"description": "The text to replace."
|
||||
},
|
||||
"new_string": {
|
||||
"type": "string",
|
||||
"description": "The replacement text."
|
||||
},
|
||||
"replace_all": {
|
||||
"type": "boolean",
|
||||
"description": "Replace all occurrences. Default false."
|
||||
}
|
||||
},
|
||||
"required": ["path", "old_string", "new_string"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_get_definition",
|
||||
"description": (
|
||||
"Get the full source code of a specific class, function, or method definition. "
|
||||
"This is more efficient than reading the whole file if you know what you're looking for."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the .py file.",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the class or function to retrieve. Use 'ClassName.method_name' for methods.",
|
||||
}
|
||||
},
|
||||
"required": ["path", "name"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "py_update_definition",
|
||||
"description": "Surgically replace the definition of a class or function in a Python file using AST to find line ranges.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the .py file."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of class/function/method."
|
||||
},
|
||||
"new_content": {
|
||||
"type": "string",
|
||||
"description": "Complete new source for the definition."
|
||||
}
|
||||
},
|
||||
"required": ["path", "name", "new_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_get_signature",
|
||||
"description": "Get only the signature part of a Python function or method (from def until colon).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the .py file."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the function/method (e.g. 'ClassName.method_name')."
|
||||
}
|
||||
},
|
||||
"required": ["path", "name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_set_signature",
|
||||
"description": "Surgically replace only the signature of a Python function or method.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the .py file."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the function/method."
|
||||
},
|
||||
"new_signature": {
|
||||
"type": "string",
|
||||
"description": "Complete new signature string (including def and trailing colon)."
|
||||
}
|
||||
},
|
||||
"required": ["path", "name", "new_signature"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_get_class_summary",
|
||||
"description": "Get a summary of a Python class, listing its docstring and all method signatures.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the .py file."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the class."
|
||||
}
|
||||
},
|
||||
"required": ["path", "name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_get_var_declaration",
|
||||
"description": "Get the assignment/declaration line for a variable.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the .py file."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the variable."
|
||||
}
|
||||
},
|
||||
"required": ["path", "name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_set_var_declaration",
|
||||
"description": "Surgically replace a variable assignment/declaration.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the .py file."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the variable."
|
||||
},
|
||||
"new_declaration": {
|
||||
"type": "string",
|
||||
"description": "Complete new assignment/declaration string."
|
||||
}
|
||||
},
|
||||
"required": ["path", "name", "new_declaration"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_git_diff",
|
||||
"description": (
|
||||
"Returns the git diff for a file or directory. "
|
||||
"Use this to review changes efficiently without reading entire files."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the file or directory.",
|
||||
},
|
||||
"base_rev": {
|
||||
"type": "string",
|
||||
"description": "Base revision (e.g. 'HEAD', 'HEAD~1', or a commit hash). Defaults to 'HEAD'.",
|
||||
},
|
||||
"head_rev": {
|
||||
"type": "string",
|
||||
"description": "Head revision (optional).",
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "web_search",
|
||||
"description": "Search the web using DuckDuckGo. Returns the top 5 search results with titles, URLs, and snippets. Chain this with fetch_url to read specific pages.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query."
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fetch_url",
|
||||
"description": "Fetch the full text content of a URL (stripped of HTML tags). Use this after web_search to read relevant information from the web.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The full URL to fetch."
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_ui_performance",
|
||||
"description": "Get a snapshot of the current UI performance metrics, including FPS, Frame Time (ms), CPU usage (%), and Input Lag (ms). Use this to diagnose UI slowness or verify that your changes haven't degraded the user experience.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_find_usages",
|
||||
"description": "Finds exact string matches of a symbol in a given file or directory.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path to file or directory to search." },
|
||||
"name": { "type": "string", "description": "The symbol/string to search for." }
|
||||
},
|
||||
"required": ["path", "name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_get_imports",
|
||||
"description": "Parses a file's AST and returns a strict list of its dependencies.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path to the .py file." }
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_check_syntax",
|
||||
"description": "Runs a quick syntax check on a Python file.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path to the .py file." }
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_get_hierarchy",
|
||||
"description": "Scans the project to find subclasses of a given class.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Directory path to search in." },
|
||||
"class_name": { "type": "string", "description": "Name of the base class." }
|
||||
},
|
||||
"required": ["path", "class_name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "py_get_docstring",
|
||||
"description": "Extracts the docstring for a specific module, class, or function.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path to the .py file." },
|
||||
"name": { "type": "string", "description": "Name of symbol or 'module' for the file docstring." }
|
||||
},
|
||||
"required": ["path", "name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_tree",
|
||||
"description": "Returns a directory structure up to a max depth.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Directory path." },
|
||||
"max_depth": { "type": "integer", "description": "Maximum depth to recurse (default 2)." }
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bd_create",
|
||||
"description": "Create a new Bead in the active Beads repository.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": { "type": "string", "description": "Title of the Bead." },
|
||||
"description": { "type": "string", "description": "Description of the Bead." }
|
||||
},
|
||||
"required": ["title", "description"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bd_update",
|
||||
"description": "Update an existing Bead.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bead_id": { "type": "string", "description": "ID of the Bead to update." },
|
||||
"status": { "type": "string", "description": "New status for the Bead." }
|
||||
},
|
||||
"required": ["bead_id", "status"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bd_list",
|
||||
"description": "List all Beads in the active Beads repository.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bd_ready",
|
||||
"description": "Check if the Beads repository is initialized in the current workspace.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "derive_code_path",
|
||||
"description": (
|
||||
"Recursively traces the execution path of a specific function or method across multiple files. "
|
||||
"Identifies call chains and data hand-offs to build an intensive technical map."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Fully qualified name of the target (e.g., 'src.ai_client.send') or class.method.",
|
||||
},
|
||||
"max_depth": {
|
||||
"type": "integer",
|
||||
"description": "Maximum recursion depth for the call graph (default 5).",
|
||||
},
|
||||
},
|
||||
"required": ["target"],
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
TOOL_NAMES: set[str] = mcp_tool_specs.tool_names()
|
||||
TOOL_NAMES: set[str] = {t['name'] for t in MCP_TOOL_SPECS}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
"""Tool specification module for the Manual Slop MCP tool registry.
|
||||
|
||||
Promotes the legacy `MCP_TOOL_SPECS: list[dict[str, Any]]` from
|
||||
`src/mcp_client.py` to typed dataclass instances. Follows the
|
||||
`src/vendor_capabilities.py` reference pattern: `frozen=True` dataclass
|
||||
+ module-level `_REGISTRY` dict + factory functions.
|
||||
|
||||
Each tool has:
|
||||
- name (str): unique tool identifier
|
||||
- description (str): human-readable purpose
|
||||
- parameters (tuple[ToolParameter, ...]): the parameter schema
|
||||
|
||||
The legacy dict shape (JSON-compatible) is preserved via `to_dict()` so
|
||||
downstream consumers (provider API requests, comms logging) can still
|
||||
serialize tool specs to JSON without knowing the dataclass layout.
|
||||
|
||||
CONVENTION: 1-space indentation. NO COMMENTS.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolParameter:
|
||||
name: str
|
||||
type: str
|
||||
description: str
|
||||
required: bool = False
|
||||
enum: tuple[str, ...] | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
d: dict[str, Any] = {"type": self.type, "description": self.description}
|
||||
if self.enum is not None:
|
||||
d["enum"] = list(self.enum)
|
||||
return d
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolSpec:
|
||||
name: str
|
||||
description: str
|
||||
parameters: tuple[ToolParameter, ...]
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
properties: dict[str, Any] = {p.name: p.to_dict() for p in self.parameters}
|
||||
required: list[str] = [p.name for p in self.parameters if p.required]
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
_REGISTRY: dict[str, ToolSpec] = {}
|
||||
|
||||
|
||||
def register(spec: ToolSpec) -> None:
|
||||
_REGISTRY[spec.name] = spec
|
||||
|
||||
|
||||
def get_tool_spec(name: str) -> ToolSpec:
|
||||
if name not in _REGISTRY:
|
||||
raise KeyError(f"No tool registered with name {name!r}")
|
||||
return _REGISTRY[name]
|
||||
|
||||
|
||||
def get_tool_schemas() -> list[ToolSpec]:
|
||||
return list(_REGISTRY.values())
|
||||
|
||||
|
||||
def tool_names() -> set[str]:
|
||||
return set(_REGISTRY.keys())
|
||||
|
||||
register(ToolSpec(name='py_remove_def', description='Excises a specific class or function definition from a Python file using AST-derived line ranges, preserving surrounding formatting and comments.', parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True), ToolParameter( name='name', type='string', description="The name of the class or function to remove. Use 'ClassName.method_name' for methods.", required=True))))
|
||||
register(ToolSpec(name='py_add_def', description='Inserts a new definition into a specific context (module level or within a specific class).', parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True), ToolParameter( name='name', type='string', description="Context path (e.g. 'ClassName' or empty for module level).", required=True), ToolParameter( name='new_content', type='string', description='The code to insert.', required=True), ToolParameter( name='anchor_type', type='string', description='Where to insert relative to the anchor.', required=True, enum=('before', 'after', 'top', 'bottom',)), ToolParameter( name='anchor_symbol', type='string', description="Symbol name to anchor to if anchor_type is 'before' or 'after'."))))
|
||||
register(ToolSpec(name='py_move_def', description='Relocates a definition within a file or across different Python files.', parameters=(ToolParameter( name='src_path', type='string', description='Path to the source .py file.', required=True), ToolParameter( name='dest_path', type='string', description='Path to the destination .py file.', required=True), ToolParameter( name='name', type='string', description='The name of the class or function to move.', required=True), ToolParameter( name='dest_name', type='string', description="Context path in destination file (e.g. 'ClassName' or empty).", required=True), ToolParameter( name='anchor_type', type='string', description='Where to insert in destination.', required=True, enum=('before', 'after', 'top', 'bottom',)), ToolParameter( name='anchor_symbol', type='string', description='Anchor symbol in destination.'))))
|
||||
register(ToolSpec(name='py_region_wrap', description='Wraps a specified block of code (e.g., a set of methods) in #region: Name and #endregion: Name tags.', parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True), ToolParameter( name='start_line', type='integer', description='1-based start line number.', required=True), ToolParameter( name='end_line', type='integer', description='1-based end line number (inclusive).', required=True), ToolParameter( name='region_name', type='string', description='The name of the region.', required=True))))
|
||||
register(ToolSpec(name='read_file', description='Read the full UTF-8 content of a file within the allowed project paths. Use get_file_summary first to decide whether you need the full content.', parameters=(ToolParameter( name='path', type='string', description='Absolute or relative path to the file to read.', required=True),)))
|
||||
register(ToolSpec(name='list_directory', description='List files and subdirectories within an allowed directory. Shows name, type (file/dir), and size. Use this to explore the project structure.', parameters=(ToolParameter( name='path', type='string', description='Absolute path to the directory to list.', required=True),)))
|
||||
register(ToolSpec(name='search_files', description="Search for files matching a glob pattern within an allowed directory. Supports recursive patterns like '**/*.py'. Use this to find files by extension or name pattern.", parameters=(ToolParameter( name='path', type='string', description='Absolute path to the directory to search within.', required=True), ToolParameter( name='pattern', type='string', description="Glob pattern, e.g. '*.py', '**/*.toml', 'src/**/*.rs'.", required=True))))
|
||||
register(ToolSpec(name='get_file_summary', description='Get a compact heuristic summary of a file without reading its full content. For Python: imports, classes, methods, functions, constants. For TOML: table keys. For Markdown: headings. Others: line count + preview. Use this before read_file to decide if you need the full content.', parameters=(ToolParameter( name='path', type='string', description='Absolute or relative path to the file to summarise.', required=True),)))
|
||||
register(ToolSpec(name='py_get_skeleton', description="Get a skeleton view of a Python file. This returns all classes and function signatures with their docstrings, but replaces function bodies with '...'. Use this to understand module interfaces without reading the full implementation.", parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True),)))
|
||||
register(ToolSpec(name='py_get_code_outline', description="Get a hierarchical outline of a code file. This returns classes, functions, and methods with their line ranges and brief docstrings. Use this to quickly map out a file's structure before reading specific sections.", parameters=(ToolParameter( name='path', type='string', description='Path to the code file (currently supports .py).', required=True),)))
|
||||
register(ToolSpec(name='ts_c_get_skeleton', description="Get a skeleton view of a C file. This returns all function signatures and structs, but replaces function bodies with '...'. Use this to understand C interfaces without reading the full implementation.", parameters=(ToolParameter( name='path', type='string', description='Path to the C file.', required=True),)))
|
||||
register(ToolSpec(name='ts_cpp_get_skeleton', description="Get a skeleton view of a C++ file. This returns all classes, structs and function signatures, but replaces function bodies with '...'. Use this to understand C++ interfaces without reading the full implementation.", parameters=(ToolParameter( name='path', type='string', description='Path to the C++ file.', required=True),)))
|
||||
register(ToolSpec(name='ts_c_get_code_outline', description="Get a hierarchical outline of a C file. This returns structs and functions with their line ranges. Use this to quickly map out a file's structure before reading specific sections.", parameters=(ToolParameter( name='path', type='string', description='Path to the C file.', required=True),)))
|
||||
register(ToolSpec(name='ts_cpp_get_code_outline', description="Get a hierarchical outline of a C++ file. This returns classes, structs and functions with their line ranges. Use this to quickly map out a file's structure before reading specific sections.", parameters=(ToolParameter( name='path', type='string', description='Path to the C++ file.', required=True),)))
|
||||
register(ToolSpec(name='ts_c_get_definition', description="Get the full source code of a specific function or struct definition in a C file. This is more efficient than reading the whole file if you know what you're looking for.", parameters=(ToolParameter( name='path', type='string', description='Path to the C file.', required=True), ToolParameter( name='name', type='string', description='The name of the function or struct to retrieve.', required=True))))
|
||||
register(ToolSpec(name='ts_cpp_get_definition', description="Get the full source code of a specific class, function, or method definition in a C++ file. This is more efficient than reading the whole file if you know what you're looking for.", parameters=(ToolParameter( name='path', type='string', description='Path to the C++ file.', required=True), ToolParameter( name='name', type='string', description="The name of the class or function to retrieve. Use 'ClassName::method_name' for methods.", required=True))))
|
||||
register(ToolSpec(name='ts_c_get_signature', description='Get only the signature part of a C function.', parameters=(ToolParameter( name='path', type='string', description='Path to the C file.', required=True), ToolParameter( name='name', type='string', description='Name of the function.', required=True))))
|
||||
register(ToolSpec(name='ts_cpp_get_signature', description='Get only the signature part of a C++ function or method.', parameters=(ToolParameter( name='path', type='string', description='Path to the C++ file.', required=True), ToolParameter( name='name', type='string', description="Name of the function/method (e.g. 'ClassName::method_name').", required=True))))
|
||||
register(ToolSpec(name='ts_c_update_definition', description='Surgically replace the definition of a function in a C file using AST to find line ranges.', parameters=(ToolParameter( name='path', type='string', description='Path to the C file.', required=True), ToolParameter( name='name', type='string', description='Name of function.', required=True), ToolParameter( name='new_content', type='string', description='Complete new source for the definition.', required=True))))
|
||||
register(ToolSpec(name='ts_cpp_update_definition', description='Surgically replace the definition of a class or function in a C++ file using AST to find line ranges.', parameters=(ToolParameter( name='path', type='string', description='Path to the C++ file.', required=True), ToolParameter( name='name', type='string', description='Name of class/function/method.', required=True), ToolParameter( name='new_content', type='string', description='Complete new source for the definition.', required=True))))
|
||||
register(ToolSpec(name='get_file_slice', description='Read a specific line range from a file. Useful for reading parts of very large files.', parameters=(ToolParameter( name='path', type='string', description='Path to the file.', required=True), ToolParameter( name='start_line', type='integer', description='1-based start line number.', required=True), ToolParameter( name='end_line', type='integer', description='1-based end line number (inclusive).', required=True))))
|
||||
register(ToolSpec(name='set_file_slice', description='Replace a specific line range in a file with new content. Surgical edit tool.', parameters=(ToolParameter( name='path', type='string', description='Path to the file.', required=True), ToolParameter( name='start_line', type='integer', description='1-based start line number.', required=True), ToolParameter( name='end_line', type='integer', description='1-based end line number (inclusive).', required=True), ToolParameter( name='new_content', type='string', description='New content to insert.', required=True))))
|
||||
register(ToolSpec(name='edit_file', description='Replace exact string match in a file. Preserves indentation and line endings. Drop-in replacement for native edit tool.', parameters=(ToolParameter( name='path', type='string', description='Path to the file.', required=True), ToolParameter( name='old_string', type='string', description='The text to replace.', required=True), ToolParameter( name='new_string', type='string', description='The replacement text.', required=True), ToolParameter( name='replace_all', type='boolean', description='Replace all occurrences. Default false.'))))
|
||||
register(ToolSpec(name='py_get_definition', description="Get the full source code of a specific class, function, or method definition. This is more efficient than reading the whole file if you know what you're looking for.", parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True), ToolParameter( name='name', type='string', description="The name of the class or function to retrieve. Use 'ClassName.method_name' for methods.", required=True))))
|
||||
register(ToolSpec(name='py_update_definition', description='Surgically replace the definition of a class or function in a Python file using AST to find line ranges.', parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True), ToolParameter( name='name', type='string', description='Name of class/function/method.', required=True), ToolParameter( name='new_content', type='string', description='Complete new source for the definition.', required=True))))
|
||||
register(ToolSpec(name='py_get_signature', description='Get only the signature part of a Python function or method (from def until colon).', parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True), ToolParameter( name='name', type='string', description="Name of the function/method (e.g. 'ClassName.method_name').", required=True))))
|
||||
register(ToolSpec(name='py_set_signature', description='Surgically replace only the signature of a Python function or method.', parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True), ToolParameter( name='name', type='string', description='Name of the function/method.', required=True), ToolParameter( name='new_signature', type='string', description='Complete new signature string (including def and trailing colon).', required=True))))
|
||||
register(ToolSpec(name='py_get_class_summary', description='Get a summary of a Python class, listing its docstring and all method signatures.', parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True), ToolParameter( name='name', type='string', description='Name of the class.', required=True))))
|
||||
register(ToolSpec(name='py_get_var_declaration', description='Get the assignment/declaration line for a variable.', parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True), ToolParameter( name='name', type='string', description='Name of the variable.', required=True))))
|
||||
register(ToolSpec(name='py_set_var_declaration', description='Surgically replace a variable assignment/declaration.', parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True), ToolParameter( name='name', type='string', description='Name of the variable.', required=True), ToolParameter( name='new_declaration', type='string', description='Complete new assignment/declaration string.', required=True))))
|
||||
register(ToolSpec(name='get_git_diff', description='Returns the git diff for a file or directory. Use this to review changes efficiently without reading entire files.', parameters=(ToolParameter( name='path', type='string', description='Path to the file or directory.', required=True), ToolParameter( name='base_rev', type='string', description="Base revision (e.g. 'HEAD', 'HEAD~1', or a commit hash). Defaults to 'HEAD'."), ToolParameter( name='head_rev', type='string', description='Head revision (optional).'))))
|
||||
register(ToolSpec(name='web_search', description='Search the web using DuckDuckGo. Returns the top 5 search results with titles, URLs, and snippets. Chain this with fetch_url to read specific pages.', parameters=(ToolParameter( name='query', type='string', description='The search query.', required=True),)))
|
||||
register(ToolSpec(name='fetch_url', description='Fetch the full text content of a URL (stripped of HTML tags). Use this after web_search to read relevant information from the web.', parameters=(ToolParameter( name='url', type='string', description='The full URL to fetch.', required=True),)))
|
||||
register(ToolSpec(name='get_ui_performance', description="Get a snapshot of the current UI performance metrics, including FPS, Frame Time (ms), CPU usage (%), and Input Lag (ms). Use this to diagnose UI slowness or verify that your changes haven't degraded the user experience.", parameters=()))
|
||||
register(ToolSpec(name='py_find_usages', description='Finds exact string matches of a symbol in a given file or directory.', parameters=(ToolParameter( name='path', type='string', description='Path to file or directory to search.', required=True), ToolParameter( name='name', type='string', description='The symbol/string to search for.', required=True))))
|
||||
register(ToolSpec(name='py_get_imports', description="Parses a file's AST and returns a strict list of its dependencies.", parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True),)))
|
||||
register(ToolSpec(name='py_check_syntax', description='Runs a quick syntax check on a Python file.', parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True),)))
|
||||
register(ToolSpec(name='py_get_hierarchy', description='Scans the project to find subclasses of a given class.', parameters=(ToolParameter( name='path', type='string', description='Directory path to search in.', required=True), ToolParameter( name='class_name', type='string', description='Name of the base class.', required=True))))
|
||||
register(ToolSpec(name='py_get_docstring', description='Extracts the docstring for a specific module, class, or function.', parameters=(ToolParameter( name='path', type='string', description='Path to the .py file.', required=True), ToolParameter( name='name', type='string', description="Name of symbol or 'module' for the file docstring.", required=True))))
|
||||
register(ToolSpec(name='get_tree', description='Returns a directory structure up to a max depth.', parameters=(ToolParameter( name='path', type='string', description='Directory path.', required=True), ToolParameter( name='max_depth', type='integer', description='Maximum depth to recurse (default 2).'))))
|
||||
register(ToolSpec(name='bd_create', description='Create a new Bead in the active Beads repository.', parameters=(ToolParameter( name='title', type='string', description='Title of the Bead.', required=True), ToolParameter( name='description', type='string', description='Description of the Bead.', required=True))))
|
||||
register(ToolSpec(name='bd_update', description='Update an existing Bead.', parameters=(ToolParameter( name='bead_id', type='string', description='ID of the Bead to update.', required=True), ToolParameter( name='status', type='string', description='New status for the Bead.', required=True))))
|
||||
register(ToolSpec(name='bd_list', description='List all Beads in the active Beads repository.', parameters=()))
|
||||
register(ToolSpec(name='bd_ready', description='Check if the Beads repository is initialized in the current workspace.', parameters=()))
|
||||
register(ToolSpec(name='derive_code_path', description='Recursively traces the execution path of a specific function or method across multiple files. Identifies call chains and data hand-offs to build an intensive technical map.', parameters=(ToolParameter( name='target', type='string', description="Fully qualified name of the target (e.g., 'src.ai_client.send') or class.method.", required=True), ToolParameter( name='max_depth', type='integer', description='Maximum recursion depth for the call graph (default 5).'))))
|
||||
+46
-78
@@ -1,59 +1,42 @@
|
||||
"""OpenAI-compatible API client for the Manual Slop ai_client layer.
|
||||
|
||||
Provides `send_openai_compatible(client, request, *, capabilities)` which
|
||||
calls any OpenAI-compatible chat completion endpoint and returns a
|
||||
`NormalizedResponse` (re-exported from src.openai_schemas).
|
||||
|
||||
CONVENTION: 1-space indentation. NO COMMENTS.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from openai import (
|
||||
APIConnectionError,
|
||||
APIStatusError,
|
||||
AuthenticationError,
|
||||
BadRequestError,
|
||||
OpenAIError,
|
||||
PermissionDeniedError,
|
||||
RateLimitError,
|
||||
)
|
||||
from openai import OpenAIError, RateLimitError, AuthenticationError, PermissionDeniedError, APIConnectionError, APIStatusError, BadRequestError
|
||||
|
||||
from src.openai_schemas import (
|
||||
ChatMessage,
|
||||
NormalizedResponse,
|
||||
OpenAICompatibleRequest,
|
||||
ToolCall,
|
||||
ToolCallFunction,
|
||||
UsageStats,
|
||||
)
|
||||
from src.result_types import ErrorInfo, ErrorKind, Result
|
||||
|
||||
__all__ = [
|
||||
"ChatMessage",
|
||||
"NormalizedResponse",
|
||||
"OpenAICompatibleRequest",
|
||||
"ToolCall",
|
||||
"ToolCallFunction",
|
||||
"UsageStats",
|
||||
]
|
||||
|
||||
|
||||
def _to_typed_tool_call(tc: Any) -> ToolCall:
|
||||
return ToolCall(
|
||||
id=getattr(tc, "id", "") or "",
|
||||
type=getattr(tc, "type", "function"),
|
||||
function=ToolCallFunction(
|
||||
name=getattr(tc.function, "name", "") or "",
|
||||
arguments=getattr(tc.function, "arguments", "{}") or "{}",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _to_dict_tool_call(tc: ToolCall) -> dict[str, Any]:
|
||||
return tc.to_dict()
|
||||
@dataclass(frozen=True)
|
||||
class NormalizedResponse:
|
||||
text: str
|
||||
tool_calls: list[dict[str, Any]]
|
||||
usage_input_tokens: int
|
||||
usage_output_tokens: int
|
||||
usage_cache_read_tokens: int
|
||||
usage_cache_creation_tokens: int
|
||||
raw_response: Any
|
||||
|
||||
@dataclass
|
||||
class OpenAICompatibleRequest:
|
||||
messages: list[dict[str, Any]]
|
||||
model: str
|
||||
temperature: float = 0.0
|
||||
top_p: float = 1.0
|
||||
max_tokens: int = 8192
|
||||
tools: Optional[list[dict[str, Any]]] = None
|
||||
tool_choice: str = "auto"
|
||||
stream: bool = False
|
||||
stream_callback: Optional[Callable[[str], None]] = None
|
||||
extra_body: Optional[dict[str, Any]] = None
|
||||
def _to_dict_tool_call(tc: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"id": getattr(tc, "id", None),
|
||||
"type": getattr(tc, "type", "function"),
|
||||
"function": {
|
||||
"name": getattr(tc.function, "name", None),
|
||||
"arguments": getattr(tc.function, "arguments", "{}"),
|
||||
},
|
||||
}
|
||||
|
||||
def _classify_openai_compatible_error(exc: Exception, source: str = "openai_compatible") -> ErrorInfo:
|
||||
if isinstance(exc, RateLimitError):
|
||||
@@ -76,17 +59,15 @@ def _classify_openai_compatible_error(exc: Exception, source: str = "openai_comp
|
||||
return ErrorInfo(kind=ErrorKind.QUOTA, message=str(exc), source=source, original=exc)
|
||||
return ErrorInfo(kind=ErrorKind.UNKNOWN, message=str(exc), source=source, original=exc)
|
||||
|
||||
|
||||
def send_openai_compatible(
|
||||
client: Any,
|
||||
request: OpenAICompatibleRequest,
|
||||
*,
|
||||
capabilities: Any,
|
||||
) -> Result[NormalizedResponse]:
|
||||
messages_dicts = [m.to_dict() if hasattr(m, "to_dict") else m for m in request.messages]
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": request.model,
|
||||
"messages": messages_dicts,
|
||||
"messages": request.messages,
|
||||
"temperature": request.temperature,
|
||||
"top_p": request.top_p,
|
||||
"max_tokens": request.max_tokens,
|
||||
@@ -104,32 +85,27 @@ def send_openai_compatible(
|
||||
response = _send_blocking(client, kwargs)
|
||||
return Result(data=response)
|
||||
except OpenAIError as exc:
|
||||
empty_resp = NormalizedResponse(
|
||||
text="",
|
||||
tool_calls=(),
|
||||
usage=UsageStats(input_tokens=0, output_tokens=0),
|
||||
raw_response=None,
|
||||
)
|
||||
empty_resp = NormalizedResponse(text="", tool_calls=[], usage_input_tokens=0, usage_output_tokens=0, usage_cache_read_tokens=0, usage_cache_creation_tokens=0, raw_response=None)
|
||||
return Result(data=empty_resp, errors=[_classify_openai_compatible_error(exc, source="openai_compatible")])
|
||||
|
||||
|
||||
def _send_blocking(client: Any, kwargs: dict[str, Any]) -> NormalizedResponse:
|
||||
resp = client.chat.completions.create(**kwargs)
|
||||
msg = resp.choices[0].message
|
||||
tool_calls_raw = msg.tool_calls or []
|
||||
tool_calls: tuple[ToolCall, ...] = tuple(_to_typed_tool_call(tc) for tc in tool_calls_raw)
|
||||
tool_calls: list[dict[str, Any]] = []
|
||||
for tc in tool_calls_raw:
|
||||
tool_calls.append(_to_dict_tool_call(tc))
|
||||
usage = getattr(resp, "usage", None)
|
||||
return NormalizedResponse(
|
||||
text=msg.content or "",
|
||||
tool_calls=tool_calls,
|
||||
usage=UsageStats(
|
||||
input_tokens=int(getattr(usage, "prompt_tokens", 0) or 0),
|
||||
output_tokens=int(getattr(usage, "completion_tokens", 0) or 0),
|
||||
),
|
||||
usage_input_tokens=int(getattr(usage, "prompt_tokens", 0) or 0),
|
||||
usage_output_tokens=int(getattr(usage, "completion_tokens", 0) or 0),
|
||||
usage_cache_read_tokens=0,
|
||||
usage_cache_creation_tokens=0,
|
||||
raw_response=resp,
|
||||
)
|
||||
|
||||
|
||||
def _send_streaming(client: Any, kwargs: dict[str, Any], callback: Optional[Callable[[str], None]]) -> NormalizedResponse:
|
||||
kwargs_stream = dict(kwargs)
|
||||
kwargs_stream["stream"] = True
|
||||
@@ -163,20 +139,12 @@ def _send_streaming(client: Any, kwargs: dict[str, Any], callback: Optional[Call
|
||||
if chunk_usage is not None:
|
||||
usage_input = int(getattr(chunk_usage, "prompt_tokens", 0) or 0)
|
||||
usage_output = int(getattr(chunk_usage, "completion_tokens", 0) or 0)
|
||||
tool_calls_typed: tuple[ToolCall, ...] = tuple(
|
||||
ToolCall(
|
||||
id=acc["id"] or "",
|
||||
type=acc["type"],
|
||||
function=ToolCallFunction(
|
||||
name=acc["function"]["name"] or "",
|
||||
arguments=acc["function"]["arguments"] or "{}",
|
||||
),
|
||||
)
|
||||
for acc in (tool_calls_acc[k] for k in sorted(tool_calls_acc.keys()))
|
||||
)
|
||||
return NormalizedResponse(
|
||||
text="".join(text_parts),
|
||||
tool_calls=tool_calls_typed,
|
||||
usage=UsageStats(input_tokens=usage_input, output_tokens=usage_output),
|
||||
tool_calls=[tool_calls_acc[k] for k in sorted(tool_calls_acc.keys())],
|
||||
usage_input_tokens=usage_input,
|
||||
usage_output_tokens=usage_output,
|
||||
usage_cache_read_tokens=0,
|
||||
usage_cache_creation_tokens=0,
|
||||
raw_response=None,
|
||||
)
|
||||
@@ -1,105 +0,0 @@
|
||||
"""OpenAI-compatible dataclasses for the Manual Slop ai_client layer.
|
||||
|
||||
Promotes `NormalizedResponse` and `OpenAICompatibleRequest` from
|
||||
`src/openai_compatible.py` to typed dataclasses. The 4 dataclasses
|
||||
here model the OpenAI Chat Completion API shape:
|
||||
|
||||
- ToolCall: a single tool call from the model
|
||||
- ToolCallFunction: the function portion of a tool call (name + JSON args)
|
||||
- ChatMessage: a single message in the conversation (system/user/assistant/tool)
|
||||
- UsageStats: token usage accounting (input, output, cache hits/creation)
|
||||
|
||||
`NormalizedResponse` and `OpenAICompatibleRequest` keep their public
|
||||
shapes but consume these typed shapes internally.
|
||||
|
||||
CONVENTION: 1-space indentation. NO COMMENTS.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolCallFunction:
|
||||
name: str
|
||||
arguments: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolCall:
|
||||
id: str
|
||||
function: ToolCallFunction
|
||||
type: str = "function"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"type": self.type,
|
||||
"function": {
|
||||
"name": self.function.name,
|
||||
"arguments": self.function.arguments,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChatMessage:
|
||||
role: str
|
||||
content: str
|
||||
tool_calls: Optional[tuple[ToolCall, ...]] = None
|
||||
tool_call_id: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
d: dict[str, Any] = {"role": self.role, "content": self.content}
|
||||
if self.tool_calls is not None:
|
||||
d["tool_calls"] = [tc.to_dict() for tc in self.tool_calls]
|
||||
if self.tool_call_id is not None:
|
||||
d["tool_call_id"] = self.tool_call_id
|
||||
if self.name is not None:
|
||||
d["name"] = self.name
|
||||
return d
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UsageStats:
|
||||
input_tokens: int
|
||||
output_tokens: int
|
||||
cache_read_tokens: int = 0
|
||||
cache_creation_tokens: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NormalizedResponse:
|
||||
text: str
|
||||
tool_calls: tuple[ToolCall, ...]
|
||||
usage: UsageStats
|
||||
raw_response: Any
|
||||
|
||||
def to_legacy_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"text": self.text,
|
||||
"tool_calls": [tc.to_dict() for tc in self.tool_calls],
|
||||
"usage": {
|
||||
"input_tokens": self.usage.input_tokens,
|
||||
"output_tokens": self.usage.output_tokens,
|
||||
"cache_read_tokens": self.usage.cache_read_tokens,
|
||||
"cache_creation_tokens": self.usage.cache_creation_tokens,
|
||||
},
|
||||
"raw_response": self.raw_response,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpenAICompatibleRequest:
|
||||
messages: list[ChatMessage]
|
||||
model: str
|
||||
temperature: float = 0.0
|
||||
top_p: float = 1.0
|
||||
max_tokens: int = 8192
|
||||
tools: Optional[list[dict[str, Any]]] = None
|
||||
tool_choice: str = "auto"
|
||||
stream: bool = False
|
||||
stream_callback: Optional[Callable[[str], None]] = None
|
||||
extra_body: Optional[dict[str, Any]] = None
|
||||
@@ -1,69 +0,0 @@
|
||||
"""Per-provider history state for the AI client layer.
|
||||
|
||||
Promotes 14 module globals in src/ai_client.py:
|
||||
- 7x `_<provider>_history: list[Metadata]` (anthropic/deepseek/minimax/qwen/grok/llama)
|
||||
- 7x `_<provider>_history_lock: threading.Lock`
|
||||
|
||||
To a single `_PROVIDER_HISTORIES: dict[str, ProviderHistory]` keyed by
|
||||
provider name. Each `ProviderHistory` owns its own lock and message list;
|
||||
the cross-provider pattern is encapsulated behind a 4-method interface.
|
||||
|
||||
SDK client holders (`_gemini_chat`, `_deepseek_client`, etc.) stay as
|
||||
module-level `Any` variables per Pattern 3 (heterogeneous SDK types,
|
||||
lazy-initialized). Only the homogeneous history aspect is unified.
|
||||
|
||||
CONVENTION: 1-space indentation. NO COMMENTS.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from src.type_aliases import HistoryMessage, Metadata
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProviderHistory:
|
||||
messages: list[HistoryMessage] = field(default_factory=list)
|
||||
lock: threading.Lock = field(default_factory=threading.Lock)
|
||||
|
||||
def append(self, message: HistoryMessage) -> None:
|
||||
with self.lock:
|
||||
self.messages.append(message)
|
||||
|
||||
def get_all(self) -> list[HistoryMessage]:
|
||||
with self.lock:
|
||||
return list(self.messages)
|
||||
|
||||
def replace_all(self, messages: list[HistoryMessage]) -> None:
|
||||
with self.lock:
|
||||
self.messages = list(messages)
|
||||
|
||||
def clear(self) -> None:
|
||||
with self.lock:
|
||||
self.messages = []
|
||||
|
||||
|
||||
_PROVIDER_HISTORIES: dict[str, ProviderHistory] = {
|
||||
"anthropic": ProviderHistory(),
|
||||
"deepseek": ProviderHistory(),
|
||||
"minimax": ProviderHistory(),
|
||||
"qwen": ProviderHistory(),
|
||||
"grok": ProviderHistory(),
|
||||
"llama": ProviderHistory(),
|
||||
}
|
||||
|
||||
|
||||
def get_history(provider: str) -> ProviderHistory:
|
||||
if provider not in _PROVIDER_HISTORIES:
|
||||
raise KeyError(f"Unknown provider: {provider!r}")
|
||||
return _PROVIDER_HISTORIES[provider]
|
||||
|
||||
|
||||
def clear_all() -> None:
|
||||
for h in _PROVIDER_HISTORIES.values():
|
||||
h.clear()
|
||||
|
||||
|
||||
def providers() -> tuple[str, ...]:
|
||||
return tuple(_PROVIDER_HISTORIES.keys())
|
||||
@@ -18,9 +18,6 @@ ToolCall: TypeAlias = Metadata
|
||||
|
||||
CommsLogCallback: TypeAlias = Callable[[CommsLogEntry], None]
|
||||
|
||||
JsonPrimitive: TypeAlias = str | int | float | bool | None
|
||||
JsonValue: TypeAlias = JsonPrimitive | list["JsonValue"] | dict[str, "JsonValue"]
|
||||
|
||||
|
||||
class FileItemsDiff(NamedTuple):
|
||||
refreshed: FileItems
|
||||
|
||||
Reference in New Issue
Block a user