fast-agent-mcp 0.4.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Slash Commands for ACP
|
|
3
|
+
|
|
4
|
+
Provides slash command support for the ACP server, allowing clients to
|
|
5
|
+
discover and invoke special commands with the /command syntax.
|
|
6
|
+
|
|
7
|
+
Session commands (status, tools, save, clear, load) are always available.
|
|
8
|
+
Agent-specific commands are queried from the current agent if it implements
|
|
9
|
+
ACPAwareProtocol.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import textwrap
|
|
15
|
+
import time
|
|
16
|
+
from importlib.metadata import version as get_version
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from acp.schema import (
|
|
21
|
+
AvailableCommand,
|
|
22
|
+
AvailableCommandInput,
|
|
23
|
+
UnstructuredCommandInput,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from fast_agent.agents.agent_types import AgentType
|
|
27
|
+
from fast_agent.constants import FAST_AGENT_ERROR_CHANNEL
|
|
28
|
+
from fast_agent.history.history_exporter import HistoryExporter
|
|
29
|
+
from fast_agent.interfaces import ACPAwareProtocol, AgentProtocol
|
|
30
|
+
from fast_agent.llm.model_info import ModelInfo
|
|
31
|
+
from fast_agent.mcp.helpers.content_helpers import get_text
|
|
32
|
+
from fast_agent.mcp.prompts.prompt_load import load_history_into_agent
|
|
33
|
+
from fast_agent.types.conversation_summary import ConversationSummary
|
|
34
|
+
from fast_agent.utils.time import format_duration
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from mcp.types import ListToolsResult, Tool
|
|
38
|
+
|
|
39
|
+
from fast_agent.core.fastagent import AgentInstance
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SlashCommandHandler:
|
|
43
|
+
"""Handles slash command execution for ACP sessions."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
session_id: str,
|
|
48
|
+
instance: AgentInstance,
|
|
49
|
+
primary_agent_name: str,
|
|
50
|
+
*,
|
|
51
|
+
history_exporter: type[HistoryExporter] | HistoryExporter | None = None,
|
|
52
|
+
client_info: dict | None = None,
|
|
53
|
+
client_capabilities: dict | None = None,
|
|
54
|
+
protocol_version: int | None = None,
|
|
55
|
+
session_instructions: dict[str, str] | None = None,
|
|
56
|
+
):
|
|
57
|
+
"""
|
|
58
|
+
Initialize the slash command handler.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
session_id: The ACP session ID
|
|
62
|
+
instance: The agent instance for this session
|
|
63
|
+
primary_agent_name: Name of the primary agent
|
|
64
|
+
history_exporter: Optional history exporter
|
|
65
|
+
client_info: Client information from ACP initialize
|
|
66
|
+
client_capabilities: Client capabilities from ACP initialize
|
|
67
|
+
protocol_version: ACP protocol version
|
|
68
|
+
"""
|
|
69
|
+
self.session_id = session_id
|
|
70
|
+
self.instance = instance
|
|
71
|
+
self.primary_agent_name = primary_agent_name
|
|
72
|
+
# Track current agent (can change via setSessionMode). Ensure it exists.
|
|
73
|
+
if primary_agent_name in instance.agents:
|
|
74
|
+
self.current_agent_name = primary_agent_name
|
|
75
|
+
else:
|
|
76
|
+
# Fallback: pick the first registered agent to enable agent-specific commands.
|
|
77
|
+
self.current_agent_name = next(iter(instance.agents.keys()), primary_agent_name)
|
|
78
|
+
self.history_exporter = history_exporter or HistoryExporter
|
|
79
|
+
self._created_at = time.time()
|
|
80
|
+
self.client_info = client_info
|
|
81
|
+
self.client_capabilities = client_capabilities
|
|
82
|
+
self.protocol_version = protocol_version
|
|
83
|
+
self._session_instructions = session_instructions or {}
|
|
84
|
+
|
|
85
|
+
# Session-level commands (always available, operate on current agent)
|
|
86
|
+
self._session_commands: dict[str, AvailableCommand] = {
|
|
87
|
+
"status": AvailableCommand(
|
|
88
|
+
name="status",
|
|
89
|
+
description="Show fast-agent diagnostics",
|
|
90
|
+
input=AvailableCommandInput(
|
|
91
|
+
root=UnstructuredCommandInput(hint="[system|auth|authreset]")
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
"tools": AvailableCommand(
|
|
95
|
+
name="tools",
|
|
96
|
+
description="List available tools",
|
|
97
|
+
input=None,
|
|
98
|
+
),
|
|
99
|
+
"save": AvailableCommand(
|
|
100
|
+
name="save",
|
|
101
|
+
description="Save conversation history",
|
|
102
|
+
input=None,
|
|
103
|
+
),
|
|
104
|
+
"clear": AvailableCommand(
|
|
105
|
+
name="clear",
|
|
106
|
+
description="Clear history (`last` for prev. turn)",
|
|
107
|
+
input=AvailableCommandInput(root=UnstructuredCommandInput(hint="[last]")),
|
|
108
|
+
),
|
|
109
|
+
"load": AvailableCommand(
|
|
110
|
+
name="load",
|
|
111
|
+
description="Load conversation history from file",
|
|
112
|
+
input=AvailableCommandInput(root=UnstructuredCommandInput(hint="<filename>")),
|
|
113
|
+
),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def get_available_commands(self) -> list[AvailableCommand]:
|
|
117
|
+
"""Get combined session commands and current agent's commands."""
|
|
118
|
+
commands = list(self._get_allowed_session_commands().values())
|
|
119
|
+
|
|
120
|
+
# Add agent-specific commands if current agent is ACP-aware
|
|
121
|
+
agent = self._get_current_agent()
|
|
122
|
+
if isinstance(agent, ACPAwareProtocol):
|
|
123
|
+
for name, cmd in agent.acp_commands.items():
|
|
124
|
+
# Convert ACPCommand to AvailableCommand
|
|
125
|
+
cmd_input = None
|
|
126
|
+
if cmd.input_hint:
|
|
127
|
+
cmd_input = AvailableCommandInput(
|
|
128
|
+
root=UnstructuredCommandInput(hint=cmd.input_hint)
|
|
129
|
+
)
|
|
130
|
+
commands.append(
|
|
131
|
+
AvailableCommand(name=name, description=cmd.description, input=cmd_input)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return commands
|
|
135
|
+
|
|
136
|
+
def _get_allowed_session_commands(self) -> dict[str, AvailableCommand]:
|
|
137
|
+
"""
|
|
138
|
+
Return session-level commands filtered by the current agent's policy.
|
|
139
|
+
|
|
140
|
+
By default, all session commands are available. ACP-aware agents can restrict
|
|
141
|
+
session commands (e.g. Setup/wizard flows) by defining either:
|
|
142
|
+
- `acp_session_commands_allowlist: set[str] | None` attribute, or
|
|
143
|
+
- `acp_session_commands_allowlist() -> set[str] | None` method
|
|
144
|
+
"""
|
|
145
|
+
agent = self._get_current_agent()
|
|
146
|
+
if not isinstance(agent, ACPAwareProtocol):
|
|
147
|
+
return self._session_commands
|
|
148
|
+
|
|
149
|
+
allowlist = getattr(agent, "acp_session_commands_allowlist", None)
|
|
150
|
+
if callable(allowlist):
|
|
151
|
+
try:
|
|
152
|
+
allowlist = allowlist()
|
|
153
|
+
except Exception:
|
|
154
|
+
allowlist = None
|
|
155
|
+
|
|
156
|
+
if allowlist is None:
|
|
157
|
+
return self._session_commands
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
allowset = {str(name) for name in allowlist}
|
|
161
|
+
except Exception:
|
|
162
|
+
return self._session_commands
|
|
163
|
+
|
|
164
|
+
return {name: cmd for name, cmd in self._session_commands.items() if name in allowset}
|
|
165
|
+
|
|
166
|
+
def set_current_agent(self, agent_name: str) -> None:
|
|
167
|
+
"""
|
|
168
|
+
Update the current agent for this session.
|
|
169
|
+
|
|
170
|
+
This is called when the user switches modes via setSessionMode.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
agent_name: Name of the agent to use for slash commands
|
|
174
|
+
"""
|
|
175
|
+
self.current_agent_name = agent_name
|
|
176
|
+
|
|
177
|
+
def _get_current_agent(self) -> AgentProtocol | None:
|
|
178
|
+
"""Return the current agent or None if it does not exist."""
|
|
179
|
+
return self.instance.agents.get(self.current_agent_name)
|
|
180
|
+
|
|
181
|
+
def _get_current_agent_or_error(
|
|
182
|
+
self,
|
|
183
|
+
heading: str,
|
|
184
|
+
missing_template: str | None = None,
|
|
185
|
+
) -> tuple[AgentProtocol | None, str | None]:
|
|
186
|
+
"""
|
|
187
|
+
Return the current agent or an error response string if it is missing.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
heading: Heading for the error message.
|
|
191
|
+
missing_template: Optional custom missing-agent message.
|
|
192
|
+
"""
|
|
193
|
+
agent = self._get_current_agent()
|
|
194
|
+
if agent:
|
|
195
|
+
return agent, None
|
|
196
|
+
|
|
197
|
+
message = (
|
|
198
|
+
missing_template or f"Agent '{self.current_agent_name}' not found for this session."
|
|
199
|
+
)
|
|
200
|
+
return None, "\n".join([heading, "", message])
|
|
201
|
+
|
|
202
|
+
def is_slash_command(self, prompt_text: str) -> bool:
|
|
203
|
+
"""Check if the prompt text is a slash command."""
|
|
204
|
+
return prompt_text.strip().startswith("/")
|
|
205
|
+
|
|
206
|
+
def parse_command(self, prompt_text: str) -> tuple[str, str]:
|
|
207
|
+
"""
|
|
208
|
+
Parse a slash command into command name and arguments.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
prompt_text: The full prompt text starting with /
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Tuple of (command_name, arguments)
|
|
215
|
+
"""
|
|
216
|
+
text = prompt_text.strip()
|
|
217
|
+
if not text.startswith("/"):
|
|
218
|
+
return "", text
|
|
219
|
+
|
|
220
|
+
# Remove leading slash
|
|
221
|
+
text = text[1:]
|
|
222
|
+
|
|
223
|
+
# Split on first whitespace
|
|
224
|
+
command_name, _, arguments = text.partition(" ")
|
|
225
|
+
arguments = arguments.lstrip()
|
|
226
|
+
|
|
227
|
+
return command_name, arguments
|
|
228
|
+
|
|
229
|
+
async def execute_command(self, command_name: str, arguments: str) -> str:
|
|
230
|
+
"""
|
|
231
|
+
Execute a slash command and return the response.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
command_name: Name of the command to execute
|
|
235
|
+
arguments: Arguments passed to the command
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
The command response as a string
|
|
239
|
+
"""
|
|
240
|
+
# Check session-level commands first (filtered by agent policy)
|
|
241
|
+
allowed_session_commands = self._get_allowed_session_commands()
|
|
242
|
+
if command_name in allowed_session_commands:
|
|
243
|
+
if command_name == "status":
|
|
244
|
+
return await self._handle_status(arguments)
|
|
245
|
+
if command_name == "tools":
|
|
246
|
+
return await self._handle_tools()
|
|
247
|
+
if command_name == "save":
|
|
248
|
+
return await self._handle_save(arguments)
|
|
249
|
+
if command_name == "clear":
|
|
250
|
+
return await self._handle_clear(arguments)
|
|
251
|
+
if command_name == "load":
|
|
252
|
+
return await self._handle_load(arguments)
|
|
253
|
+
|
|
254
|
+
# Check agent-specific commands
|
|
255
|
+
agent = self._get_current_agent()
|
|
256
|
+
if isinstance(agent, ACPAwareProtocol):
|
|
257
|
+
agent_commands = agent.acp_commands
|
|
258
|
+
if command_name in agent_commands:
|
|
259
|
+
return await agent_commands[command_name].handler(arguments)
|
|
260
|
+
|
|
261
|
+
# Unknown command
|
|
262
|
+
available = self.get_available_commands()
|
|
263
|
+
return f"Unknown command: /{command_name}\n\nAvailable commands:\n" + "\n".join(
|
|
264
|
+
f" /{cmd.name} - {cmd.description}" for cmd in available
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
async def _handle_status(self, arguments: str | None = None) -> str:
|
|
268
|
+
"""Handle the /status command."""
|
|
269
|
+
# Check for subcommands
|
|
270
|
+
normalized = (arguments or "").strip().lower()
|
|
271
|
+
if normalized == "system":
|
|
272
|
+
return self._handle_status_system()
|
|
273
|
+
if normalized == "auth":
|
|
274
|
+
return self._handle_status_auth()
|
|
275
|
+
if normalized == "authreset":
|
|
276
|
+
return self._handle_status_authreset()
|
|
277
|
+
|
|
278
|
+
# Get fast-agent version
|
|
279
|
+
try:
|
|
280
|
+
fa_version = get_version("fast-agent-mcp")
|
|
281
|
+
except Exception:
|
|
282
|
+
fa_version = "unknown"
|
|
283
|
+
|
|
284
|
+
# Get model information from current agent (not primary)
|
|
285
|
+
agent = self._get_current_agent()
|
|
286
|
+
|
|
287
|
+
# Check if this is a PARALLEL agent
|
|
288
|
+
is_parallel_agent = (
|
|
289
|
+
agent and hasattr(agent, "agent_type") and agent.agent_type == AgentType.PARALLEL
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# For non-parallel agents, extract standard model info
|
|
293
|
+
model_name = "unknown"
|
|
294
|
+
model_provider = "unknown"
|
|
295
|
+
model_provider_display = "unknown"
|
|
296
|
+
context_window = "unknown"
|
|
297
|
+
capabilities_line = "Capabilities: unknown"
|
|
298
|
+
|
|
299
|
+
if agent and not is_parallel_agent and agent.llm:
|
|
300
|
+
model_info = ModelInfo.from_llm(agent.llm)
|
|
301
|
+
if model_info:
|
|
302
|
+
model_name = model_info.name
|
|
303
|
+
model_provider = str(model_info.provider.value)
|
|
304
|
+
model_provider_display = getattr(
|
|
305
|
+
model_info.provider, "display_name", model_provider
|
|
306
|
+
)
|
|
307
|
+
if model_info.context_window:
|
|
308
|
+
context_window = f"{model_info.context_window} tokens"
|
|
309
|
+
capability_parts = []
|
|
310
|
+
if model_info.supports_text:
|
|
311
|
+
capability_parts.append("Text")
|
|
312
|
+
if model_info.supports_document:
|
|
313
|
+
capability_parts.append("Document")
|
|
314
|
+
if model_info.supports_vision:
|
|
315
|
+
capability_parts.append("Vision")
|
|
316
|
+
if capability_parts:
|
|
317
|
+
capabilities_line = f"Capabilities: {', '.join(capability_parts)}"
|
|
318
|
+
|
|
319
|
+
# Get conversation statistics
|
|
320
|
+
summary_stats = self._get_conversation_stats(agent)
|
|
321
|
+
|
|
322
|
+
# Format the status response
|
|
323
|
+
status_lines = [
|
|
324
|
+
"# fast-agent ACP status",
|
|
325
|
+
"",
|
|
326
|
+
"## Version",
|
|
327
|
+
f"fast-agent-mcp: {fa_version} - https://fast-agent.ai/",
|
|
328
|
+
"",
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
# Add client information if available
|
|
332
|
+
if self.client_info or self.client_capabilities:
|
|
333
|
+
status_lines.extend(["## Client Information", ""])
|
|
334
|
+
|
|
335
|
+
if self.client_info:
|
|
336
|
+
client_name = self.client_info.get("name", "unknown")
|
|
337
|
+
client_version = self.client_info.get("version", "unknown")
|
|
338
|
+
client_title = self.client_info.get("title")
|
|
339
|
+
|
|
340
|
+
if client_title:
|
|
341
|
+
status_lines.append(f"Client: {client_title} ({client_name})")
|
|
342
|
+
else:
|
|
343
|
+
status_lines.append(f"Client: {client_name}")
|
|
344
|
+
status_lines.append(f"Client Version: {client_version}")
|
|
345
|
+
|
|
346
|
+
if self.protocol_version:
|
|
347
|
+
status_lines.append(f"ACP Protocol Version: {self.protocol_version}")
|
|
348
|
+
|
|
349
|
+
if self.client_capabilities:
|
|
350
|
+
# Filesystem capabilities
|
|
351
|
+
if "fs" in self.client_capabilities:
|
|
352
|
+
fs_caps = self.client_capabilities["fs"]
|
|
353
|
+
if fs_caps:
|
|
354
|
+
for key, value in fs_caps.items():
|
|
355
|
+
status_lines.append(f" - {key}: {value}")
|
|
356
|
+
|
|
357
|
+
# Terminal capability
|
|
358
|
+
if "terminal" in self.client_capabilities:
|
|
359
|
+
status_lines.append(f" - Terminal: {self.client_capabilities['terminal']}")
|
|
360
|
+
|
|
361
|
+
# Meta capabilities
|
|
362
|
+
if "_meta" in self.client_capabilities:
|
|
363
|
+
meta_caps = self.client_capabilities["_meta"]
|
|
364
|
+
if meta_caps:
|
|
365
|
+
status_lines.append("Meta:")
|
|
366
|
+
for key, value in meta_caps.items():
|
|
367
|
+
status_lines.append(f" - {key}: {value}")
|
|
368
|
+
|
|
369
|
+
status_lines.append("")
|
|
370
|
+
|
|
371
|
+
# Build model section based on agent type
|
|
372
|
+
if is_parallel_agent:
|
|
373
|
+
# Special handling for PARALLEL agents
|
|
374
|
+
status_lines.append("## Active Models (Parallel Mode)")
|
|
375
|
+
status_lines.append("")
|
|
376
|
+
|
|
377
|
+
# Display fan-out agents
|
|
378
|
+
if hasattr(agent, "fan_out_agents") and agent.fan_out_agents:
|
|
379
|
+
status_lines.append(f"### Fan-Out Agents ({len(agent.fan_out_agents)})")
|
|
380
|
+
for idx, fan_out_agent in enumerate(agent.fan_out_agents, 1):
|
|
381
|
+
agent_name = getattr(fan_out_agent, "name", f"agent-{idx}")
|
|
382
|
+
status_lines.append(f"**{idx}. {agent_name}**")
|
|
383
|
+
|
|
384
|
+
# Get model info for this fan-out agent
|
|
385
|
+
if fan_out_agent.llm:
|
|
386
|
+
model_info = ModelInfo.from_llm(fan_out_agent.llm)
|
|
387
|
+
if model_info:
|
|
388
|
+
provider_display = getattr(
|
|
389
|
+
model_info.provider, "display_name", str(model_info.provider.value)
|
|
390
|
+
)
|
|
391
|
+
status_lines.append(f" - Provider: {provider_display}")
|
|
392
|
+
status_lines.append(f" - Model: {model_info.name}")
|
|
393
|
+
if model_info.context_window:
|
|
394
|
+
status_lines.append(
|
|
395
|
+
f" - Context Window: {model_info.context_window} tokens"
|
|
396
|
+
)
|
|
397
|
+
else:
|
|
398
|
+
status_lines.append(" - Model: unknown")
|
|
399
|
+
|
|
400
|
+
status_lines.append("")
|
|
401
|
+
else:
|
|
402
|
+
status_lines.append("Fan-Out Agents: none configured")
|
|
403
|
+
status_lines.append("")
|
|
404
|
+
|
|
405
|
+
# Display fan-in agent
|
|
406
|
+
if hasattr(agent, "fan_in_agent") and agent.fan_in_agent:
|
|
407
|
+
fan_in_agent = agent.fan_in_agent
|
|
408
|
+
fan_in_name = getattr(fan_in_agent, "name", "aggregator")
|
|
409
|
+
status_lines.append(f"### Fan-In Agent: {fan_in_name}")
|
|
410
|
+
|
|
411
|
+
# Get model info for fan-in agent
|
|
412
|
+
if fan_in_agent.llm:
|
|
413
|
+
model_info = ModelInfo.from_llm(fan_in_agent.llm)
|
|
414
|
+
if model_info:
|
|
415
|
+
provider_display = getattr(
|
|
416
|
+
model_info.provider, "display_name", str(model_info.provider.value)
|
|
417
|
+
)
|
|
418
|
+
status_lines.append(f" - Provider: {provider_display}")
|
|
419
|
+
status_lines.append(f" - Model: {model_info.name}")
|
|
420
|
+
if model_info.context_window:
|
|
421
|
+
status_lines.append(
|
|
422
|
+
f" - Context Window: {model_info.context_window} tokens"
|
|
423
|
+
)
|
|
424
|
+
else:
|
|
425
|
+
status_lines.append(" - Model: unknown")
|
|
426
|
+
|
|
427
|
+
status_lines.append("")
|
|
428
|
+
else:
|
|
429
|
+
status_lines.append("Fan-In Agent: none configured")
|
|
430
|
+
status_lines.append("")
|
|
431
|
+
|
|
432
|
+
else:
|
|
433
|
+
# Standard single-model display
|
|
434
|
+
provider_line = f"{model_provider}"
|
|
435
|
+
if model_provider_display != "unknown":
|
|
436
|
+
provider_line = f"{model_provider_display} ({model_provider})"
|
|
437
|
+
|
|
438
|
+
# For HuggingFace, add the routing provider info
|
|
439
|
+
if agent and agent.llm:
|
|
440
|
+
get_hf_info = getattr(agent.llm, "get_hf_display_info", None)
|
|
441
|
+
if callable(get_hf_info):
|
|
442
|
+
hf_info = get_hf_info()
|
|
443
|
+
hf_provider = hf_info.get("provider", "auto-routing")
|
|
444
|
+
provider_line = f"{model_provider_display} ({model_provider}) / {hf_provider}"
|
|
445
|
+
|
|
446
|
+
status_lines.extend(
|
|
447
|
+
[
|
|
448
|
+
"## Active Model",
|
|
449
|
+
f"- Provider: {provider_line}",
|
|
450
|
+
f"- Model: {model_name}",
|
|
451
|
+
f"- Context Window: {context_window}",
|
|
452
|
+
f"- {capabilities_line}",
|
|
453
|
+
"",
|
|
454
|
+
]
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# Add conversation statistics
|
|
458
|
+
status_lines.append(
|
|
459
|
+
f"## Conversation Statistics ({getattr(agent, 'name', self.current_agent_name) if agent else 'Unknown'})"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
uptime_seconds = max(time.time() - self._created_at, 0.0)
|
|
463
|
+
status_lines.extend(summary_stats)
|
|
464
|
+
status_lines.extend(["", f"ACP Agent Uptime: {format_duration(uptime_seconds)}"])
|
|
465
|
+
status_lines.extend(["", "## Error Handling"])
|
|
466
|
+
status_lines.extend(self._get_error_handling_report(agent))
|
|
467
|
+
|
|
468
|
+
return "\n".join(status_lines)
|
|
469
|
+
|
|
470
|
+
def _handle_status_system(self) -> str:
|
|
471
|
+
"""Handle the /status system command to show the system prompt."""
|
|
472
|
+
heading = "# system prompt"
|
|
473
|
+
|
|
474
|
+
agent, error = self._get_current_agent_or_error(heading)
|
|
475
|
+
if error:
|
|
476
|
+
return error
|
|
477
|
+
|
|
478
|
+
# Get the system prompt from the agent's instruction attribute
|
|
479
|
+
system_prompt = self._session_instructions.get(
|
|
480
|
+
getattr(agent, "name", self.current_agent_name), getattr(agent, "instruction", None)
|
|
481
|
+
)
|
|
482
|
+
if not system_prompt:
|
|
483
|
+
return "\n".join(
|
|
484
|
+
[
|
|
485
|
+
heading,
|
|
486
|
+
"",
|
|
487
|
+
"No system prompt available for this agent.",
|
|
488
|
+
]
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Format the response
|
|
492
|
+
agent_name = getattr(agent, "name", self.current_agent_name)
|
|
493
|
+
lines = [
|
|
494
|
+
heading,
|
|
495
|
+
"",
|
|
496
|
+
f"**Agent:** {agent_name}",
|
|
497
|
+
"",
|
|
498
|
+
system_prompt,
|
|
499
|
+
]
|
|
500
|
+
|
|
501
|
+
return "\n".join(lines)
|
|
502
|
+
|
|
503
|
+
def _handle_status_auth(self) -> str:
|
|
504
|
+
"""Handle the /status auth command to show permissions from auths.md."""
|
|
505
|
+
heading = "# permissions"
|
|
506
|
+
auths_path = Path("./.fast-agent/auths.md")
|
|
507
|
+
resolved_path = auths_path.resolve()
|
|
508
|
+
|
|
509
|
+
if not auths_path.exists():
|
|
510
|
+
return "\n".join(
|
|
511
|
+
[
|
|
512
|
+
heading,
|
|
513
|
+
"",
|
|
514
|
+
"No permissions set",
|
|
515
|
+
"",
|
|
516
|
+
f"Path: `{resolved_path}`",
|
|
517
|
+
]
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
content = auths_path.read_text(encoding="utf-8")
|
|
522
|
+
return "\n".join(
|
|
523
|
+
[
|
|
524
|
+
heading,
|
|
525
|
+
"",
|
|
526
|
+
content.strip() if content.strip() else "No permissions set",
|
|
527
|
+
"",
|
|
528
|
+
f"Path: `{resolved_path}`",
|
|
529
|
+
]
|
|
530
|
+
)
|
|
531
|
+
except Exception as exc:
|
|
532
|
+
return "\n".join(
|
|
533
|
+
[
|
|
534
|
+
heading,
|
|
535
|
+
"",
|
|
536
|
+
f"Failed to read permissions file: {exc}",
|
|
537
|
+
"",
|
|
538
|
+
f"Path: `{resolved_path}`",
|
|
539
|
+
]
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
def _handle_status_authreset(self) -> str:
|
|
543
|
+
"""Handle the /status authreset command to remove the auths.md file."""
|
|
544
|
+
heading = "# reset permissions"
|
|
545
|
+
auths_path = Path("./.fast-agent/auths.md")
|
|
546
|
+
resolved_path = auths_path.resolve()
|
|
547
|
+
|
|
548
|
+
if not auths_path.exists():
|
|
549
|
+
return "\n".join(
|
|
550
|
+
[
|
|
551
|
+
heading,
|
|
552
|
+
"",
|
|
553
|
+
"No permissions file exists.",
|
|
554
|
+
"",
|
|
555
|
+
f"Path: `{resolved_path}`",
|
|
556
|
+
]
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
try:
|
|
560
|
+
auths_path.unlink()
|
|
561
|
+
return "\n".join(
|
|
562
|
+
[
|
|
563
|
+
heading,
|
|
564
|
+
"",
|
|
565
|
+
"Permissions file removed successfully.",
|
|
566
|
+
"",
|
|
567
|
+
f"Path: `{resolved_path}`",
|
|
568
|
+
]
|
|
569
|
+
)
|
|
570
|
+
except Exception as exc:
|
|
571
|
+
return "\n".join(
|
|
572
|
+
[
|
|
573
|
+
heading,
|
|
574
|
+
"",
|
|
575
|
+
f"Failed to remove permissions file: {exc}",
|
|
576
|
+
"",
|
|
577
|
+
f"Path: `{resolved_path}`",
|
|
578
|
+
]
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
async def _handle_tools(self) -> str:
|
|
582
|
+
"""List available tools for the current agent."""
|
|
583
|
+
heading = "# tools"
|
|
584
|
+
|
|
585
|
+
agent, error = self._get_current_agent_or_error(heading)
|
|
586
|
+
if error:
|
|
587
|
+
return error
|
|
588
|
+
|
|
589
|
+
if not isinstance(agent, AgentProtocol):
|
|
590
|
+
return "\n".join(
|
|
591
|
+
[
|
|
592
|
+
heading,
|
|
593
|
+
"",
|
|
594
|
+
"This agent does not support tool listing.",
|
|
595
|
+
]
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
try:
|
|
599
|
+
tools_result: "ListToolsResult" = await agent.list_tools()
|
|
600
|
+
except Exception as exc:
|
|
601
|
+
return "\n".join(
|
|
602
|
+
[
|
|
603
|
+
heading,
|
|
604
|
+
"",
|
|
605
|
+
"Failed to fetch tools from the agent.",
|
|
606
|
+
f"Details: {exc}",
|
|
607
|
+
]
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
tools = tools_result.tools if tools_result else None
|
|
611
|
+
if not tools:
|
|
612
|
+
return "\n".join(
|
|
613
|
+
[
|
|
614
|
+
heading,
|
|
615
|
+
"",
|
|
616
|
+
"No MCP tools available for this agent.",
|
|
617
|
+
]
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
lines = [heading, ""]
|
|
621
|
+
for index, tool in enumerate(tools, start=1):
|
|
622
|
+
lines.extend(self._format_tool_lines(tool, index))
|
|
623
|
+
lines.append("")
|
|
624
|
+
|
|
625
|
+
return "\n".join(lines).strip()
|
|
626
|
+
|
|
627
|
+
def _format_tool_lines(self, tool: "Tool", index: int) -> list[str]:
|
|
628
|
+
"""
|
|
629
|
+
Convert a Tool into markdown-friendly lines.
|
|
630
|
+
|
|
631
|
+
We avoid fragile getattr usage by relying on the typed attributes
|
|
632
|
+
provided by mcp.types.Tool. Additional guards are added for optional fields.
|
|
633
|
+
"""
|
|
634
|
+
lines: list[str] = []
|
|
635
|
+
|
|
636
|
+
meta = tool.meta or {}
|
|
637
|
+
name = tool.name or "unnamed"
|
|
638
|
+
title = (tool.title or "").strip()
|
|
639
|
+
|
|
640
|
+
header = f"{index}. **{name}**"
|
|
641
|
+
if title:
|
|
642
|
+
header = f"{header} - {title}"
|
|
643
|
+
if meta.get("openai/skybridgeEnabled"):
|
|
644
|
+
header = f"{header} _(skybridge)_"
|
|
645
|
+
lines.append(header)
|
|
646
|
+
|
|
647
|
+
description = (tool.description or "").strip()
|
|
648
|
+
if description:
|
|
649
|
+
wrapped = textwrap.wrap(description, width=92)
|
|
650
|
+
if wrapped:
|
|
651
|
+
indent = " "
|
|
652
|
+
lines.extend(f"{indent}{desc_line}" for desc_line in wrapped[:6])
|
|
653
|
+
if len(wrapped) > 6:
|
|
654
|
+
lines.append(f"{indent}...")
|
|
655
|
+
|
|
656
|
+
args_line = self._format_tool_arguments(tool)
|
|
657
|
+
if args_line:
|
|
658
|
+
lines.append(f" - Args: {args_line}")
|
|
659
|
+
|
|
660
|
+
template = meta.get("openai/skybridgeTemplate")
|
|
661
|
+
if template:
|
|
662
|
+
lines.append(f" - Template: `{template}`")
|
|
663
|
+
|
|
664
|
+
return lines
|
|
665
|
+
|
|
666
|
+
def _format_tool_arguments(self, tool: "Tool") -> str | None:
|
|
667
|
+
"""Render tool input schema fields as inline-code argument list."""
|
|
668
|
+
schema = tool.inputSchema if isinstance(tool.inputSchema, dict) else None
|
|
669
|
+
if not schema:
|
|
670
|
+
return None
|
|
671
|
+
|
|
672
|
+
properties = schema.get("properties")
|
|
673
|
+
if not isinstance(properties, dict) or not properties:
|
|
674
|
+
return None
|
|
675
|
+
|
|
676
|
+
required_raw = schema.get("required", [])
|
|
677
|
+
required = set(required_raw) if isinstance(required_raw, list) else set()
|
|
678
|
+
|
|
679
|
+
args: list[str] = []
|
|
680
|
+
for prop_name in properties.keys():
|
|
681
|
+
suffix = "*" if prop_name in required else ""
|
|
682
|
+
args.append(f"`{prop_name}{suffix}`")
|
|
683
|
+
|
|
684
|
+
return ", ".join(args) if args else None
|
|
685
|
+
|
|
686
|
+
async def _handle_save(self, arguments: str | None = None) -> str:
|
|
687
|
+
"""Handle the /save command by persisting conversation history."""
|
|
688
|
+
heading = "# save conversation"
|
|
689
|
+
|
|
690
|
+
agent, error = self._get_current_agent_or_error(
|
|
691
|
+
heading,
|
|
692
|
+
missing_template=f"Unable to locate agent '{self.current_agent_name}' for this session.",
|
|
693
|
+
)
|
|
694
|
+
if error:
|
|
695
|
+
return error
|
|
696
|
+
|
|
697
|
+
filename = arguments.strip() if arguments and arguments.strip() else None
|
|
698
|
+
|
|
699
|
+
try:
|
|
700
|
+
saved_path = await self.history_exporter.save(agent, filename)
|
|
701
|
+
except Exception as exc:
|
|
702
|
+
return "\n".join(
|
|
703
|
+
[
|
|
704
|
+
heading,
|
|
705
|
+
"",
|
|
706
|
+
"Failed to save conversation history.",
|
|
707
|
+
f"Details: {exc}",
|
|
708
|
+
]
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
return "\n".join(
|
|
712
|
+
[
|
|
713
|
+
heading,
|
|
714
|
+
"",
|
|
715
|
+
"Conversation history saved successfully.",
|
|
716
|
+
f"Filename: `{saved_path}`",
|
|
717
|
+
]
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
async def _handle_load(self, arguments: str | None = None) -> str:
|
|
721
|
+
"""Handle the /load command by loading conversation history from a file."""
|
|
722
|
+
heading = "# load conversation"
|
|
723
|
+
|
|
724
|
+
agent, error = self._get_current_agent_or_error(
|
|
725
|
+
heading,
|
|
726
|
+
missing_template=f"Unable to locate agent '{self.current_agent_name}' for this session.",
|
|
727
|
+
)
|
|
728
|
+
if error:
|
|
729
|
+
return error
|
|
730
|
+
|
|
731
|
+
filename = arguments.strip() if arguments and arguments.strip() else None
|
|
732
|
+
|
|
733
|
+
if not filename:
|
|
734
|
+
return "\n".join(
|
|
735
|
+
[
|
|
736
|
+
heading,
|
|
737
|
+
"",
|
|
738
|
+
"Filename required for /load command.",
|
|
739
|
+
"Usage: /load <filename>",
|
|
740
|
+
]
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
file_path = Path(filename)
|
|
744
|
+
if not file_path.exists():
|
|
745
|
+
return "\n".join(
|
|
746
|
+
[
|
|
747
|
+
heading,
|
|
748
|
+
"",
|
|
749
|
+
f"File not found: `{filename}`",
|
|
750
|
+
]
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
try:
|
|
754
|
+
load_history_into_agent(agent, file_path)
|
|
755
|
+
except Exception as exc:
|
|
756
|
+
return "\n".join(
|
|
757
|
+
[
|
|
758
|
+
heading,
|
|
759
|
+
"",
|
|
760
|
+
"Failed to load conversation history.",
|
|
761
|
+
f"Details: {exc}",
|
|
762
|
+
]
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
message_count = len(agent.message_history) if hasattr(agent, "message_history") else 0
|
|
766
|
+
|
|
767
|
+
return "\n".join(
|
|
768
|
+
[
|
|
769
|
+
heading,
|
|
770
|
+
"",
|
|
771
|
+
"Conversation history loaded successfully.",
|
|
772
|
+
f"Filename: `{filename}`",
|
|
773
|
+
f"Messages: {message_count}",
|
|
774
|
+
]
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
async def _handle_clear(self, arguments: str | None = None) -> str:
|
|
778
|
+
"""Handle /clear and /clear last commands."""
|
|
779
|
+
normalized = (arguments or "").strip().lower()
|
|
780
|
+
if normalized == "last":
|
|
781
|
+
return self._handle_clear_last()
|
|
782
|
+
return self._handle_clear_all()
|
|
783
|
+
|
|
784
|
+
def _handle_clear_all(self) -> str:
|
|
785
|
+
"""Clear the entire conversation history."""
|
|
786
|
+
heading = "# clear conversation"
|
|
787
|
+
agent, error = self._get_current_agent_or_error(
|
|
788
|
+
heading,
|
|
789
|
+
missing_template=f"Unable to locate agent '{self.current_agent_name}' for this session.",
|
|
790
|
+
)
|
|
791
|
+
if error:
|
|
792
|
+
return error
|
|
793
|
+
|
|
794
|
+
try:
|
|
795
|
+
history = getattr(agent, "message_history", None)
|
|
796
|
+
original_count = len(history) if isinstance(history, list) else None
|
|
797
|
+
|
|
798
|
+
cleared = False
|
|
799
|
+
clear_method = getattr(agent, "clear", None)
|
|
800
|
+
if callable(clear_method):
|
|
801
|
+
clear_method()
|
|
802
|
+
cleared = True
|
|
803
|
+
elif isinstance(history, list):
|
|
804
|
+
history.clear()
|
|
805
|
+
cleared = True
|
|
806
|
+
except Exception as exc:
|
|
807
|
+
return "\n".join(
|
|
808
|
+
[
|
|
809
|
+
heading,
|
|
810
|
+
"",
|
|
811
|
+
"Failed to clear conversation history.",
|
|
812
|
+
f"Details: {exc}",
|
|
813
|
+
]
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
if not cleared:
|
|
817
|
+
return "\n".join(
|
|
818
|
+
[
|
|
819
|
+
heading,
|
|
820
|
+
"",
|
|
821
|
+
"Agent does not expose a clear() method or message history list.",
|
|
822
|
+
]
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
removed_text = (
|
|
826
|
+
f"Removed {original_count} message(s)." if isinstance(original_count, int) else ""
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
response_lines = [
|
|
830
|
+
heading,
|
|
831
|
+
"",
|
|
832
|
+
"Conversation history cleared.",
|
|
833
|
+
]
|
|
834
|
+
|
|
835
|
+
if removed_text:
|
|
836
|
+
response_lines.append(removed_text)
|
|
837
|
+
|
|
838
|
+
return "\n".join(response_lines)
|
|
839
|
+
|
|
840
|
+
def _handle_clear_last(self) -> str:
|
|
841
|
+
"""Remove the most recent conversation message."""
|
|
842
|
+
heading = "# clear last conversation turn"
|
|
843
|
+
agent, error = self._get_current_agent_or_error(
|
|
844
|
+
heading,
|
|
845
|
+
missing_template=f"Unable to locate agent '{self.current_agent_name}' for this session.",
|
|
846
|
+
)
|
|
847
|
+
if error:
|
|
848
|
+
return error
|
|
849
|
+
|
|
850
|
+
try:
|
|
851
|
+
removed = None
|
|
852
|
+
pop_method = getattr(agent, "pop_last_message", None)
|
|
853
|
+
if callable(pop_method):
|
|
854
|
+
removed = pop_method()
|
|
855
|
+
else:
|
|
856
|
+
history = getattr(agent, "message_history", None)
|
|
857
|
+
if isinstance(history, list) and history:
|
|
858
|
+
removed = history.pop()
|
|
859
|
+
except Exception as exc:
|
|
860
|
+
return "\n".join(
|
|
861
|
+
[
|
|
862
|
+
heading,
|
|
863
|
+
"",
|
|
864
|
+
"Failed to remove the last message.",
|
|
865
|
+
f"Details: {exc}",
|
|
866
|
+
]
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
if removed is None:
|
|
870
|
+
return "\n".join(
|
|
871
|
+
[
|
|
872
|
+
heading,
|
|
873
|
+
"",
|
|
874
|
+
"No messages available to remove.",
|
|
875
|
+
]
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
role = getattr(removed, "role", "message")
|
|
879
|
+
return "\n".join(
|
|
880
|
+
[
|
|
881
|
+
heading,
|
|
882
|
+
"",
|
|
883
|
+
f"Removed last {role} message.",
|
|
884
|
+
]
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
def _get_conversation_stats(self, agent) -> list[str]:
|
|
888
|
+
"""Get conversation statistics from the agent's message history."""
|
|
889
|
+
if not agent or not hasattr(agent, "message_history"):
|
|
890
|
+
return [
|
|
891
|
+
"- Turns: 0",
|
|
892
|
+
"- Tool Calls: 0",
|
|
893
|
+
"- Context Used: 0%",
|
|
894
|
+
]
|
|
895
|
+
|
|
896
|
+
try:
|
|
897
|
+
# Create a conversation summary from message history
|
|
898
|
+
summary = ConversationSummary(messages=agent.message_history)
|
|
899
|
+
|
|
900
|
+
# Calculate turns (user + assistant message pairs)
|
|
901
|
+
turns = min(summary.user_message_count, summary.assistant_message_count)
|
|
902
|
+
|
|
903
|
+
# Get tool call statistics
|
|
904
|
+
tool_calls = summary.tool_calls
|
|
905
|
+
tool_errors = summary.tool_errors
|
|
906
|
+
tool_successes = summary.tool_successes
|
|
907
|
+
context_usage_line = self._context_usage_line(summary, agent)
|
|
908
|
+
|
|
909
|
+
stats = [
|
|
910
|
+
f"- Turns: {turns}",
|
|
911
|
+
f"- Messages: {summary.message_count} (user: {summary.user_message_count}, assistant: {summary.assistant_message_count})",
|
|
912
|
+
f"- Tool Calls: {tool_calls} (successes: {tool_successes}, errors: {tool_errors})",
|
|
913
|
+
context_usage_line,
|
|
914
|
+
]
|
|
915
|
+
|
|
916
|
+
# Add timing information if available
|
|
917
|
+
if summary.total_elapsed_time_ms > 0:
|
|
918
|
+
stats.append(
|
|
919
|
+
f"- Total LLM Time: {format_duration(summary.total_elapsed_time_ms / 1000)}"
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
if summary.conversation_span_ms > 0:
|
|
923
|
+
span_seconds = summary.conversation_span_ms / 1000
|
|
924
|
+
stats.append(
|
|
925
|
+
f"- Conversation Runtime (LLM + tools): {format_duration(span_seconds)}"
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
# Add tool breakdown if there were tool calls
|
|
929
|
+
if tool_calls > 0 and summary.tool_call_map:
|
|
930
|
+
stats.append("")
|
|
931
|
+
stats.append("### Tool Usage Breakdown")
|
|
932
|
+
for tool_name, count in sorted(
|
|
933
|
+
summary.tool_call_map.items(), key=lambda x: x[1], reverse=True
|
|
934
|
+
):
|
|
935
|
+
stats.append(f" - {tool_name}: {count}")
|
|
936
|
+
|
|
937
|
+
return stats
|
|
938
|
+
|
|
939
|
+
except Exception as e:
|
|
940
|
+
return [
|
|
941
|
+
"- Turns: error",
|
|
942
|
+
"- Tool Calls: error",
|
|
943
|
+
f"- Context Used: error ({e})",
|
|
944
|
+
]
|
|
945
|
+
|
|
946
|
+
def _get_error_handling_report(self, agent, max_entries: int = 3) -> list[str]:
|
|
947
|
+
"""Summarize error channel availability and recent entries."""
|
|
948
|
+
channel_label = f"Error Channel: {FAST_AGENT_ERROR_CHANNEL}"
|
|
949
|
+
if not agent or not hasattr(agent, "message_history"):
|
|
950
|
+
return ["_No errors recorded_"]
|
|
951
|
+
|
|
952
|
+
recent_entries: list[str] = []
|
|
953
|
+
history = getattr(agent, "message_history", []) or []
|
|
954
|
+
|
|
955
|
+
for message in reversed(history):
|
|
956
|
+
channels = getattr(message, "channels", None) or {}
|
|
957
|
+
channel_blocks = channels.get(FAST_AGENT_ERROR_CHANNEL)
|
|
958
|
+
if not channel_blocks:
|
|
959
|
+
continue
|
|
960
|
+
|
|
961
|
+
for block in channel_blocks:
|
|
962
|
+
text = get_text(block)
|
|
963
|
+
if text:
|
|
964
|
+
cleaned = text.replace("\n", " ").strip()
|
|
965
|
+
if cleaned:
|
|
966
|
+
recent_entries.append(cleaned)
|
|
967
|
+
else:
|
|
968
|
+
# Truncate long content (e.g., base64 image data)
|
|
969
|
+
block_str = str(block)
|
|
970
|
+
if len(block_str) > 60:
|
|
971
|
+
recent_entries.append(f"{block_str[:60]}... ({len(block_str)} characters)")
|
|
972
|
+
else:
|
|
973
|
+
recent_entries.append(block_str)
|
|
974
|
+
if len(recent_entries) >= max_entries:
|
|
975
|
+
break
|
|
976
|
+
if len(recent_entries) >= max_entries:
|
|
977
|
+
break
|
|
978
|
+
|
|
979
|
+
if recent_entries:
|
|
980
|
+
lines = [channel_label, "Recent Entries:"]
|
|
981
|
+
lines.extend(f"- {entry}" for entry in recent_entries)
|
|
982
|
+
return lines
|
|
983
|
+
|
|
984
|
+
return ["_No errors recorded_"]
|
|
985
|
+
|
|
986
|
+
def _context_usage_line(self, summary: ConversationSummary, agent) -> str:
|
|
987
|
+
"""Generate a context usage line with token estimation and fallbacks."""
|
|
988
|
+
# Prefer usage accumulator when available (matches enhanced/interactive prompt display)
|
|
989
|
+
usage = getattr(agent, "usage_accumulator", None)
|
|
990
|
+
if usage:
|
|
991
|
+
window = usage.context_window_size
|
|
992
|
+
tokens = usage.current_context_tokens
|
|
993
|
+
pct = usage.context_usage_percentage
|
|
994
|
+
if window and pct is not None:
|
|
995
|
+
return f"- Context Used: {min(pct, 100.0):.1f}% (~{tokens:,} tokens of {window:,})"
|
|
996
|
+
if tokens:
|
|
997
|
+
return f"- Context Used: ~{tokens:,} tokens (window unknown)"
|
|
998
|
+
|
|
999
|
+
# Fallback to tokenizing the actual conversation text
|
|
1000
|
+
token_count, char_count = self._estimate_tokens(summary, agent)
|
|
1001
|
+
|
|
1002
|
+
model_info = ModelInfo.from_llm(agent.llm) if getattr(agent, "llm", None) else None
|
|
1003
|
+
if model_info and model_info.context_window:
|
|
1004
|
+
percentage = (
|
|
1005
|
+
(token_count / model_info.context_window) * 100
|
|
1006
|
+
if model_info.context_window
|
|
1007
|
+
else 0.0
|
|
1008
|
+
)
|
|
1009
|
+
percentage = min(percentage, 100.0)
|
|
1010
|
+
return f"- Context Used: {percentage:.1f}% (~{token_count:,} tokens of {model_info.context_window:,})"
|
|
1011
|
+
|
|
1012
|
+
token_text = f"~{token_count:,} tokens" if token_count else "~0 tokens"
|
|
1013
|
+
return f"- Context Used: {char_count:,} chars ({token_text} est.)"
|
|
1014
|
+
|
|
1015
|
+
def _estimate_tokens(self, summary: ConversationSummary, agent) -> tuple[int, int]:
|
|
1016
|
+
"""Estimate tokens and return (tokens, characters) for the conversation history."""
|
|
1017
|
+
text_parts: list[str] = []
|
|
1018
|
+
for message in summary.messages:
|
|
1019
|
+
for content in getattr(message, "content", []) or []:
|
|
1020
|
+
text = get_text(content)
|
|
1021
|
+
if text:
|
|
1022
|
+
text_parts.append(text)
|
|
1023
|
+
|
|
1024
|
+
combined = "\n".join(text_parts)
|
|
1025
|
+
char_count = len(combined)
|
|
1026
|
+
if not combined:
|
|
1027
|
+
return 0, 0
|
|
1028
|
+
|
|
1029
|
+
model_name = None
|
|
1030
|
+
llm = getattr(agent, "llm", None)
|
|
1031
|
+
if llm:
|
|
1032
|
+
model_name = getattr(llm, "model_name", None)
|
|
1033
|
+
|
|
1034
|
+
token_count = self._count_tokens_with_tiktoken(combined, model_name)
|
|
1035
|
+
return token_count, char_count
|
|
1036
|
+
|
|
1037
|
+
def _count_tokens_with_tiktoken(self, text: str, model_name: str | None) -> int:
|
|
1038
|
+
"""Try to count tokens with tiktoken; fall back to a rough chars/4 estimate."""
|
|
1039
|
+
try:
|
|
1040
|
+
import tiktoken
|
|
1041
|
+
|
|
1042
|
+
if model_name:
|
|
1043
|
+
encoding = tiktoken.encoding_for_model(model_name)
|
|
1044
|
+
else:
|
|
1045
|
+
encoding = tiktoken.get_encoding("cl100k_base")
|
|
1046
|
+
|
|
1047
|
+
return len(encoding.encode(text))
|
|
1048
|
+
except Exception:
|
|
1049
|
+
# Rough heuristic: ~4 characters per token (matches default bytes/token constant)
|
|
1050
|
+
return max(1, (len(text) + 3) // 4)
|