Guide for implementing fast-agent hooks. Use when adding hook functions to agent cards or Python code for tasks like history compaction, saving sessions, modifying tool calls, or managing agent lifecycle.
Hooks let you intercept and customize agent behavior at specific points in the tool loop (per-turn) or agent lifecycle (start/shutdown).
Command actions are related but explicit: users invoke them with /command or
optional TUI keybindings. Use command actions for add-ins such as editing recent
history, drafting the next user message, or asking another agent to review
context.
Audience: Developers adding custom hook logic to fast-agent agents.
commands:.| Use | Choose | Why |
| --- | --- | --- |
| Always trim history after each turn | Hook | Automatic lifecycle behavior |
| Save history after each completed turn | Hook | No user decision required |
| Start/stop an external service with an agent | Lifecycle hook | Agent instance lifecycle |
| Draft the next user message into the input buffer | Command action | User should review before submitting |
| Edit or annotate the previous assistant message | Command action | Explicit user-directed history change |
| Ask critic to review recent context on demand | Command action | User chooses when to run it |
Run during the LLM/tool execution cycle. Configure via tool_hooks: in agent cards
or ToolRunnerHooks in Python.
| Hook | When it fires |
| --------------------- | ----------------------------------------------------- |
| before_llm_call | Before each LLM call (receives pending messages) |
| after_llm_call | After each assistant response |
| before_tool_call | Before executing tool calls |
| after_tool_call | After tool results are received |
| after_turn_complete | Once after the turn finishes (stop reason ≠ TOOL_USE) |
Run when agent instances start or shut down. Configure via lifecycle_hooks: in
agent cards or AgentLifecycleHooks in Python.
| Hook | When it fires |
| ------------- | --------------------------- |
| on_start | During agent initialization |
| on_shutdown | During agent shutdown |
Set trim_tool_history: true in agent cards to apply the history trimmer after each turn.
Reference hook functions using module.py:function specs. Paths are resolved
relative to the agent card location.
tool_hooks:
before_llm_call: hooks.py:log_pending_messages
after_turn_complete: hooks.py:save_after_turn
lifecycle_hooks:
on_start: hooks.py:start_service
on_shutdown: hooks.py:stop_service
Hook functions must be async def with the appropriate context type:
# hooks.py
from fast_agent.hooks import HookContext, AgentLifecycleContext
async def save_after_turn(ctx: HookContext) -> None:
if ctx.is_turn_complete:
# Access and modify history
history = ctx.message_history
ctx.load_message_history(history[-10:])
async def start_service(ctx: AgentLifecycleContext) -> None:
# Store state on the agent instance
ctx.agent._service_handle = "started"
For programmatic control, assign hooks directly to agent instances or use the dataclass constructors.
from fast_agent.agents.tool_runner import ToolRunnerHooks
from fast_agent.hooks.lifecycle_hook_loader import AgentLifecycleHooks
# Tool loop hooks
async def my_after_turn(runner, message):
print(f"Turn complete: {message.stop_reason}")
hooks = ToolRunnerHooks(after_turn_complete=my_after_turn)
agent.tool_runner_hooks = hooks
# Lifecycle hooks
async def my_on_start(ctx):
print(f"Agent {ctx.agent_name} starting")
lifecycle = AgentLifecycleHooks(on_start=my_on_start)
Note: Python class hooks use raw signatures (runner, message) while agent card
hooks receive a HookContext wrapper.
from fast_agent.hooks import HookContext
async def my_hook(ctx: HookContext) -> None:
ctx.agent_name # Agent name
ctx.iteration # Current tool loop iteration
ctx.is_turn_complete # True if stop_reason != TOOL_USE
ctx.message_history # Current message history
ctx.message # The message that triggered this hook
ctx.hook_type # "before_llm_call", "after_turn_complete", etc.
ctx.usage # Token usage stats (UsageAccumulator | None)
ctx.context # Agent's Context object if available
ctx.get_agent(name) # Look up another agent by name
ctx.load_message_history(messages) # Replace history
from fast_agent.hooks import AgentLifecycleContext
async def my_hook(ctx: AgentLifecycleContext) -> None:
ctx.agent_name # Agent name
ctx.agent # The agent instance
ctx.context # Context object (or None)
ctx.config # AgentConfig
ctx.hook_type # "on_start" or "on_shutdown"
ctx.has_context # True if context is available
ctx.get_agent(name) # Look up another agent by name
Use these to render consistent status messages to users:
from fast_agent.hooks import show_hook_message, show_hook_failure
async def my_hook(ctx: HookContext) -> None:
# Show status message (yellow prefix)
show_hook_message(
ctx,
"trimmed 6 messages",
hook_name="after_turn_complete",
hook_kind="tool",
)
# Show failure (red prefix) - typically before re-raising
try:
...
except Exception as exc:
show_hook_failure(ctx, hook_name="my_hook", hook_kind="tool", error=exc)
raise
| Hook | Import | Purpose |
| ------------------------ | ------------------ | ------------------------------- |
| trim_tool_loop_history | fast_agent.hooks | Compact tool call/result pairs |
| save_session_history | fast_agent.hooks | Save history to session storage |
The built-ins can be wired directly in agent cards (typically as
after_turn_complete hooks) or via the trim_tool_history: true shortcut.
Trim tool loops (explicit hook):
tool_hooks:
after_turn_complete: fast_agent.hooks.history_trimmer:trim_tool_loop_history
Trim tool loops (shortcut):
trim_tool_history: true
Save session history:
tool_hooks:
after_turn_complete: fast_agent.hooks:save_session_history
Agent attribute storage (recommended):
async def on_start(ctx: AgentLifecycleContext) -> None:
ctx.agent._my_state = {"started": True}
async def after_turn(ctx: HookContext) -> None:
state = getattr(ctx.agent, "_my_state", {})
Cross-agent access:
async def my_hook(ctx: HookContext) -> None:
companion = ctx.get_agent("helper-agent")
if companion:
# Access companion agent
...
Example: translate the assistant response via a helper agent (the helper must be
configured in the same app, e.g. agents: [translator] in the card):
from mcp.types import TextContent
from fast_agent.hooks import show_hook_message
async def translate_after_turn(ctx: HookContext) -> None:
if not ctx.is_turn_complete:
return
translator = ctx.get_agent("translator")
if translator is None:
return
history = ctx.message_history
for message in reversed(history):
if message.role != "assistant":
continue
text = message.all_text().strip()
if not text:
return
translated = await translator.send(
"Translate the following assistant response into French. "
"Reply with the translation only.\n\n"
f"{text}"
)
message.content = [TextContent(type="text", text=translated)]
ctx.load_message_history(list(history))
show_hook_message(
ctx,
"translated assistant response to French",
hook_name="translate",
hook_kind="extension",
)
return
Related example files:
assets/examples/translate_hook.pyassets/examples/hook_translate_agent.mdassets/examples/translator_agent.mdCommand actions are async Python add-ins invoked by users:
/draft-next concise
/review-last critic
They can be configured globally in fast-agent.yaml or on an AgentCard.
Built-in slash commands win first, AgentCard commands win over global commands,
and unknown commands fall through normally.
Global config:
commands:
draft-next:
description: Draft the next user message
input_hint: "[format]"
handler: "./command-actions.py:draft_next"
key: "c-x d"
AgentCard frontmatter:
---
type: smart
name: dev
commands:
review-last:
description: Ask another agent to review the latest response
input_hint: "[agent-name]"
handler: "./commands.py:review_last"
---
Handler functions receive PluginCommandActionContext and return
PluginCommandActionResult, str, or None:
from fast_agent.command_actions import (
PluginCommandActionContext,
PluginCommandActionResult,
)
async def draft_next(ctx: PluginCommandActionContext) -> PluginCommandActionResult:
drafter = ctx.get_agent("drafter") or ctx.agent
style = ctx.arguments.strip() or "concise"
draft = await drafter.send(
"Draft the next user message based on the current conversation. "
f"Use this format: {style}. "
"Return only the proposed user message."
)
return PluginCommandActionResult(
message="Drafted the next user message.",
buffer_prefill=draft.strip(),
)
Key command-action points:
buffer_prefill replaces the TUI input buffer but does not submit.ctx.get_agent(name) can call another configured agent.ctx.message_history and ctx.load_message_history(...) can inspect/mutate current history.ctx.mark_user_adjusted(message, note=...) records provenance in fast-agent.audit.asyncio.to_thread(...),
use ctx.session_cwd as the subprocess cwd when available, and return
reviewable edits as buffer_prefill unless you intentionally mutate history.except Exception; return a concise
PluginCommandActionResult(message=...) for expected subprocess/I/O errors.fast-agent.yaml.See references/command-actions.md for the detailed API,
including a complete $VISUAL/$EDITOR /editlast example.
Use the bundled smoke test to run a hook against saved history without a full agent:
python scripts/hook_smoke_test.py \
--hook path/to/hooks.py:after_turn_complete \
--history ./history.json \
--hook-type after_turn_complete \
--output ./history-modified.json
Options:
--hook (required): Hook spec in module.py:function format--history (required): Path to history file (JSON or delimited)--hook-type: Hook type label for HookContext (default: after_turn_complete)--output: Save modified history to this path--agent-name: Agent name for the test context--base-path: Resolve relative hook paths from this directoryFor full integration tests:
PassthroughLLM subclasses to drive tool callstmp_path for files written during teststests/integration/tool_hooks/ and tests/integration/agent_hooks/Run checks after changes:
uv run scripts/lint.py --fix
uv run scripts/typecheck.py
pytest tests/unit
Example hook implementations are provided in assets/examples/:
| File | Hook type | What it demonstrates |
| ----------------------- | --------------------- | ----------------------------------------------------- |
| cache_rate_display.py | after_turn_complete | Rich text output with show_hook_message |
| append_context.py | before_llm_call | Appending messages via ctx.runner.append_messages() |
| save_history.py | after_turn_complete | Saving history to timestamped files |
| fix_tool_calls.py | before_tool_call | Modifying tool calls before execution |
Copy and adapt these for your use case.
npx skills add fast-agent-ai/agent-card-hooks下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer