Agent Hooks System¶
The Agent Hooks system provides a unified, extensible architecture for intercepting and modifying agent behavior at various lifecycle points. It replaces the fragmented approach of separate middleware, processors, and criteria with a consistent API.
Overview¶
Hooks are middleware-style processors that intercept specific lifecycle events in agent execution. They can:
- Allow execution to proceed (with or without modifications)
- Block a specific action (e.g., prevent a dangerous tool call)
- Stop the entire agent execution
Quick Start¶
use Cognesy\Addons\AgentBuilder\AgentBuilder;
use Cognesy\Addons\Agent\Hooks\Data\HookOutcome;
use Cognesy\Addons\Agent\Hooks\Data\ToolHookContext;
use Cognesy\Addons\Agent\Hooks\Data\ExecutionHookContext;
$agent = AgentBuilder::new()
// Block dangerous bash commands
->onBeforeToolUse(
callback: function (ToolHookContext $ctx): HookOutcome {
$command = $ctx->toolCall()->args()['command'] ?? '';
if (str_contains($command, 'rm -rf')) {
return HookOutcome::block('Dangerous command blocked');
}
return HookOutcome::proceed();
},
priority: 100,
matcher: 'bash',
)
// Log execution start
->onExecutionStart(function (ExecutionHookContext $ctx): HookOutcome {
$this->logger->info("Agent started: {$ctx->state()->agentId}");
return HookOutcome::proceed();
})
// Handle failures
->onAgentFailed(function (FailureHookContext $ctx): void {
$this->alerting->send("Agent failed: {$ctx->errorMessage()}");
})
->build();
Lifecycle Events¶
The hook system supports 9 lifecycle events organized into categories:
Tool Lifecycle¶
| Event | Context Class | Description |
|---|---|---|
PreToolUse |
ToolHookContext |
Before a tool is executed. Can modify args or block. |
PostToolUse |
ToolHookContext |
After a tool completes. Can modify the result. |
Step Lifecycle¶
| Event | Context Class | Description |
|---|---|---|
BeforeStep |
StepHookContext |
Before each agent step begins. |
AfterStep |
StepHookContext |
After each agent step completes. |
Execution Lifecycle¶
| Event | Context Class | Description |
|---|---|---|
ExecutionStart |
ExecutionHookContext |
When agent.run() begins. |
ExecutionEnd |
ExecutionHookContext |
When agent.run() completes. |
Continuation¶
| Event | Context Class | Description |
|---|---|---|
Stop |
StopHookContext |
When agent is about to stop. Can force continuation. |
SubagentStop |
StopHookContext |
When a subagent is about to stop. |
Error Handling¶
| Event | Context Class | Description |
|---|---|---|
AgentFailed |
FailureHookContext |
When agent encounters an unrecoverable error. |
HookOutcome¶
Every hook returns a HookOutcome indicating what should happen next:
use Cognesy\Addons\Agent\Hooks\Data\HookOutcome;
// Allow execution to proceed
HookOutcome::proceed();
// Proceed with a modified context
HookOutcome::proceed($modifiedContext);
// Block this specific action (tool call, stop decision)
// Agent continues but this action is prevented
HookOutcome::block('Dangerous command detected');
// Stop the entire agent execution
HookOutcome::stop('Budget exceeded');
Semantic Differences¶
block(): Prevents a single action but execution continues. Use for filtering dangerous tool calls.stop(): Halts the entire agent execution immediately. Use for hard limits like budget or time.
Context Classes¶
Each event type has a specialized context class providing relevant data:
ToolHookContext¶
use Cognesy\Addons\Agent\Hooks\Data\ToolHookContext;
function beforeTool(ToolHookContext $ctx): HookOutcome {
// Access the tool call
$toolName = $ctx->toolCall()->name();
$args = $ctx->toolCall()->args();
// Access agent state
$state = $ctx->state();
// Check event type
if ($ctx->isBeforeTool()) {
// PreToolUse event
}
// Modify the tool call (PreToolUse only)
$newContext = $ctx->withToolCall($modifiedCall);
return HookOutcome::proceed($newContext);
}
function afterTool(ToolHookContext $ctx): HookOutcome {
// Access the execution result (PostToolUse only)
$execution = $ctx->execution();
$success = $execution->result()->isSuccess();
return HookOutcome::proceed();
}
StepHookContext¶
use Cognesy\Addons\Agent\Hooks\Data\StepHookContext;
function onStep(StepHookContext $ctx): HookOutcome {
// Get step information
$stepIndex = $ctx->stepIndex(); // 0-based
$stepNumber = $ctx->stepNumber(); // 1-based (for display)
// AfterStep only: access the completed step
$step = $ctx->step();
if ($step?->hasErrors()) {
// Handle errors
}
return HookOutcome::proceed();
}
StopHookContext¶
use Cognesy\Addons\Agent\Hooks\Data\StopHookContext;
function onStop(StopHookContext $ctx): HookOutcome {
// Access continuation outcome
$outcome = $ctx->continuationOutcome();
$reason = $outcome->stopReason();
// Force continuation if needed
if ($this->hasUnfinishedWork($ctx->state())) {
return HookOutcome::block('Work remaining');
}
return HookOutcome::proceed(); // Allow stop
}
ExecutionHookContext¶
use Cognesy\Addons\Agent\Hooks\Data\ExecutionHookContext;
function onExecution(ExecutionHookContext $ctx): HookOutcome {
$state = $ctx->state();
if ($ctx->isStart()) {
// ExecutionStart event
} else {
// ExecutionEnd event
}
return HookOutcome::proceed();
}
FailureHookContext¶
use Cognesy\Addons\Agent\Hooks\Data\FailureHookContext;
function onFailure(FailureHookContext $ctx): HookOutcome {
$exception = $ctx->exception();
$message = $ctx->errorMessage();
$class = $ctx->errorClass();
// Log, alert, cleanup...
return HookOutcome::proceed();
}
Matchers¶
Matchers allow hooks to be conditionally executed based on context.
ToolNameMatcher¶
Match tool calls by name pattern:
use Cognesy\Addons\Agent\Hooks\Matchers\ToolNameMatcher;
// Exact match
new ToolNameMatcher('bash');
// Wildcard patterns
new ToolNameMatcher('read_*'); // read_file, read_stdin, etc.
new ToolNameMatcher('*_file'); // read_file, write_file, etc.
new ToolNameMatcher('*'); // Match all
// Regex patterns
new ToolNameMatcher('/^(read|write)_.+$/');
EventTypeMatcher¶
Filter by event type:
use Cognesy\Addons\Agent\Hooks\Matchers\EventTypeMatcher;
use Cognesy\Addons\Agent\Hooks\Data\HookEvent;
// Single event
new EventTypeMatcher(HookEvent::PreToolUse);
// Multiple events
new EventTypeMatcher(HookEvent::BeforeStep, HookEvent::AfterStep);
CompositeMatcher¶
Combine matchers with AND/OR logic:
use Cognesy\Addons\Agent\Hooks\Matchers\CompositeMatcher;
use Cognesy\Addons\Agent\Hooks\Matchers\CallableMatcher;
// AND: all conditions must match
$matcher = CompositeMatcher::and(
new ToolNameMatcher('bash'),
new CallableMatcher(fn($ctx) => $ctx->state()->stepCount() < 5),
);
// OR: any condition can match
$matcher = CompositeMatcher::or(
new ToolNameMatcher('read_*'),
new ToolNameMatcher('write_*'),
);
// Nested combinations
$matcher = CompositeMatcher::and(
CompositeMatcher::or(
new ToolNameMatcher('bash'),
new ToolNameMatcher('shell'),
),
new CallableMatcher(fn($ctx) => $ctx->state()->metadata()->get('safe_mode')),
);
CallableMatcher¶
Custom matching logic:
use Cognesy\Addons\Agent\Hooks\Matchers\CallableMatcher;
$matcher = new CallableMatcher(function (HookContext $ctx): bool {
return $ctx->state()->metadata()->get('priority') === 'high';
});
Priority¶
Hooks are executed in priority order (higher priority runs first):
$builder
// Security hooks run first (high priority)
->onBeforeToolUse($securityCheck, priority: 100)
// Normal hooks
->onBeforeToolUse($normalHook, priority: 0)
// Logging hooks run last (low priority)
->onAfterToolUse($logger, priority: -100);
Priority Guidelines:
- 100+: Security/validation hooks (run first, can block)
- 0: Normal hooks (default)
- -100: Logging/monitoring hooks (run last, observe final state)
When priorities are equal, hooks run in registration order.
AgentBuilder Methods¶
Tool Hooks¶
// Before tool execution
$builder->onBeforeToolUse(
callback: callable, // (ToolHookContext) -> HookOutcome|ToolCall|null|void
priority: int = 0,
matcher: string|HookMatcher|null = null,
);
// After tool execution
$builder->onAfterToolUse(
callback: callable, // (ToolHookContext) -> HookOutcome|AgentExecution|void
priority: int = 0,
matcher: string|HookMatcher|null = null,
);
Step Hooks¶
// Before each step
$builder->onBeforeStep(
callback: callable, // (AgentState) -> AgentState
);
// After each step
$builder->onAfterStep(
callback: callable, // (AgentState) -> AgentState
);
Execution Hooks¶
// When execution starts
$builder->onExecutionStart(
callback: callable, // (ExecutionHookContext) -> HookOutcome|void
priority: int = 0,
);
// When execution ends
$builder->onExecutionEnd(
callback: callable, // (ExecutionHookContext) -> HookOutcome|void
priority: int = 0,
);
Continuation Hooks¶
// When agent is about to stop
$builder->onStop(
callback: callable, // (StopHookContext) -> HookOutcome|void
priority: int = 0,
);
// When subagent is about to stop
$builder->onSubagentStop(
callback: callable, // (StopHookContext) -> HookOutcome|void
priority: int = 0,
);
Error Hooks¶
// When agent fails
$builder->onAgentFailed(
callback: callable, // (FailureHookContext) -> HookOutcome|void
priority: int = 0,
);
Unified Registration¶
For advanced use cases, use the unified addHook method:
use Cognesy\Addons\Agent\Hooks\Data\HookEvent;
use Cognesy\Addons\Agent\Hooks\Hooks\CallableHook;
$builder->addHook(
event: HookEvent::ExecutionStart,
hook: new CallableHook($callback, $matcher),
priority: 100,
);
Creating Custom Hooks¶
Implement the Hook interface for reusable hook logic:
use Cognesy\Addons\Agent\Hooks\Contracts\Hook;
use Cognesy\Addons\Agent\Hooks\Contracts\HookContext;
use Cognesy\Addons\Agent\Hooks\Data\HookOutcome;
class RateLimitHook implements Hook
{
public function __construct(
private RateLimiter $limiter,
private int $maxCallsPerMinute,
) {}
public function handle(HookContext $context, callable $next): HookOutcome
{
if (!$this->limiter->check($this->maxCallsPerMinute)) {
return HookOutcome::block('Rate limit exceeded');
}
$this->limiter->increment();
return $next($context);
}
}
Using HookStack Directly¶
For advanced scenarios, use HookStack directly:
use Cognesy\Addons\Agent\Hooks\Stack\HookStack;
use Cognesy\Addons\Agent\Hooks\Data\ExecutionHookContext;
use Cognesy\Addons\Agent\Hooks\Data\HookOutcome;
$stack = (new HookStack())
->with(new SecurityHook(), priority: 100)
->with(new LoggingHook(), priority: -100)
->with(new MetricsHook());
$context = ExecutionHookContext::onStart($state);
$outcome = $stack->process($context, fn($ctx) => HookOutcome::proceed($ctx));
if ($outcome->isBlocked()) {
// Handle blocked
} elseif ($outcome->isStopped()) {
// Handle stopped
}
Backward Compatibility¶
The legacy ToolMiddlewareStack has been removed. All tool hooks now use the unified HookStack system with BeforeToolHook and AfterToolHook.
Adapters are provided for other components:
StateProcessorAdapter¶
use Cognesy\Addons\Agent\Hooks\Adapters\StateProcessorAdapter;
$processor = new YourStateProcessor();
$hook = new StateProcessorAdapter($processor, position: 'after');
ContinuationCriteriaAdapter¶
use Cognesy\Addons\Agent\Hooks\Adapters\ContinuationCriteriaAdapter;
$criterion = new YourContinuationCriterion();
$hook = new ContinuationCriteriaAdapter($criterion);
Common Patterns¶
Security Validation¶
$builder->onBeforeToolUse(
callback: function (ToolHookContext $ctx): HookOutcome {
// Block dangerous patterns
$command = $ctx->toolCall()->args()['command'] ?? '';
$dangerous = ['rm -rf', 'sudo', '> /dev/', 'mkfs'];
foreach ($dangerous as $pattern) {
if (str_contains($command, $pattern)) {
return HookOutcome::block("Dangerous command blocked: {$pattern}");
}
}
return HookOutcome::proceed();
},
priority: 100, // Run first
matcher: 'bash',
);
Execution Timing¶
$builder
->onExecutionStart(function (ExecutionHookContext $ctx): HookOutcome {
return HookOutcome::proceed(
$ctx->withMetadata('started_at', microtime(true))
);
})
->onExecutionEnd(function (ExecutionHookContext $ctx): void {
$started = $ctx->get('started_at');
$duration = microtime(true) - $started;
$this->metrics->record('execution_duration', $duration);
});
Conditional Continuation¶
$builder->onStop(function (StopHookContext $ctx): HookOutcome {
$state = $ctx->state();
// Check for incomplete tasks
$tasks = $state->metadata()->get('pending_tasks', []);
if (count($tasks) > 0) {
return HookOutcome::block('Tasks remaining: ' . count($tasks));
}
// Check for quality threshold
$quality = $state->metadata()->get('output_quality', 1.0);
if ($quality < 0.8) {
return HookOutcome::block('Quality below threshold');
}
return HookOutcome::proceed();
});
Error Recovery Logging¶
$builder->onAgentFailed(function (FailureHookContext $ctx): void {
$state = $ctx->state();
$exception = $ctx->exception();
// Log with full context
$this->logger->error('Agent failed', [
'agent_id' => $state->agentId,
'parent_id' => $state->parentAgentId,
'steps_completed' => $state->stepCount(),
'error_class' => $ctx->errorClass(),
'error_message' => $ctx->errorMessage(),
'stack_trace' => $exception->getTraceAsString(),
'last_tool' => $state->currentStep()?->toolCalls()->first()?->name(),
]);
// Send alert for critical errors
if ($exception instanceof CriticalException) {
$this->alerting->sendCritical($exception);
}
});
Architecture¶
┌─────────────────────────────────────────────────────────────┐
│ HookStack │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Hook (p:100)│→ │ Hook (p:0) │→ │ Hook (p:-100)│→ Terminal│
│ │ Security │ │ Normal │ │ Logging │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ HookContext │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ ToolHookContext │ │ StepHookContext │ ... │
│ │ - toolCall │ │ - stepIndex │ │
│ │ - execution │ │ - step │ │
│ │ - state │ │ - state │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ HookOutcome │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ proceed │ │ block │ │ stop │ │
│ │ Continue │ │ Prevent │ │ Halt │ │
│ │ execution│ │ action │ │ entirely │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
File Structure¶
packages/addons/src/Agent/Hooks/
├── Contracts/
│ ├── Hook.php # Core hook interface
│ ├── HookContext.php # Base context interface
│ └── HookMatcher.php # Matcher interface
├── Data/
│ ├── HookEvent.php # Lifecycle events enum
│ ├── HookOutcome.php # Hook return type
│ ├── AbstractHookContext.php
│ ├── ToolHookContext.php
│ ├── StepHookContext.php
│ ├── StopHookContext.php
│ ├── ExecutionHookContext.php
│ └── FailureHookContext.php
├── Stack/
│ └── HookStack.php # Priority-based execution
├── Matchers/
│ ├── ToolNameMatcher.php
│ ├── EventTypeMatcher.php
│ ├── CompositeMatcher.php
│ └── CallableMatcher.php
├── Hooks/
│ ├── CallableHook.php
│ ├── BeforeToolHook.php
│ ├── AfterToolHook.php
│ ├── BeforeStepHook.php
│ ├── AfterStepHook.php
│ ├── ExecutionStartHook.php
│ ├── ExecutionEndHook.php
│ ├── StopHook.php
│ ├── SubagentStopHook.php
│ └── AgentFailedHook.php
└── Adapters/
├── StateProcessorAdapter.php
└── ContinuationCriteriaAdapter.php