# Pre-LLM request hooks

> Transform a normal conversational provider request with a workspace lambda

Source: https://github.com/christianbalevski/adf/blob/v0.7.4/docs/guides/pre-llm-hook.md (adf v0.7.4)

`pre_llm_hook` optionally runs a workspace lambda immediately before each **normal conversational** provider request. It can rewrite the request-local system prompt, messages, provider-facing tool definitions, and model request options.

**Triggers and hooks are different.** Triggers react to events and start work. Hooks run inside work already underway; the pre-LLM hook transforms the request before the model call continues.

It is for bounded transformations such as adding a policy reminder, selecting a smaller presented tool set, adapting a provider option, or running a fast RAG lambda with the existing `adf.*` capabilities. It is deliberately a small interception point, not a separate built-in RAG or tool-selection subsystem, and it is never a way to grant capabilities.

## Configuration

```json
{
  "pre_llm_hook": {
    "source": "lib/request-policy.ts:transform",
    "scope": "all",
    "timeout_ms": 5000
  }
}
```

- `source` is a workspace `.js` or `.ts` lambda path, optionally followed by `:functionName`; omitted function name means `main`.
- `scope` is `all` (main and every current/future inner loop), `main`, or `loops`. Existing configurations keep this shape.
- `loops` is required only for `scope: "loops"`; it is a non-empty unique list of named **inner** loops. It cannot contain `main`.
- `include_main` is optional and valid only with `scope: "loops"`; when true, the hook also runs for Main. This additive field lets Studio represent Main plus specific inner loops without putting `main` into the inner-loop list.
- `timeout_ms` is optional. It is always capped by `limits.execution_timeout_ms`.

Target named inner loops without requiring that they already exist:

```json
{
  "pre_llm_hook": {
    "source": "lib/request-policy.ts:transform",
    "scope": "loops",
    "loops": ["researcher", "reviewer"]
  }
}
```

Studio presents targets as rows: a new hook starts with **Main**, and **Add target** adds a declared or future inner-loop name. The **All streams (including future loops)** option is exclusive with specific target rows and remains an explicit `scope: "all"` configuration; opening an existing `all` hook never narrows it to a finite list. Main plus named loops is stored as `scope: "loops"`, `include_main: true`, and a `loops` array.

The hook is an explicit runtime lambda entry point. It does **not** require—or expose—`sys_lambda` as a conversational tool. Its private execution bridge may call nested `adf.sys_lambda` only when the independent `code_execution.sys_lambda` gate and normal source-authorization rules allow it. That private backend does not register `sys_lambda` for ordinary LLM calls or for unrelated `sys_code` executions; those remain declaration- and enabled-state dependent.

## Contract

The lambda receives one JSON-safe object:

```ts
async function transform({ request, loop }) {
  // loop.name is "main" or the current inner-loop name
  return {
    system: request.system,
    messages: request.messages,
    tools: request.tools,
    options: request.options,
  }
}
```

`request.options` may contain:

- `maxTokens`, `temperature`, `topP`, `thinkingBudget`
- `reasoning`
- `dynamicInstructions`
- `providerParams`

The lambda must return a complete replacement `request` object—not the outer `{ request, loop }` object. Returned values must be JSON, use valid conversational message/tool shapes, and preserve valid tool-use/tool-result pairing.

Example: present just two existing tool schemas on ordinary conversational calls.

```ts
export function transform({ request }) {
  return {
    ...request,
    tools: request.tools.filter((tool) =>
      tool.name === 'fs_read' || tool.name === 'fs_write'
    ),
  }
}
```

### Fast RAG with an existing lambda

A hook can call an existing retrieval lambda and return its enriched messages
with the rest of the original request unchanged. This is a normal `adf.*` call:
the nested `sys_lambda` follows its own file authorization and execution gates.
For example, `lib/fast-rag.ts:enrichMessages` can retrieve from an approved
index and return `{ messages: LLMMessage[] }`.

```ts
export async function transform({ request, loop }) {
  const enrichment = await adf.sys_lambda({
    source: 'lib/fast-rag.ts:enrichMessages',
    args: {
      messages: request.messages,
      loop: loop.name,
    },
  })

  return {
    ...request,
    messages: enrichment.messages,
  }
}
```

The retrieval lambda must return valid conversational messages. The hook still
returns the full replacement request, and a retrieval failure fails that call
closed rather than sending the un-enriched original request. Hook workers are
fresh request-scoped sandbox workers, so do not rely on module/global state
persisting between tool rounds.

## Boundaries and failures

- The hook runs after context repair and immediately before each normal conversational call, including later calls after tool results.
- It does **not** run for `adf.model_invoke()` or automatic history compaction. Calling `adf.model_invoke()` inside a hook uses that direct path and does not recurse.
- Abort signal and streaming callbacks stay runtime-owned; a hook cannot replace them.
- Each hook invocation runs in a fresh, isolated worker and is destroyed on completion, failure, or abort. Worker termination is the supported prompt cancellation mechanism; because the VM has no per-execution interrupt, termination would cancel other executions sharing that worker, which is why hooks do not share workers with ordinary callers.
- A malformed result, missing source, execution error, timeout, or cancellation fails the current call closed. The original request is never silently dispatched. Hook failures are recorded as a local structural `[Turn error]` in the loop and do not enter provider-auth, transient-provider, image, or tool-mismatch recovery. When the established bounded structural recovery is enabled, a later retry reruns the hook; retry notices are labeled `Turn error`, not as provider failures. An already-dispatched host-side `adf.*` RPC cannot be retracted by abort; worker termination prevents the sandbox continuation and late writes, but the host handler may finish independently.
- Hook code gets a private `adf.*` RPC bridge with the same live loop-specific restrictions, HIL rules, and source-file authorization semantics as other lambdas. Its nested `sys_lambda` backend is hook-local and independently gated by `code_execution.sys_lambda`; ordinary shared tool registration is unchanged. Inner-loop hooks use their loop's attenuated handler.
- Changing `request.tools` changes only what the provider is shown. Actual tool execution still uses the loop's original enabled-tool snapshot, validation, HIL, and file/protection checks. A hook cannot grant a tool or bypass authorization.

See [code execution](https://agentdocumentformat.org/guides/code-execution) and [authorized code](https://agentdocumentformat.org/guides/authorized-code) for lambda capabilities and file authorization.
