Files
zk-data-agent/src/agent_slash_commands.py
T
Abdelrahman Abdallah 27aff5611b first commit
2026-04-01 21:51:49 +02:00

229 lines
7.3 KiB
Python

from __future__ import annotations
import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Callable
if TYPE_CHECKING:
from .agent_runtime import LocalCodingAgent
@dataclass(frozen=True)
class ParsedSlashCommand:
command_name: str
args: str
is_mcp: bool
@dataclass(frozen=True)
class SlashCommandResult:
handled: bool
should_query: bool
prompt: str | None = None
output: str = ''
transcript: tuple[dict[str, Any], ...] = ()
SlashCommandHandler = Callable[['LocalCodingAgent', str, str], SlashCommandResult]
@dataclass(frozen=True)
class SlashCommandSpec:
names: tuple[str, ...]
description: str
handler: SlashCommandHandler
def parse_slash_command(input_text: str) -> ParsedSlashCommand | None:
trimmed = input_text.strip()
if not trimmed.startswith('/'):
return None
without_slash = trimmed[1:]
words = without_slash.split(' ')
if not words or not words[0]:
return None
command_name = words[0]
is_mcp = False
args_start_index = 1
if len(words) > 1 and words[1] == '(MCP)':
command_name = f'{command_name} (MCP)'
is_mcp = True
args_start_index = 2
return ParsedSlashCommand(
command_name=command_name,
args=' '.join(words[args_start_index:]),
is_mcp=is_mcp,
)
def looks_like_command(command_name: str) -> bool:
return re.search(r'[^a-zA-Z0-9:\-_]', command_name) is None
def preprocess_slash_command(
agent: 'LocalCodingAgent',
input_text: str,
) -> SlashCommandResult:
if not input_text.strip().startswith('/'):
return SlashCommandResult(handled=False, should_query=True, prompt=input_text)
parsed = parse_slash_command(input_text)
if parsed is None:
return _local_result(
input_text,
'Commands are in the form `/command [args]`.',
)
if parsed.is_mcp:
return _local_result(
input_text,
'MCP slash commands are not implemented in the Python runtime yet.',
)
spec = find_slash_command(parsed.command_name)
if spec is None:
if looks_like_command(parsed.command_name):
return _local_result(input_text, f'Unknown skill: {parsed.command_name}')
return SlashCommandResult(handled=False, should_query=True, prompt=input_text)
return spec.handler(agent, parsed.args.strip(), input_text)
def get_slash_command_specs() -> tuple[SlashCommandSpec, ...]:
return (
SlashCommandSpec(
names=('help', 'commands'),
description='Show the built-in Python slash commands.',
handler=_handle_help,
),
SlashCommandSpec(
names=('context', 'usage'),
description='Show estimated session context usage similar to the npm /context command.',
handler=_handle_context,
),
SlashCommandSpec(
names=('context-raw', 'env'),
description='Show the raw environment, user context, and system context snapshot.',
handler=_handle_context_raw,
),
SlashCommandSpec(
names=('prompt', 'system-prompt'),
description='Render the effective Python system prompt.',
handler=_handle_prompt,
),
SlashCommandSpec(
names=('permissions',),
description='Show the active tool permission mode.',
handler=_handle_permissions,
),
SlashCommandSpec(
names=('model',),
description='Show or update the active model for the current agent instance.',
handler=_handle_model,
),
SlashCommandSpec(
names=('tools',),
description='List the registered tools and whether the current permissions allow them.',
handler=_handle_tools,
),
SlashCommandSpec(
names=('memory',),
description='Show the currently loaded CLAUDE.md memory bundle and discovered files.',
handler=_handle_memory,
),
SlashCommandSpec(
names=('status', 'session'),
description='Show a short runtime/session status summary.',
handler=_handle_status,
),
SlashCommandSpec(
names=('clear',),
description='Clear ephemeral Python runtime state for this process.',
handler=_handle_clear,
),
)
def find_slash_command(command_name: str) -> SlashCommandSpec | None:
lowered = command_name.lower()
for spec in get_slash_command_specs():
if lowered in spec.names:
return spec
return None
def _handle_help(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
lines = ['# Slash Commands', '']
for spec in get_slash_command_specs():
primary = f'/{spec.names[0]}'
aliases = ', '.join(f'/{name}' for name in spec.names[1:])
label = f'{primary} ({aliases})' if aliases else primary
lines.append(f'- `{label}`: {spec.description}')
lines.extend(
[
'',
'These commands are handled locally before the model loop, similar to the npm runtime.',
]
)
return _local_result(input_text, '\n'.join(lines))
def _handle_context(agent: 'LocalCodingAgent', args: str, input_text: str) -> SlashCommandResult:
prompt = args or None
return _local_result(input_text, agent.render_context_report(prompt))
def _handle_context_raw(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
return _local_result(input_text, agent.render_context_snapshot_report())
def _handle_prompt(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
return _local_result(input_text, agent.render_system_prompt())
def _handle_permissions(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
return _local_result(input_text, agent.render_permissions_report())
def _handle_model(agent: 'LocalCodingAgent', args: str, input_text: str) -> SlashCommandResult:
if not args:
return _local_result(input_text, f'Current model: {agent.model_config.model}')
agent.set_model(args)
return _local_result(input_text, f'Set model to {agent.model_config.model}')
def _handle_tools(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
return _local_result(input_text, agent.render_tools_report())
def _handle_memory(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
return _local_result(input_text, agent.render_memory_report())
def _handle_status(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
return _local_result(input_text, agent.render_status_report())
def _handle_clear(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
agent.clear_runtime_state()
return _local_result(
input_text,
'Cleared ephemeral Python agent state for this process.',
)
def _local_result(input_text: str, output: str) -> SlashCommandResult:
transcript = (
{'role': 'user', 'content': input_text},
{'role': 'assistant', 'content': output},
)
return SlashCommandResult(
handled=True,
should_query=False,
output=output,
transcript=transcript,
)