# 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.

Reach for an extension when callers should enable a configured feature in one place,
especially when it owns resources, has saved state, or combines several integration
points. For the choice between a standalone transformation and a packaged feature, see
[middleware and hooks](../hooks/#which-one-should-i-use).

**An extension can use middleware.** Inside `install`, call
`ctx.add_middleware(MyMiddleware(...))`. That middleware runs in the same engine
pipeline as directly registered middleware. It does not bypass permissions or change
ordering rules. The built-ins work this way: `LangSmithExtension` installs middleware
and hooks for tracing, and `ResultSpillExtension` combines result middleware, a
`read_result` tool, stored files, and cleanup.

## A complete extension containing middleware

This extension installs a search tool, normalizes its queries, adds prompt context,
and observes completions. The caller selects one extension and gets all four.

```python
from dataclasses import replace
from harnessx import Agent, Extension, ExtensionContext, HookEvent, Middleware, ToolCall

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

class SearchExtension(Extension):
    name = "search_feature"

    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.log_completion)
        ctx.register_prompt_provider(lambda: "search covers the product docs.")

    def search(self, query: str) -> str:
        """Search the product docs collection."""
        return f"product_docs: {query}"

    def log_completion(self, event) -> None:
        print("tool finished:", event.data["tool_call"].name)

agent = Agent(extensions=[SearchExtension()])
```

Tools registered without an explicit permission inherit the agent's manager, normally
ALLOW. `safe` suits a search with no external effect; choose the replay policy for the
real integration separately.

Use a fresh extension instance for each agent. Instances hold mutable state,
registrations, and clients, so do not share one across agents. Constructor-defined
subagents do not inherit parent extensions. Use a manual child wrapper when the child
needs 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 and knowledge | 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 skip completion callbacks and
teardown entirely.

Use `async with Agent(...) as agent:` to close normally. `aclose()` attempts every
extension teardown and reports the cleanup errors it collected. Keep resource
initialization in `on_turn_start` lazy and idempotent: one agent can handle many
turns, and durable recovery can repeat lifecycle work. An extension that creates an
async client should close it in `teardown`. An injected, caller-owned client stays
caller-owned unless the API explicitly transfers ownership.

A failed install rolls back its tool, hook, middleware, and prompt registrations. It
does not roll back external effects or asynchronously acquired resources. Do not open
those resources in `install`.

## 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 |

These four registration methods return disposable handles. `handle.close()` removes
the registration, and closing twice is harmless. On agent shutdown, each extension's
registrations are removed in reverse order. Registering through `ctx.agent.tools` or
other direct mutation gets no ownership tracking. Duplicate tools require an explicit
`replace=True`; a tracked replacement restores the previous binding when removed, as
long as the extension still owns the active binding.

`ctx.register_tool` supports `name`, `description`, `input_schema`, `permission`,
`concurrent`, `replay_policy`, `timeout_seconds`, `retry`, `retry_if_result`, and
`replace`. Permission defaults
match other registration paths, and an extension's tool passes 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 request's system
prompt. Their exceptions are ignored, so put authorization and configuration checks
elsewhere. The `get_state`/`set_state` metadata dictionary is shared, so prefix keys
with your extension's name. Save and 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. Include a version field and validate the version and shape on restore.

`on_save_session` runs for engine snapshots as well as explicit session saving, so it
can run many times during a turn. `on_load_session` can run during recovery. Neither
is a one-time startup or shutdown signal. Rebuild resources from application
configuration rather than persisting their Python objects.

Put managed business effects into tools and enforce required checks in the execution
path. Lifecycle callbacks and middleware can repeat on recovery, and an external write
inside a callback gets no execution key or replay policy. Hooks and `on_turn_complete`
are best-effort observation.

## 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 run again to inspect a stored result. The feature needs a tool,
files, persistence integration, and cleanup, which is why it is an extension. See
[flight recording](../flight-recorder/) for how spilled artifacts are retained and
exported.

### LangSmith tracing

`LangSmithExtension` combines hooks and middleware to produce connected agent, model,
and tool spans. Model spans carry the request, the response, the stop reason, token
usage, and the `ls_model_name` and `ls_provider` fields LangSmith uses for pricing.
It requires the optional LangSmith dependency and service configuration for uploads.
See `examples/07-quality/tracing_with_langsmith.py` for the full example, with a span
for each model call and tool call.

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