CatalogSkillsMP · lobehub

agent-runtime-hooks

Use for agent lifecycle hooks, tool mocks, intervention, sub-agent calls and context compression.

Pending analysisCommunity83k stars

Savant verdict: Pending analysis

Imported from the source hub; Savant's analysis is still running.

Live evaluation

Not evaluated yet. Workspaces can request a live evaluation.

Safety (NVIDIA SkillSpector)

Scan pending.

Structure

  • No license declaredConfirm you may reuse this skill before importing it into your repository.

SKILL.md

---
name: agent-runtime-hooks
description: 'Use for agent lifecycle hooks, tool mocks, intervention, sub-agent calls and context compression.'
user-invocable: false
---

# Agent Runtime Hooks

Register lifecycle hooks through `execAgent({ hooks })`. `HookDispatcher` stores them per operation and dispatches them in registration order.

## Hook Types

16 hook types across 5 categories:

```
execAgent({ hooks })
  │
  ├─ beforeStep ──────────── Before each step executes
  │     │
  │     ├─ [call_llm]        LLM inference
  │     │
  │     ├─ [call_tool]
  │     │     ├─ beforeToolCall ── Before tool executes (supports mocking)
  │     │     ├─ (tool execution)
  │     │     ├─ afterToolCall ─── After tool completes (optional result control)
  │     │     └─ onToolCallError ─ Tool threw an exception
  │     │
  │     ├─ [request_human_approve]
  │     │     ├─ beforeHumanIntervention ── Before agent pauses
  │     │     ├─ afterHumanIntervention ─── After approve/reject + resume
  │     │     └─ onStopByHumanIntervention ── User rejected, agent halted
  │     │
  │     ├─ [compress_context]
  │     │     ├─ beforeCompact ──── Before compression starts
  │     │     ├─ afterCompact ───── After compression completes
  │     │     └─ onCompactError ─── Compression failed
  │     │
  │     ├─ [callAgent] (via execSubAgentTask)
  │     │     ├─ beforeCallAgent ── Before sub-agent starts
  │     │     ├─ afterCallAgent ─── After creation/start returns
  │     │     └─ onCallAgentError ── Sub-agent failed
  │     │
  │     └─ afterStep ──────────── After step completes
  │
  ├─ (next step...)
  │
  ├─ onComplete ───────────── Operation reaches terminal state
  └─ onError ──────────────── Error during execution
```

## Key Files

| File                                                                   | Role                                                      |
| ---------------------------------------------------------------------- | --------------------------------------------------------- |
| `packages/agent-runtime/src/types/hooks.ts`                            | Event types and required/optional fields                  |
| `apps/server/src/services/agentRuntime/hooks/types.ts`                 | Registration and webhook types                            |
| `apps/server/src/services/agentRuntime/hooks/HookDispatcher.ts`        | Registration, dispatch, local mocks, HTTP/QStash delivery |
| `apps/server/src/services/agentRuntime/hooks/webhookPayload.ts`        | Event projection and email enrichment                     |
| `apps/server/src/modules/AgentRuntime/adapters/toolCallHookContext.ts` | Shared tool-event context builder                         |
| `apps/server/src/modules/AgentRuntime/adapters/ServerToolTransport.ts` | Tool notifications and execution results                  |
| `apps/server/src/modules/AgentRuntime/RuntimeExecutors.ts`             | Tool, compression and human-intervention execution        |
| `apps/server/src/services/agentRuntime/AgentRuntimeService.ts`         | Step events and human-intervention continuation           |
| `apps/server/src/services/agentRuntime/CompletionLifecycle.ts`         | Terminal events                                           |
| `apps/server/src/services/aiAgent/subAgentRuns.ts`                     | Sub-agent events                                          |

## Registration

```ts
const hooks: AgentHook[] = [
  {
    id: 'observe-step',
    type: 'afterStep',
    handler: async (event) => {
      console.log(event.operationId, event.stepIndex);
    },
  },
];
await aiAgentService.execAgent({ agentId, prompt, hooks });
```

Completion cleans up registrations through `hookDispatcher.unregister(operationId)`.

### Webhook Hooks

Webhook-only hooks deliver in both local and queue modes. For hooks with both `handler` and `webhook`, local dispatch calls the handler; queue dispatch uses the serialized webhook. Only webhook configuration is persisted, so functions are unavailable after a process restart.

```ts
const hook: AgentHook = {
  id: 'tool-notification',
  type: 'afterToolCall',
  matcher: '^fs/readFile$',
  webhook: {
    url: 'https://example.com/hooks',
    delivery: 'fetch',
    timeout: 5, // seconds; default 30
    headers: { Authorization: 'Bearer ${HOOK_TOKEN}' },
    allowedEnvVars: ['HOOK_TOKEN'],
  },
};
```

- `matcher` is a regex against `${identifier}/${apiName}`, supported only on tool events. Omitted, empty, or `*` matches all tools.
- Header environment templates resolve only at send time from `allowedEnvVars`; persist templates, never resolved secrets.
- Notifications ignore response content and may return HTTP 204.
- `beforeToolCall` with `responseHandling: 'toolCall'` awaits an optional HTTP decision before tool execution. Return HTTP 2xx JSON with `{ "decision": "allow" }` or `{ "decision": "deny", "reason": "禁止执行该操作" }` to intervene. The type is `{ decision: 'allow' } | { decision: 'deny'; reason?: string }`; a deny with an omitted or empty reason uses `Blocked by beforeToolCall hook.`. Denials preserve the reason in the tool result/card with classification `hook_denied`.
- `afterToolCall` also accepts `responseHandling: 'toolCall'`. It synchronously checks the full request/result before archival and model consumption; deny replaces the original result with the receiver's optional reason in both content and blocked state. A deny with an omitted or empty reason uses `Blocked by afterToolCall hook.`, following the same `Blocked by ${type} hook.` rule as `beforeToolCall`. The reason enters model context and the tool card, as with `beforeToolCall`. It does not undo tool side effects, retries, or charges. See [the result-control contract](../../../docs/development/agent-tool-result-hooks.md) for deferred completions and runtime boundaries.
- Environment response modes are `ignore` (default notifications) and `toolCall` (optional decisions for both before and after). Only subscribed events are enabled. Replace removed `toolResult`/`toolCallAndResult` values with `toolCall`; use the events list to select after-only checking. Existing after responses with a valid decision now control results.
- HTTP 204, empty successful bodies, invalid JSON, and JSON without a top-level decision (including arrays, scalars, null, or only the old nested `hookSpecificOutput`) are notification responses, even with `onError: 'block'`. Continue checking subsequent hooks; do not treat a notification as permission to skip later policies. Explicit invalid decisions, non-2xx status, oversized bodies, invalid UTF-8, network failures, and timeouts follow `onError: 'continue' | 'block'` (default `continue`); blocking uses `hook_control_error` as the tool-result content and reason for both before and after hooks. Cancellation never grants permission.
- Extra response fields are allowed and discarded. Only `decision` and a deny's optional string `reason` are consumed; `updatedInput`/`additionalContext` do not change tool arguments or conversation context.
- Configuration: `packages/types/src/agentHook.ts`; response parsing: `packages/types/src/agentHookResponse.ts`; HTTP delivery: `apps/server/src/services/agentRuntime/hooks/httpWebhook.ts`.

### Hook configuration lifetime

- Environment hooks (`AGENT_HOOK_WEBHOOK_*`) always come from the executing worker's **current environment**, including after human approval and deferred completion. Never persist their configuration or restore a previous environment's hooks.
- Code-supplied webhook hooks (`execAgent({ hooks })`) retain the existing per-operation serialization and persistence. Recover these caller hooks from the runtime snapshot or durable operation record; preserve environment-variable templates without expanding secrets into storage.
- Evaluate each operation's recovered caller hooks in order, then evaluate current environment hooks once in the final round. Do not deduplicate caller hook IDs across operations. Environment-only hooks do not require durable operation storage. Removing an environment hook stops it from controlling subsequent results; when no matching result control remains, allow the result. Do not freeze a previous environment policy across an approval or restart.
- Deferred completion publishes into its empty placeholder once; duplicate callbacks retain that first final result, including a denial. Propagate caller-policy storage errors rather than treating failed reads as an empty hook list. Do not extend default allow to configured-hook delivery failures: those follow `onError`.
- Cover environment removal/addition across approval and snapshot expiry, publication without review markers, and continued enforcement of persisted caller hooks in regression tests.

### Publish after the result hook

- Await result control before archival, message content/state writes, streaming, and model consumption. Persist the allowed result or the sanitized denial directly; do not add `toolResultControl` pending/allowed/blocked metadata or hide raw output through history projections.
- Human approval sources claim the decision and retain the answer in their private resolution/outbox. `MessageModel.resolveHumanApproval` defaults to claim-only; only the runtime calls it with `publishResult: true` after result control. Stop/rejection receipts can publish directly because they contain no executed tool output.
- Recover original caller hooks through the existing intervention operation identity or deferred callback's parent operation. Keep the in-memory hook verdict separate from tool-owned state; a tool returning `type: 'blocked'` is not a hook verdict.
- Every deferred backfill, including a member that fails to start, follows the same hook-before-write order. Use the existing empty placeholder to arbitrate duplicate final writes; do not introduce a replacement review state machine.
- When a thread has a sub-agent or group-member completion bridge, that bridge exclusively publishes its tool result. Thread lifecycle hooks update thread metadata only; never prefill the tool message with an ungated summary, including after approval recovery.
- Cover an answer held while a hook runs, atomic publication and rollback, duplicate callbacks after policy removal, and partial member startup failure. History readers should need no result-review logic.

## Events

| Event                       | Timing and payload                                                                         |
| --------------------------- | ------------------------------------------------------------------------------------------ |
| `beforeStep`                | Before a step; `AgentHookEvent`                                                            |
| `afterStep`                 | After a step; content, tool calls/results and usage totals                                 |
| `onComplete`                | Terminal state; reason such as `done`, `error`, `interrupted`, `max_steps` or `cost_limit` |
| `onError`                   | Operation error; `errorMessage`, `errorDetail` and available error metadata                |
| `beforeToolCall`            | Before tool execution; shared tool context and local `mock()` callback                     |
| `afterToolCall`             | After tool execution; structured `result` and `mocked`                                     |
| `onToolCallError`           | Tool execution throws; shared tool context and `error`                                     |
| `beforeHumanIntervention`   | Before approval; `pendingTools`, `operationId`, `stepIndex`                                |
| `afterHumanIntervention`    | Approval decision and continuation; `action`, optional `toolCallId` and `rejectionReason`  |
| `onStopByHumanIntervention` | Human rejection stops the run; optional `toolCallId` and `rejectionReason`                 |
| `beforeCompact`             | Before compression; `messageCount`, `tokenCount`, `stepIndex`                              |
| `afterCompact`              | After compression; `groupId`, `messagesBefore`, `messagesAfter`, `summary`                 |
| `onCompactError`            | Compression error; `error`, `tokenCount`, `stepIndex`                                      |
| `beforeCallAgent`           | Before sub-agent creation; `agentId`, `instruction`                                        |
| `afterCallAgent`            | Creation/start returns; `agentId`, `subOperationId`, `success`, optional `threadId`        |
| `onCallAgentError`          | Sub-agent call fails; `agentId`, `error`                                                   |

CallAgent events dispatch on the parent operation identified by `parentOperationId`. An isolated child supplies `threadId`; shared group members use the shared conversation. Child completion is reported by the child's `onComplete` event.

## Tool Context

Use `buildToolCallHookContext()` for the three tool events.

- Required fields: native `toolCallId`, `assistantMessageId`, `identifier`, `apiName`, effective `args`, `callIndex`, `stepIndex`, `operationId` and `executor`.
- Optional associations come from the run context: agent, topic, session, thread, group, task, workspace, document, source/tool message and parent operation IDs.
- `userId` uses `runtime.userId ?? origin.userId`.
- `toolSource` identifies the tool's origin; `executor` identifies the server/client dispatch route. `executionTarget` and `activeDeviceId` describe the run's execution plan and device selection.
- `afterToolCall.result` carries `content`, `success`, optional `executionTime` and error/state data such as `state.type: 'blocked'`. `mocked` marks results supplied by a local hook.

Sandbox and MCP tools use the same event path. Filter with `identifier`, `apiName` or `toolSource`.

### Local Mocking

`dispatchBeforeToolCall()` exposes `event.mock(result)`. The first accepted mock wins and short-circuits the remaining mock handlers. The method returns `{ isMocked: true, result }` for a mock, or `null` to continue execution.

```ts
event.mock({ content: '{"items":[]}', success: true });
```

Tool observation payloads use `BeforeToolCallObservationEvent`; the callback belongs to the in-memory handler event. `dispatchBeforeToolCall()` does not deliver webhooks.

## Webhook Payloads

`createWebhookPayloadBuilder()` selects `eventFields`, adds `hookId`/`hookType`, then merges `webhook.body`. The body determines the final value of overlapping fields. Tool-result notification payloads use `redactResultForEvents()` to trim raw skill Work data; controls receive the full result. Work registration retains the full in-process result. `finalState` is available to local handlers; serialized payloads carry the event's data fields.

When email is selected, the builder resolves `userEmail` from the final effective `userId`, replacing supplied email values. Email-only projections use the event ID. An explicit invalid ID, missing email or lookup error leaves email omitted.

Email queries share a per-dispatcher cache of up to 1,000 users for five minutes. Query timeouts belong to the database layer. The builder's optional fourth `{ signal }` argument lets a waiter cancel independently; cancellation returns `undefined` so the caller can stop delivery. Fetch and QStash use the enriched payload.

## Delivery Errors

Dispatch awaits each handler or webhook. Ordinary errors are logged and dispatch continues. Webhooks with `fallback: 'none'` accumulate a `CriticalHookDeliveryError`, which is thrown after sibling hooks have run. QStash delivery defaults to a fetch fallback; use `fallback: 'none'` for QStash-signed endpoints.