Hook events

Hook event catalogue

Lifecycle events grouped by category. Each hook callback receives a typed context dataclass. Some events accept return values that influence runtime flow; the rest are observation-only.
Hook firing status

Each event below is tagged live or planned.

Live events fire from runtime call sites today — your callbacks will be invoked.

Planned events have typed contexts and registration works, but the runtime doesn't fire them yet. Subscribing is harmless — the callback simply never runs. Call sites are added on demand; open an issue if you need one wired.

Action protocols

Hooks can return objects that change runtime behaviour. The first matching action wins — non-matching returns are silently ignored, so observer-only hooks coexist freely with action hooks.

python
from flowly.agent.hooks import BlockAction, RewriteAction, SkipAction

# pre_tool_call → abort dispatch with a message
return BlockAction(message="rate-limited")

# pre_gateway_dispatch → drop the message entirely (no reply)
return SkipAction(reason="spam filter")

# pre_gateway_dispatch → replace the inbound text and continue
return RewriteAction(text="redacted: <user query>")

# transform_tool_result / transform_terminal_output
# → return a bare string to replace the output
return "results redacted for privacy"

# pre_llm_call → return context to inject into the user message
return {"context": "Previous insight: user prefers dark mode."}
# or simply:
return "Previous insight: user prefers dark mode."
All callbacks run inside their own try/except. An exception in your hook is logged but never breaks the agent loop.

Tool lifecycle

pre_tool_call(ToolHookContext)live

Fires before any tool dispatch. Plugin can short-circuit by returning BlockAction.

Returns: BlockAction | None
post_tool_call(ToolHookContext)live

Fires after tool dispatch with result, duration, success populated. Observation only.

Returns: None
transform_tool_result(ToolHookContext)live

Fires after post_tool_call. Return a string to replace the result the agent sees.

Returns: str | None
transform_terminal_output(ToolHookContext)planned

Defined for exec/shell tools to rewrite stdout/stderr before the agent sees it. Call site not yet wired in core.

Returns: str | None

LLM lifecycle

pre_llm_call(LLMHookContext)live

Fires before each LLM API call. Returned strings/dicts get appended to the user message — never the system prompt — preserving cache.

Returns: str | {"context": str} | None
post_llm_call(LLMHookContext)planned

Defined for plugins that need to react to each response (custom analytics). Note: token/usage tracking is already handled at the provider layer — surfaced via LLMResponse.usage.

Returns: None
pre_api_request(LLMHookContext)planned

Defined: lower level, fires before raw HTTP request to provider. Useful for external observability tools.

Returns: None
post_api_request(LLMHookContext)planned

Defined: lower level, fires after raw HTTP response. Useful for external observability tools.

Returns: None

Session lifecycle

on_session_start(SessionHookContext)live

Fires the first time a session_key is seen by the agent. Use for warming caches or initialization.

Returns: None
on_session_end(SessionHookContext)live

Fires after every turn (each user message + agent reply pair). Used by disk-cleanup for per-turn cleanup.

Returns: None
on_session_finalize(SessionHookContext)planned

Defined for end-of-process flush (export to S3/Notion, archive). Call site not yet wired.

Returns: None
on_session_reset(SessionHookContext)planned

Defined to fire when a user clears a session via /clear or /new. Call site not yet wired.

Returns: None

Subagents

subagent_stop(SubagentStopContext)planned

Defined to fire when a delegated subagent task terminates. Call site not yet wired.

Returns: None

Gateway dispatch

pre_gateway_dispatch(GatewayDispatchContext)live

Fires for every inbound message (telegram/web/desktop/iOS/cli) before session work begins. Return SkipAction to drop the message or RewriteAction to replace its content. Used by the pii-redactor example.

Returns: SkipAction | RewriteAction | None

Context shapes

Imported from flowly.agent.hooks. Every context inherits common session_id and task_id fields.

python
@dataclass
class HookContext:
    session_id: str = ""
    task_id: str = ""


@dataclass
class ToolHookContext(HookContext):
    tool_name: str = ""
    params: dict = field(default_factory=dict)
    tool_call_id: str = ""
    # Populated for post_tool_call / transform_*:
    result: str | None = None
    duration_ms: float = 0.0
    success: bool | None = None


@dataclass
class LLMHookContext(HookContext):
    model: str = ""
    messages: list[dict] = field(default_factory=list)
    tools: list[dict] = field(default_factory=list)
    system: str = ""
    user_message: str = ""
    # Populated for post_llm_call:
    response: Any = None
    assistant_message: str = ""
    usage: dict = field(default_factory=dict)
    truncated: bool = False
    interrupted: bool = False


@dataclass
class SessionHookContext(HookContext):
    model: str = ""
    platform: str = ""        # "telegram" | "web" | "desktop" | ...
    completed: bool = True
    interrupted: bool = False


@dataclass
class SubagentStopContext(HookContext):
    subagent_id: str = ""
    reason: str = ""


@dataclass
class GatewayDispatchContext(HookContext):
    event: Any = None         # InboundMessage instance
    gateway: Any = None
    session_store: Any = None