Skip to content

Tutorial 27: Advanced Hooks — Write-Protected Events, Cancel, Retry

This tutorial covers:

  • Write-protected event objects (read-only fields raise AttributeError)
  • Cancelling tool calls via event.cancel
  • Retrying model calls via event.retry
  • Reverse ordering of "after" hooks

Prerequisites:

  • Configure model via environment variables

Difficulty: Advanced

Source

# Copyright (c) 2025, 2026 Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v1.0 as shown at
# https://oss.oracle.com/licenses/upl/
"""
Tutorial 27: Advanced Hooks — Write-Protected Events, Cancel, Retry

This tutorial covers:
- Write-protected event objects (read-only fields raise AttributeError)
- Cancelling tool calls via event.cancel
- Retrying model calls via event.retry
- Reverse ordering of "after" hooks

Prerequisites:
- Configure model via environment variables

Difficulty: Advanced
"""

from config import get_model

from locus.agent import Agent, AgentConfig
from locus.hooks.provider import HookProvider
from locus.tools.decorator import tool


# =============================================================================
# Part 1: Cancel a dangerous tool call
# =============================================================================


def example_cancel_tool():
    """Hook that blocks dangerous tools using write-protected events."""
    print("=== Part 1: Cancel Tool via Hook ===\n")

    model = get_model()

    class SecurityHook(HookProvider):
        """Block any tool with 'delete' in its name."""

        @property
        def priority(self):
            return 50  # Security hooks run first

        async def on_before_tool_call(self, event):
            if "delete" in event.tool_name:
                event.cancel = f"BLOCKED: {event.tool_name} is forbidden"
                # event.tool_name = "hacked"  # This would raise AttributeError!

    @tool
    def delete_file(path: str) -> str:
        """Delete a file."""
        return f"Deleted {path}"

    @tool
    def read_file(path: str) -> str:
        """Read a file."""
        return f"Contents of {path}"

    agent = Agent(
        config=AgentConfig(
            system_prompt="You manage files. If blocked, tell the user.",
            max_iterations=5,
            model=model,
            tools=[delete_file, read_file],
            hooks=[SecurityHook()],
        )
    )

    result = agent.run_sync("Delete /tmp/secret.txt")
    print(f"Response: {result.message[:150]}")
    for te in result.tool_executions:
        print(f"  Tool: {te.tool_name}{te.result}")


# =============================================================================
# Part 2: Write protection demo
# =============================================================================


def example_write_protection():
    """Demonstrate read-only fields on events."""
    print("\n=== Part 2: Write Protection ===\n")

    from locus.hooks.provider import BeforeToolCallEvent

    event = BeforeToolCallEvent(tool_name="test", tool_call_id="c1", arguments={"x": 1})

    # Writable fields work fine
    event.arguments = {"x": 2}
    event.cancel = "blocked"
    print(f"arguments (writable): {event.arguments}")
    print(f"cancel (writable): {event.cancel}")

    # Read-only fields raise
    try:
        event.tool_name = "hacked"
    except AttributeError as e:
        print(f"tool_name (read-only): {e}")

    # AI commentary so this Part also exercises the configured provider
    import time as _t

    agent = Agent(model=get_model(max_tokens=80), system_prompt="Reply in one short sentence.")
    t0 = _t.perf_counter()
    res = agent.run_sync(
        "In one sentence, why does Locus mark BeforeToolCallEvent.tool_name as "
        "read-only while letting hooks edit `arguments` and `cancel`?"
    )
    dt = _t.perf_counter() - t0
    print(
        f"  [model call: {dt:.2f}s · {res.metrics.prompt_tokens}{res.metrics.completion_tokens} tokens]"
    )
    print(f"  AI rationale: {res.message.strip()}")


if __name__ == "__main__":
    example_cancel_tool()
    example_write_protection()