diff --git a/docs/ACP_IMPLEMENTATION_OVERVIEW.md b/docs/ACP_IMPLEMENTATION_OVERVIEW.md index c62447840..c68c2fb57 100644 --- a/docs/ACP_IMPLEMENTATION_OVERVIEW.md +++ b/docs/ACP_IMPLEMENTATION_OVERVIEW.md @@ -395,6 +395,14 @@ fast-agent serve --transport acp \ --model haiku ``` +### With AgentCards + Auto-Reload +```bash +fast-agent serve --transport acp \ + --card ./agents \ + --model haiku \ + --watch # Auto-reload AgentCards on change +``` + ### With Instance Scoping ```bash fast-agent serve --transport acp \ @@ -411,6 +419,8 @@ fast-agent serve --transport acp \ --servers filesystem,github # Expose MCP tools to agent ``` +Note: `--reload` enables the `/reload` slash command in ACP sessions. + --- ## Integration with Editors diff --git a/docs/ACP_TESTING.md b/docs/ACP_TESTING.md index 4376380f7..82691b5c8 100644 --- a/docs/ACP_TESTING.md +++ b/docs/ACP_TESTING.md @@ -13,17 +13,27 @@ The Agent Client Protocol (ACP) support in fast-agent allows it to act as an ACP export OPENAI_API_KEY="your-key-here" ``` -2. **Instruction File**: Create a simple instruction file +2. **Agent instructions**: Use either an instruction file *or* AgentCards ```bash + # Option A: instruction file echo "You are a helpful AI assistant." > /tmp/instruction.md ``` + ```bash + # Option B: AgentCards directory + mkdir -p ./agents + # Drop one or more AgentCard .md/.yaml files into ./agents + ``` ### Running the Server -Start the ACP server: +Start the ACP server (pick one): ```bash -fast-agent serve --transport acp --instruction /tmp/instruction.md --model haiku +fast-agent serve --transport acp --instruction /tmp/instruction.md --model haiku --watch +``` + +```bash +fast-agent serve --transport acp --card ./agents --model haiku --watch ``` The server will: @@ -57,12 +67,12 @@ class SimpleClient: # Add other required methods... async def test(): - # Spawn fast-agent as ACP server + # Spawn fast-agent as ACP server (pick one) async with spawn_agent_process( lambda agent: SimpleClient(agent), 'fast-agent', 'serve', '--transport', 'acp', '--instruction', '/tmp/instruction.md', - '--model', 'haiku', + '--model', 'haiku', '--watch', ) as (connection, process): # 1. Initialize @@ -97,13 +107,16 @@ if __name__ == "__main__": asyncio.run(test()) ``` +Note: replace `--instruction /tmp/instruction.md` with `--card ./agents` if you want to run from AgentCards. + ### Testing with Manual JSON-RPC You can also test by sending raw JSON-RPC messages: ```bash -# Terminal 1: Start server +# Terminal 1: Start server (pick one) fast-agent serve --transport acp --instruction /tmp/instruction.md --model haiku +fast-agent serve --transport acp --card ./agents --model haiku # Terminal 2: Send messages # Initialize diff --git a/plan/acp-mcp-watch.md b/plan/acp-mcp-watch.md new file mode 100644 index 000000000..b7617212d --- /dev/null +++ b/plan/acp-mcp-watch.md @@ -0,0 +1,80 @@ +# ACP/MCP `--watch` + `--reload` support (spec) + +## Goal +Enable AgentCard auto-reload and manual reload in server mode for: +- ACP transport (`fast-agent serve --transport acp`) +- MCP transports (`http`, `sse`, `stdio`) + +`fast-agent go --watch/--reload` already works for interactive mode. This spec covers +equivalent behavior in `serve`. + +## Current behavior (baseline) +- `fast-agent go --card --watch` starts the AgentCard watcher. +- `serve` does **not** expose `--watch` or `--reload`. +- `serve` uses `allow_extra_args` + `ignore_unknown_options`, so `--watch` is silently ignored. +- `FastAgent` only starts `_watch_agent_cards()` when `args.watch` is True. + +## Desired behavior +1) `fast-agent serve --card --watch` automatically reloads AgentCards on change. +2) `fast-agent serve --card --reload` enables `/reload` (manual) from ACP slash command or MCP command hook. +3) Behavior mirrors `go` where possible: + - Watcher only runs when AgentCards were loaded and `--watch` is set. + - Reload is safe in both shared and per-session scopes. + +## Implementation plan + +### 1) CLI: add flags to `serve` +File: `src/fast_agent/cli/commands/serve.py` +- Add options: + - `reload: bool = typer.Option(False, "--reload", help="Enable manual AgentCard reloads (/reload)")` + - `watch: bool = typer.Option(False, "--watch", help="Watch AgentCard paths and reload")` +- Pass through to `run_async_agent(..., reload=reload, watch=watch)`. + +### 2) CLI: pass flags through server runner +File: `src/fast_agent/cli/commands/go.py` +- `run_async_agent()` already accepts `reload` and `watch`. +- Ensure `serve` path forwards them (no other changes needed). + +### 3) FastAgent: start watcher in server mode +Already implemented in `FastAgent.run()`: +- Watcher starts when `args.watch` is True and `_agent_card_roots` is non-empty. +- `args.watch` is only set by CLI or programmatic calls. +No code change required beyond setting `args.watch` via CLI. + +### 4) Manual reload in ACP +Behavior: +- `/reload` is provided by ACP slash command handler. +- It calls the `load_card_callback`/`reload` hooks supplied by `FastAgent`. +Ensure the reload callback is set for server mode: +- In `FastAgent.run()`, `wrapper.set_reload_callback(...)` runs when `args.reload` or `args.watch` is True. +No change required if `args.reload` is passed through. + +### 5) Manual reload in MCP +There is no built-in MCP slash command. Options: +1) Add an MCP tool, e.g., `reload_agent_cards`, that calls `AgentApp.reload_agents()`. +2) Expose reload via an MCP resource or prompt (lower priority). + +Spec choice: **Add MCP tool**. +- File: `src/fast_agent/mcp/server/agent_server.py` +- Register a tool `reload_agent_cards` when `args.reload` or `args.watch` is True. +- Implementation: call `instance.app.reload_agents()` (or via `AgentApp`). +- Return a boolean (changed or not). + +### 6) Documentation +Update: +- `docs/ACP_TESTING.md`: include `--watch` in ACP server examples. +- `docs/ACP_IMPLEMENTATION_OVERVIEW.md`: mention watch/reload availability in server mode. +- CLI docs: `src/fast_agent/cli/commands/README.md` to list `serve --watch/--reload`. + +## Edge cases +- **No AgentCards loaded**: `--watch` should be a no-op (same as now). +- **Shared instance scope**: reloading replaces the primary instance; session maps are refreshed (already handled). +- **Connection/request scope**: each session instance should refresh safely; reload should update the instance assigned to that session. +- **Concurrent prompts**: ACP already blocks per-session overlap; MCP tool should respect request concurrency (use existing locks). + +## Validation checklist +- `fast-agent serve --transport acp --card ./agents --watch` reloads on file change. +- `fast-agent serve --transport acp --card ./agents --reload` responds to `/reload`. +- `fast-agent serve --transport http --card ./agents --watch` reloads on file change. +- MCP tool `reload_agent_cards` is exposed when `--reload`/`--watch` is set. +- No regressions in `go` behavior. diff --git a/src/fast_agent/acp/server/agent_acp_server.py b/src/fast_agent/acp/server/agent_acp_server.py index 9fe2ce064..05d9488b9 100644 --- a/src/fast_agent/acp/server/agent_acp_server.py +++ b/src/fast_agent/acp/server/agent_acp_server.py @@ -202,6 +202,7 @@ def __init__( permissions_enabled: bool = True, get_registry_version: Callable[[], int] | None = None, load_card_callback: Callable[[str], Awaitable[list[str]]] | None = None, + reload_callback: Callable[[], Awaitable[bool]] | None = None, ) -> None: """ Initialize the ACP server. @@ -216,6 +217,7 @@ def __init__( skills_directory_override: Optional skills directory override (relative to session cwd) permissions_enabled: Whether to request tool permissions from client (default: True) load_card_callback: Optional callback to load AgentCards at runtime + reload_callback: Optional callback to reload AgentCards from disk """ super().__init__() @@ -225,6 +227,7 @@ def __init__( self._instance_scope = instance_scope self._get_registry_version = get_registry_version self._load_card_callback = load_card_callback + self._reload_callback = reload_callback self._primary_registry_version = getattr(primary_instance, "registry_version", 0) self._shared_reload_lock = asyncio.Lock() self._stale_instances: list[AgentInstance] = [] @@ -616,6 +619,9 @@ def _refresh_session_state( async def load_card(source: str) -> tuple[AgentInstance, list[str]]: return await self._load_agent_card_for_session(session_state, source) + async def reload_cards() -> bool: + return await self._reload_agent_cards_for_session(session_state.session_id) + slash_handler = SlashCommandHandler( session_state.session_id, instance, @@ -625,6 +631,7 @@ async def load_card(source: str) -> tuple[AgentInstance, list[str]]: protocol_version=self._protocol_version, session_instructions=resolved_for_session, card_loader=load_card if self._load_card_callback else None, + reload_callback=reload_cards if self._reload_callback else None, ) session_state.slash_handler = slash_handler @@ -695,6 +702,50 @@ async def _load_agent_card_for_session( return instance, loaded_names + async def _reload_agent_cards_for_session(self, session_id: str) -> bool: + if not self._reload_callback: + return False + if session_id in self._active_prompts: + current_task = asyncio.current_task() + session_task = self._session_tasks.get(session_id) + if current_task != session_task: + raise RuntimeError("Cannot reload while a prompt is active for this session.") + + changed = await self._reload_callback() + if not changed: + return False + + if self._instance_scope == "shared": + await self._maybe_refresh_shared_instance() + return True + + async with self._session_lock: + session_state = self._session_state.get(session_id) + if not session_state: + return True + + instance = await self._create_instance_task() + old_instance = session_state.instance + session_state.instance = instance + async with self._session_lock: + self.sessions[session_id] = instance + self._refresh_session_state(session_state, instance) + if old_instance != self.primary_instance: + try: + await self._dispose_instance_task(old_instance) + except Exception as exc: + logger.warning( + "Failed to dispose old session instance after reload", + name="acp_reload_dispose_error", + session_id=session_id, + error=str(exc), + ) + + if session_state.acp_context: + await session_state.acp_context.send_available_commands_update() + + return True + async def _dispose_stale_instances_if_idle(self) -> None: if self._active_prompts: return @@ -965,6 +1016,9 @@ async def new_session( async def load_card(source: str) -> tuple[AgentInstance, list[str]]: return await self._load_agent_card_for_session(session_state, source) + async def reload_cards() -> bool: + return await self._reload_agent_cards_for_session(session_id) + slash_handler = SlashCommandHandler( session_id, instance, @@ -974,6 +1028,7 @@ async def load_card(source: str) -> tuple[AgentInstance, list[str]]: protocol_version=self._protocol_version, session_instructions=resolved_prompts, card_loader=load_card if self._load_card_callback else None, + reload_callback=reload_cards if self._reload_callback else None, ) session_state.slash_handler = slash_handler diff --git a/src/fast_agent/acp/slash_commands.py b/src/fast_agent/acp/slash_commands.py index 6a10c0e32..5792ed6d7 100644 --- a/src/fast_agent/acp/slash_commands.py +++ b/src/fast_agent/acp/slash_commands.py @@ -119,6 +119,7 @@ def __init__( protocol_version: int | None = None, session_instructions: dict[str, str] | None = None, card_loader: Callable[[str], Awaitable[tuple["AgentInstance", list[str]]]] | None = None, + reload_callback: Callable[[], Awaitable[bool]] | None = None, ): """ Initialize the slash command handler. @@ -149,6 +150,7 @@ def __init__( self.protocol_version = protocol_version self._session_instructions = session_instructions or {} self._card_loader = card_loader + self._reload_callback = reload_callback # Session-level commands (always available, operate on current agent) self._session_commands: dict[str, AvailableCommand] = { @@ -194,6 +196,12 @@ def __init__( ), ), } + if self._reload_callback is not None: + self._session_commands["reload"] = AvailableCommand( + name="reload", + description="Reload AgentCards from disk", + input=None, + ) def get_available_commands(self) -> list[AvailableCommand]: """Get combined session commands and current agent's commands.""" @@ -348,6 +356,8 @@ async def execute_command(self, command_name: str, arguments: str) -> str: return await self._handle_load(arguments) if command_name == "card": return await self._handle_card(arguments) + if command_name == "reload": + return await self._handle_reload() # Check agent-specific commands agent = self._get_current_agent() @@ -1327,6 +1337,17 @@ async def _handle_card(self, arguments: str | None = None) -> str: return summary return f"{summary}\nAdded tool(s): {', '.join(added_tools)}" + async def _handle_reload(self) -> str: + if not self._reload_callback: + return "AgentCard reload is not available in this session." + try: + changed = await self._reload_callback() + except Exception as exc: # noqa: BLE001 + return f"# reload\n\nFailed to reload AgentCards: {exc}" + if not changed: + return "# reload\n\nNo AgentCard changes detected." + return "# reload\n\nReloaded AgentCards." + async def _handle_clear(self, arguments: str | None = None) -> str: """Handle /clear and /clear last commands.""" normalized = (arguments or "").strip().lower() diff --git a/src/fast_agent/cli/commands/README.md b/src/fast_agent/cli/commands/README.md index 469e8fc57..44a6b9cbb 100644 --- a/src/fast_agent/cli/commands/README.md +++ b/src/fast_agent/cli/commands/README.md @@ -91,12 +91,14 @@ fast-agent serve [OPTIONS] - `--npx TEXT`: NPX package and args to run as an MCP server (quoted) - `--uvx TEXT`: UVX package and args to run as an MCP server (quoted) - `--stdio TEXT`: Command to run as STDIO MCP server (quoted) -- `--transport [http|sse|stdio]`: Transport protocol to expose (default: http) +- `--transport [http|sse|stdio|acp]`: Transport protocol to expose (default: http) - `--host TEXT`: Host address when using HTTP or SSE transport (default: 0.0.0.0) - `--port INTEGER`: Port when using HTTP or SSE transport (default: 8000) - `--shell`, `-x`: Enable a local shell runtime and expose the execute tool - `--description`, `-d TEXT`: Description used for each send tool (supports `{agent}` placeholder) - `--instance-scope [shared|connection|request]`: Control how MCP clients receive isolated agent instances (default: shared) +- `--reload`: Enable manual AgentCard reloads (ACP: `/reload`, MCP: `reload_agent_cards`) +- `--watch`: Watch AgentCard paths and reload ### Skills behavior @@ -123,6 +125,9 @@ fast-agent serve --description "Interact with the {agent} workflow via MCP" # Load AgentCards from a file or directory fast-agent serve --card ./agents --transport=http +# Watch AgentCard directory for changes +fast-agent serve --card ./agents --watch --transport=http + # Use per-connection instances to isolate history between clients fast-agent serve --instance-scope=connection --transport=http ``` diff --git a/src/fast_agent/cli/commands/acp.py b/src/fast_agent/cli/commands/acp.py index 2719b4140..576aa32b9 100644 --- a/src/fast_agent/cli/commands/acp.py +++ b/src/fast_agent/cli/commands/acp.py @@ -101,6 +101,8 @@ def run_acp( "--no-permissions", help="Disable tool permission requests (allow all tool executions without asking)", ), + reload: bool = typer.Option(False, "--reload", help="Enable manual AgentCard reloads"), + watch: bool = typer.Option(False, "--watch", help="Watch AgentCard paths and reload"), ) -> None: """ Run FastAgent with ACP transport defaults. @@ -135,6 +137,8 @@ def run_acp( tool_description=description, instance_scope=instance_scope.value, permissions_enabled=not no_permissions, + reload=reload, + watch=watch, ) diff --git a/src/fast_agent/cli/commands/serve.py b/src/fast_agent/cli/commands/serve.py index d3d42eee3..2afbb2d82 100644 --- a/src/fast_agent/cli/commands/serve.py +++ b/src/fast_agent/cli/commands/serve.py @@ -109,6 +109,8 @@ def serve( "--no-permissions", help="Disable tool permission requests (allow all tool executions without asking) - ACP only", ), + reload: bool = typer.Option(False, "--reload", help="Enable manual AgentCard reloads"), + watch: bool = typer.Option(False, "--watch", help="Watch AgentCard paths and reload"), ) -> None: """ Run FastAgent as an MCP server. @@ -148,4 +150,6 @@ def serve( tool_description=description, instance_scope=instance_scope.value, permissions_enabled=not no_permissions, + reload=reload, + watch=watch, ) diff --git a/src/fast_agent/core/fastagent.py b/src/fast_agent/core/fastagent.py index 0c1277184..788e07e42 100644 --- a/src/fast_agent/core/fastagent.py +++ b/src/fast_agent/core/fastagent.py @@ -1036,6 +1036,7 @@ async def load_card_source(source: str) -> list[str]: getattr(self.args, "reload", False) or getattr(self.args, "watch", False) ) + reload_callback = self.reload_agents if reload_enabled else None wrapper.set_reload_callback(reload_and_refresh if reload_enabled else None) wrapper.set_refresh_callback( refresh_shared_instance if reload_enabled else None @@ -1113,6 +1114,7 @@ async def load_card_source(source: str) -> list[str]: skills_directory_override=skills_override, permissions_enabled=permissions_enabled, load_card_callback=load_card_source, + reload_callback=reload_callback, ) # Run the ACP server (this is a blocking call) @@ -1135,6 +1137,7 @@ async def load_card_source(source: str) -> list[str]: tool_description=tool_description, host=self.args.host, get_registry_version=self._get_registry_version, + reload_callback=reload_callback, ) # Run the server directly (this is a blocking call) @@ -1622,6 +1625,10 @@ async def start_server( self.args.card_tools = original_args.card_tools if original_args is not None and hasattr(original_args, "agent"): self.args.agent = original_args.agent + if original_args is not None and hasattr(original_args, "reload"): + self.args.reload = original_args.reload + if original_args is not None and hasattr(original_args, "watch"): + self.args.watch = original_args.watch # Run the application, which will detect the server flag and start server mode async with self.run(): diff --git a/src/fast_agent/mcp/server/agent_server.py b/src/fast_agent/mcp/server/agent_server.py index eb1b38be3..cd083339a 100644 --- a/src/fast_agent/mcp/server/agent_server.py +++ b/src/fast_agent/mcp/server/agent_server.py @@ -79,6 +79,7 @@ def __init__( tool_description: str | None = None, host: str = "0.0.0.0", get_registry_version: Callable[[], int] | None = None, + reload_callback: Callable[[], Awaitable[bool]] | None = None, ) -> None: """Initialize the server with the provided agent app.""" self.primary_instance = primary_instance @@ -86,6 +87,7 @@ def __init__( self._dispose_instance_task = dispose_instance self._instance_scope = instance_scope self._get_registry_version = get_registry_version + self._reload_callback = reload_callback self._primary_registry_version = getattr(primary_instance, "registry_version", 0) self._shared_instance_lock = asyncio.Lock() self._shared_active_requests = 0 @@ -175,6 +177,8 @@ def setup_tools(self) -> None: """Register all agents as MCP tools.""" for agent_name in self.primary_instance.agents.keys(): self.register_agent_tools(agent_name) + if self._reload_callback is not None: + self._register_reload_tool() def register_agent_tools(self, agent_name: str) -> None: """Register tools for a specific agent.""" @@ -289,6 +293,49 @@ async def get_history_prompt(ctx: MCPContext) -> list: finally: await self._release_instance(ctx, instance, reuse_connection=True) + def _register_missing_agents(self, instance: AgentInstance) -> None: + new_agents = set(instance.agents.keys()) + missing = new_agents - self._registered_agents + for agent_name in sorted(missing): + self.register_agent_tools(agent_name) + + def _register_reload_tool(self) -> None: + @self.mcp_server.tool( + name="reload_agent_cards", + description="Reload AgentCards from disk", + structured_output=False, + ) + async def reload_agent_cards(ctx: MCPContext) -> str: + if not self._reload_callback: + return "Reload not available." + changed = await self._reload_callback() + if not changed: + return "No AgentCard changes detected." + + if self._instance_scope == "shared": + await self._maybe_refresh_shared_instance() + return "Reloaded AgentCards." + + if self._instance_scope == "connection": + session_key = self._connection_key(ctx) + new_instance = await self._create_instance_task() + old_instance = None + async with self._connection_lock: + old_instance = self._connection_instances.get(session_key) + self._connection_instances[session_key] = new_instance + self._register_missing_agents(new_instance) + if old_instance is not None: + await self._dispose_instance_task(old_instance) + return "Reloaded AgentCards." + + # request scope: register tools from a fresh instance, then dispose it + new_instance = await self._create_instance_task() + try: + self._register_missing_agents(new_instance) + finally: + await self._dispose_instance_task(new_instance) + return "Reloaded AgentCards." + async def _acquire_instance(self, ctx: MCPContext | None) -> AgentInstance: if self._instance_scope == "shared": await self._maybe_refresh_shared_instance() diff --git a/tests/integration/acp/test_acp_reload.py b/tests/integration/acp/test_acp_reload.py new file mode 100644 index 000000000..79233553f --- /dev/null +++ b/tests/integration/acp/test_acp_reload.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +import asyncio +import sys +from pathlib import Path +from typing import Any + +import pytest +from acp.helpers import text_block +from acp.schema import ClientCapabilities, FileSystemCapability, Implementation +from acp.stdio import spawn_agent_process + +TEST_DIR = Path(__file__).parent +if str(TEST_DIR) not in sys.path: + sys.path.append(str(TEST_DIR)) + +from test_client import TestClient # noqa: E402 + +CONFIG_PATH = TEST_DIR / "fastagent.config.yaml" + +pytestmark = pytest.mark.asyncio(loop_scope="module") + + +def _write_card(path: Path, instruction: str) -> None: + content = f"name: reload-test\ninstruction: |\n {instruction}\n" + path.write_text(content, encoding="utf-8") + + +def _get_session_update_type(update: Any) -> str | None: + if hasattr(update, "sessionUpdate"): + return update.sessionUpdate + if isinstance(update, dict): + return update.get("sessionUpdate") + return None + + +def _get_update_text(update: Any) -> str | None: + if hasattr(update, "content"): + content = update.content + elif isinstance(update, dict): + content = update.get("content") + else: + content = None + if not content: + return None + return getattr(content, "text", None) + + +def _get_stop_reason(response: object) -> str | None: + return getattr(response, "stop_reason", None) or getattr(response, "stopReason", None) + + +async def _wait_for_message_text( + client: TestClient, + session_id: str, + needle: str, + *, + start_index: int = 0, + timeout: float = 2.0, +) -> str: + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + while loop.time() < deadline: + for notification in client.notifications[start_index:]: + if notification["session_id"] != session_id: + continue + update = notification["update"] + if _get_session_update_type(update) != "agent_message_chunk": + continue + text = _get_update_text(update) + if text and needle in text: + return text + await asyncio.sleep(0.05) + raise AssertionError(f"Expected message containing {needle!r}") + + +@pytest.mark.integration +async def test_acp_reload_agent_cards(tmp_path: Path) -> None: + card_dir = tmp_path / "cards" + card_dir.mkdir() + card_path = card_dir / "reload_agent.yaml" + _write_card(card_path, "You are a helpful assistant.") + + client = TestClient() + cmd = [ + sys.executable, + "-m", + "fast_agent.cli", + "serve", + "--config-path", + str(CONFIG_PATH), + "--transport", + "acp", + "--model", + "passthrough", + "--name", + "fast-agent-acp-reload-test", + "--card", + str(card_dir), + "--reload", + ] + + async with spawn_agent_process(lambda _: client, *cmd) as (connection, _process): + await connection.initialize( + protocol_version=1, + client_capabilities=ClientCapabilities( + fs=FileSystemCapability(read_text_file=True, write_text_file=True), + terminal=False, + ), + client_info=Implementation(name="pytest-client", version="0.0.1"), + ) + + session_response = await connection.new_session(mcp_servers=[], cwd=str(TEST_DIR)) + session_id = session_response.session_id + + start_index = len(client.notifications) + response = await connection.prompt( + session_id=session_id, prompt=[text_block("/reload")] + ) + assert _get_stop_reason(response) == "end_turn" + await _wait_for_message_text( + client, + session_id, + "No AgentCard changes detected.", + start_index=start_index, + ) + + _write_card(card_path, "You are a reloaded assistant.") + await asyncio.sleep(0.05) + + start_index = len(client.notifications) + response = await connection.prompt( + session_id=session_id, prompt=[text_block("/reload")] + ) + assert _get_stop_reason(response) == "end_turn" + await _wait_for_message_text( + client, + session_id, + "Reloaded AgentCards.", + start_index=start_index, + )