feat: implement tweaks plan - modals, smart shell, spinner, /models, debug log, skills
Phase 1: Permission modal dialog, session resume modal, HistoryInput with up/down arrow cycling, remove "You:" echo from chat log, LLM client cleanup on unmount. Phase 2: Smart shell auto-approve using allowed/denied command lists from ToolsConfig, animated thinking spinner with live token count in status bar. Phase 3: /models slash command (list + switch), CLI directory positional argument, JSONL debug logger with rotation. Phase 4: Skills system with SkillsManager, load_skill LLM tool, /skills listing, skill invocation via slash commands, system prompt integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
"""AgentLoop — ReAct-style tool-call loop for autonomous task execution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from app.agent.context import SessionContext
|
||||
from app.models.config import AppConfig
|
||||
@@ -14,6 +17,10 @@ from app.tools.registry import ToolRegistry
|
||||
from app.utils.display import DisplayAdapter
|
||||
from app.utils.logging import get_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.services.debug_log import DebugLogger
|
||||
from app.services.skills import SkillsManager
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
_MAX_REASONING_RETRIES = 2
|
||||
@@ -36,6 +43,8 @@ class AgentLoop:
|
||||
registry: ToolRegistry,
|
||||
permissions: PermissionsService,
|
||||
display: DisplayAdapter | None = None,
|
||||
debug_logger: DebugLogger | None = None,
|
||||
skills_manager: SkillsManager | None = None,
|
||||
) -> None:
|
||||
self._config = config
|
||||
self._ctx = ctx
|
||||
@@ -44,6 +53,8 @@ class AgentLoop:
|
||||
self._registry = registry
|
||||
self._permissions = permissions
|
||||
self._display = display
|
||||
self._debug = debug_logger
|
||||
self._skills = skills_manager
|
||||
self._tools_schema = registry.get_openai_tools_schema()
|
||||
self._system_prompt = self._build_system_prompt()
|
||||
self._cancelled = False
|
||||
@@ -55,7 +66,7 @@ class AgentLoop:
|
||||
def _build_system_prompt(self) -> str:
|
||||
"""Build the system prompt including tool schemas and agent instructions."""
|
||||
tool_names = [t["function"]["name"] for t in self._tools_schema]
|
||||
return (
|
||||
prompt = (
|
||||
"You are SneakyCode, a local AI coding agent. "
|
||||
"You help users with software engineering tasks by reading files, "
|
||||
"searching code, and answering questions about their project.\n\n"
|
||||
@@ -68,6 +79,9 @@ class AgentLoop:
|
||||
"with a brief summary. If you can answer directly without tools, just respond "
|
||||
"with text (no tool call needed)."
|
||||
)
|
||||
if self._skills:
|
||||
prompt += self._skills.get_system_prompt_snippet()
|
||||
return prompt
|
||||
|
||||
def _get_messages_with_system_prompt(self) -> list[Message]:
|
||||
"""Prepend the system prompt to conversation history."""
|
||||
@@ -195,9 +209,16 @@ class AgentLoop:
|
||||
The assistant Message, or None if an error occurred.
|
||||
"""
|
||||
messages = self._get_messages_with_system_prompt()
|
||||
if self._debug:
|
||||
self._debug.log_request(messages, self._config.llm.model)
|
||||
t0 = time.monotonic()
|
||||
try:
|
||||
chunk_iter = self._client.stream_chat_with_retry(messages, tools=self._tools_schema)
|
||||
return await self._handler.process_stream(chunk_iter)
|
||||
result = await self._handler.process_stream(chunk_iter)
|
||||
if result and self._debug:
|
||||
elapsed = (time.monotonic() - t0) * 1000
|
||||
self._debug.log_response(result, self._handler.usage, elapsed)
|
||||
return result
|
||||
except KeyboardInterrupt:
|
||||
if self._display:
|
||||
self._display.write_warning("Response interrupted.")
|
||||
@@ -268,7 +289,7 @@ class AgentLoop:
|
||||
|
||||
# Check permissions (truncate args for display in prompt)
|
||||
desc = tc.function.arguments[:120] + "..." if len(tc.function.arguments) > 120 else tc.function.arguments
|
||||
if not await self._permissions.check(name, description=desc):
|
||||
if not await self._permissions.check(name, description=desc, arguments=tc.function.arguments):
|
||||
result = ToolResult(
|
||||
tool_call_id=tc_id,
|
||||
tool_name=name,
|
||||
@@ -281,9 +302,14 @@ class AgentLoop:
|
||||
continue
|
||||
|
||||
# Execute tool (BaseTool.run never raises)
|
||||
tool_t0 = time.monotonic()
|
||||
result = tool.run(tc_id, parsed_args)
|
||||
results.append(result)
|
||||
|
||||
if self._debug:
|
||||
tool_elapsed = (time.monotonic() - tool_t0) * 1000
|
||||
self._debug.log_tool_execution(name, result.status.value, tool_elapsed)
|
||||
|
||||
if self._config.display.show_tool_calls and self._display:
|
||||
is_error = result.status == ToolResultStatus.ERROR
|
||||
output = result.error if is_error else result.output
|
||||
|
||||
Reference in New Issue
Block a user