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,
+ )