# Extensions & artifacts

Package middleware, tools, hooks, saved state, and cleanup into a reusable agent feature.

An extension is a reusable feature installed with `Agent(extensions=[...])`.
It can register tools, hooks, middleware, and dynamic prompt context, preserve
serializable state, and release resources when the agent closes.

## Extensions versus middleware

| Interface | Responsibility | Example |
| --- | --- | --- |
| Middleware | Transform data in one or more execution stages | Normalize a tool argument or shorten a tool result |
| Hook | Observe a lifecycle event | Emit a diagnostic metric after a tool finishes |
| Extension | Package capabilities, configuration, state, and lifecycle | Install search tools together with normalization and metrics |

Use standalone middleware when you only need a transformation. Use an extension
when callers should enable a configured feature in one place, especially if it
owns resources, has saved state, or combines multiple integration points.

**An extension can use middleware.** Inside `install`, call
`ctx.add_middleware(MyMiddleware(...))`. The middleware still runs in the same
engine pipeline as directly registered middleware. Being inside an extension
does not bypass permissions, change execution ordering rules, or make middleware
side effects durable transactions.

Built-in examples demonstrate this composition: `LangSmithExtension` installs
middleware and hooks for tracing; `ResultSpillExtension` combines result middleware,
a `read_result` tool, stored files, and cleanup.

## A complete extension containing middleware

The following extension installs a fixture search tool, normalizes its queries,
adds prompt context, and observes completions. No external search service is
required to define the feature. The completion count is diagnostic, not an
exactly-once audit ledger.

```python
from dataclasses import replace
from typing import Any
from harnessx import (
    Agent, Extension, ExtensionContext, HookContext, HookEvent,
    Middleware, ToolCall,
)

class NormalizeSearch(Middleware):
    async def before_tool_execution(self, call: ToolCall) -> ToolCall:
        query = call.input.get("query")
        if call.name == "search" and isinstance(query, str):
            return replace(call, input={**call.input, "query": query.strip()})
        return call

class SearchExtension(Extension):
    name = "search_feature"
    priority = 50

    def __init__(self, collection: str) -> None:
        self.collection = collection
        self.completed_searches = 0

    def install(self, ctx: ExtensionContext) -> None:
        ctx.register_tool(self.search, replay_policy="safe")
        ctx.add_middleware(NormalizeSearch())
        ctx.on_hook(HookEvent.TOOL_CALL_END, self.record_completion)
        ctx.register_prompt_provider(
            lambda: f"The search tool searches the {self.collection} collection."
        )

    def search(self, query: str) -> str:
        """Return a fixture result from the configured collection."""
        return f"{self.collection}: {query}"

    def record_completion(self, event: HookContext) -> None:
        call = event.data["tool_call"]
        if call.name == "search":
            self.completed_searches += 1

    async def on_save_session(self, ctx: ExtensionContext) -> dict[str, Any]:
        return {"version": 1, "completed_searches": self.completed_searches}

    async def on_load_session(self, ctx: ExtensionContext, state: dict[str, Any]) -> None:
        count = state.get("completed_searches", 0)
        if state.get("version") != 1 or type(count) is not int or count < 0:
            raise ValueError("Invalid search extension state")
        self.completed_searches = count

agent = Agent(extensions=[SearchExtension(collection="product_docs")])
# Run with `async with agent:` once your model provider is configured.
```

The application only selects `SearchExtension`; the extension takes care of wiring
its pieces. Tools registered without an explicit permission inherit the agent's
manager, normally ALLOW. `safe` is appropriate here because the fixture search has
no external effect; choose the replay policy for the real integration separately.

Use a fresh extension instance for each agent. Instances can contain mutable state,
registrations, and clients; sharing one across agents is not a supported lifecycle
shortcut. Constructor-defined subagents do not inherit parent extensions. Use a
manual child wrapper when the child requires its own extension configuration.

## Lifecycle and ordering

Every extension needs a unique, nonempty `name`. `priority` is an integer, default
100; lower numbers install first, and equal priorities preserve input order.
Name collisions are rejected before installation.

| Method | When it runs | Contract |
| --- | --- | --- |
| `install(ctx)` | Synchronously during agent construction, after skills | Register capabilities; do not acquire async resources |
| `on_turn_start(ctx, user_message)` | At the start of a user turn | Async; may initialize required resources; errors fail the turn |
| `on_turn_end(ctx, final_text)` | On the successful finish path | Async; errors can fail the run |
| `on_turn_complete(ctx, result)` | Terminal completion, failure, or cancellation | Best-effort observer; receives `RunResult` |
| `on_save_session(ctx)` | When capturing execution/session state | Return JSON-compatible state; keep it fast and side-effect free |
| `on_load_session(ctx, state)` | When restoring captured state | Restore the feature's data; validate its version and shape |
| `teardown()` | During `agent.aclose()` | Release extension-owned resources |

Start, finish, save, and load callbacks follow installation order. Terminal
completion and teardown run in reverse installation order. Pauses and approval
waits are not terminal completion. Hard process termination can prevent completion
callbacks and teardown entirely.

Use `async with Agent(...) as agent:` to close normally. `aclose()` attempts all
extension teardowns and reports collected cleanup errors. Resource initialization
in `on_turn_start` should be lazy and idempotent: the same agent can handle multiple
turns, and durable recovery can repeat lifecycle work. An extension that owns an
async client should close it in `teardown`; an injected, caller-owned client should
remain caller-owned unless the API explicitly transfers ownership.

Installation rolls back tool, hook, middleware, and prompt registration mutations
when an install fails. It does not roll back arbitrary external effects, arbitrary
changes to the agent, or asynchronously acquired resources. Do not open those
resources in `install`; asynchronous teardown is not an installation rollback.

## ExtensionContext reference

Use the context's registration methods so the SDK tracks ownership:

| Method | Purpose |
| --- | --- |
| `ctx.register_tool(handler, ...)` | Register an inferred tool or pass `input_schema` explicitly |
| `ctx.add_middleware(middleware)` | Add a normal pipeline transformation |
| `ctx.on_hook(event, callback)` | Subscribe an observer |
| `ctx.register_prompt_provider(callback)` | Append dynamic text to the system prompt |
| `ctx.get_state(key, default)` / `ctx.set_state(key, value)` | Access extension metadata |

The four registration methods return disposable handles. `handle.close()` removes
the registration; closing twice is harmless. On agent shutdown, registrations made
through these methods are removed in reverse order for each extension. Registration
through `ctx.agent.tools` or other direct mutations does not receive this automatic
ownership tracking. Duplicate tools require an intentional `replace=True`; tracked
replacement restores the previous binding when removed if the extension still owns
the active binding.

`ctx.register_tool` supports `name`, `description`, `input_schema`, `permission`,
`concurrent`, `replay_policy`, `timeout_seconds`, and `replace`. Permission defaults
are the same as other registration paths. A tool installed by an extension is
subject to the same engine checks as any other tool.

Context properties include `config`, `tools`, `hooks`, `middleware`, `memory`,
`session_id`, and `agent`. The context is a convenience interface, not a security
sandbox: extensions are trusted Python code with access to the agent. Use
`ctx.agent` explicitly for advanced access; implicit forwarding of unknown context
attributes is deprecated.

Prompt providers are synchronous callbacks evaluated while building a model
request's system prompt. Their exceptions are currently ignored, so a prompt
provider is not a place for mandatory authorization or configuration checks.
The `get_state`/`set_state` metadata dictionary is shared; prefix keys with your
extension's name to avoid collisions. Save/load callback output is stored under
the extension name and is the preferred contract for structured feature state.

## Persistence and durable execution

Save state as JSON-compatible dictionaries, lists, strings, booleans, finite
numbers, and null values. Do not serialize clients, locks, handlers, file objects,
or credentials. Use a version field and validate on restoration, as in the example.

`on_save_session` is used for engine snapshots as well as explicit session saving;
it can run many times during a turn. `on_load_session` can run during recovery.
Neither callback is a one-time startup/shutdown signal. Reconstruct resources from
application configuration instead of persisting their Python objects.

When restoring an agent with `Agent.load_session`, supply the extension bindings
again through `extensions=[...]`, along with provider/tool bindings where needed.
Required saved extension names must be present. Durable runtime factories similarly
need consistent, versioned bindings; snapshots do not serialize extension code.

Lifecycle callbacks and middleware can be repeated on recovery. External writes
inside a callback do not inherit tool execution keys, replay policy, or an
exactly-once guarantee. Put managed business effects into tools and enforce
required checks in the execution path. Hooks and `on_turn_complete` are best-effort
observation; their failure is not a reliable signal to stop an effect.

## Built-in extensions

### Result spilling

```python
from harnessx import Agent, ResultSpillExtension

agent = Agent(extensions=[ResultSpillExtension(threshold_bytes=32_000)])
```

Large tool results become compact envelopes while the extension stores their
contents and exposes `read_result` for slices, searches, and supported tabular
operations. The original tool need not execute again to inspect a stored result.
This feature needs more than a result transform: it also needs a tool, files,
persistence integration, and cleanup, which is why it is an extension.

Spill files are temporary by default. Snapshot and durable artifact bindings
preserve captured data beyond cleanup. A path in arbitrary tool text is not
automatically a portable artifact, and a spill reference is not evidence of
business validity or provenance. Distributed workers need shared artifact storage
and explicit retention. Incident export controls optional artifact inclusion.

### LangSmith tracing

`LangSmithExtension` combines hooks and middleware to produce connected agent,
model, and tool spans. It requires the optional LangSmith dependency and service
configuration for uploads. See `examples/langsmith_tracing.py` for the full
example, including a child agent with its own tracing extension. Tracing observes
execution; it does not replace durable recording or permission enforcement.

Each model span records what was sent: the system prompt, the messages, the tool
definitions, and the invocation parameters (model, `max_tokens`, `temperature`,
streaming), plus the prompt-cache `prefix_key`. On completion it records the
response, the stop reason, and usage with cache-read, cache-creation, and
reasoning token details, and it carries `ls_model_name` and `ls_provider` so
LangSmith can price the run.

## Test an extension before reuse

Use a scripted provider to verify installation, transformations, and tool calls
without a live model. Exercise successful and failed runs, cancellation, repeated
turns, snapshot restoration, and cleanup. Assert that registered tools and
middleware are removed when the extension closes and that an interrupted external
effect is handled by the executor, not inferred from a hook counter.

See [middleware and hooks](../hooks/) for all transformation stages, and
[flight recording](../flight-recorder/) for incident export and artifact inclusion.
