diff --git a/.changeset/clean-core-refactor.md b/.changeset/clean-core-refactor.md
new file mode 100644
index 00000000..a862633d
--- /dev/null
+++ b/.changeset/clean-core-refactor.md
@@ -0,0 +1,5 @@
+---
+"@anvia/core": patch
+---
+
+Refactor core internals for improved maintainability while preserving public API and behavior.
diff --git a/.changeset/config.json b/.changeset/config.json
new file mode 100644
index 00000000..424278dd
--- /dev/null
+++ b/.changeset/config.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
+ "changelog": "@changesets/cli/changelog",
+ "commit": false,
+ "fixed": [],
+ "linked": [],
+ "access": "public",
+ "baseBranch": "main",
+ "updateInternalDependencies": "patch",
+ "ignore": ["docs", "cookbook", "anvia-cli-agent"]
+}
diff --git a/.env.example b/.env.example
index fde3d97b..16d28f1c 100644
--- a/.env.example
+++ b/.env.example
@@ -1,4 +1,5 @@
OPENAI_API_KEY=
+OPENAI_BASEURL=
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-sonnet-4-20250514
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..c27a049a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,77 @@
+name: CI
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ packages:
+ name: Packages
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 11.0.4
+ run_install: false
+
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: pnpm
+ cache-dependency-path: pnpm-lock.yaml
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Build core package
+ run: pnpm --filter @anvia/core build
+
+ - name: Build remaining packages
+ run: pnpm --filter './packages/**' --filter '!@anvia/core' build
+
+ - name: Typecheck packages
+ run: pnpm --filter './packages/**' typecheck
+
+ - name: Test packages
+ run: pnpm --filter './packages/**' test
+
+ docs:
+ name: Docs
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 11.0.4
+ run_install: false
+
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: pnpm
+ cache-dependency-path: pnpm-lock.yaml
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Typecheck docs
+ run: pnpm --filter docs typecheck
+
+ - name: Build docs
+ run: pnpm --filter docs build
diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
new file mode 100644
index 00000000..85b486ac
--- /dev/null
+++ b/.github/workflows/docs-deploy.yml
@@ -0,0 +1,51 @@
+name: Deploy Docs
+
+on:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: docs-production
+ cancel-in-progress: false
+
+jobs:
+ deploy:
+ name: Deploy docs
+ runs-on: ubuntu-latest
+ environment: production
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 11.0.4
+ run_install: false
+
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: pnpm
+ cache-dependency-path: pnpm-lock.yaml
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Typecheck docs
+ run: pnpm --filter docs typecheck
+
+ - name: Build docs
+ run: pnpm --filter docs build
+
+ - name: Deploy docs
+ run: pnpm --filter docs run deploy
+ env:
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..347c40ef
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,104 @@
+name: Release Packages
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: package-release
+ cancel-in-progress: false
+
+jobs:
+ version:
+ name: Create release pull request
+ if: github.event_name == 'push'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 11.0.4
+ run_install: false
+
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: pnpm
+ cache-dependency-path: pnpm-lock.yaml
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Create release pull request
+ uses: changesets/action@v1
+ with:
+ version: pnpm version-packages
+ title: Version Packages
+ commit: Version Packages
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ publish:
+ name: Publish packages to npm
+ if: github.event_name == 'workflow_dispatch'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ id-token: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Require main branch
+ run: |
+ if [ "${GITHUB_REF_NAME}" != "main" ]; then
+ echo "Manual package publishing must run from the main branch."
+ exit 1
+ fi
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 11.0.4
+ run_install: false
+
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: pnpm
+ cache-dependency-path: pnpm-lock.yaml
+ registry-url: https://registry.npmjs.org
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Publish packages
+ run: pnpm release
+ env:
+ NPM_CONFIG_PROVENANCE: true
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Push release tags
+ run: git push --follow-tags
+
+ - name: Create GitHub Releases
+ run: pnpm github-releases
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CHANGESET_GUIDE.md b/CHANGESET_GUIDE.md
new file mode 100644
index 00000000..fe288098
--- /dev/null
+++ b/CHANGESET_GUIDE.md
@@ -0,0 +1,116 @@
+# Changeset Guide
+
+Use Changesets whenever a change should publish one or more packages from `packages/` to npm.
+
+## When To Add A Changeset
+
+Add a changeset for any user-facing package change:
+
+- `patch`: bug fixes, small behavior fixes, documentation fixes that affect the published package, or internal changes that should ship.
+- `minor`: new backwards-compatible APIs, features, options, exports, or capabilities.
+- `major`: breaking API, behavior, package export, runtime, or type changes.
+
+Do not add a changeset for docs-only changes, CI changes, examples-only changes, or changes to private workspaces.
+
+## Create A Changeset
+
+From the repository root:
+
+```sh
+pnpm changeset
+```
+
+Then:
+
+1. Select every package that should be released.
+2. Choose the bump type for each selected package.
+3. Write a short release note that explains the user-visible change.
+4. Commit the generated file under `.changeset/` with your code changes.
+
+Example changeset text:
+
+```md
+---
+"@anvia/core": minor
+"@anvia/openai": patch
+---
+
+Add streaming metadata support to core runs and align the OpenAI adapter output.
+```
+
+## Before Opening A PR
+
+Run the relevant checks:
+
+```sh
+pnpm install --frozen-lockfile
+pnpm --filter './packages/**' typecheck
+pnpm --filter './packages/**' test
+pnpm --filter './packages/**' build
+```
+
+If docs changed, also run:
+
+```sh
+pnpm --filter docs typecheck
+pnpm --filter docs build
+```
+
+## Release Flow
+
+After a PR with one or more changesets is merged into `main`, the GitHub Actions release workflow runs Changesets.
+
+The workflow will:
+
+1. Open or update a `Version Packages` release PR.
+2. Apply version bumps to changed packages.
+3. Update package changelogs.
+4. Remove consumed `.changeset/*.md` files.
+
+Review the release PR before merging it. Make sure package versions and changelog entries are correct.
+
+## Publish To npm Manually
+
+Merging the `Version Packages` release PR into `main` does not publish to npm. Publishing is manual dispatch only.
+
+Publishing requires the repository secret:
+
+```txt
+NPM_TOKEN
+```
+
+To publish:
+
+1. Merge the `Version Packages` release PR into `main`.
+2. Open GitHub Actions.
+3. Select the `Release Packages` workflow.
+4. Click `Run workflow`.
+5. Run it from the `main` branch.
+
+The manual publish job builds packages before publishing:
+
+```sh
+pnpm release
+```
+
+That command runs:
+
+```sh
+pnpm --filter './packages/**' build && changeset publish
+```
+
+## Manual Version Check
+
+To preview what Changesets sees locally:
+
+```sh
+pnpm exec changeset status --since main
+```
+
+To apply version bumps locally for inspection only:
+
+```sh
+pnpm version-packages
+```
+
+Only commit local version bumps if you are intentionally preparing the release PR by hand. In normal development, let GitHub Actions create the release PR.
diff --git a/README.md b/README.md
index 39679391..ae6d4d0c 100644
--- a/README.md
+++ b/README.md
@@ -5,236 +5,106 @@
Anvia
- Provider-agnostic agents, tool workflows, and structured extraction for TypeScript applications.
+ Build provider-agnostic AI agents and workflows in TypeScript.
-
-
-
-Anvia is a TypeScript runtime for building provider-agnostic agents, tool workflows, and structured extraction inside your application code.
+Anvia is a TypeScript runtime for agents, tools, structured extraction, retrieval, pipelines, and observability inside your application code.
-It is designed for teams that want more structure than raw model calls, but less framework weight than a full orchestration stack. Provider clients create models. Builders configure behavior. Your application still owns data, permissions, persistence, and side effects.
+It gives teams more structure than raw model calls without forcing a heavyweight orchestration stack. You bring the product, data, permissions, persistence, deployment, and side effects. Anvia gives you typed AI workflow primitives that fit around them.
-## Product Shape
+## Why Anvia
-Anvia gives you a compact set of primitives for production-adjacent AI workflows:
+- Provider-neutral clients for OpenAI-compatible APIs, Anthropic, Gemini, and Mistral.
+- Agent and tool APIs that keep application behavior explicit and typed.
+- Structured extraction and output schemas for turning model responses into usable data.
+- Pipeline primitives for composing functions, agents, extractors, batches, and parallel branches.
+- Retrieval adapters for in-memory search, local embeddings, ChromaDB, Qdrant, and pgvector.
+- Optional Studio, MCP, local skills, Langfuse, and OpenTelemetry integrations.
-- Agents, tools, extractors, pipelines, streaming, and typed output schemas.
-- Provider-backed completion and embedding models across OpenAI, Anthropic, Gemini, Mistral, and compatible APIs.
-- RAG primitives with in-memory search, local embeddings, ChromaDB, Qdrant, and pgvector adapters.
-- Runtime integrations for MCP, local skills, multimodal attachments, observability, and Anvia Studio.
+## Quick Start
-## When To Choose Anvia
+Install the core runtime and a provider adapter:
-Choose Anvia when you are building AI features inside a TypeScript product and want provider-agnostic agents, extractors, tools, pipelines, and Studio workflows without giving up control of your application's data, permissions, persistence, and side effects.
-
-For deeper positioning, see [Comparison](apps/docs/content/docs/guides/comparison.mdx) and [Design Philosophy](apps/docs/content/docs/guides/design-philosophy.mdx).
+```sh
+pnpm add @anvia/core @anvia/openai
+```
-## API Shape
+Create a provider client, build an agent, and run it from your app:
```ts
-import { AgentBuilder, ExtractorBuilder, PipelineBuilder } from "@anvia/core";
+import { AgentBuilder } from "@anvia/core";
import { OpenAIClient } from "@anvia/openai";
-const client = new OpenAIClient({
- baseUrl: "https://openrouter.ai/api/v1",
- apiKey,
-});
-const model = client.completionModel("qwen/qwen3.6-35b-a3b");
+const client = new OpenAIClient({ apiKey });
+const model = client.completionModel("gpt-5.5");
-const agent = new AgentBuilder("support", model)
- .instructions("Answer support questions clearly.")
+const supportAgent = new AgentBuilder("support", model)
+ .instructions("Answer support questions clearly. Ask for missing details.")
.build();
-const response = await agent.prompt("How do I reset my password?").send();
-
-const extractor = new ExtractorBuilder(model, ticketSchema).build();
-const ticket = await extractor.extract(response.output);
-
-const workflow = new PipelineBuilder()
- .step((input) => `Summarize this support ticket:\n\n${input}`)
- .prompt(agent)
- .extract(extractor)
- .build();
-
-const normalizedTicket = await workflow.run(
- "Acme Co. reports checkout failures. Priority is high.",
-);
-```
-
-## Packages
-
-| Package | Path | Purpose |
-| --- | --- | --- |
-| `@anvia/core` | `packages/core` | Core runtime for agents, tools, streaming, extraction, RAG primitives, MCP, skills, attachments, and observability interfaces. |
-| `@anvia/openai` | `packages/providers/openai` | OpenAI and OpenAI-compatible provider adapter. |
-| `@anvia/anthropic` | `packages/providers/anthropic` | Anthropic and Anthropic-compatible provider adapter. |
-| `@anvia/gemini` | `packages/providers/gemini` | Gemini and Vertex AI provider adapter. |
-| `@anvia/mistral` | `packages/providers/mistral` | Mistral completion and embedding provider adapter. |
-| `@anvia/chroma` | `packages/vector-stores/chroma` | ChromaDB vector store adapter for Anvia embeddings and RAG. |
-| `@anvia/qdrant` | `packages/vector-stores/qdrant` | Qdrant vector store adapter for Anvia embeddings and RAG. |
-| `@anvia/pgvector` | `packages/vector-stores/pgvector` | Postgres pgvector store adapter for Anvia embeddings and RAG. |
-| `@anvia/langfuse` | `packages/observability/langfuse` | Langfuse tracing adapter for Anvia observers. |
-| `@anvia/otel` | `packages/observability/otel` | OpenTelemetry tracing adapter for Anvia observers. |
-| `@anvia/transformers` | `packages/embeddings/transformers` | Transformers.js embedding model adapter, defaulting to local All-MiniLM. |
-| `@anvia/studio` | `packages/tools/studio` | HTTP runtime and browser UI for serving agents, sessions, traces, and approvals. |
-| `docs` | `apps/docs` | Private documentation app. |
-| `cookbook` | `examples/cookbook` | Runnable examples that document the public learning path. |
-
-## Getting Started
-
-Install dependencies:
-
-```sh
-pnpm install
-```
-
-Create a local `.env` file for cookbook runs:
-
-```sh
-OPENROUTER_API_KEY=...
-OPENAI_API_KEY=...
-GEMINI_API_KEY=...
-MISTRAL_API_KEY=...
-```
-
-Run the first basic text call:
-
-```sh
-pnpm cookbook:basics:01
-```
-
-Run Studio locally:
+const response = await supportAgent
+ .prompt("A customer cannot reset their password. What should I check first?")
+ .send();
-```sh
-pnpm cookbook:studio:01
+console.log(response.output);
```
-Start the cookbook ChromaDB service before running the Chroma-backed RAG examples:
+Use the same runtime shape with other providers:
```sh
-docker compose -f examples/cookbook/compose.cookbook.yml up -d
-pnpm cookbook:retrieval:05
-pnpm cookbook:retrieval:06
-pnpm cookbook:retrieval:07
-pnpm cookbook:retrieval:08
+pnpm add @anvia/anthropic @anvia/gemini @anvia/mistral
```
-## Cookbook
+Anvia clients take explicit constructor values and do not read environment variables on their own, so credentials stay in your existing configuration layer.
-The cookbook is a product learning path. It starts with a plain text call, then adds history, streaming, tools, extraction, providers and multimodal APIs, pipelines, retrieval, multi-agent workflows, evals, Studio, and integrations one concept at a time.
+## What You Can Build
-| Level | Focus |
+| Capability | Use it for |
| --- | --- |
-| Basics | Text calls, chat history, context, streaming, and `ReadableStream` output. |
-| Tools | Tool calls, streamed tool events, hooks, concurrency, application state, guarded tools, and dynamic tool selection. |
-| Structured output | Schema-first extraction, output schemas, context, retries, and extraction with history. |
-| Providers and multimodal | Provider adapters, model capabilities, reasoning streams, attachments, image generation, audio generation, and transcription. |
-| Pipelines | Step transforms, composition, named parallel branches, batching, agents, extraction, and richer workflows. |
-| Retrieval | Local embeddings, vector search, metadata filters, RAG context, document loaders, ChromaDB, Qdrant, pgvector, FastEmbed, and Mistral embeddings. |
-| Multi-agent | Agents as tools and pipeline-backed parallel specialists. |
-| Evals | Deterministic metrics, semantic similarity, custom metrics, agent eval targets, and LLM judge/score. |
-| Studio | Served agents, browser sessions, traces, multi-agent runners, tool approvals, human feedback, and Knowledge. |
-| Integrations | MCP tools, local skills, Langfuse tracing, Langfuse eval reporting, and OpenTelemetry tracing. |
-
-Run the default example for a level:
+| Agents | Promptable workflows with instructions, context, tools, hooks, history, streaming, and typed outputs. |
+| Tools | Safe, typed access to application-owned actions such as lookup, search, mutation, approval, or dispatch. |
+| Extractors | Schema-shaped data from text, tickets, documents, messages, and model responses. |
+| Pipelines | Explicit multi-step workflows that combine functions, agents, extraction, branching, and batching. |
+| Retrieval | Embeddings, vector search, document context, metadata filters, and RAG workflows. |
+| Observability | Run, generation, tool, usage, trace, and eval events for production visibility. |
+| Studio | A local browser UI for inspecting agents, sessions, traces, pipelines, tools, approvals, and knowledge. |
-```sh
-pnpm cookbook:basics
-pnpm cookbook:tools
-pnpm cookbook:structured-output
-pnpm cookbook:providers
-pnpm cookbook:pipelines
-pnpm cookbook:retrieval
-pnpm cookbook:multi-agent
-pnpm cookbook:evals
-pnpm cookbook:studio
-pnpm cookbook:integrations
-```
-
-Numbered scripts are available for each level when you want to step through the path in order, for example `pnpm cookbook:basics:01`. Existing `basic`, `intermediate`, `pipeline`, `rag`, and `multimodal` cookbook scripts remain as compatibility aliases.
+## Cookbook
-## Development
+The [cookbook](examples/cookbook/README.md) is the fastest way to see Anvia in motion. It walks from a first text call through tools, structured output, providers, multimodal inputs, pipelines, retrieval, multi-agent workflows, evals, Studio, and integrations.
-Install dependencies before running workspace tasks:
+Run the first example from the repository root:
```sh
pnpm install
+pnpm cookbook:basics:01
```
-Common commands:
-
-```sh
-pnpm typecheck
-pnpm test
-pnpm build
-pnpm check
-```
-
-Package-scoped commands:
+Run Studio locally:
```sh
-pnpm --filter @anvia/core typecheck
-pnpm --filter @anvia/core test
-pnpm --filter @anvia/core build
-
-pnpm --filter @anvia/studio typecheck
-pnpm --filter @anvia/studio test
-pnpm --filter @anvia/studio build
-
-pnpm --filter cookbook typecheck
-```
-
-## Repository Layout
-
-```txt
-.
-├── packages/
-│ ├── core/ # @anvia/core
-│ ├── providers/
-│ │ ├── openai/ # @anvia/openai
-│ │ ├── anthropic/ # @anvia/anthropic
-│ │ ├── gemini/ # @anvia/gemini
-│ │ └── mistral/ # @anvia/mistral
-│ ├── vector-stores/
-│ │ ├── chroma/ # @anvia/chroma
-│ │ ├── qdrant/ # @anvia/qdrant
-│ │ └── pgvector/ # @anvia/pgvector
-│ ├── observability/
-│ │ └── langfuse/ # @anvia/langfuse
-│ ├── embeddings/
-│ │ └── transformers/ # @anvia/transformers
-│ └── tools/
-│ └── studio/ # @anvia/studio
-├── apps/
-│ └── docs/ # documentation app
-├── examples/
-│ ├── cli-agent/ # runnable CLI agent example
-│ └── cookbook/ # runnable examples
-├── biome.json
-├── pnpm-workspace.yaml
-└── tsconfig.base.json
+pnpm cookbook:studio:01
```
-## Contributing
+## Learn More
-Keep changes small and covered by the relevant package tests. For API changes, add or update cookbook coverage so the behavior is easy to verify from the command line.
+- [Introduction](apps/docs/content/docs/guides/index.mdx)
+- [Getting Started](apps/docs/content/docs/guides/getting-started.mdx)
+- [Cookbook Guide](apps/docs/content/docs/guides/cookbook.mdx)
+- [Comparison](apps/docs/content/docs/guides/comparison.mdx)
+- [Design Philosophy](apps/docs/content/docs/guides/design-philosophy.mdx)
+- [Contributing](CONTRIBUTING.md)
-Before opening a change, run:
+## Project Activity
-```sh
-pnpm typecheck
-pnpm test
-pnpm build
-pnpm check
-```
+
## License
-MIT.
+MIT
diff --git a/apps/docs/content/docs/best-practices/common-patterns/agent-structure.mdx b/apps/docs/content/docs/best-practices/common-patterns/agent-structure.mdx
new file mode 100644
index 00000000..9d9b5eb1
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/common-patterns/agent-structure.mdx
@@ -0,0 +1,140 @@
+---
+title: Agent Structure
+description: Keep stable runtime objects separate from request-local state.
+---
+
+Build agents from explicit TypeScript values. A built agent is an immutable runtime configuration: id, model, instructions, static context, tools, hooks, observers, memory, and defaults. It should be safe to import from routes, jobs, tests, and Studio when its tools do not capture the current request.
+
+When tools or context need the current user, tenant, request, transaction, or permission state, create a scoped agent in a factory or runner. Do not hide request state in module-level variables.
+
+## Stable Agent Module
+
+Use a shared built agent when every registered tool is context-free, read-only, or already enforces its own context through injected services.
+
+```ts
+// src/ai/support-agent.ts
+import { AgentBuilder } from "@anvia/core";
+import { OpenAIClient } from "@anvia/openai";
+import { supportTools } from "./support-tools";
+
+const client = new OpenAIClient({ apiKey: process.env.OPENAI_API_KEY });
+const model = client.completionModel("gpt-5.5");
+
+export const supportAgent = new AgentBuilder("support", model)
+ .name("Support Agent")
+ .description("Answers support questions and uses support tools.")
+ .instructions(
+ "Answer support questions clearly. Ask for missing details before guessing.",
+ )
+ .tools(supportTools)
+ .defaultMaxTurns(3)
+ .build();
+```
+
+The stable agent id is part of the runtime contract. It appears in traces, Studio, sessions, nested-agent tools, and workflows that reference the agent.
+
+## Request Boundary
+
+Keep user input, auth state, tenant data, message history, trace metadata, route-specific limits, and application error mapping at the call site or runner.
+
+```ts
+import { Message } from "@anvia/core";
+import { supportAgent } from "./support-agent";
+
+export async function runSupportTurn(input: SupportTurnInput) {
+ const history = await input.conversations.loadMessages(input.conversationId);
+
+ const response = await supportAgent
+ .prompt([...history, Message.user(input.message)])
+ .withTrace({
+ name: "support-chat",
+ userId: input.user.id,
+ metadata: {
+ tenantId: input.user.tenantId,
+ conversationId: input.conversationId,
+ },
+ })
+ .maxTurns(2)
+ .send();
+
+ await input.conversations.append(input.conversationId, response.messages);
+
+ return response.output;
+}
+```
+
+This keeps the agent reusable. A background job can call the same agent with a different trace name, a test can replace history with fixtures, and Studio can run the built agent without product-route state.
+
+## Scoped Agent Factory
+
+Use a factory when tools or context need current user data, tenant data, feature flags, service handles, or a transaction.
+
+```ts
+import type { CompletionModel } from "@anvia/core";
+import { AgentBuilder } from "@anvia/core";
+import { createSupportTools } from "./support-tools";
+
+type SupportAgentScope = {
+ userId: string;
+ tenantId: string;
+ plan: "free" | "pro" | "enterprise";
+ services: {
+ orders: OrdersService;
+ tickets: TicketsService;
+ };
+};
+
+export function createSupportAgent(model: CompletionModel, scope: SupportAgentScope) {
+ return new AgentBuilder("support", model)
+ .instructions("Answer support questions clearly. Use tools for account data.")
+ .tools(
+ createSupportTools({
+ userId: scope.userId,
+ tenantId: scope.tenantId,
+ orders: scope.services.orders,
+ tickets: scope.services.tickets,
+ }),
+ )
+ .context(`Current customer plan: ${scope.plan}`, "customer-plan")
+ .defaultMaxTurns(3)
+ .build();
+}
+```
+
+The factory should receive explicit dependencies. Avoid reading the current user, request, tenant, or transaction from global state inside the agent module.
+
+## Shared Configuration Helper
+
+If stable and scoped agents share most behavior, extract a small builder helper.
+
+```ts
+import type { AgentBuilder, CompletionModel } from "@anvia/core";
+
+function configureSupportBehavior(builder: AgentBuilder) {
+ return builder
+ .name("Support Agent")
+ .description("Answers support questions and performs safe support actions.")
+ .instructions("Answer clearly. Ask before guessing. Use tools for account data.")
+ .defaultMaxTurns(3);
+}
+
+export function createScopedSupportAgent(model: CompletionModel, scope: SupportAgentScope) {
+ return configureSupportBehavior(new AgentBuilder("support", model))
+ .tools(createSupportTools(scope))
+ .context(`Current customer plan: ${scope.plan}`, "customer-plan")
+ .build();
+}
+```
+
+Prefer factories and helpers over mutating a built agent. Build once for stable configuration, or build per request when the tool set captures request-local state.
+
+## Decision Table
+
+| Put it here | When |
+| --- | --- |
+| `AgentBuilder` | stable identity, instructions, static context, context-free tools, default limits, observers |
+| scoped agent factory | request-scoped tools, tenant-aware context, feature-flagged tools, transaction-bound services |
+| runner | validation, auth, history, trace metadata, persistence, error mapping |
+| prompt request | current input, one-off max turns, tool concurrency, request trace metadata |
+| tool factory | user id, tenant id, service handles, permission scope |
+| route or job | transport parsing, response shape, job retry policy, caller-specific timeouts |
diff --git a/apps/docs/content/docs/best-practices/common-patterns/context-and-memory.mdx b/apps/docs/content/docs/best-practices/common-patterns/context-and-memory.mdx
new file mode 100644
index 00000000..33424edb
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/common-patterns/context-and-memory.mdx
@@ -0,0 +1,136 @@
+---
+title: Context and Memory
+description: Assemble instructions, facts, retrieval, history, and sessions deliberately.
+---
+
+Context works best when each layer has a clear job. Keep durable behavior in instructions, small stable facts in static context, request facts in the scoped agent or prompt text, prompt-dependent knowledge in retrieval, and conversation continuity in history or sessions.
+
+Do not treat context as permission. Filter records by tenant, user, and access level before they can be included in a prompt or retrieval index.
+
+## Choose the Right Layer
+
+| Layer | Use it for | Owner |
+| --- | --- | --- |
+| instructions | behavior rules that apply to every run | stable agent |
+| static context | short stable facts always relevant to the agent | stable agent |
+| request facts | current user, tenant, plan, locale, feature flags, route state | runner or scoped factory |
+| dynamic context | retrieved documents or records selected for the prompt | index plus runner filtering |
+| message history | previous turns needed to continue a conversation | application storage |
+| sessions | durable memory managed through an agent memory store | Anvia memory plus app-chosen store |
+| tools | data that should be fetched or changed only when needed | model decision, app execution |
+
+## Instructions vs Facts
+
+Put behavior in instructions. Put facts in context or loaded prompt data.
+
+```ts
+const agent = new AgentBuilder("billing", model)
+ .instructions(`
+Answer billing questions clearly.
+Use billing tools for account-specific data.
+Do not guess invoice status.
+ `)
+ .context("Invoices are generated on the first day of each month.", "invoice-cycle")
+ .build();
+```
+
+Avoid hiding changing account facts in instructions. If a value depends on the current request, load it in the runner and attach it to the scoped agent or prompt.
+
+```ts
+const agent = new AgentBuilder("billing", model)
+ .instructions("Answer billing questions clearly.")
+ .context(`Current account plan: ${account.plan}`, "account-plan")
+ .context(`Billing country: ${account.country}`, "billing-country")
+ .build();
+```
+
+## Request Context
+
+Use request context for small facts the application already knows are relevant.
+
+```ts
+const prompt = [
+ ...history,
+ Message.user(`
+Current route: support chat
+Current plan: ${account.plan}
+Open ticket count: ${openTickets.length}
+
+User message:
+${message}
+ `),
+];
+
+const response = await agent.prompt(prompt).send();
+```
+
+Keep request context compact. If the agent only needs the current plan, do not paste the whole account record. If the model may need additional fields conditionally, expose a tool instead.
+
+## Dynamic Context
+
+Use dynamic context when relevant facts depend on the prompt.
+
+```ts
+const agent = new AgentBuilder("support", model)
+ .instructions("Use retrieved support context when it is relevant.")
+ .dynamicContext(supportKnowledgeIndex, {
+ topK: 5,
+ threshold: 0.72,
+ format: (result) => ({
+ id: result.id,
+ text: `${result.document.title}\n${result.document.body}`,
+ }),
+ })
+ .build();
+```
+
+Dynamic context should be tenant-safe before search. Use separate indexes, metadata filters, or a prefiltered index handle so records from another tenant cannot be retrieved.
+
+## History and Sessions
+
+Use explicit `Message[]` history when your application already owns conversation storage.
+
+```ts
+import { Message } from "@anvia/core";
+
+const history = await conversations.loadMessages(conversationId);
+
+const response = await agent
+ .prompt([...history, Message.user(message)])
+ .send();
+
+await conversations.append(conversationId, response.messages);
+```
+
+Use `agent.session(...)` when the agent has a memory store configured and you want Anvia to manage durable conversation state for that session.
+
+```ts
+const response = await agent
+ .session(sessionId)
+ .prompt("What did we decide last time?")
+ .send();
+```
+
+Use one approach per workflow unless there is a clear reason to combine them. Mixing explicit history and sessions casually makes prompts harder to inspect.
+
+## Context Assembly Checklist
+
+| Question | Default |
+| --- | --- |
+| Is it a rule for every run? | instruction |
+| Is it a short stable fact? | static `.context(...)` |
+| Is it current user or tenant data? | runner or scoped agent context |
+| Is it large or frequently changing knowledge? | retrieval |
+| Is it previous conversation state? | explicit history or session memory |
+| Does access need to be checked at call time? | tool or service |
+| Could it leak another tenant's data? | filter before context assembly |
+
+## Practical Rule
+
+Context should explain the current run, not become a data dump. If a fact is needed every time, make it stable. If a fact changes per request, load it in the runner. If a fact is large, search for it. If a fact requires permission, put it behind a tool or prefilter it before retrieval.
+
+## Related Patterns
+
+- Use [Support Agent](/docs/best-practices/real-cases/support-agent) for account-aware chat with history and retrieval.
+- Use [Research Agent](/docs/best-practices/real-cases/research-agent) for retrieval-heavy workflows with many read-only tools.
+- Use [Dynamic Tool Catalogs](/docs/best-practices/tool-patterns/dynamic-tool-catalogs) when searchable tools are a better fit than broad context.
diff --git a/apps/docs/content/docs/best-practices/common-patterns/harness-blueprint.mdx b/apps/docs/content/docs/best-practices/common-patterns/harness-blueprint.mdx
new file mode 100644
index 00000000..5fe324d3
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/common-patterns/harness-blueprint.mdx
@@ -0,0 +1,120 @@
+---
+title: Harness Blueprint
+description: The standard production shape around an Anvia agent run.
+---
+
+An agent harness is the application-owned code around the Anvia runtime. It accepts a product request, prepares the agent run, controls access to application behavior, records what happened, and returns a product response.
+
+The harness is not a new framework layer. It is ordinary TypeScript that makes the boundary explicit: Anvia runs the prompt, tools, hooks, retrieval, memory, streaming, and observers; your application owns auth, data, permissions, transactions, storage, retries, deployment, and side effects.
+
+## Ownership Map
+
+| Layer | Owns |
+| --- | --- |
+| route, server action, job, or queue worker | transport parsing, auth entrypoint, response shape, job retry policy |
+| harness runner | one product workflow, request validation, scoped dependencies, trace metadata, error mapping |
+| stable agent factory | agent id, instructions, model behavior, static defaults, reusable observers |
+| request-scoped tools | product service calls, permissions, tenant filtering, side-effect safety |
+| context assembly | static facts, request facts, retrieval results, history, session id |
+| storage and audit | conversation history, event stream, memory store, idempotency records, approval decisions |
+| Anvia runtime | model-tool loop, schemas, hooks, turn limits, tool calls, streaming events, observer events |
+| model provider | completion behavior and provider usage accounting |
+
+Keep the ownership split boring. If a decision affects product correctness or security, it belongs in app code or a tool. If it affects how the model is prompted, constrained, or observed, configure it on the agent or prompt request.
+
+## Recommended Modules
+
+```txt
+src/
+ ai/
+ model.ts # provider clients and reusable models
+ support-agent.ts # stable agent factory or shared built agent
+ support-tools.ts # request-scoped tool factories
+ support-runner.ts # harness runner for one product workflow
+ services/
+ orders.ts # product services and permission-aware data access
+ tickets.ts
+ storage/
+ conversations.ts # history, sessions, events, and audit records
+ routes/
+ support.ts # thin transport boundary
+```
+
+This layout is a convention, not a requirement. The important part is the direction of dependencies: routes call runners, runners create scoped tools and agents, tools call services, and services own product data.
+
+## Request Lifecycle
+
+| Step | Harness responsibility | Anvia responsibility |
+| --- | --- | --- |
+| validate input | parse payload, reject missing fields, normalize product ids | none |
+| resolve app context | authenticate, load user, tenant, feature flags, services, transaction | none |
+| load state | read history, session state, idempotency record, relevant product records | read memory only if configured |
+| create runtime | build or select agent, create request-scoped tools, add context | hold agent configuration |
+| run prompt | attach trace metadata, set turn limit, call `.send()` or `.stream()` | run model-tool loop |
+| handle effects | tools enforce permissions, services run transactions and audit writes | call tools and return tool results to model |
+| persist result | append messages, event logs, summaries, metrics, user-visible output | return response messages and usage |
+| map response | return HTTP body, job result, UI stream, or domain object | none |
+
+## Minimal Harness
+
+```ts
+import { AgentBuilder, Message } from "@anvia/core";
+import { model } from "./model";
+import { createSupportTools } from "./support-tools";
+
+export async function runSupportHarness(input: SupportHarnessInput) {
+ const user = await input.auth.requireUser();
+ const history = await input.conversations.loadMessages(input.conversationId);
+
+ const agent = new AgentBuilder("support", model)
+ .name("Support Agent")
+ .instructions("Answer support questions clearly. Use tools for account data.")
+ .tools(
+ createSupportTools({
+ userId: user.id,
+ tenantId: user.tenantId,
+ orders: input.services.orders,
+ tickets: input.services.tickets,
+ }),
+ )
+ .context(`Current customer plan: ${user.plan}`, "customer-plan")
+ .defaultMaxTurns(3)
+ .build();
+
+ const response = await agent
+ .prompt([...history, Message.user(input.message)])
+ .withTrace({
+ name: "support-chat",
+ userId: user.id,
+ metadata: {
+ tenantId: user.tenantId,
+ conversationId: input.conversationId,
+ },
+ })
+ .send();
+
+ await input.conversations.append(input.conversationId, response.messages);
+
+ return {
+ output: response.output,
+ usage: response.usage,
+ };
+}
+```
+
+The runner can be called from a route, background job, CLI script, test, or Studio setup. Keep transport details outside it unless the workflow itself is transport-specific.
+
+## Harness Checklist
+
+| Concern | Production default |
+| --- | --- |
+| identity | stable agent id and trace name |
+| input | validate before constructing the prompt |
+| permissions | enforce inside tools and services, not in prompt text |
+| context | attach only scoped, relevant facts |
+| side effects | use idempotency keys or transactions in service code |
+| persistence | save `response.messages` and any app-owned audit records |
+| observability | use stable traces and safe metadata |
+| failures | map known errors to product responses at the runner boundary |
+
+Start with this shape before adding more agents, nested agents, or pipelines. Most harness problems are easier to fix when the single-agent boundary is explicit.
diff --git a/apps/docs/content/docs/best-practices/common-patterns/meta.json b/apps/docs/content/docs/best-practices/common-patterns/meta.json
new file mode 100644
index 00000000..d03b27cb
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/common-patterns/meta.json
@@ -0,0 +1,15 @@
+{
+ "title": "Common Patterns",
+ "defaultOpen": true,
+ "collapsible": true,
+ "pages": [
+ "harness-blueprint",
+ "agent-structure",
+ "request-runners",
+ "tools-and-services",
+ "context-and-memory",
+ "pipeline",
+ "production-guardrails",
+ "testing-and-observability"
+ ]
+}
diff --git a/apps/docs/content/docs/best-practices/common-patterns/pipeline.mdx b/apps/docs/content/docs/best-practices/common-patterns/pipeline.mdx
new file mode 100644
index 00000000..4f8b4951
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/common-patterns/pipeline.mdx
@@ -0,0 +1,104 @@
+---
+title: Pipeline
+description: Choose one agent, a runner, nested agents, or a pipeline based on the job shape.
+---
+
+Start with one runner around one agent. Add orchestration only when the job has a real boundary: a deterministic step, a specialist capability, a parallel branch, a typed extraction stage, batching, or a separately testable process.
+
+Many production workflows do not need a pipeline. They need a clear harness runner.
+
+## One Agent in a Runner
+
+Use one agent when one promptable runtime can own the task and a small set of tools is enough.
+
+```ts
+const agent = new AgentBuilder("support", model)
+ .instructions("Answer support questions and use tools when account data is needed.")
+ .tools(createSupportTools(scope))
+ .defaultMaxTurns(3)
+ .build();
+
+const response = await agent.prompt(message).send();
+```
+
+This is the simplest shape to trace, test, and run in Studio. Prefer it until the workflow has a boundary that should be named and tested separately.
+
+## Runner Before Pipeline
+
+Use a runner when the workflow is mostly application wiring: auth, input validation, history, scoped tools, context, trace metadata, persistence, and error mapping.
+
+```ts
+export async function runSupportTurn(input: SupportRunnerInput) {
+ const user = await input.auth.requireUser();
+ const history = await input.conversations.loadMessages(input.conversationId);
+ const agent = createSupportAgent(model, { user, services: input.services });
+
+ const response = await agent
+ .prompt([...history, Message.user(input.message)])
+ .withTrace({ name: "support-chat", userId: user.id })
+ .send();
+
+ await input.conversations.append(input.conversationId, response.messages);
+ return response.output;
+}
+```
+
+Do not add a pipeline just to hide application setup. If the steps are not reusable or independently testable, the runner is the right abstraction.
+
+## Agent as a Tool
+
+Use an agent tool when a subdomain has stable instructions and should be callable by another agent.
+
+```ts
+const refundsAgent = new AgentBuilder("refunds", model)
+ .description("Answers refund-policy questions.")
+ .instructions("Answer only refund-related questions.")
+ .build();
+
+const triageAgent = new AgentBuilder("triage", model)
+ .instructions("Route refund questions to the refund specialist.")
+ .tool(
+ refundsAgent.asTool({
+ name: "ask_refunds_agent",
+ maxTurns: 2,
+ }),
+ )
+ .defaultMaxTurns(3)
+ .build();
+```
+
+Keep nested agents narrow. A broad agent calling another broad agent usually makes traces harder to understand and quality harder to evaluate.
+
+## Pipeline
+
+Use a pipeline when the job has explicit typed stages that should be tested, reused, or observed independently.
+
+```ts
+const pipeline = new PipelineBuilder()
+ .step((input) => input.trim())
+ .prompt(triageAgent)
+ .extract(ticketExtractor)
+ .step((ticket) => ({
+ ...ticket,
+ needsEscalation: ticket.priority === "high",
+ }))
+ .build();
+```
+
+Pipelines are a good fit for preprocessing, enrichment, extraction, branching, batching, and post-processing. They are a poor fit for hiding route logic or permission policy.
+
+## Decision Table
+
+| Use | When |
+| --- | --- |
+| one agent | one conversational runtime can own the task |
+| runner | the complexity is app context, persistence, tracing, or error mapping |
+| agent tool | another agent owns a narrow specialist boundary |
+| pipeline | the job has explicit typed stages or deterministic steps |
+| direct service call | the step does not need a model |
+| extractor | model output must match a schema for downstream code |
+| parallel branches | independent model or service work can run at the same time |
+
+## Practical Rule
+
+Reach for the smallest named boundary that matches the job. A runner names a product workflow. An agent names model behavior. A tool names product capability. A pipeline names typed process stages.
diff --git a/apps/docs/content/docs/best-practices/common-patterns/production-guardrails.mdx b/apps/docs/content/docs/best-practices/common-patterns/production-guardrails.mdx
new file mode 100644
index 00000000..0d373442
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/common-patterns/production-guardrails.mdx
@@ -0,0 +1,168 @@
+---
+title: Production Guardrails
+description: Put limits, approvals, retries, timeouts, and idempotency at the boundary they protect.
+---
+
+Production guardrails should live at the boundary they protect. Agents can own default runtime limits and hooks. Tools should own side-effect safety. The application should own retries, timeouts, idempotency records, persistence, audit logs, and response shape.
+
+Do not try to solve product safety with prompt text alone. Prompt instructions help the model choose behavior, but product code must enforce the boundary.
+
+## Turn Limits
+
+Keep tool-call loops bounded. Set a conservative default on the agent and override it per request only when the workflow needs more room.
+
+```ts
+const agent = new AgentBuilder("support", model)
+ .instructions("Use tools only when account data is required.")
+ .tools(supportTools)
+ .defaultMaxTurns(3)
+ .build();
+
+const response = await agent.prompt(message).maxTurns(2).send();
+```
+
+Low limits make failures faster and traces easier to inspect. If a workflow often needs a high turn limit, check whether it should be split into deterministic steps, a pipeline, or a narrower tool.
+
+## Permission Checks
+
+Use tool or service code for normal permission checks. The model should never be the authority for whether a user can read data or perform a side effect.
+
+```ts
+async execute({ orderId }) {
+ await orders.requireAccess({
+ userId: scope.userId,
+ tenantId: scope.tenantId,
+ orderId,
+ });
+
+ return orders.find(orderId);
+}
+```
+
+Permission failures can either return a typed state when the model can continue, or throw when the product request should fail.
+
+## Hooks and Approvals
+
+Use hooks when the run should decide whether to run, skip, or cancel a tool call. Use application code for reviewer identity, storage, notification, timeout, and audit policy.
+
+```ts
+import { createHook } from "@anvia/core";
+
+const approvalHook = createHook({
+ async onToolCall({ toolName, tool }) {
+ if (toolName !== "issue_refund") {
+ return tool.run();
+ }
+
+ const approved = await approvals.waitForDecision({
+ toolName,
+ reason: "Refunds require reviewer approval.",
+ });
+
+ return approved ? tool.run() : tool.cancel("Refund was not approved.");
+ },
+});
+
+const agent = new AgentBuilder("support", model)
+ .instructions("Use refund tools only when policy allows it.")
+ .tools(refundTools)
+ .hook(approvalHook)
+ .build();
+```
+
+Studio is useful for local approval iteration. Production approval storage, notifications, reviewer identity, and audit logs stay in your application.
+
+## Timeouts and Cancellation
+
+Use hooks to cancel unsafe prompt runs. Use app-level timeout wrappers when the caller needs bounded latency.
+
+```ts
+import { createHook } from "@anvia/core";
+
+const policyHook = createHook({
+ async onCompletionCall({ prompt, run }) {
+ if (containsSensitiveExportRequest(prompt)) {
+ return run.cancel("Sensitive exports are not allowed in this workflow.");
+ }
+
+ return run.continue();
+ },
+});
+```
+
+```ts
+const result = await withTimeout(runSupportTurn(input), 30_000);
+```
+
+Use cancellation for policy decisions and user-denied workflows. Use timeouts for caller latency. Use retries for transient infrastructure failures only when the operation is safe to retry.
+
+## Idempotent Tools
+
+For side effects, pass an application-generated operation id into the service layer and let the service deduplicate.
+
+```ts
+async execute({ orderId, amount }) {
+ return billing.issueRefund({
+ userId: scope.userId,
+ tenantId: scope.tenantId,
+ orderId,
+ amount,
+ operationId: `refund:${scope.tenantId}:${orderId}:${amount}`,
+ });
+}
+```
+
+The model should not invent the idempotency boundary. Your application should.
+
+## Retries
+
+Retry at the runner or job boundary, not inside individual tools by default.
+
+```ts
+export async function runSupportJob(job: SupportJob) {
+ return retryTransient(
+ () => runSupportTurn(job.input),
+ {
+ attempts: 2,
+ retryIf: (error) => isProviderTimeout(error) || isTemporaryStorageError(error),
+ },
+ );
+}
+```
+
+Do not retry non-idempotent write tools unless the service method has an idempotency key, transaction boundary, or durable operation record.
+
+## Audit Records
+
+Audit product decisions in application storage. Traces help debug agent behavior, but they should not be the only record of a sensitive product operation.
+
+```ts
+await audit.write({
+ actorId: scope.userId,
+ tenantId: scope.tenantId,
+ action: "refund.issued",
+ targetId: orderId,
+ operationId,
+});
+```
+
+Use traces for runtime inspection. Use audit records for product accountability.
+
+## Decision Table
+
+| Risk | Guardrail |
+| --- | --- |
+| endless tool loop | `.defaultMaxTurns(...)` and request `.maxTurns(...)` |
+| restricted read | permission check in tool or service code |
+| restricted write | permission check, hook, approval runtime, audit record |
+| human approval required | application-owned approval runtime called from a hook |
+| caller needs bounded latency | app-level timeout around the runner |
+| duplicate side effect | idempotency key or transaction in the service layer |
+| provider or transport failure | retry at the application boundary when safe |
+| sensitive operation | product audit record plus trace metadata |
+
+## Related Patterns
+
+- Use [Side Effect Tools](/docs/best-practices/tool-patterns/side-effect-tools) for writes, approvals, idempotency, and audit records.
+- Use [Backoffice Agent](/docs/best-practices/real-cases/backoffice-agent) for admin workflows with approval-heavy operations.
+- Use [Production Readiness Checklist](/docs/best-practices/operations/production-readiness-checklist) before deploying a harness.
diff --git a/apps/docs/content/docs/best-practices/common-patterns/request-runners.mdx b/apps/docs/content/docs/best-practices/common-patterns/request-runners.mdx
new file mode 100644
index 00000000..a8fe2f06
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/common-patterns/request-runners.mdx
@@ -0,0 +1,184 @@
+---
+title: Request Runners
+description: Wrap one product request in a testable agent harness function.
+---
+
+A request runner is the function your route, job, queue worker, or test calls. It owns the product workflow around one agent run. Keep it small enough to test directly and explicit enough that auth, context, tools, persistence, and error mapping are visible.
+
+## Runner Responsibilities
+
+| Responsibility | Why it belongs in the runner |
+| --- | --- |
+| validate input | reject bad product requests before model calls |
+| resolve app context | auth, tenant, feature flags, service handles, and transactions are app-owned |
+| load state | conversation history, memory ids, idempotency records, and product records are storage-owned |
+| create scoped runtime | request-scoped tools and context should not leak into globals |
+| call the agent | the runner controls trace metadata, limits, and streaming vs final response |
+| persist output | `response.messages`, events, audit records, and summaries are product state |
+| map failures | routes and jobs need predictable product errors |
+
+## Standard Runner Shape
+
+```ts
+import { Message, PromptCancelledError } from "@anvia/core";
+import { model } from "./model";
+import { createSupportAgent } from "./support-agent";
+
+type SupportRunnerInput = {
+ conversationId: string;
+ message: string;
+ auth: AuthService;
+ conversations: ConversationStore;
+ services: {
+ orders: OrdersService;
+ tickets: TicketsService;
+ };
+};
+
+export async function runSupportTurn(input: SupportRunnerInput) {
+ const message = input.message.trim();
+
+ if (message.length === 0) {
+ return { ok: false as const, error: "message_required" };
+ }
+
+ const user = await input.auth.requireUser();
+ const history = await input.conversations.loadMessages(input.conversationId);
+
+ const agent = createSupportAgent(model, {
+ userId: user.id,
+ tenantId: user.tenantId,
+ plan: user.plan,
+ services: input.services,
+ });
+
+ try {
+ const response = await agent
+ .prompt([...history, Message.user(message)])
+ .withTrace({
+ name: "support-chat",
+ userId: user.id,
+ metadata: {
+ tenantId: user.tenantId,
+ conversationId: input.conversationId,
+ },
+ })
+ .send();
+
+ await input.conversations.append(input.conversationId, response.messages);
+
+ return {
+ ok: true as const,
+ output: response.output,
+ usage: response.usage,
+ };
+ } catch (error) {
+ if (error instanceof PromptCancelledError) {
+ return { ok: false as const, error: "cancelled" };
+ }
+
+ throw error;
+ }
+}
+```
+
+The route can stay thin:
+
+```ts
+export async function POST(request: Request) {
+ const body = await request.json();
+
+ const result = await runSupportTurn({
+ conversationId: body.conversationId,
+ message: body.message,
+ auth,
+ conversations,
+ services: { orders, tickets },
+ });
+
+ if (!result.ok) {
+ return Response.json({ error: result.error }, { status: 400 });
+ }
+
+ return Response.json({ output: result.output });
+}
+```
+
+## Load Context Before the Prompt
+
+Do not ask the model to discover product context that the application can resolve deterministically. Load the known values first and attach only the facts the agent needs.
+
+```ts
+const account = await accounts.findForUser(user.id);
+const openTickets = await tickets.listOpen({ userId: user.id, limit: 5 });
+
+const agent = createSupportAgent(model, {
+ userId: user.id,
+ tenantId: user.tenantId,
+ plan: account.plan,
+ services: input.services,
+});
+
+const prompt = [
+ ...history,
+ Message.user(`
+Current account plan: ${account.plan}
+Open ticket count: ${openTickets.length}
+
+User message:
+${message}
+ `),
+];
+
+const response = await agent.prompt(prompt).send();
+```
+
+Use tools when the model needs to decide whether to fetch or change state. Use preloaded context when the application already knows the fact is required.
+
+## Timeouts and Retries
+
+Anvia prompt requests do not replace product-level timeout or retry policy. Put bounded-latency behavior around the runner.
+
+```ts
+export async function withTimeout(promise: Promise, timeoutMs: number): Promise {
+ let timeout: ReturnType | undefined;
+
+ const timeoutPromise = new Promise((_, reject) => {
+ timeout = setTimeout(() => {
+ reject(new Error("support_agent_timeout"));
+ }, timeoutMs);
+ });
+
+ try {
+ return await Promise.race([promise, timeoutPromise]);
+ } finally {
+ if (timeout !== undefined) {
+ clearTimeout(timeout);
+ }
+ }
+}
+
+const result = await withTimeout(runSupportTurn(input), 30_000);
+```
+
+Retry only when the whole operation is safe to retry. If tools can create side effects, the service layer should use idempotency keys or transactions before the runner retries the prompt.
+
+## Testing Boundary
+
+A runner is easy to test because every product dependency is passed in.
+
+```ts
+it("rejects empty messages before calling the agent", async () => {
+ const result = await runSupportTurn({
+ conversationId: "conv_123",
+ message: " ",
+ auth: fakeAuth(),
+ conversations: fakeConversations(),
+ services: fakeServices(),
+ });
+
+ expect(result).toEqual({ ok: false, error: "message_required" });
+});
+```
+
+Test validation, auth failures, storage calls, known cancellations, and persistence without a provider call. Use provider-backed tests only for the model behavior the workflow depends on.
diff --git a/apps/docs/content/docs/best-practices/common-patterns/testing-and-observability.mdx b/apps/docs/content/docs/best-practices/common-patterns/testing-and-observability.mdx
new file mode 100644
index 00000000..af5f4c8f
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/common-patterns/testing-and-observability.mdx
@@ -0,0 +1,160 @@
+---
+title: Testing and Observability
+description: Test deterministic harness boundaries before inspecting model behavior.
+---
+
+Most correctness should be tested before a provider call is made. Tools, service wrappers, retrieval filters, pipeline steps, prompt wrappers, runners, and error mapping can all be checked with fakes.
+
+Use provider-backed tests, Studio, traces, and evals after the application boundaries are covered.
+
+## Test Tools Directly
+
+Tools have schemas and executable handlers. Call them directly or through a `ToolSet` to verify validation, permissions, expected states, and output shape.
+
+```ts
+const tools = createSupportToolSet({
+ userId: "user_123",
+ tenantId: "tenant_123",
+ orders: fakeOrders,
+ tickets: fakeTickets,
+});
+
+const result = await tools.call(
+ "lookup_order",
+ JSON.stringify({ orderId: "A-100" }),
+);
+
+expect(JSON.parse(result)).toEqual({
+ status: "found",
+ orderId: "A-100",
+ fulfillmentStatus: "shipped",
+});
+```
+
+This keeps product policy testable without asking a model to choose the right branch.
+
+## Test Runners With Fakes
+
+Runner tests should verify application behavior: validation, auth, history loading, scoped tool creation, trace metadata, persistence, and known errors.
+
+```ts
+it("persists new messages after a successful support turn", async () => {
+ const conversations = fakeConversations({
+ history: [Message.user("Earlier question")],
+ });
+
+ const result = await runSupportTurn({
+ conversationId: "conv_123",
+ message: "Where is order A-100?",
+ auth: fakeAuth({ userId: "user_123", tenantId: "tenant_123" }),
+ conversations,
+ services: fakeServices(),
+ });
+
+ expect(result.ok).toBe(true);
+ expect(conversations.append).toHaveBeenCalledWith(
+ "conv_123",
+ expect.any(Array),
+ );
+});
+```
+
+If the runner currently creates the real agent internally, extract a factory dependency for tests or keep provider-backed tests narrow. Do not make every route test depend on a live model.
+
+## Test Retrieval Filters
+
+Retrieval bugs are often permission bugs. Test the filter or index handle before testing answer quality.
+
+```ts
+const results = await tenantKnowledge.search("refund policy", {
+ tenantId: "tenant_123",
+ topK: 5,
+});
+
+expect(results.every((result) => result.metadata.tenantId === "tenant_123")).toBe(true);
+```
+
+Then use Studio, traces, or evals to inspect whether the retrieved context produces the answer you expect.
+
+## Observe Runs
+
+Attach observers when you need traces, usage records, external reporting, or debugging evidence.
+
+```ts
+const agent = new AgentBuilder("support", model)
+ .instructions("Answer support questions clearly.")
+ .observe(observer)
+ .build();
+```
+
+Use stable trace names such as `support-chat`, `ticket-summary`, or `retrieval-answer`. Include application identifiers in trace metadata when they are safe to store.
+
+```ts
+const response = await agent
+ .prompt(message)
+ .withTrace({
+ name: "support-chat",
+ userId,
+ metadata: {
+ tenantId,
+ conversationId,
+ channel: "web",
+ },
+ })
+ .send();
+```
+
+Trace metadata should help connect an agent run to application state without leaking secrets or large records.
+
+## Use Studio During Iteration
+
+Use Studio to inspect messages, tool calls, approvals, sessions, traces, and quick prompts while the workflow is still changing.
+
+```ts
+import { Studio } from "@anvia/studio";
+import { supportAgent } from "./ai/support-agent";
+
+new Studio([supportAgent]).start({ port: 3000 });
+```
+
+For request-scoped agents, create a safe development scope with fake or sandbox services and register that built agent in Studio. Do not wire Studio directly to production credentials just to test prompt behavior.
+
+## Use Evals for Repeatable Behavior
+
+Use evals when the workflow has known prompts, regression cases, or output expectations that should be checked repeatedly.
+
+Keep eval targets narrow:
+
+| Eval target | Good use |
+| --- | --- |
+| tool output | permission and state contracts |
+| runner output | product response shape and known error mapping |
+| agent output | answer quality for stable prompts |
+| extractor output | schema adherence and downstream contract |
+| pipeline output | stage composition and typed workflow behavior |
+
+Evals should complement unit tests, not replace them. Unit tests prove app-owned boundaries. Evals watch model-dependent behavior.
+
+## Harness Test Matrix
+
+| Area | Test without provider | Inspect with provider |
+| --- | --- | --- |
+| input validation | empty, malformed, unsupported payloads | not needed |
+| auth and permissions | fake users, tenants, denied reads and writes | trace denied tool calls |
+| tools | direct `ToolSet.call(...)` with fake services | observe model tool choice |
+| context assembly | snapshot prompt messages or scoped facts | inspect retrieved documents in traces |
+| history and persistence | fake conversation store calls | verify multi-turn behavior |
+| guardrails | hook cancellation, approval decisions, idempotency | inspect approval and cancellation traces |
+| final output | runner response shape | eval answer quality |
+
+## Practical Rule
+
+Test product policy with fakes and direct calls. Use provider tests, Studio, traces, and evals to inspect model behavior after the application-owned boundaries are already covered.
+
+## Related Patterns
+
+- Use [Tool Validation and Contracts](/docs/best-practices/tool-patterns/tool-validation-and-contracts) for schema and direct-call tests.
+- Use [MCP Tool Inspection](/docs/best-practices/mcp-patterns/mcp-tool-inspection) for server capability checks.
+- Use [Eval Strategy](/docs/best-practices/quality-observability/eval-strategy) for repeatable model-dependent checks.
+- Use [Tracing and Debugging](/docs/best-practices/quality-observability/tracing-and-debugging) to connect traces, logs, and eval outcomes.
+- Use [Production Readiness Checklist](/docs/best-practices/operations/production-readiness-checklist) for deployment validation.
diff --git a/apps/docs/content/docs/best-practices/common-patterns/tools-and-services.mdx b/apps/docs/content/docs/best-practices/common-patterns/tools-and-services.mdx
new file mode 100644
index 00000000..a9e1abff
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/common-patterns/tools-and-services.mdx
@@ -0,0 +1,180 @@
+---
+title: Tools and Services
+description: Wrap application services as typed tools without moving product policy into the model.
+---
+
+Tools are the boundary between model decisions and product behavior. Keep tool names, descriptions, schemas, and result shapes model-friendly. Keep permissions, database access, side effects, transactions, and audit policy in application code.
+
+A good tool is a narrow adapter over a product capability. It should not be a second service layer, and it should not trust the model to enforce product policy.
+
+## Tool Factory Pattern
+
+Use factories when a tool needs the current user, tenant, database transaction, feature flags, or service client.
+
+```ts
+import { createTool } from "@anvia/core";
+import { z } from "zod";
+
+type OrderToolScope = {
+ userId: string;
+ tenantId: string;
+ orders: OrdersService;
+};
+
+export function createOrderTools(scope: OrderToolScope) {
+ const lookupOrder = createTool({
+ name: "lookup_order",
+ description: "Look up one order for the current customer.",
+ input: z.object({
+ orderId: z.string().min(1),
+ }),
+ output: z.object({
+ status: z.enum(["found", "not_found"]),
+ orderId: z.string(),
+ fulfillmentStatus: z.string().optional(),
+ }),
+ async execute({ orderId }) {
+ await scope.orders.requireAccess({
+ userId: scope.userId,
+ tenantId: scope.tenantId,
+ orderId,
+ });
+
+ const order = await scope.orders.find(orderId);
+
+ if (!order) {
+ return { status: "not_found" as const, orderId };
+ }
+
+ return {
+ status: "found" as const,
+ orderId,
+ fulfillmentStatus: order.fulfillmentStatus,
+ };
+ },
+ });
+
+ return [lookupOrder];
+}
+```
+
+The model sees a small action. Your application still owns access checks, tenant scoping, service calls, and product states.
+
+## Compose Tool Groups
+
+Create groups by domain, then combine them in the scoped agent factory or runner.
+
+```ts
+import { ToolSet } from "@anvia/core/tool";
+
+export function createSupportToolSet(scope: SupportToolScope) {
+ return ToolSet.fromTools([
+ ...createOrderTools(scope),
+ ...createTicketTools(scope),
+ ...createPolicyTools(scope),
+ ]);
+}
+```
+
+Use `.tools([...])` for a request-scoped list. Use `.useToolSet(...)` when you need a shared mutable catalog that can be inspected or updated at runtime.
+
+```ts
+const agent = new AgentBuilder("support", model)
+ .instructions("Use tools when account-specific data is required.")
+ .useToolSet(createSupportToolSet(scope))
+ .defaultMaxTurns(3)
+ .build();
+```
+
+## Return Expected States
+
+Return structured expected states when the model can continue productively. Throw only when the workflow should fail, be retried, or be handled by the application error boundary.
+
+```ts
+async execute({ ticketId }) {
+ const ticket = await tickets.findForUser(scope.userId, ticketId);
+
+ if (!ticket) {
+ return { status: "not_found" as const, ticketId };
+ }
+
+ if (ticket.locked) {
+ return {
+ status: "blocked" as const,
+ reason: "ticket_locked",
+ ticketId,
+ };
+ }
+
+ return {
+ status: "ready" as const,
+ ticketId,
+ subject: ticket.subject,
+ priority: ticket.priority,
+ };
+}
+```
+
+Expected states make the model's next step easier to inspect. Unexpected errors should still throw so the runner can log, retry, or return a stable product error.
+
+## Keep Side Effects Behind Services
+
+For write tools, the tool validates model input and calls a product service. The service owns permissions, transactions, idempotency, and audit records.
+
+```ts
+export function createRefundTools(scope: RefundToolScope) {
+ return [
+ createTool({
+ name: "issue_refund",
+ description: "Issue an approved refund for a paid order.",
+ input: z.object({
+ orderId: z.string(),
+ amount: z.number().positive(),
+ reason: z.string().min(1),
+ }),
+ async execute({ orderId, amount, reason }) {
+ return scope.billing.issueRefund({
+ userId: scope.userId,
+ tenantId: scope.tenantId,
+ orderId,
+ amount,
+ reason,
+ operationId: `refund:${scope.tenantId}:${orderId}:${amount}`,
+ });
+ },
+ }),
+ ];
+}
+```
+
+The model can request the operation. It should not invent the permission check, transaction boundary, or idempotency key policy.
+
+## Tool Contract Checklist
+
+| Concern | Good default |
+| --- | --- |
+| name | verb-noun, stable, specific, such as `lookup_order` |
+| description | tell the model when to use it, not how the service works internally |
+| input schema | require product ids, enums, and bounded strings where possible |
+| output schema | return compact states the model can reason about |
+| permissions | enforce in service or tool code before reading or writing data |
+| side effects | route through service methods with idempotency and audit behavior |
+| errors | return expected states, throw unexpected failures |
+| tests | call tools directly or through `ToolSet.call(...)` with fake services |
+
+## Decision Table
+
+| Situation | Pattern |
+| --- | --- |
+| tool needs current user or tenant | create tools inside a scoped factory |
+| tool reads product data | filter and authorize in the service call |
+| product state is recoverable | return `not_found`, `blocked`, `ready`, or another typed state |
+| product operation failed unexpectedly | throw and let the runner boundary handle it |
+| write operation can be retried | require an idempotency key or transaction in the service layer |
+| many tools need reuse or direct tests | group them with `ToolSet` |
+
+## Related Patterns
+
+- Use [Dynamic Tool Catalogs](/docs/best-practices/tool-patterns/dynamic-tool-catalogs) when the catalog is too large to send every turn.
+- Use [Tool Validation and Contracts](/docs/best-practices/tool-patterns/tool-validation-and-contracts) when tool output drives downstream code.
+- Use [Side Effect Tools](/docs/best-practices/tool-patterns/side-effect-tools) when a tool writes data or calls an external write API.
diff --git a/apps/docs/content/docs/best-practices/index.mdx b/apps/docs/content/docs/best-practices/index.mdx
new file mode 100644
index 00000000..ffbe25b2
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/index.mdx
@@ -0,0 +1,140 @@
+---
+title: Best Practices
+description: A production pattern library for Anvia agent harnesses.
+---
+
+Use these patterns when an agent is moving from a demo into product code. The goal is to keep model behavior explicit while your application keeps ownership of users, permissions, data, side effects, storage, deployment, and audit trails.
+
+This section is organized as a pattern library. Start with the common harness shape, then jump to the real case that matches the system you are building.
+
+## Pattern Map
+
+| Need | Pattern |
+| --- | --- |
+| Understand the standard agent harness boundary | [Harness Blueprint](/docs/best-practices/common-patterns/harness-blueprint) |
+| Keep stable agent config separate from request state | [Agent Structure](/docs/best-practices/common-patterns/agent-structure) |
+| Wrap one route, job, or queue request | [Request Runners](/docs/best-practices/common-patterns/request-runners) |
+| Wrap product services as safe model-callable actions | [Tools and Services](/docs/best-practices/common-patterns/tools-and-services) |
+| Assemble instructions, facts, retrieval, history, and sessions | [Context and Memory](/docs/best-practices/common-patterns/context-and-memory) |
+| Choose one agent, nested agents, or a typed pipeline | [Pipeline](/docs/best-practices/common-patterns/pipeline) |
+| Add limits, approvals, idempotency, retries, and audit records | [Production Guardrails](/docs/best-practices/common-patterns/production-guardrails) |
+| Test deterministic boundaries and inspect model behavior | [Testing and Observability](/docs/best-practices/common-patterns/testing-and-observability) |
+
+## Tool Patterns
+
+| Real problem | Pattern |
+| --- | --- |
+| The agent has dozens or hundreds of tools | [Dynamic Tool Catalogs](/docs/best-practices/tool-patterns/dynamic-tool-catalogs) |
+| Tool arguments, outputs, and product states need contracts | [Tool Validation and Contracts](/docs/best-practices/tool-patterns/tool-validation-and-contracts) |
+| A tool writes data, sends messages, or changes external state | [Side Effect Tools](/docs/best-practices/tool-patterns/side-effect-tools) |
+
+## Long Process Pipelines
+
+| Need | Pattern |
+| --- | --- |
+| Run Anvia pipelines outside the request path with BullMQ and Redis | [Overview](/docs/best-practices/long-process-pipelines) |
+| Enqueue validated pipeline work from an API boundary | [Enqueue Pipeline Jobs](/docs/best-practices/long-process-pipelines/enqueue-pipeline-jobs) |
+| Run pipeline jobs from a separate BullMQ worker | [Run Pipeline Workers](/docs/best-practices/long-process-pipelines/run-pipeline-workers) |
+| Persist status, handle retries, and test long-running jobs | [Status, Retries, and Testing](/docs/best-practices/long-process-pipelines/status-retries-testing) |
+
+## Multi-agent Patterns
+
+| Need | Pattern |
+| --- | --- |
+| Stream coordinator output and nested specialist progress | [Overview](/docs/best-practices/multi-agent-patterns) |
+| Build specialist agents and expose them as streaming tools | [Build Streaming Specialists](/docs/best-practices/multi-agent-patterns/build-streaming-specialists) |
+| Group parent and child stream events in a UI | [Consume Nested Events](/docs/best-practices/multi-agent-patterns/consume-nested-events) |
+| Persist, replay, and test multi-agent streams | [Persistence and Testing](/docs/best-practices/multi-agent-patterns/persistence-and-testing) |
+
+## MCP Patterns
+
+| Real problem | Pattern |
+| --- | --- |
+| You need to connect and reconnect external MCP servers | [MCP Server Lifecycle](/docs/best-practices/mcp-patterns/mcp-server-lifecycle) |
+| You need to inspect, validate, filter, or wrap MCP tools | [MCP Tool Inspection](/docs/best-practices/mcp-patterns/mcp-tool-inspection) |
+| You need an agent harness that combines app tools and MCP tools | [MCP Agent Harness](/docs/best-practices/mcp-patterns/mcp-agent-harness) |
+
+## Knowledge Patterns
+
+| Real problem | Pattern |
+| --- | --- |
+| You need to build or refresh a retrieval index safely | [RAG Ingestion](/docs/best-practices/knowledge-patterns/rag-ingestion) |
+| You need to decide how retrieval enters an agent run | [RAG Agent Context](/docs/best-practices/knowledge-patterns/rag-agent-context) |
+
+## Real Cases
+
+| System | Pattern |
+| --- | --- |
+| Customer support chat with account tools, retrieval, and history | [Support Agent](/docs/best-practices/real-cases/support-agent) |
+| Backoffice workflow with writes, approvals, audit, and idempotency | [Backoffice Agent](/docs/best-practices/real-cases/backoffice-agent) |
+| Research workflow with many read-only tools and extraction | [Research Agent](/docs/best-practices/real-cases/research-agent) |
+| Coding assistant over a codebase with file, command, patch, and git boundaries | [Coding Agent](/docs/best-practices/real-cases/coding-agent) |
+
+## Quality and Observability
+
+| Need | Pattern |
+| --- | --- |
+| Build regression checks for prompts, tools, and workflows | [Eval Strategy](/docs/best-practices/quality-observability/eval-strategy) |
+| Connect traces, tool calls, retrieval evidence, logs, and eval scores | [Tracing and Debugging](/docs/best-practices/quality-observability/tracing-and-debugging) |
+
+## Operations
+
+| Need | Pattern |
+| --- | --- |
+| Check whether a harness is ready for production | [Production Readiness Checklist](/docs/best-practices/operations/production-readiness-checklist) |
+
+## Baseline Shape
+
+Most production agents follow the same boundaries:
+
+- stable provider clients, models, reusable agents, static tool catalogs, observers, and default limits are created at startup
+- request-local user, tenant, conversation, permission, feature flag, MCP availability, and caller timeout policy are resolved at the application boundary
+- tools wrap product services and enforce permissions before reading or changing data
+- context is assembled from instructions, static facts, request facts, retrieval, history, and memory
+- persistence, retries, idempotency, audit logs, and product response shapes stay in application code
+
+```ts
+import { AgentBuilder, Message } from "@anvia/core";
+import { model } from "./ai/model";
+import { createSupportTools } from "./ai/support-tools";
+
+export async function runSupportTurn(input: SupportTurnInput) {
+ const user = await input.auth.requireUser();
+ const conversation = await input.conversations.load(input.conversationId);
+
+ const agent = new AgentBuilder("support", model)
+ .instructions("Answer support questions clearly. Use tools for account data.")
+ .tools(
+ createSupportTools({
+ userId: user.id,
+ tenantId: user.tenantId,
+ orders: input.services.orders,
+ tickets: input.services.tickets,
+ }),
+ )
+ .context(`Current plan: ${user.plan}`, "current-plan")
+ .defaultMaxTurns(3)
+ .build();
+
+ const response = await agent
+ .prompt([...conversation.history, Message.user(input.message)])
+ .withTrace({
+ name: "support-chat",
+ userId: user.id,
+ metadata: {
+ tenantId: user.tenantId,
+ conversationId: input.conversationId,
+ },
+ })
+ .send();
+
+ await input.conversations.append(input.conversationId, response.messages);
+
+ return {
+ output: response.output,
+ messages: response.messages,
+ };
+}
+```
+
+The exact route, worker, queue, or UI framework can vary. The ownership boundary should not: Anvia owns the model-tool loop, and your application owns product state and side effects.
diff --git a/apps/docs/content/docs/best-practices/knowledge-patterns/meta.json b/apps/docs/content/docs/best-practices/knowledge-patterns/meta.json
new file mode 100644
index 00000000..dc379ffb
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/knowledge-patterns/meta.json
@@ -0,0 +1,6 @@
+{
+ "title": "Knowledge Patterns",
+ "defaultOpen": false,
+ "collapsible": true,
+ "pages": ["rag-ingestion", "rag-agent-context"]
+}
diff --git a/apps/docs/content/docs/best-practices/knowledge-patterns/rag-agent-context.mdx b/apps/docs/content/docs/best-practices/knowledge-patterns/rag-agent-context.mdx
new file mode 100644
index 00000000..4894d2ba
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/knowledge-patterns/rag-agent-context.mdx
@@ -0,0 +1,140 @@
+---
+title: RAG Agent Context
+description: Choose how retrieval enters an agent run and how to test the retrieved evidence.
+---
+
+RAG context is the runtime side of retrieval. The harness decides whether retrieval should happen automatically with `.dynamicContext(...)`, through a search tool the model can call, or through preloaded context from application code.
+
+Retrieval is input, not permission. Filter documents before they can enter the model request.
+
+## Scenario
+
+A support agent needs policy docs on every answer, while a research agent should decide when to search. Both use the same indexed knowledge, but retrieval enters the run differently.
+
+## When to Use It
+
+Use this pattern when:
+
+- the agent needs facts that are too large or change too often for static context
+- answers should include source-backed evidence
+- retrieved documents need formatting, thresholds, or metadata filters
+- you need evals for retrieval quality and answer quality separately
+
+## Architecture Shape
+
+| Pattern | Use when | Tradeoff |
+| --- | --- | --- |
+| `.dynamicContext(...)` | every prompt should receive relevant knowledge | automatic, but search happens every run |
+| search tool | model should decide whether and when to search | more flexible, but uses a tool turn |
+| runner-preloaded context | app already knows required facts | deterministic, but less adaptive |
+| `.dynamicTools(...)` | the retrieved object is a tool definition | solves capability selection, not knowledge |
+
+## Automatic Dynamic Context
+
+```ts
+const agent = new AgentBuilder("support", model)
+ .instructions("Use retrieved support docs when answering policy questions.")
+ .dynamicContext(supportDocsIndex, {
+ topK: 4,
+ threshold: 0.72,
+ format: (result) => ({
+ id: result.id,
+ text: [
+ `Source: ${result.document.title}`,
+ `Product: ${result.metadata?.product ?? "unknown"}`,
+ result.document.body,
+ ].join("\n"),
+ }),
+ })
+ .defaultMaxTurns(3)
+ .build();
+```
+
+Use `format(...)` to control exactly what the model sees. Include source ids or titles when answers should be traceable.
+
+## Search Tool
+
+Use a tool when retrieval should be optional or iterative.
+
+```ts
+const searchSupportDocs = supportDocsIndex.asTool({
+ name: "search_support_docs",
+ description: "Search support documentation by query.",
+ topK: 5,
+ threshold: 0.7,
+});
+
+const agent = new AgentBuilder("support-research", model)
+ .instructions("Search docs when the answer depends on current support policy.")
+ .tool(searchSupportDocs)
+ .defaultMaxTurns(4)
+ .build();
+```
+
+## Tenant and Access Filtering
+
+Filter before retrieval results become model input. Use separate indexes when strict isolation is simpler than filtering.
+
+```ts
+const tenantDocsIndex = docsStore.indexForTenant(user.tenantId);
+
+const agent = new AgentBuilder("tenant-support", model)
+ .dynamicContext(tenantDocsIndex, {
+ topK: 4,
+ threshold: 0.75,
+ })
+ .build();
+```
+
+If a shared index is used, make the index handle enforce metadata filters so another tenant's document cannot be returned.
+
+## Tuning TopK and Threshold
+
+| Symptom | Adjustment |
+| --- | --- |
+| answers miss obvious docs | lower `threshold`, raise `topK`, improve titles/chunks |
+| irrelevant docs enter prompts | raise `threshold`, lower `topK`, improve chunking |
+| prompts are too large | lower `topK` or shorten formatted text |
+| model ignores citations | include source ids in text and instruction |
+| retrieval is slow | use a production vector store and avoid request-time ingestion |
+
+## Test Queries
+
+Test retrieval separately from answer generation.
+
+```ts
+const matches = await supportDocsIndex.searchIds({
+ query: "How long does a password reset link last?",
+ topK: 3,
+ threshold: 0.72,
+});
+
+expect(matches.map((match) => match.id)).toContain("password-reset-policy");
+```
+
+Then test the full agent answer with evals.
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| source is stale | fix ingestion refresh policy |
+| answer lacks evidence | include source ids in formatted context and traces |
+| tenant leak | use tenant-specific index handles or enforced metadata filters |
+| model overuses search tool | use `.dynamicContext(...)` for always-needed knowledge |
+| retrieval hides missing data | return no context and make the model state uncertainty |
+
+## Test Checklist
+
+- Search representative queries and assert expected document ids.
+- Test threshold behavior for weak matches.
+- Test tenant and visibility filters.
+- Inspect traces or Studio knowledge view for retrieved documents.
+- Add evals that fail when required facts are missing.
+
+## Related Docs
+
+- [RAG Ingestion](/docs/best-practices/knowledge-patterns/rag-ingestion)
+- [RAG Context](/docs/guides/retrieval/rag-context)
+- [Eval Strategy](/docs/best-practices/quality-observability/eval-strategy)
+
diff --git a/apps/docs/content/docs/best-practices/knowledge-patterns/rag-ingestion.mdx b/apps/docs/content/docs/best-practices/knowledge-patterns/rag-ingestion.mdx
new file mode 100644
index 00000000..1b454072
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/knowledge-patterns/rag-ingestion.mdx
@@ -0,0 +1,138 @@
+---
+title: RAG Ingestion
+description: Build retrieval indexes deliberately before agent runs need them.
+---
+
+RAG quality starts before the agent runs. Ingestion is the application-owned workflow that loads source material, normalizes it, chooses ids and metadata, embeds it, writes it to a vector store, and decides when indexes are refreshed.
+
+Do not rebuild a retrieval index inside a hot prompt path. Build it during deploy, startup, admin ingestion, or background work.
+
+## Scenario
+
+A support agent answers product questions from Markdown docs, PDFs, release notes, and internal policy pages. The application needs a repeatable ingestion job so every agent run sees fresh, filtered, traceable context.
+
+## When to Use It
+
+Use this pattern when:
+
+- knowledge is larger than static `.context(...)`
+- documents change outside the request path
+- documents need metadata filters such as tenant, product, visibility, version, or locale
+- retrieval evidence should be debugged in traces and evals
+- multiple agents share the same knowledge source
+
+## Architecture Shape
+
+| Layer | Responsibility |
+| --- | --- |
+| loader | read files, directories, PDFs, bytes, or application records |
+| normalizer | clean text, split sections, remove boilerplate, attach source ids |
+| metadata | tenant, product, visibility, version, locale, source path, updated time |
+| embedding job | call `embedDocuments(...)` with bounded concurrency |
+| vector store | store embedded documents and expose an index |
+| refresh policy | rebuild or incrementally update at an explicit boundary |
+
+## Code Example
+
+```ts
+import { InMemoryVectorStore } from "@anvia/core/vector-store";
+import { embedDocuments } from "@anvia/core/embeddings";
+import { FileLoader, fileLoaderToDocuments } from "@anvia/core/loaders";
+import { embeddings } from "./models";
+
+const loaded = await fileLoaderToDocuments(
+ FileLoader.withGlob("content/support/**/*.md")
+ .readWithPath()
+ .ignoreErrors(),
+);
+
+const documents = loaded.map((doc) => ({
+ id: doc.id,
+ title: doc.metadata.path,
+ body: doc.text,
+ product: "support",
+ visibility: "public",
+}));
+
+const embedded = await embedDocuments(embeddings, documents, {
+ id: (doc) => doc.id,
+ content: (doc) => `${doc.title}\n${doc.body}`,
+ metadata: (doc) => ({
+ product: doc.product,
+ visibility: doc.visibility,
+ title: doc.title,
+ }),
+ concurrency: 2,
+});
+
+export const supportDocsIndex = InMemoryVectorStore.fromDocuments(embedded).index(embeddings);
+```
+
+For production storage, build the same embedded document shape and write it to the vector store your application owns.
+
+## Document Ids and Metadata
+
+Stable ids make refreshes and debugging easier.
+
+```ts
+const document = {
+ id: `support:${locale}:${slug}:${sectionId}`,
+ title,
+ body,
+ locale,
+ product,
+ visibility,
+ sourceUrl,
+ updatedAt,
+};
+```
+
+Metadata should support the filters and trace evidence you need later. Do not rely on prompt instructions to prevent a tenant or visibility leak.
+
+## Chunking Ownership
+
+Anvia loaders convert source files into documents. Your application owns semantic chunking policy.
+
+| Source | Chunk default |
+| --- | --- |
+| Markdown docs | one document per page or heading section |
+| PDFs | one document per page, then merge or split if needed |
+| tickets or records | one document per record or summary |
+| code files | one document per file or symbol-level section |
+| long policies | one document per rule group with source id |
+
+## Refresh Strategy
+
+| Strategy | Use when |
+| --- | --- |
+| deploy-time rebuild | docs change with code deploys |
+| startup build | small indexes and local development |
+| background ingestion | docs change frequently or source data is remote |
+| tenant-specific index | strict tenant isolation is required |
+| metadata-filtered shared index | source store supports reliable filtering |
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| stale answers | define refresh triggers and store `updatedAt` metadata |
+| weak retrieval | improve chunking, titles, content selector, and source text |
+| tenant leakage | separate indexes or enforce metadata filters before search |
+| slow ingestion | bound `concurrency` and move ingestion off request paths |
+| hard-to-debug context | include stable ids, source paths, titles, and versions |
+
+## Test Checklist
+
+- Test loaders against representative files and PDFs.
+- Snapshot normalized documents before embedding.
+- Verify ids are stable across repeated ingestion.
+- Verify metadata includes tenant, visibility, product, and source fields where needed.
+- Run search probes and assert expected document ids appear.
+- Add eval cases for known questions that depend on retrieved facts.
+
+## Related Docs
+
+- [Embed Documents](/docs/guides/retrieval/embed-documents)
+- [Loaders](/docs/guides/retrieval/loaders)
+- [RAG Agent Context](/docs/best-practices/knowledge-patterns/rag-agent-context)
+
diff --git a/apps/docs/content/docs/best-practices/long-process-pipelines/enqueue-pipeline-jobs.mdx b/apps/docs/content/docs/best-practices/long-process-pipelines/enqueue-pipeline-jobs.mdx
new file mode 100644
index 00000000..e716e731
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/long-process-pipelines/enqueue-pipeline-jobs.mdx
@@ -0,0 +1,115 @@
+---
+title: Enqueue Pipeline Jobs
+description: Validate API input and add stable BullMQ jobs for later pipeline execution.
+---
+
+The enqueue boundary is product code. It should authorize the caller, validate input, create a durable pending record, and then call `queue.add(...)` with a stable job id.
+
+## Job Data
+
+```ts
+import { z } from "zod";
+
+export const ticketInputSchema = z.object({
+ tenantId: z.string(),
+ ticketId: z.string(),
+ summary: z.string().min(1),
+});
+
+export type TicketJobData = z.infer;
+```
+
+Keep job data scoped and serializable. Do not enqueue request objects, database clients, provider clients, or unbounded conversation history.
+
+## Queue Setup
+
+```ts
+import { Queue, type JobsOptions } from "bullmq";
+import IORedis from "ioredis";
+import type { TicketJobData } from "./schema";
+
+export const queueName = "ticket-triage";
+
+const producerConnection = new IORedis(process.env.REDIS_URL);
+
+export const triageQueue = new Queue(queueName, {
+ connection: producerConnection,
+});
+```
+
+Use separate producer and worker connections when API and worker processes have different runtime constraints.
+
+## Job Options
+
+```ts
+export const jobOptions: JobsOptions = {
+ attempts: 3,
+ backoff: {
+ type: "exponential",
+ delay: 5_000,
+ },
+ removeOnComplete: {
+ age: 60 * 60 * 24,
+ count: 1_000,
+ },
+ removeOnFail: {
+ age: 60 * 60 * 24 * 7,
+ },
+};
+```
+
+Retries are useful for transient provider, network, and database failures. They are dangerous when the worker performs non-idempotent side effects.
+
+## Enqueue Function
+
+```ts
+import { ticketInputSchema } from "./schema";
+import { jobOptions, triageQueue } from "./queue";
+import { ticketJobs } from "./storage";
+
+export async function enqueueTicketTriage(input: unknown) {
+ const data = ticketInputSchema.parse(input);
+ const jobId = `ticket-triage:${data.tenantId}:${data.ticketId}`;
+
+ await ticketJobs.createPending({
+ jobId,
+ tenantId: data.tenantId,
+ ticketId: data.ticketId,
+ });
+
+ const job = await triageQueue.add("triage", data, {
+ ...jobOptions,
+ jobId,
+ });
+
+ return {
+ jobId: job.id,
+ status: "queued",
+ };
+}
+```
+
+The stable `jobId` prevents duplicate queue jobs for the same product operation. Keep a matching idempotency constraint in app storage because the database is the source of product truth.
+
+## API Boundary
+
+```ts
+import { enqueueTicketTriage } from "./enqueue";
+
+export async function POST(request: Request) {
+ const body = await request.json();
+ const result = await enqueueTicketTriage(body);
+
+ return Response.json(result, {
+ status: 202,
+ });
+}
+```
+
+Do authentication, tenant scoping, quotas, and validation before `queue.add(...)`. The worker should still trust only scoped job data, but the API is the first product boundary.
+
+## Related Docs
+
+- [Request Runners](/docs/best-practices/common-patterns/request-runners)
+- [Tool Validation and Contracts](/docs/best-practices/tool-patterns/tool-validation-and-contracts)
+- [Run Pipeline Workers](/docs/best-practices/long-process-pipelines/run-pipeline-workers)
diff --git a/apps/docs/content/docs/best-practices/long-process-pipelines/index.mdx b/apps/docs/content/docs/best-practices/long-process-pipelines/index.mdx
new file mode 100644
index 00000000..c5dc88e2
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/long-process-pipelines/index.mdx
@@ -0,0 +1,69 @@
+---
+title: Long Process Pipelines
+description: Run Anvia pipelines outside the request path with a durable queue.
+---
+
+Use a long process pipeline when the work should not finish inside one HTTP request. The request validates input and creates a job. A worker process runs the Anvia pipeline, persists the result, and reports progress through app-owned status records.
+
+## What is BullMQ?
+
+[BullMQ](https://docs.bullmq.io/) is a Node.js queue library built on Redis. A [`Queue`](https://docs.bullmq.io/guide/queues) adds jobs, a [`Worker`](https://docs.bullmq.io/guide/workers) processes jobs, and [`QueueEvents`](https://docs.bullmq.io/guide/events) can observe queue lifecycle events. BullMQ uses Redis [connections](https://docs.bullmq.io/guide/connections) so API producers and worker processes can run separately and scale independently.
+
+Install BullMQ in the application that owns the queue and worker:
+
+```sh
+pnpm add bullmq ioredis
+```
+
+The docs app does not need these dependencies unless it is also running a queue worker.
+
+## When to Use It
+
+Use this pattern when:
+
+- a pipeline can run longer than the caller should wait
+- work should survive API process restarts
+- jobs need retry, backoff, delayed execution, or worker concurrency
+- pipeline results should be persisted for later polling or review
+- API and worker processes scale independently
+
+## Architecture Shape
+
+| Layer | Responsibility |
+| --- | --- |
+| API route | authorize caller, validate input, create app record, enqueue job |
+| BullMQ queue | store pending jobs, retry policy, backoff, job lifecycle |
+| worker process | consume jobs, run the Anvia pipeline, update progress |
+| pipeline | own typed stages, agents, extractors, and deterministic transforms |
+| app storage | durable status, result, errors, audit metadata |
+| observability | connect queue job ids, pipeline stage events, traces, and logs |
+
+## Baseline Flow
+
+ B["Validate and authorize"]
+ B --> C["Create pending job record"]
+ C --> D["BullMQ Queue.add"]
+ D --> E["BullMQ Worker"]
+ E --> F["pipeline.run"]
+ F --> G["Persist result or error"]
+ G --> H["Status endpoint or review UI"]
+`}
+/>
+
+Keep the API response small. Return a job id and store user-facing status in your own database. BullMQ return values are useful for worker internals and queue inspection, but product status pages should read durable app records.
+
+## Pages
+
+- [Enqueue Pipeline Jobs](/docs/best-practices/long-process-pipelines/enqueue-pipeline-jobs) shows the API producer boundary.
+- [Run Pipeline Workers](/docs/best-practices/long-process-pipelines/run-pipeline-workers) shows the worker and `pipeline.run(...)` boundary.
+- [Status, Retries, and Testing](/docs/best-practices/long-process-pipelines/status-retries-testing) covers production behavior and tests.
+
+## Related Docs
+
+- [Pipeline](/docs/best-practices/common-patterns/pipeline)
+- [Request Runners](/docs/best-practices/common-patterns/request-runners)
+- [Production Guardrails](/docs/best-practices/common-patterns/production-guardrails)
+- [Testing and Observability](/docs/best-practices/common-patterns/testing-and-observability)
diff --git a/apps/docs/content/docs/best-practices/long-process-pipelines/meta.json b/apps/docs/content/docs/best-practices/long-process-pipelines/meta.json
new file mode 100644
index 00000000..95ef938d
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/long-process-pipelines/meta.json
@@ -0,0 +1,6 @@
+{
+ "title": "Long Process Pipelines",
+ "defaultOpen": false,
+ "collapsible": true,
+ "pages": ["index", "enqueue-pipeline-jobs", "run-pipeline-workers", "status-retries-testing"]
+}
diff --git a/apps/docs/content/docs/best-practices/long-process-pipelines/run-pipeline-workers.mdx b/apps/docs/content/docs/best-practices/long-process-pipelines/run-pipeline-workers.mdx
new file mode 100644
index 00000000..88f15ef4
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/long-process-pipelines/run-pipeline-workers.mdx
@@ -0,0 +1,119 @@
+---
+title: Run Pipeline Workers
+description: Consume BullMQ jobs and execute Anvia pipelines in a separate worker process.
+---
+
+The worker owns execution. It should mark jobs running, call `pipeline.run(...)`, update progress from pipeline stage events, persist the final result, and let BullMQ retry failures that should be retried.
+
+## Build the Pipeline
+
+```ts
+import { PipelineBuilder } from "@anvia/core/pipeline";
+import { ticketAgent, ticketExtractor } from "./ai";
+import type { TicketJobData } from "./schema";
+
+export const ticketPipeline = new PipelineBuilder({
+ id: "ticket-triage-pipeline",
+ name: "Ticket triage pipeline",
+})
+ .step((input) => ({
+ ...input,
+ summary: input.summary.trim(),
+ }))
+ .prompt(ticketAgent, {
+ name: "Draft triage",
+ })
+ .extract(ticketExtractor, {
+ name: "Extract ticket result",
+ })
+ .build();
+```
+
+Build provider clients, agents, extractors, and reusable pipelines at worker startup. Keep request-local user and tenant data inside the job payload.
+
+## Worker Connection
+
+```ts
+import IORedis from "ioredis";
+
+export const workerConnection = new IORedis(process.env.REDIS_URL, {
+ maxRetriesPerRequest: null,
+});
+```
+
+BullMQ workers need a Redis connection suitable for blocking commands. Keep this separate from short-lived HTTP request behavior.
+
+## Worker Handler
+
+```ts
+import type { PipelineRunEvent } from "@anvia/core/pipeline";
+import { Worker } from "bullmq";
+import { queueName } from "./queue";
+import { ticketPipeline } from "./pipeline";
+import type { TicketJobData } from "./schema";
+import { ticketJobs } from "./storage";
+import { workerConnection } from "./worker-connection";
+
+export const triageWorker = new Worker(
+ queueName,
+ async (job) => {
+ await ticketJobs.markRunning(job.id!, {
+ startedAt: new Date(),
+ attempt: job.attemptsMade + 1,
+ });
+
+ const output = await ticketPipeline.run(job.data, {
+ observer: {
+ async onEvent(event: PipelineRunEvent) {
+ await job.updateProgress({
+ type: event.type,
+ stage: event.node.label,
+ });
+ },
+ },
+ });
+
+ await ticketJobs.markCompleted(job.id!, {
+ completedAt: new Date(),
+ output,
+ });
+
+ return {
+ ticketId: job.data.ticketId,
+ status: "completed",
+ };
+ },
+ {
+ connection: workerConnection,
+ concurrency: 4,
+ },
+);
+```
+
+Use the pipeline observer for metadata-only progress. Do not put sensitive model output, retrieved documents, or tool results into queue progress if those values should only live in app storage or traces.
+
+## Worker Events
+
+```ts
+triageWorker.on("failed", async (job, error) => {
+ if (!job) return;
+
+ await ticketJobs.markFailed(job.id!, {
+ failedAt: new Date(),
+ message: error.message,
+ attempt: job.attemptsMade,
+ });
+});
+
+triageWorker.on("error", (error) => {
+ console.error("ticket triage worker error", error);
+});
+```
+
+The `failed` event is job-level state. The `error` event is worker-level runtime state. Handle both so job status and worker health are visible.
+
+## Related Docs
+
+- [Pipeline](/docs/best-practices/common-patterns/pipeline)
+- [Tracing and Debugging](/docs/best-practices/quality-observability/tracing-and-debugging)
+- [Inspect Pipelines](/docs/studio/pipelines/inspect-pipelines)
diff --git a/apps/docs/content/docs/best-practices/long-process-pipelines/status-retries-testing.mdx b/apps/docs/content/docs/best-practices/long-process-pipelines/status-retries-testing.mdx
new file mode 100644
index 00000000..84cbdcf2
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/long-process-pipelines/status-retries-testing.mdx
@@ -0,0 +1,84 @@
+---
+title: Status, Retries, and Testing
+description: Persist product status and test long-running BullMQ pipeline jobs.
+---
+
+BullMQ owns queue mechanics. Your application owns product status, permissions, idempotency, audit records, and user-facing results.
+
+## Durable Status
+
+```ts
+export type TicketJobRecord = {
+ jobId: string;
+ tenantId: string;
+ ticketId: string;
+ status: "queued" | "running" | "completed" | "failed";
+ output?: unknown;
+ errorMessage?: string;
+ updatedAt: Date;
+};
+```
+
+Store status in app storage before enqueueing. Update the same record from the worker. Use BullMQ job state for operations and debugging, not as the only user-facing product record.
+
+## Queue Events
+
+```ts
+import { QueueEvents } from "bullmq";
+import { queueName } from "./queue";
+import { workerConnection } from "./worker-connection";
+
+export const triageQueueEvents = new QueueEvents(queueName, {
+ connection: workerConnection,
+});
+
+triageQueueEvents.on("completed", ({ jobId }) => {
+ console.info("ticket triage job completed", { jobId });
+});
+
+triageQueueEvents.on("failed", ({ jobId, failedReason }) => {
+ console.warn("ticket triage job failed", { jobId, failedReason });
+});
+```
+
+Queue events are useful for operational logs and metrics. Product state should still be written by the API and worker paths that understand tenant, ticket, and result data.
+
+## Retry Policy
+
+| Case | Suggested behavior |
+| --- | --- |
+| provider timeout | retry with backoff |
+| temporary Redis or database error | retry with backoff |
+| invalid job data | fail without re-enqueueing |
+| permission or tenant mismatch | fail and alert |
+| non-idempotent side effect | protect with operation id before retrying |
+
+If the pipeline writes data, sends messages, or calls external write APIs, use service-layer idempotency keys. A retried worker must not create duplicate side effects.
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| duplicate jobs for one ticket | stable `jobId` plus app-level idempotency |
+| Redis outage blocks API too long | tune producer connection retry behavior and return a retryable API error |
+| worker retries repeat side effects | keep side effects idempotent and persist operation ids |
+| user cannot see progress after completion cleanup | store status and result in app storage |
+| worker process hides emitted errors | attach a worker `error` listener |
+| pipeline stage is hard to debug | pass a pipeline observer and include job id in logs and traces |
+
+## Test Checklist
+
+- Test input validation before enqueueing.
+- Test stable job ids for duplicate enqueue attempts.
+- Test pending records are created before `queue.add(...)`.
+- Test worker success writes completed status and output.
+- Test worker failure writes failed status and leaves retry behavior to BullMQ.
+- Test idempotent side effects when BullMQ retries the same job.
+- Test progress updates from pipeline stage events.
+- Run the pipeline directly in unit tests before testing BullMQ wiring.
+
+## Related Docs
+
+- [Production Guardrails](/docs/best-practices/common-patterns/production-guardrails)
+- [Testing and Observability](/docs/best-practices/common-patterns/testing-and-observability)
+- [Side Effect Tools](/docs/best-practices/tool-patterns/side-effect-tools)
diff --git a/apps/docs/content/docs/best-practices/mcp-patterns/mcp-agent-harness.mdx b/apps/docs/content/docs/best-practices/mcp-patterns/mcp-agent-harness.mdx
new file mode 100644
index 00000000..85a39747
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/mcp-patterns/mcp-agent-harness.mdx
@@ -0,0 +1,151 @@
+---
+title: MCP Agent Harness
+description: Combine local tools, MCP tools, registry state, and error handling in one runner.
+---
+
+An MCP agent harness is a normal request runner with one extra dependency: connected MCP servers. Keep MCP connection ownership outside prompt logic, then pass validated servers or filtered tools into the scoped agent.
+
+## Scenario
+
+A support agent uses local account tools plus a remote docs MCP server. If the docs server is unavailable, the agent can still answer from account tools and say it cannot search docs right now.
+
+## When to Use It
+
+Use this pattern when an agent combines application-owned tools with MCP tools, or when MCP availability changes independently from the rest of the app.
+
+## Architecture Shape
+
+| Layer | Responsibility |
+| --- | --- |
+| registry | owns connected MCP servers and health state |
+| startup | connects required servers and optional servers |
+| runner | selects servers or filtered MCP tools for this request |
+| scoped agent | registers local tools plus MCP tools |
+| tools | enforce local permissions and side-effect policy |
+| Studio | inspects registered MCP metadata during development |
+
+## Code Example
+
+```ts
+import { AgentBuilder } from "@anvia/core";
+import type { McpServer } from "@anvia/core/mcp";
+
+type McpRegistry = {
+ get(name: string): McpServer | undefined;
+};
+
+export function createSupportAgent(input: {
+ docsServer?: McpServer;
+ scope: SupportToolScope;
+}) {
+ const builder = new AgentBuilder("support", model)
+ .instructions(`
+Answer support questions clearly.
+Use account tools for customer-specific data.
+Use docs tools when policy or product documentation is needed.
+ `)
+ .tools(createSupportTools(input.scope))
+ .defaultMaxTurns(4);
+
+ if (input.docsServer) {
+ builder.mcp([input.docsServer]);
+ }
+
+ return builder.build();
+}
+```
+
+The runner chooses the MCP capability for the current request.
+
+```ts
+export async function runSupportWithMcp(input: SupportRunnerInput) {
+ const user = await input.auth.requireUser();
+ const history = await input.conversations.loadMessages(input.conversationId);
+ const docsServer = input.mcp.get("docs");
+
+ const agent = createSupportAgent({
+ docsServer,
+ scope: {
+ userId: user.id,
+ tenantId: user.tenantId,
+ orders: input.services.orders,
+ tickets: input.services.tickets,
+ },
+ });
+
+ const response = await agent
+ .prompt([...history, Message.user(input.message)])
+ .withTrace({
+ name: "support-chat",
+ userId: user.id,
+ metadata: {
+ tenantId: user.tenantId,
+ docsMcpAvailable: docsServer !== undefined,
+ },
+ })
+ .send();
+
+ await input.conversations.append(input.conversationId, response.messages);
+
+ return response.output;
+}
+```
+
+## Filter MCP Tools in the Harness
+
+If the docs server exposes more tools than support should use, filter before building the agent.
+
+```ts
+const docsTools = docsServer?.tools.filter((tool) =>
+ ["search_docs", "read_doc"].includes(tool.name),
+);
+
+const agent = new AgentBuilder("support", model)
+ .tools(createSupportTools(scope))
+ .tools(docsTools ?? [])
+ .defaultMaxTurns(4)
+ .build();
+```
+
+## Error Handling
+
+MCP call errors are tool errors during an agent run. Keep the agent bounded and make server health visible in trace metadata.
+
+```ts
+const response = await agent
+ .prompt(message)
+ .withTrace({
+ name: "support-chat",
+ metadata: {
+ docsMcpAvailable: docsServer !== undefined,
+ },
+ })
+ .maxTurns(3)
+ .send();
+```
+
+Use registry health checks and reconnects outside the prompt run. The model should not be responsible for repairing infrastructure.
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| optional MCP unavailable | build agent without it and include trace metadata |
+| wrong MCP tools exposed | filter or wrap tools before registration |
+| remote tool loops after errors | keep max turns low |
+| credentials are request-scoped | connect in `try/finally` for that request |
+| Studio does not show expected MCP tools | verify `.mcp([server])` or filtered tools are registered on the built agent |
+
+## Test Checklist
+
+- Test runner behavior with MCP available and unavailable.
+- Test filtered MCP tool list includes only allowed names.
+- Test required server validation during startup.
+- Test traces include MCP availability metadata.
+- Use Studio MCP inspection for local verification.
+
+## Related Docs
+
+- [MCP Server Lifecycle](/docs/best-practices/mcp-patterns/mcp-server-lifecycle)
+- [MCP Tool Inspection](/docs/best-practices/mcp-patterns/mcp-tool-inspection)
+- [Support Agent](/docs/best-practices/real-cases/support-agent)
diff --git a/apps/docs/content/docs/best-practices/mcp-patterns/mcp-server-lifecycle.mdx b/apps/docs/content/docs/best-practices/mcp-patterns/mcp-server-lifecycle.mdx
new file mode 100644
index 00000000..4954ce23
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/mcp-patterns/mcp-server-lifecycle.mdx
@@ -0,0 +1,149 @@
+---
+title: MCP Server Lifecycle
+description: Connect, close, and reconnect MCP servers at the right application boundary.
+---
+
+MCP servers expose external tools to an agent. Treat each MCP connection as infrastructure: decide whether it is required, when it connects, how it is closed, and what happens when it fails.
+
+## Scenario
+
+An agent needs tools from a filesystem MCP server, an internal docs MCP server, and a remote CRM MCP server. Some are required for startup. Others are optional and should degrade gracefully.
+
+## When to Use It
+
+Use this pattern whenever an agent registers tools with `.mcp(...)` or wraps MCP tools with local Anvia tools.
+
+## Architecture Shape
+
+| Boundary | Pattern |
+| --- | --- |
+| required startup dependency | connect once during application boot and fail fast on error |
+| optional dependency | attempt connection, log failure, run agent without that server |
+| request-scoped server | connect inside `try/finally` and always close |
+| long-lived process | close all servers during shutdown |
+| reconnect | close previous server, connect next server, rebuild agents or update tool sets |
+
+## Connect at Startup
+
+```ts
+import { connectMcp, mcp, type McpConnection, type McpServer } from "@anvia/core/mcp";
+
+export async function connectMcpServers() {
+ const filesystem = await connectMcp(
+ mcp.stdio({
+ name: "filesystem",
+ command: "npx",
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
+ }),
+ );
+
+ const docs = await connectMcp(
+ mcp.http({
+ name: "docs",
+ url: "https://mcp.example.com/mcp",
+ }),
+ );
+
+ return { filesystem, docs };
+}
+```
+
+Register connected servers on an agent.
+
+```ts
+const { filesystem, docs } = await connectMcpServers();
+
+const agent = new AgentBuilder("research", model)
+ .instructions("Use MCP tools when external context is needed.")
+ .mcp([filesystem, docs])
+ .defaultMaxTurns(4)
+ .build();
+```
+
+## Optional Servers
+
+Optional servers should not block the whole application.
+
+```ts
+async function connectOptional(connection: McpConnection) {
+ try {
+ return await connectMcp(connection);
+ } catch (error) {
+ logger.warn({ error, server: connection.name }, "optional MCP server unavailable");
+ return undefined;
+ }
+}
+
+const docs = await connectOptional(
+ mcp.http({ name: "docs", url: "https://mcp.example.com/mcp" }),
+);
+
+const mcpServers = [docs].filter((server): server is McpServer => server !== undefined);
+```
+
+## Request-Scoped Connections
+
+Use request scope only when the connection depends on request-local credentials or a short-lived resource.
+
+```ts
+const server = await connectMcp(connection);
+
+try {
+ const agent = new AgentBuilder("tenant-docs", model)
+ .mcp([server])
+ .defaultMaxTurns(3)
+ .build();
+
+ return await agent.prompt(message).send();
+} finally {
+ await server.close();
+}
+```
+
+Most application servers should prefer startup connections over request-scoped connections.
+
+## Registry and Reconnect
+
+```ts
+type McpRegistry = {
+ get(name: string): McpServer | undefined;
+ set(server: McpServer): void;
+ closeAll(): Promise;
+};
+
+async function reconnect(connection: McpConnection, registry: McpRegistry) {
+ const previous = registry.get(connection.name);
+ await previous?.close();
+
+ const next = await connectMcp(connection);
+ registry.set(next);
+
+ return next;
+}
+```
+
+After reconnecting, create new agents or update the shared tool catalog used by future runs. A built agent keeps the tools it was built with unless it uses a shared mutable `ToolSet`.
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| app starts without required MCP tools | fail fast during startup |
+| optional MCP outage breaks all agents | omit optional server and log degraded capability |
+| server process leaks | close on shutdown or in `finally` |
+| reconnected tools not visible | rebuild agents or update the shared tool set |
+| tool call loops on remote errors | keep turn limits low and expose clear tool error text |
+
+## Test Checklist
+
+- Test required server connection failure at startup.
+- Test optional server failure produces a reduced agent capability set.
+- Test request-scoped connections close in `finally`.
+- Test reconnect closes the previous server before replacing it.
+- Use Studio or `/agents/:agentId/mcps` to inspect registered MCP servers.
+
+## Related Docs
+
+- [MCP Connections](/docs/guides/mcp/connections)
+- [Errors and Reconnects](/docs/guides/mcp/errors-and-reconnects)
+- [MCP Agent Harness](/docs/best-practices/mcp-patterns/mcp-agent-harness)
diff --git a/apps/docs/content/docs/best-practices/mcp-patterns/mcp-tool-inspection.mdx b/apps/docs/content/docs/best-practices/mcp-patterns/mcp-tool-inspection.mdx
new file mode 100644
index 00000000..a7d25f94
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/mcp-patterns/mcp-tool-inspection.mdx
@@ -0,0 +1,148 @@
+---
+title: MCP Tool Inspection
+description: Check, filter, validate, and wrap MCP tools before exposing them to an agent.
+---
+
+MCP tools are adapted automatically, but a production harness should still inspect what the server exposed. Tool names, descriptions, input schemas, and permission expectations decide whether a tool is safe to show to a model.
+
+## Scenario
+
+A remote MCP server exposes 40 tools. Your support agent should use only read-only documentation tools, while an internal admin agent can use a smaller set of write tools behind approval.
+
+## When to Use It
+
+Use this pattern when:
+
+- a server exposes more tools than one agent should see
+- tool names are unclear or conflict with local tools
+- a tool needs product permissions or audit records
+- you need to verify required tools exist before startup succeeds
+
+## Architecture Shape
+
+| Step | Purpose |
+| --- | --- |
+| connect server | get adapted Anvia tools |
+| inspect definitions | verify name, description, and schema |
+| validate required tools | fail fast for missing required capabilities |
+| filter safe tools | expose only the tools this agent should use |
+| wrap tools | rename, audit, permission-check, or normalize output when needed |
+
+## Inspect Definitions
+
+```ts
+import { connectMcp, mcp, type McpServer } from "@anvia/core/mcp";
+
+const docsServer = await connectMcp(
+ mcp.http({
+ name: "docs",
+ url: "https://mcp.example.com/mcp",
+ }),
+);
+
+for (const tool of docsServer.tools) {
+ const definition = await tool.definition("");
+ logger.info({
+ server: docsServer.name,
+ tool: definition.name,
+ description: definition.description,
+ parameters: definition.parameters,
+ });
+}
+```
+
+## Validate Required Tools
+
+```ts
+async function requireMcpTools(server: McpServer, names: string[]) {
+ const available = new Set(server.tools.map((tool) => tool.name));
+ const missing = names.filter((name) => !available.has(name));
+
+ if (missing.length > 0) {
+ throw new Error(`MCP server ${server.name} is missing tools: ${missing.join(", ")}`);
+ }
+}
+
+await requireMcpTools(docsServer, ["search_docs", "read_doc"]);
+```
+
+## Filter Tools
+
+```ts
+function pickMcpTools(server: McpServer, allowedNames: string[]) {
+ const allowed = new Set(allowedNames);
+ return server.tools.filter((tool) => allowed.has(tool.name));
+}
+
+const safeDocsTools = pickMcpTools(docsServer, ["search_docs", "read_doc"]);
+
+const agent = new AgentBuilder("support", model)
+ .instructions("Use docs tools for public support documentation.")
+ .tools(safeDocsTools)
+ .defaultMaxTurns(3)
+ .build();
+```
+
+Use `.mcp([server])` when the agent should receive every tool from that server. Use `.tools(filteredTools)` when the agent should receive only selected tools.
+
+## Wrap Tools
+
+Wrap an MCP tool when you need product-specific names, permissions, audit records, or normalized output.
+
+```ts
+import { createTool } from "@anvia/core";
+import { z } from "zod";
+
+const searchDocs = docsServer.tools.find((tool) => tool.name === "search_docs");
+
+if (!searchDocs) {
+ throw new Error("docs MCP server is missing search_docs");
+}
+
+const searchInternalDocs = createTool({
+ name: "search_internal_docs",
+ description: "Search internal support documentation for the current tenant.",
+ input: z.object({
+ query: z.string().min(1),
+ }),
+ async execute({ query }) {
+ await permissions.require(scope.userId, "docs:read");
+
+ const result = await searchDocs.call({
+ query: `${query}\ntenant:${scope.tenantId}`,
+ });
+
+ await audit.write({
+ actorId: scope.userId,
+ action: "docs.search",
+ tenantId: scope.tenantId,
+ });
+
+ return result;
+ },
+});
+```
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| server exposes unsafe tools | filter or wrap before registering |
+| tool names conflict | wrap with product-specific names |
+| schema is too loose | wrap with a stricter Zod schema |
+| server changes tool names | validate required tools at startup |
+| remote tool returns noisy output | wrap and normalize result shape |
+
+## Test Checklist
+
+- Assert required tools exist after connection.
+- Snapshot provider-facing tool definitions for critical MCP tools.
+- Test wrapped tools with fake permission and audit services.
+- Verify agents receive only allowed MCP tools.
+- Inspect MCP metadata in Studio when available.
+
+## Related Docs
+
+- [MCP Tool Adapters](/docs/guides/mcp/tool-adapters)
+- [MCP Result Handling](/docs/guides/mcp/result-handling)
+- [Tool Validation and Contracts](/docs/best-practices/tool-patterns/tool-validation-and-contracts)
diff --git a/apps/docs/content/docs/best-practices/mcp-patterns/meta.json b/apps/docs/content/docs/best-practices/mcp-patterns/meta.json
new file mode 100644
index 00000000..b869ef67
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/mcp-patterns/meta.json
@@ -0,0 +1,6 @@
+{
+ "title": "MCP Patterns",
+ "defaultOpen": false,
+ "collapsible": true,
+ "pages": ["mcp-server-lifecycle", "mcp-tool-inspection", "mcp-agent-harness"]
+}
diff --git a/apps/docs/content/docs/best-practices/meta.json b/apps/docs/content/docs/best-practices/meta.json
new file mode 100644
index 00000000..d7803061
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/meta.json
@@ -0,0 +1,18 @@
+{
+ "title": "Best Practices",
+ "description": "Agent pattern library",
+ "icon": "BookCheck",
+ "root": true,
+ "pages": [
+ "index",
+ "common-patterns",
+ "long-process-pipelines",
+ "multi-agent-patterns",
+ "tool-patterns",
+ "mcp-patterns",
+ "knowledge-patterns",
+ "real-cases",
+ "quality-observability",
+ "operations"
+ ]
+}
diff --git a/apps/docs/content/docs/best-practices/multi-agent-patterns/build-streaming-specialists.mdx b/apps/docs/content/docs/best-practices/multi-agent-patterns/build-streaming-specialists.mdx
new file mode 100644
index 00000000..32cc0926
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/multi-agent-patterns/build-streaming-specialists.mdx
@@ -0,0 +1,93 @@
+---
+title: Build Streaming Specialists
+description: Create specialist agents and expose them as streaming tools on a coordinator.
+---
+
+Build multi-agent streaming around one coordinator and a small set of narrow specialists. Each specialist should have a clear role, stable agent id, and focused output.
+
+## Build Specialist Agents
+
+```ts
+import { AgentBuilder } from "@anvia/core";
+import { model } from "./model";
+
+export const supportAgent = new AgentBuilder("support", model)
+ .name("Support Specialist")
+ .description("Summarize customer impact and support next steps.")
+ .instructions("Return compact support triage bullets using only the provided facts.")
+ .build();
+
+export const engineeringAgent = new AgentBuilder("engineering", model)
+ .name("Engineering Specialist")
+ .description("Summarize diagnostics and engineering next steps.")
+ .instructions("Return compact engineering triage bullets without unverified root-cause claims.")
+ .build();
+
+export const commsAgent = new AgentBuilder("communications", model)
+ .name("Communications Specialist")
+ .description("Draft customer-facing incident communication guidance.")
+ .instructions("Return concise customer-safe communication notes.")
+ .build();
+```
+
+Keep specialist instructions narrow. Each child agent should own one role and return a focused result that the coordinator can use.
+
+## Expose Specialists With Streaming
+
+```ts
+import { AgentBuilder } from "@anvia/core";
+import { commsAgent, engineeringAgent, supportAgent } from "./specialists";
+import { model } from "./model";
+
+export const coordinator = new AgentBuilder("incident-coordinator", model)
+ .name("Incident Coordinator")
+ .instructions(
+ [
+ "Coordinate specialist agents through tools.",
+ "Call specialists only when their expertise is useful.",
+ "Combine specialist findings into one concise incident brief.",
+ "Do not expose internal-only notes in the final customer-facing summary.",
+ ].join("\n"),
+ )
+ .tools([
+ supportAgent.asTool({ name: "ask_support_agent", stream: true }),
+ engineeringAgent.asTool({ name: "ask_engineering_agent", stream: true }),
+ commsAgent.asTool({ name: "ask_comms_agent", stream: true }),
+ ])
+ .defaultMaxTurns(4)
+ .build();
+```
+
+The coordinator should decide which specialists to call. Do not attach every possible specialist if the model cannot choose reliably.
+
+## Run the Coordinator Stream
+
+```ts
+const prompt = [
+ "Acme Co. reports webhook retries fail for payloads larger than 512 KB.",
+ "They have missed several order updates in the last hour.",
+ "Prepare an incident brief for support, engineering, and communications.",
+].join(" ");
+
+for await (const event of coordinator
+ .prompt(prompt)
+ .withToolConcurrency(3)
+ .withTrace({
+ name: "incident-multi-agent-stream",
+ metadata: {
+ incidentId: "inc_123",
+ tenantId: "tenant_123",
+ },
+ })
+ .stream()) {
+ renderEvent(event);
+}
+```
+
+Use `.withToolConcurrency(...)` only when specialist calls are independent. If one specialist must wait for another specialist's finding, keep concurrency at `1` or split the workflow into explicit stages.
+
+## Related Docs
+
+- [Multi-Agent Workflows](/docs/guides/agents/multi-agent-workflows)
+- [Agent Structure](/docs/best-practices/common-patterns/agent-structure)
+- [Consume Nested Events](/docs/best-practices/multi-agent-patterns/consume-nested-events)
diff --git a/apps/docs/content/docs/best-practices/multi-agent-patterns/consume-nested-events.mdx b/apps/docs/content/docs/best-practices/multi-agent-patterns/consume-nested-events.mdx
new file mode 100644
index 00000000..4d9c159f
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/multi-agent-patterns/consume-nested-events.mdx
@@ -0,0 +1,91 @@
+---
+title: Consume Nested Events
+description: Group parent and child stream events from streaming agent tools.
+---
+
+When a coordinator runs with `.stream()`, normal parent events and nested child-agent events arrive in the same stream. Render coordinator text directly and group child progress by `internalCallId`.
+
+## Render Parent and Child Events
+
+```ts
+import type { AgentStreamEvent } from "@anvia/core/agent";
+
+function renderEvent(event: AgentStreamEvent): void {
+ switch (event.type) {
+ case "text_delta":
+ renderCoordinatorText(event.delta);
+ break;
+ case "tool_call":
+ renderDelegation(event.toolCall.function.name);
+ break;
+ case "agent_tool_event":
+ renderSpecialistEvent({
+ groupId: event.internalCallId,
+ agentLabel: event.agentName ?? event.agentId,
+ toolName: event.toolName,
+ event: event.event,
+ });
+ break;
+ case "final":
+ saveFinalResponse(event);
+ break;
+ case "error":
+ renderStreamError(event.error);
+ break;
+ }
+}
+```
+
+Group nested progress by `internalCallId` when the same specialist can be called more than once. Use `agentId`, `agentName`, and `toolName` for labels.
+
+## Render Specialist Progress
+
+```ts
+import type { AgentStreamEvent } from "@anvia/core/agent";
+
+type SpecialistEvent = Extract;
+
+function renderSpecialistEvent(input: {
+ groupId: string;
+ agentLabel: string;
+ toolName: string;
+ event: SpecialistEvent["event"];
+}) {
+ if (input.event.type === "text_delta") {
+ appendSpecialistText(input.groupId, input.agentLabel, input.event.delta);
+ }
+
+ if (input.event.type === "tool_call") {
+ showSpecialistToolCall(input.groupId, input.event.toolCall.function.name);
+ }
+
+ if (input.event.type === "tool_result") {
+ markSpecialistToolDone(input.groupId, input.event.toolName);
+ }
+
+ if (input.event.type === "final") {
+ markSpecialistComplete(input.groupId, input.event.output);
+ }
+}
+```
+
+Do not render raw tool results, sensitive retrieved context, or internal reasoning in a user-facing UI unless the product intentionally exposes those details. Prefer status labels and safe summaries for customer-facing screens.
+
+## Event Mapping
+
+| Event | UI use |
+| --- | --- |
+| `tool_call` | coordinator is delegating to a specialist |
+| `agent_tool_event.text_delta` | specialist is streaming visible progress |
+| `agent_tool_event.tool_call` | specialist is using one of its tools |
+| `agent_tool_event.tool_result` | specialist tool completed |
+| `agent_tool_event.final` | specialist returned final output to the coordinator |
+| `tool_result` | coordinator received the specialist's final output |
+| `text_delta` | coordinator is streaming the final answer |
+| `final` | persist final messages, usage, trace, and run id |
+
+## Related Docs
+
+- [Streaming Events](/docs/guides/streaming/streaming-events)
+- [Readable Streams](/docs/guides/streaming/readable-streams)
+- [Build Streaming Specialists](/docs/best-practices/multi-agent-patterns/build-streaming-specialists)
diff --git a/apps/docs/content/docs/best-practices/multi-agent-patterns/index.mdx b/apps/docs/content/docs/best-practices/multi-agent-patterns/index.mdx
new file mode 100644
index 00000000..9fc34f48
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/multi-agent-patterns/index.mdx
@@ -0,0 +1,65 @@
+---
+title: Multi-agent Streaming
+description: Stream coordinator output while surfacing nested specialist-agent progress.
+---
+
+Use multi-agent streaming when one coordinator agent delegates work to specialist agents and the caller should see progress while the specialists run. The coordinator still owns the final answer, but the UI can show nested child-agent activity through `agent_tool_event`.
+
+## Scenario
+
+An incident coordinator receives a customer-impact report. It delegates to support, engineering, and communications specialists. The user sees the coordinator's final brief stream in normally, while each specialist's progress appears in grouped status panels.
+
+## When to Use It
+
+Use this pattern when:
+
+- one coordinator should decide which specialists to call
+- specialist work may take long enough that hidden progress feels stalled
+- the UI should group nested progress by specialist or tool call
+- the final answer should still come from one coordinator
+- child-agent progress should be replayable or inspectable after the run
+
+Do not use this pattern just to run known independent work. Use parallel pipelines when every branch should run and the coordinator does not need to decide.
+
+## Architecture Shape
+
+| Layer | Responsibility |
+| --- | --- |
+| coordinator agent | decides which specialists to call and writes the final response |
+| specialist agents | own narrow role instructions and return focused findings |
+| streaming agent tools | expose specialists with `asTool({ stream: true })` |
+| request runner | applies trace metadata, tool concurrency, and stream response shape |
+| UI stream consumer | renders parent events and groups nested `agent_tool_event` values |
+| event store | optionally persists parent and child events for replay and debugging |
+
+## Requirements
+
+Nested streaming requires:
+
+- the parent run uses `.stream()`
+- specialist tools are created with `stream: true`
+- the child model supports streaming
+
+If nested streaming is unavailable, the specialist still behaves like a normal opaque `asTool(...)` call. The parent model receives only the final specialist output as the normal `tool_result`; intermediate child events are for the caller, trace, or event store.
+
+## Pages
+
+- [Build Streaming Specialists](/docs/best-practices/multi-agent-patterns/build-streaming-specialists) shows how to build specialist agents, expose them with `asTool({ stream: true })`, and run the coordinator stream.
+- [Consume Nested Events](/docs/best-practices/multi-agent-patterns/consume-nested-events) shows how to group and render parent and child events safely.
+- [Persistence and Testing](/docs/best-practices/multi-agent-patterns/persistence-and-testing) covers event-store replay, failure modes, and test coverage.
+
+## Choose the Right Shape
+
+| Shape | Use it when |
+| --- | --- |
+| `agent.asTool(...)` | child progress can stay hidden |
+| `agent.asTool({ stream: true })` | coordinator decides which specialists to call and the caller should see child progress |
+| parallel pipelines | every branch should always run and return typed outputs |
+| separate Studio agents | operators should run specialists manually during development |
+
+## Related Docs
+
+- [Multi-Agent Workflows](/docs/guides/agents/multi-agent-workflows)
+- [Streaming Events](/docs/guides/streaming/streaming-events)
+- [Readable Streams](/docs/guides/streaming/readable-streams)
+- [Pipeline](/docs/best-practices/common-patterns/pipeline)
diff --git a/apps/docs/content/docs/best-practices/multi-agent-patterns/meta.json b/apps/docs/content/docs/best-practices/multi-agent-patterns/meta.json
new file mode 100644
index 00000000..17b5750c
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/multi-agent-patterns/meta.json
@@ -0,0 +1,11 @@
+{
+ "title": "Multi-agent Patterns",
+ "defaultOpen": false,
+ "collapsible": true,
+ "pages": [
+ "index",
+ "build-streaming-specialists",
+ "consume-nested-events",
+ "persistence-and-testing"
+ ]
+}
diff --git a/apps/docs/content/docs/best-practices/multi-agent-patterns/persistence-and-testing.mdx b/apps/docs/content/docs/best-practices/multi-agent-patterns/persistence-and-testing.mdx
new file mode 100644
index 00000000..17ec7cc5
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/multi-agent-patterns/persistence-and-testing.mdx
@@ -0,0 +1,55 @@
+---
+title: Persistence and Testing
+description: Persist, replay, and test multi-agent streaming runs.
+---
+
+Multi-agent streams are useful live, but production systems often need replay, support inspection, and regression coverage. Persist events when users may refresh, support teams need a run history, or child-agent progress is part of the product record.
+
+## Persist Events for Replay
+
+Add an event store when nested progress must be replayed after the run, inspected in support tools, or attached to a product audit trail.
+
+```ts
+import { AgentBuilder } from "@anvia/core";
+import type { AgentEventStore } from "@anvia/core/agent";
+import { supportAgent } from "./specialists";
+import { model } from "./model";
+
+declare const eventStore: AgentEventStore;
+
+export const coordinatorWithEvents = new AgentBuilder("incident-coordinator", model)
+ .instructions("Delegate support triage, then produce a short final brief.")
+ .tool(supportAgent.asTool({ name: "ask_support_agent", stream: true }))
+ .eventStore(eventStore, { include: "all" })
+ .defaultMaxTurns(3)
+ .build();
+```
+
+The final stream event includes `runId`. Use that id to load saved parent and child events from the event store.
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| child progress never appears | check parent `.stream()`, `stream: true`, and child model streaming support |
+| nested events are hard to group | group by `internalCallId`, then label with `agentName` or `toolName` |
+| coordinator calls too many specialists | tighten instructions, lower max turns, or reduce available specialist tools |
+| tool results leak sensitive data | render safe status labels and persist sensitive details only in protected storage |
+| parallel calls overload services | lower `.withToolConcurrency(...)` or add service-level limits |
+| progress disappears after refresh | attach an event store and replay by `runId` |
+
+## Test Checklist
+
+- Test specialist agents directly with representative prompts.
+- Test coordinator tool registration includes `stream: true` for visible specialists.
+- Test stream handling for `agent_tool_event`, `tool_result`, `text_delta`, `final`, and `error`.
+- Test UI grouping when the same specialist is called more than once.
+- Test event-store replay with saved parent and child events.
+- Inspect a provider-backed run in Studio or traces before exposing nested progress to users.
+
+## Related Docs
+
+- [Event Store](/docs/guides/agents/event-store)
+- [Testing and Observability](/docs/best-practices/common-patterns/testing-and-observability)
+- [Tracing and Debugging](/docs/best-practices/quality-observability/tracing-and-debugging)
+- [Consume Nested Events](/docs/best-practices/multi-agent-patterns/consume-nested-events)
diff --git a/apps/docs/content/docs/best-practices/operations/meta.json b/apps/docs/content/docs/best-practices/operations/meta.json
new file mode 100644
index 00000000..73d3d3cf
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/operations/meta.json
@@ -0,0 +1,6 @@
+{
+ "title": "Operations",
+ "defaultOpen": false,
+ "collapsible": true,
+ "pages": ["production-readiness-checklist"]
+}
diff --git a/apps/docs/content/docs/best-practices/operations/production-readiness-checklist.mdx b/apps/docs/content/docs/best-practices/operations/production-readiness-checklist.mdx
new file mode 100644
index 00000000..62554da3
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/operations/production-readiness-checklist.mdx
@@ -0,0 +1,111 @@
+---
+title: Production Readiness Checklist
+description: Check an agent harness before it handles real users, data, and side effects.
+---
+
+Use this checklist before deploying an agent harness to production or expanding it from read-only behavior into product actions.
+
+## Runtime Boundaries
+
+| Check | Ready when |
+| --- | --- |
+| stable agent id | traces, sessions, Studio, and logs use a stable id |
+| runner boundary | route or job calls a named runner instead of embedding all prompt logic inline |
+| scoped tools | request-local user, tenant, services, and transactions are passed explicitly |
+| turn limits | every agent has `.defaultMaxTurns(...)`, and high-risk runs override lower |
+| timeouts | HTTP routes, jobs, and approval waits have app-owned timeout policy |
+
+## Tools
+
+| Check | Ready when |
+| --- | --- |
+| input schemas | every tool validates model arguments |
+| output contracts | important tool results are typed states or validated output schemas |
+| permission checks | every data-bearing tool checks actor and tenant scope |
+| side effects | writes use service-layer transactions, idempotency, and audit records |
+| direct tests | high-risk tools are tested through direct calls or `ToolSet.call(...)` |
+
+## Dynamic Tools
+
+| Check | Ready when |
+| --- | --- |
+| catalog ownership | a `ToolSet` owns the full catalog |
+| index lifecycle | tool index is built at startup, deploy time, or another explicit boundary |
+| search tests | representative prompts select expected tool ids |
+| critical tools | required tools are either static or reliably selected |
+| trace review | traces show which dynamic tools were available during runs |
+
+## MCP
+
+| Check | Ready when |
+| --- | --- |
+| lifecycle | required servers fail fast, optional servers degrade gracefully |
+| tool inspection | required MCP tools are validated after connection |
+| filtering | agents receive only the MCP tools they should use |
+| reconnect | reconnect closes the previous server and updates future agents |
+| cleanup | long-lived servers close during shutdown, scoped servers close in `finally` |
+
+## Context and Memory
+
+| Check | Ready when |
+| --- | --- |
+| instructions | durable behavior is in instructions |
+| request facts | current user, tenant, plan, locale, or flags are loaded by the runner |
+| retrieval filters | indexes or searches enforce tenant and access boundaries |
+| history | conversation history is stored and replayed deliberately |
+| session policy | workflows use explicit history or sessions deliberately, not casually mixed |
+
+## Observability
+
+| Check | Ready when |
+| --- | --- |
+| trace names | every workflow uses stable names such as `support-chat` |
+| metadata | traces include safe ids such as tenant, conversation, ticket, or channel |
+| usage | usage is available for cost and regression checks |
+| external telemetry | observers or integrations are attached where needed |
+| Studio | local Studio can inspect tools, MCPs, context, sessions, approvals, and traces |
+
+## Testing and Evals
+
+| Check | Ready when |
+| --- | --- |
+| unit tests | tools, services, runners, filters, and known errors are tested with fakes |
+| integration tests | narrow provider-backed tests cover model-dependent behavior |
+| evals | known prompts or regression cases have repeatable evals |
+| approval tests | approval accepted, rejected, and timeout paths are covered |
+| MCP tests | missing, unavailable, and filtered MCP tools are covered |
+
+## Deployment
+
+| Check | Ready when |
+| --- | --- |
+| secrets | provider and MCP credentials are server-only |
+| storage | history, memory, traces, audit, approval, and idempotency stores are durable |
+| retries | job retries are safe for side effects or disabled for non-idempotent runs |
+| streaming | proxies and platforms do not buffer streams that need live events |
+| rollback | agents can be rebuilt with a previous prompt, tool catalog, or MCP config |
+
+## Smoke Test Shape
+
+```ts
+const result = await runSupportTurn({
+ conversationId: "smoke_test",
+ message: "Say hello and do not call tools.",
+ auth: smokeAuth,
+ conversations: smokeConversations,
+ services: smokeServices,
+});
+
+expect(result.ok).toBe(true);
+```
+
+For tool workflows, add a smoke test that calls one read-only tool and verifies trace metadata. For side-effect workflows, run against sandbox services with explicit idempotency records.
+
+## Related Docs
+
+- [Testing and Observability](/docs/best-practices/common-patterns/testing-and-observability)
+- [Eval Strategy](/docs/best-practices/quality-observability/eval-strategy)
+- [Tracing and Debugging](/docs/best-practices/quality-observability/tracing-and-debugging)
+- [RAG Ingestion](/docs/best-practices/knowledge-patterns/rag-ingestion)
+- [Production Guardrails](/docs/best-practices/common-patterns/production-guardrails)
+- [MCP Server Lifecycle](/docs/best-practices/mcp-patterns/mcp-server-lifecycle)
diff --git a/apps/docs/content/docs/best-practices/quality-observability/eval-strategy.mdx b/apps/docs/content/docs/best-practices/quality-observability/eval-strategy.mdx
new file mode 100644
index 00000000..ff20fa64
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/quality-observability/eval-strategy.mdx
@@ -0,0 +1,151 @@
+---
+title: Eval Strategy
+description: Build repeatable quality checks for tools, agents, RAG, and workflows.
+---
+
+Evals are for behavior that cannot be fully proven with unit tests. Use unit tests for deterministic boundaries, then use eval suites for model-dependent behavior, retrieval quality, answer quality, tool choice, and regressions.
+
+Do not make every route test call a provider. Keep provider-backed evals focused and repeatable.
+
+## Scenario
+
+A support agent should answer refund-policy questions correctly, use account tools only when needed, and avoid claiming a refund happened unless the refund tool confirms it. Unit tests cover tools and services. Evals cover model behavior.
+
+## When to Use It
+
+Use evals when:
+
+- prompts, instructions, tools, or retrieval change often
+- failures are quality regressions rather than type errors
+- you need a repeatable signal before deploying prompt changes
+- outputs are judged by required facts, schema, similarity, or a rubric
+- traces or Langfuse scores should connect eval outcomes to runs
+
+## Architecture Shape
+
+| Layer | Test with |
+| --- | --- |
+| service and permission policy | unit tests with fakes |
+| tool contracts | direct `tool.call(...)` or `ToolSet.call(...)` |
+| retrieval selection | search probes against indexes |
+| runner response shape | unit tests with fake agents or sandbox agents |
+| agent answer quality | eval suite with `agentEvalTarget(...)` or custom target |
+| traced workflow | eval reporter plus trace metadata |
+
+## Golden Cases
+
+Store cases close to the workflow they protect. Keep them small and named by the behavior they assert.
+
+```ts
+const supportCases = [
+ {
+ id: "refund-window",
+ input: "How long do I have to request a refund?",
+ expected: "30 days",
+ },
+ {
+ id: "password-reset-expiry",
+ input: "Can I use yesterday's password reset link?",
+ expected: "30 minutes",
+ },
+];
+```
+
+## Agent Eval
+
+```ts
+import { agentEvalTarget, contains, runEvalSuite } from "@anvia/core/evals";
+
+const result = await runEvalSuite({
+ name: "support-agent-regression",
+ cases: supportCases,
+ target: agentEvalTarget(supportAgent),
+ metrics: [
+ contains(),
+ ],
+});
+
+console.log(result.passed, result.failed, result.invalid);
+```
+
+Use `agentEvalTarget(...)` when the target is exactly `agent.prompt(input).send()`.
+
+## Custom Harness Eval
+
+Use a custom target when the real behavior lives in your runner.
+
+```ts
+const result = await runEvalSuite({
+ name: "support-runner-regression",
+ cases: supportCases,
+ target: async (input) => {
+ const response = await runSupportTurn({
+ conversationId: `eval:${input}`,
+ message: input,
+ auth: evalAuth,
+ conversations: evalConversations,
+ services: evalServices,
+ });
+
+ return response.output;
+ },
+ metrics: [contains()],
+});
+```
+
+This covers the harness boundary: history, tools, retrieval, trace metadata, and final output.
+
+## Metric Choice
+
+| Metric | Use it for |
+| --- | --- |
+| `exactMatch(...)` | deterministic booleans, labels, JSON-shaped values |
+| `contains(...)` | required phrases, facts, or regex matches |
+| `semanticSimilarity(...)` | answer similarity when wording can vary |
+| `llmJudge(...)` | schema-shaped rubric checks |
+| `llmScore(...)` | scored quality feedback with a threshold |
+
+Start with deterministic metrics. Add LLM judges when the behavior cannot be checked with simple selectors.
+
+## Report to Langfuse
+
+```ts
+import { createLangfuseEvalReporter, langfuse } from "@anvia/langfuse";
+
+const tracing = langfuse.create({ publicKey, secretKey, baseUrl });
+
+await runEvalSuite({
+ name: "support-agent-regression",
+ cases: supportCases,
+ target: agentEvalTarget(supportAgent),
+ metrics: [contains()],
+ reporters: [createLangfuseEvalReporter(tracing)],
+});
+```
+
+When your target returns a prompt response with trace ids, reporters can connect eval scores back to traces.
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| evals are flaky | prefer deterministic metrics and sandbox services |
+| cases are too broad | split into one behavior per case |
+| evals duplicate unit tests | move deterministic policy checks back to unit tests |
+| judges hide regressions | store judge feedback and add required fact metrics |
+| no trace linkage | include trace metadata or return prompt response trace ids |
+
+## Test Checklist
+
+- Cover tool contracts with unit tests before provider evals.
+- Add retrieval probe tests before answer-quality evals.
+- Keep golden cases small, named, and versioned.
+- Run evals on prompt, retrieval, tool, and model changes.
+- Report eval scores to the same observability system used for traces when useful.
+
+## Related Docs
+
+- [Evals](/docs/guides/testing/evals)
+- [Testing and Observability](/docs/best-practices/common-patterns/testing-and-observability)
+- [Tracing and Debugging](/docs/best-practices/quality-observability/tracing-and-debugging)
+
diff --git a/apps/docs/content/docs/best-practices/quality-observability/meta.json b/apps/docs/content/docs/best-practices/quality-observability/meta.json
new file mode 100644
index 00000000..d8949248
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/quality-observability/meta.json
@@ -0,0 +1,6 @@
+{
+ "title": "Quality and Observability",
+ "defaultOpen": false,
+ "collapsible": true,
+ "pages": ["eval-strategy", "tracing-and-debugging"]
+}
diff --git a/apps/docs/content/docs/best-practices/quality-observability/tracing-and-debugging.mdx b/apps/docs/content/docs/best-practices/quality-observability/tracing-and-debugging.mdx
new file mode 100644
index 00000000..e2ff808c
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/quality-observability/tracing-and-debugging.mdx
@@ -0,0 +1,137 @@
+---
+title: Tracing and Debugging
+description: Connect agent runs, tool calls, retrieval evidence, product logs, and eval scores.
+---
+
+Tracing gives you the evidence needed to debug an agent harness: prompt runs, model generations, tool calls, errors, usage, retrieval evidence, trace metadata, and eval scores.
+
+Use traces for runtime debugging. Use product logs and audit records for product accountability.
+
+## Scenario
+
+A support answer was wrong. You need to know which prompt ran, which user and conversation it belonged to, which tools were available, which retrieval documents were injected, what the model returned, and whether the same case fails in evals.
+
+## When to Use It
+
+Use this pattern for every production harness that handles real users, private data, side effects, or recurring quality checks.
+
+## Architecture Shape
+
+| Layer | Responsibility |
+| --- | --- |
+| observer | records runs, tool calls, model calls, errors, and usage |
+| `.withTrace(...)` | attaches workflow name, user id, session id, tags, version, and safe metadata |
+| product logs | store trace ids next to product events |
+| Studio | inspect local runs, tools, MCPs, context, approvals, and traces |
+| external tracing | long-term search, metrics, dashboards, eval scores |
+| shutdown | flush and close buffered telemetry |
+
+## Code Example
+
+```ts
+import { AgentBuilder } from "@anvia/core";
+import { langfuse } from "@anvia/langfuse";
+
+const tracing = langfuse.create({
+ publicKey,
+ secretKey,
+ baseUrl,
+});
+
+const agent = new AgentBuilder("support", model)
+ .instructions("Answer support questions clearly.")
+ .observe(tracing)
+ .defaultMaxTurns(3)
+ .build();
+
+const response = await agent
+ .prompt(message)
+ .withTrace({
+ name: "support-chat",
+ userId: user.id,
+ sessionId: conversationId,
+ tags: ["support", channel],
+ version: "2026-05-11",
+ metadata: {
+ tenantId: user.tenantId,
+ conversationId,
+ plan: user.plan,
+ },
+ })
+ .send();
+
+logger.info({
+ traceId: response.trace?.traceId,
+ observationId: response.trace?.observationId,
+ conversationId,
+}, "support agent completed");
+```
+
+## Trace Metadata Rules
+
+| Include | Avoid |
+| --- | --- |
+| stable workflow name | raw secrets or API keys |
+| user id when safe | full prompts in app logs |
+| tenant id | large records |
+| conversation, ticket, or job id | private document bodies in metadata |
+| model or prompt version | unbounded objects |
+| channel, route, or feature flag | values your policy forbids storing |
+
+Trace metadata should connect systems. It should not become a second database.
+
+## Debugging Flow
+
+1. Find the product event, ticket, conversation, or eval case.
+2. Open the linked trace id.
+3. Check prompt input, instructions, retrieved context, and available tools.
+4. Check tool calls, tool outputs, errors, and turn count.
+5. Reproduce with the same case in Studio or an eval suite.
+6. Fix the deterministic boundary first: tool, retrieval, runner, or prompt.
+7. Add an eval case when the issue is model-dependent.
+
+## Local Studio vs External Tracing
+
+| Use Studio | Use external tracing |
+| --- | --- |
+| local development | production traffic |
+| inspect agent registration | long-term trace search |
+| exercise approvals and questions | aggregate cost and usage |
+| debug context and MCP visibility | dashboards and alerting |
+| iterate before product UI exists | eval score reporting |
+
+Studio and tracing are complementary. Studio shortens local iteration; tracing preserves production evidence.
+
+## Flush and Shutdown
+
+```ts
+await tracing.flush?.();
+await tracing.shutdown?.();
+```
+
+Call `flush()` before process exit when pending events matter. Call `shutdown()` during application shutdown for long-lived integrations.
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| traces cannot be tied to product events | log `traceId` and product ids together |
+| trace metadata leaks sensitive data | restrict metadata to ids and small safe fields |
+| eval scores are disconnected | report evals with trace ids or case metadata |
+| tool failures are invisible | attach observer before building production agents |
+| buffered traces are missing | flush or shutdown the observer on exit |
+
+## Test Checklist
+
+- Assert runners attach stable trace names.
+- Assert safe metadata includes product correlation ids.
+- Verify `response.trace` is logged or returned where needed.
+- Inspect one local Studio run for context, tools, and trace evidence.
+- Verify external observer flushes during shutdown.
+
+## Related Docs
+
+- [Tracing](/docs/guides/observability/tracing)
+- [Langfuse](/docs/guides/observability/langfuse)
+- [Eval Strategy](/docs/best-practices/quality-observability/eval-strategy)
+
diff --git a/apps/docs/content/docs/best-practices/real-cases/backoffice-agent.mdx b/apps/docs/content/docs/best-practices/real-cases/backoffice-agent.mdx
new file mode 100644
index 00000000..692408c9
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/real-cases/backoffice-agent.mdx
@@ -0,0 +1,160 @@
+---
+title: Backoffice Agent
+description: An admin workflow with side effects, approvals, audit records, and idempotency.
+---
+
+This pattern is for internal operations agents that can change product state. The harness must treat every write as a product operation with permissions, approvals, idempotency, and audit records.
+
+## Scenario
+
+A support lead asks an agent to resolve a billing ticket, issue a refund, and notify the customer. The model can help coordinate the work, but application code owns every restricted operation.
+
+## When to Use It
+
+Use this pattern when the agent can:
+
+- issue refunds
+- close or transition tickets
+- update accounts
+- send customer messages
+- trigger jobs or webhooks
+- access admin-only data
+
+## Architecture Shape
+
+| Layer | Responsibility |
+| --- | --- |
+| runner | resolve admin actor, ticket, tenant, approval run, trace metadata |
+| agent | coordinate the workflow and call tools |
+| write tools | validate inputs and call product services |
+| approval runtime | store decisions, notify reviewers, wait or resume |
+| service layer | permissions, transactions, idempotency, audit |
+| storage | ticket state, operation records, audit records, traces |
+
+## Code Example
+
+```ts
+import { AgentBuilder, Message, createHook } from "@anvia/core";
+import { model } from "./model";
+import { createBackofficeTools } from "./tools";
+
+export async function runBackofficeResolution(input: BackofficeInput) {
+ const actor = await input.auth.requireAdmin();
+ const ticket = await input.tickets.getForTenant(input.ticketId, actor.tenantId);
+
+ const approvalHook = createHook({
+ async onToolCall({ toolName, tool }) {
+ if (!["issue_refund", "send_customer_email", "close_account"].includes(toolName)) {
+ return tool.run();
+ }
+
+ const approved = await input.approvals.waitForDecision({
+ actorId: actor.id,
+ tenantId: actor.tenantId,
+ ticketId: ticket.id,
+ toolName,
+ });
+
+ return approved ? tool.run() : tool.cancel("Operation was not approved.");
+ },
+ });
+
+ const agent = new AgentBuilder("backoffice", model)
+ .instructions(`
+Help resolve backoffice tickets.
+Use tools for account data and product changes.
+Never claim that a side effect happened unless the tool result confirms it.
+ `)
+ .tools(
+ createBackofficeTools({
+ actorId: actor.id,
+ tenantId: actor.tenantId,
+ ticketId: ticket.id,
+ billing: input.services.billing,
+ tickets: input.services.tickets,
+ messages: input.services.messages,
+ audit: input.audit,
+ }),
+ )
+ .hook(approvalHook)
+ .defaultMaxTurns(5)
+ .build();
+
+ const response = await agent
+ .prompt([
+ Message.user(`Ticket ${ticket.id}: ${ticket.summary}`),
+ Message.user(input.instruction),
+ ])
+ .withTrace({
+ name: "backoffice-resolution",
+ userId: actor.id,
+ metadata: {
+ tenantId: actor.tenantId,
+ ticketId: ticket.id,
+ },
+ })
+ .send();
+
+ await input.audit.write({
+ actorId: actor.id,
+ tenantId: actor.tenantId,
+ action: "backoffice.agent_run",
+ targetId: ticket.id,
+ });
+
+ return response.output;
+}
+```
+
+## Write Tool Shape
+
+```ts
+async execute({ orderId, amount, reason }) {
+ const operationId = `refund:${scope.tenantId}:${orderId}:${amount}`;
+
+ const result = await scope.billing.issueRefund({
+ actorId: scope.actorId,
+ tenantId: scope.tenantId,
+ orderId,
+ amount,
+ reason,
+ operationId,
+ });
+
+ await scope.audit.write({
+ actorId: scope.actorId,
+ tenantId: scope.tenantId,
+ action: "refund.issued",
+ targetId: orderId,
+ operationId,
+ });
+
+ return result;
+}
+```
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| duplicate refund | idempotency key in service layer |
+| model performs write without approval | tool approval metadata or `onToolCall` hook |
+| approval blocks HTTP request too long | store approval and resume asynchronously |
+| audit only appears in traces | write product audit records |
+| admin data leaks | enforce actor and tenant permissions in services |
+
+## Test Checklist
+
+- Test admin auth and tenant scoping.
+- Test approval approved, rejected, and timed out paths.
+- Test write tools call services with idempotency keys.
+- Test audit records for each successful side effect.
+- Test runner maps cancellation to a product-safe response.
+- Use Studio streaming runs to inspect approval behavior locally.
+
+## Related Docs
+
+- [Side Effect Tools](/docs/best-practices/tool-patterns/side-effect-tools)
+- [Production Guardrails](/docs/best-practices/common-patterns/production-guardrails)
+- [Approval Handlers](/docs/guides/human-in-the-loop/approval-handlers)
+
diff --git a/apps/docs/content/docs/best-practices/real-cases/coding-agent.mdx b/apps/docs/content/docs/best-practices/real-cases/coding-agent.mdx
new file mode 100644
index 00000000..d06d3232
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/real-cases/coding-agent.mdx
@@ -0,0 +1,206 @@
+---
+title: Coding Agent
+description: A codebase assistant harness with repo search, file tools, command guardrails, patches, and evals.
+---
+
+A coding agent is a high-risk harness because it can inspect source code, run commands, and propose or apply changes. Treat it as an application over a workspace: your app owns the repository boundary, allowed paths, command policy, patch approval, git behavior, audit records, and trace metadata.
+
+This pattern does not require new SDK primitives. It composes agents, tools, MCP, approvals, tracing, and evals around a codebase workflow.
+
+## Scenario
+
+A user asks, "Find why the checkout test fails and propose a fix." The agent should search files, read relevant code, optionally run allowed test commands, propose a patch, and wait for approval before any write.
+
+## When to Use It
+
+Use this pattern when:
+
+- an agent assists with code review, debugging, test triage, migration, or docs changes
+- workspace access must be scoped to allowed repositories and paths
+- command execution must be allow-listed
+- writes require preview, approval, idempotency, and git diff inspection
+- behavior should be checked with coding-task evals
+
+## Architecture Shape
+
+| Layer | Responsibility |
+| --- | --- |
+| runner | resolve user, repo, branch, task, allowed paths, trace metadata |
+| read tools | search files, read files, inspect git diff, list tests |
+| command tool | run only allow-listed commands with timeouts and sandbox policy |
+| patch tool | propose or apply patches behind approval |
+| MCP tools | optional filesystem or git server tools, filtered before registration |
+| audit | record command runs, patch proposals, approvals, and applied changes |
+| evals | regression tasks for search, diagnosis, patch proposal, and no-write behavior |
+
+## Code Example
+
+```ts
+import { AgentBuilder, createHook } from "@anvia/core";
+import { model } from "./model";
+import { createCodebaseTools } from "./tools";
+
+export async function runCodingAgent(input: CodingAgentInput) {
+ const user = await input.auth.requireUser();
+ const workspace = await input.workspaces.open({
+ repoId: input.repoId,
+ userId: user.id,
+ });
+
+ const approvalHook = createHook({
+ async onToolCall({ toolName, tool }) {
+ if (!["apply_patch", "run_command"].includes(toolName)) {
+ return tool.run();
+ }
+
+ const approved = await input.approvals.waitForDecision({
+ actorId: user.id,
+ repoId: input.repoId,
+ toolName,
+ reason: "Codebase mutation or command execution requires approval.",
+ });
+
+ return approved ? tool.run() : tool.cancel("Operation was not approved.");
+ },
+ });
+
+ const agent = new AgentBuilder("coding", model)
+ .instructions(`
+Help with codebase tasks.
+Search and read files before proposing changes.
+Prefer minimal patches.
+Do not run commands unless a tool allows them.
+Do not claim a patch was applied unless the tool confirms it.
+ `)
+ .tools(
+ createCodebaseTools({
+ workspace,
+ allowedPaths: input.allowedPaths,
+ allowedCommands: ["pnpm test", "pnpm lint", "pnpm typecheck"],
+ audit: input.audit,
+ }),
+ )
+ .hook(approvalHook)
+ .defaultMaxTurns(8)
+ .build();
+
+ const response = await agent
+ .prompt(input.task)
+ .withTrace({
+ name: "coding-agent-task",
+ userId: user.id,
+ metadata: {
+ repoId: input.repoId,
+ branch: workspace.branch,
+ taskId: input.taskId,
+ },
+ })
+ .send();
+
+ return {
+ output: response.output,
+ trace: response.trace,
+ };
+}
+```
+
+## Tool Boundaries
+
+Keep read tools separate from mutation tools.
+
+```ts
+export function createCodebaseTools(scope: CodebaseToolScope) {
+ return [
+ createSearchFilesTool(scope),
+ createReadFileTool(scope),
+ createGitDiffTool(scope),
+ createRunCommandTool(scope),
+ createApplyPatchTool(scope),
+ ];
+}
+```
+
+Read tools should enforce allowed paths.
+
+```ts
+async execute({ path }) {
+ scope.workspace.requireAllowedPath(path, scope.allowedPaths);
+ return scope.workspace.readFile(path);
+}
+```
+
+Command tools should enforce exact allow lists, timeouts, and working directory policy.
+
+```ts
+async execute({ command }) {
+ if (!scope.allowedCommands.includes(command)) {
+ return { status: "blocked" as const, reason: "command_not_allowed" };
+ }
+
+ return scope.workspace.run(command, {
+ timeoutMs: 60_000,
+ audit: scope.audit,
+ });
+}
+```
+
+Patch tools should support preview-first behavior.
+
+```ts
+async execute({ patch, mode }) {
+ if (mode === "preview") {
+ return scope.workspace.previewPatch(patch);
+ }
+
+ return scope.workspace.applyPatch({
+ patch,
+ operationId: `patch:${scope.workspace.id}:${hashPatch(patch)}`,
+ });
+}
+```
+
+## MCP Filesystem Tools
+
+If a filesystem MCP server is used, filter or wrap its tools before the coding agent sees them. Prefer local wrapper tools when you need path allow lists, audit records, command policy, or patch approval.
+
+```ts
+const filesystem = await connectMcp(
+ mcp.stdio({
+ name: "filesystem",
+ command: "npx",
+ args: ["-y", "@modelcontextprotocol/server-filesystem", workspace.root],
+ }),
+);
+
+const readOnlyTools = filesystem.tools.filter((tool) =>
+ ["read_file", "list_directory"].includes(tool.name),
+);
+```
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| agent reads outside repo | enforce allowed paths in every file tool |
+| command is too broad | exact allow list and timeout in command tool |
+| patch applies twice | idempotency key from patch hash |
+| write happens without approval | hook or approval metadata on mutation tools |
+| traces leak source code | keep trace metadata to ids, paths, and summaries |
+| evals only check final prose | add task cases for tool choice, no-write mode, and patch preview |
+
+## Test Checklist
+
+- Test file reads inside and outside allowed paths.
+- Test blocked commands and approved commands.
+- Test patch preview without mutation.
+- Test approved and rejected patch application.
+- Test git diff inspection after a patch.
+- Add eval cases for diagnosis, minimal patch proposal, and refusal to run disallowed commands.
+
+## Related Docs
+
+- [MCP Tool Inspection](/docs/best-practices/mcp-patterns/mcp-tool-inspection)
+- [Side Effect Tools](/docs/best-practices/tool-patterns/side-effect-tools)
+- [Eval Strategy](/docs/best-practices/quality-observability/eval-strategy)
+- [Tracing and Debugging](/docs/best-practices/quality-observability/tracing-and-debugging)
+
diff --git a/apps/docs/content/docs/best-practices/real-cases/meta.json b/apps/docs/content/docs/best-practices/real-cases/meta.json
new file mode 100644
index 00000000..9f5d39f2
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/real-cases/meta.json
@@ -0,0 +1,6 @@
+{
+ "title": "Real Cases",
+ "defaultOpen": false,
+ "collapsible": true,
+ "pages": ["support-agent", "backoffice-agent", "research-agent", "coding-agent"]
+}
diff --git a/apps/docs/content/docs/best-practices/real-cases/research-agent.mdx b/apps/docs/content/docs/best-practices/real-cases/research-agent.mdx
new file mode 100644
index 00000000..569458ec
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/real-cases/research-agent.mdx
@@ -0,0 +1,129 @@
+---
+title: Research Agent
+description: A research workflow with many read-only tools, dynamic selection, retrieval, and extraction.
+---
+
+This pattern is for research, analysis, due diligence, competitive intelligence, and internal knowledge workflows. The agent often has many read-only tools, uses retrieval heavily, and returns structured output for downstream processing.
+
+## Scenario
+
+A research agent answers a market question by searching internal docs, querying external MCP tools, reading product notes, and extracting a structured brief.
+
+## When to Use It
+
+Use this pattern when:
+
+- the workflow is read-heavy
+- the tool catalog is large
+- sources need to be inspected in traces
+- the final output should match a schema
+- deterministic post-processing is useful
+
+## Architecture Shape
+
+| Layer | Responsibility |
+| --- | --- |
+| dynamic tool catalog | selects relevant read-only tools |
+| retrieval | provides internal knowledge context |
+| MCP tools | expose external research systems |
+| agent | gathers and synthesizes evidence |
+| extractor or output schema | produces typed downstream output |
+| pipeline | separates normalize, research, extract, and enrich steps |
+
+## Code Example
+
+```ts
+import { AgentBuilder } from "@anvia/core";
+import { ExtractorBuilder } from "@anvia/core/extractor";
+import { PipelineBuilder } from "@anvia/core/pipeline";
+import { ToolSet, createToolIndex } from "@anvia/core/tool";
+import { z } from "zod";
+import { embeddingModel, model } from "./models";
+import { internalDocsIndex } from "./retrieval";
+import { createResearchTools } from "./tools";
+
+const researchTools = ToolSet.fromTools(createResearchTools());
+
+const researchToolIndex = await createToolIndex(embeddingModel, researchTools, {
+ metadata: (tool) => ({ name: tool.name, domain: "research" }),
+});
+
+const researchAgent = new AgentBuilder("research", model)
+ .instructions(`
+Research the question using available tools and retrieved context.
+Prefer cited facts over guesses.
+Call tools only when they are relevant to the question.
+ `)
+ .dynamicContext(internalDocsIndex, {
+ topK: 6,
+ threshold: 0.72,
+ })
+ .dynamicTools(researchToolIndex, {
+ topK: 8,
+ threshold: 0.68,
+ })
+ .defaultMaxTurns(6)
+ .build();
+
+const briefSchema = z.object({
+ summary: z.string(),
+ confidence: z.enum(["low", "medium", "high"]),
+ keyFindings: z.array(z.string()),
+ openQuestions: z.array(z.string()),
+});
+
+const briefExtractor = new ExtractorBuilder(model, briefSchema).build();
+
+export const researchWorkflow = new PipelineBuilder()
+ .step((question) => question.trim())
+ .prompt(researchAgent)
+ .extract(briefExtractor)
+ .build();
+```
+
+## Add MCP Research Tools
+
+If external research systems are MCP servers, inspect and filter their tools before adding them to the catalog.
+
+```ts
+import { connectMcp, mcp } from "@anvia/core/mcp";
+
+const docsServer = await connectMcp(mcp.http({
+ name: "external-docs",
+ url: "https://mcp.example.com/mcp",
+}));
+
+const allowedMcpTools = docsServer.tools.filter((tool) =>
+ ["search_docs", "read_doc"].includes(tool.name),
+);
+
+researchTools.addTools(allowedMcpTools);
+```
+
+Rebuild the dynamic tool index after changing the catalog.
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| agent picks irrelevant tools | improve tool descriptions, lower `topK`, raise `threshold` |
+| important tools are not selected | improve embedding text or lower `threshold` |
+| final output is hard to consume | use an extractor or agent output schema |
+| citations are weak | include source ids in retrieved context and tool outputs |
+| tool catalog changes | rebuild the index before future runs |
+
+## Test Checklist
+
+- Test dynamic tool search for representative research prompts.
+- Test retrieval filters and source formatting.
+- Test pipeline output schema with known questions.
+- Inspect traces for selected tools and retrieved context.
+- Add evals for answer quality and missing-evidence behavior.
+
+## Related Docs
+
+- [Dynamic Tool Catalogs](/docs/best-practices/tool-patterns/dynamic-tool-catalogs)
+- [RAG Agent Context](/docs/best-practices/knowledge-patterns/rag-agent-context)
+- [Eval Strategy](/docs/best-practices/quality-observability/eval-strategy)
+- [MCP Tool Inspection](/docs/best-practices/mcp-patterns/mcp-tool-inspection)
+- [Pipeline](/docs/best-practices/common-patterns/pipeline)
diff --git a/apps/docs/content/docs/best-practices/real-cases/support-agent.mdx b/apps/docs/content/docs/best-practices/real-cases/support-agent.mdx
new file mode 100644
index 00000000..4055fa9f
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/real-cases/support-agent.mdx
@@ -0,0 +1,131 @@
+---
+title: Support Agent
+description: A practical support chat harness with history, retrieval, account tools, and traces.
+---
+
+This pattern is for customer support chat, help center assistants, and account-aware product support. The agent answers user questions, retrieves support knowledge, reads customer state through tools, persists conversation history, and emits trace metadata.
+
+## Scenario
+
+A signed-in customer asks, "Where is my order A-100 and can I change the address?" The agent needs previous conversation history, support documentation, order tools, and tenant-safe account context.
+
+## When to Use It
+
+Use this pattern when:
+
+- the user is authenticated
+- the agent needs conversation history
+- account-specific data must be fetched through tools
+- documentation or policy should come from retrieval
+- the response is user-facing and should be traceable
+
+## Architecture Shape
+
+| Layer | Responsibility |
+| --- | --- |
+| route | parse request and return product response |
+| runner | resolve user, load history, create scoped tools, persist messages |
+| support agent | instructions, dynamic context, scoped account tools, turn limit |
+| tools | order, ticket, account, and subscription service calls |
+| retrieval | tenant-safe or public support docs |
+| storage | conversation history and trace correlation ids |
+
+## Code Example
+
+```ts
+import { AgentBuilder, Message } from "@anvia/core";
+import { model } from "./model";
+import { supportDocsIndex } from "./support-docs";
+import { createSupportTools } from "./support-tools";
+
+export async function runSupportTurn(input: SupportTurnInput) {
+ const user = await input.auth.requireUser();
+ const history = await input.conversations.loadMessages(input.conversationId);
+
+ const agent = new AgentBuilder("support", model)
+ .instructions(`
+Answer support questions clearly.
+Use account tools for customer-specific data.
+Use retrieved support docs for policy and product behavior.
+Ask for missing details before guessing.
+ `)
+ .dynamicContext(supportDocsIndex, {
+ topK: 4,
+ threshold: 0.72,
+ })
+ .tools(
+ createSupportTools({
+ userId: user.id,
+ tenantId: user.tenantId,
+ orders: input.services.orders,
+ tickets: input.services.tickets,
+ subscriptions: input.services.subscriptions,
+ }),
+ )
+ .context(`Current customer plan: ${user.plan}`, "customer-plan")
+ .defaultMaxTurns(4)
+ .build();
+
+ const response = await agent
+ .prompt([...history, Message.user(input.message)])
+ .withTrace({
+ name: "support-chat",
+ userId: user.id,
+ metadata: {
+ tenantId: user.tenantId,
+ conversationId: input.conversationId,
+ channel: input.channel,
+ },
+ })
+ .send();
+
+ await input.conversations.append(input.conversationId, response.messages);
+
+ return {
+ output: response.output,
+ usage: response.usage,
+ };
+}
+```
+
+## Tool Scope
+
+Support tools should be scoped to the current user and tenant.
+
+```ts
+export function createSupportTools(scope: SupportToolScope) {
+ return [
+ createLookupOrderTool(scope),
+ createCreateTicketTool(scope),
+ createSubscriptionStatusTool(scope),
+ ];
+}
+```
+
+Do not give the model raw database clients. Give it narrow tools that call permission-aware services.
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| agent answers from stale docs | rebuild or refresh the retrieval index |
+| account data leaks across tenants | enforce tenant filters in every tool and retrieval source |
+| route tests call live providers | test runner and tools with fakes first |
+| history grows too large | summarize, window, or use session memory policy |
+| model calls too many tools | lower `defaultMaxTurns` or split tools by workflow |
+
+## Test Checklist
+
+- Test empty and malformed messages in the runner.
+- Test order lookup allowed, denied, and not found paths.
+- Test conversation history is appended with `response.messages`.
+- Test retrieval filters only return allowed documents.
+- Use Studio to inspect tool calls and retrieved context.
+- Add evals for common support questions and known policy answers.
+
+## Related Docs
+
+- [Request Runners](/docs/best-practices/common-patterns/request-runners)
+- [Context and Memory](/docs/best-practices/common-patterns/context-and-memory)
+- [Tools and Services](/docs/best-practices/common-patterns/tools-and-services)
+
diff --git a/apps/docs/content/docs/best-practices/tool-patterns/dynamic-tool-catalogs.mdx b/apps/docs/content/docs/best-practices/tool-patterns/dynamic-tool-catalogs.mdx
new file mode 100644
index 00000000..18226819
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/tool-patterns/dynamic-tool-catalogs.mdx
@@ -0,0 +1,130 @@
+---
+title: Dynamic Tool Catalogs
+description: Select relevant tools at runtime when a static tool list is too large.
+---
+
+Use dynamic tools when an agent has a large catalog of possible actions but only a small subset should be sent to the model on each turn. This is common for internal platforms, operations agents, research agents, and backoffice assistants with many read-only service tools.
+
+Do not use dynamic tools to hide permission checks. The selected tool still needs to enforce permissions in code.
+
+## Scenario
+
+A support operations agent can inspect orders, refunds, tickets, subscriptions, feature flags, warehouse records, fraud signals, and policy documents. Sending every tool definition on every turn is noisy and expensive. Dynamic tools let Anvia search a tool index with the current prompt and send only the matching definitions.
+
+## When to Use It
+
+| Use dynamic tools | Prefer static tools |
+| --- | --- |
+| many tools compete for the same agent | the agent has a small stable tool set |
+| tools are discoverable by name, description, and schema | every tool is needed in almost every run |
+| the prompt usually needs only a few tools | missing one tool would be more costly than sending all tools |
+| tool catalog can be indexed at startup or deploy time | tools are created entirely per request |
+
+## Architecture Shape
+
+| Layer | Responsibility |
+| --- | --- |
+| `ToolSet` | owns the full catalog and direct test calls |
+| embedding model | embeds provider-facing tool definitions |
+| `createToolIndex(...)` | builds a searchable dynamic tool index |
+| `AgentBuilder.dynamicTools(...)` | selects top matching tools per prompt |
+| tool code | still enforces permissions, tenant scope, and side-effect policy |
+
+## Code Example
+
+```ts
+import { AgentBuilder } from "@anvia/core";
+import { ToolSet, createToolIndex } from "@anvia/core/tool";
+import { embeddingModel, model } from "./models";
+import { createAdminTools, createBillingTools, createSupportTools } from "./tools";
+
+const toolCatalog = ToolSet.fromTools([
+ ...createSupportTools(),
+ ...createBillingTools(),
+ ...createAdminTools(),
+]);
+
+const toolIndex = await createToolIndex(embeddingModel, toolCatalog, {
+ content: (tool, definition) => [
+ definition.name,
+ definition.description,
+ JSON.stringify(definition.parameters),
+ ],
+ metadata: (tool) => ({
+ name: tool.name,
+ domain: tool.name.split("_")[0],
+ }),
+});
+
+export const operationsAgent = new AgentBuilder("operations", model)
+ .instructions("Use the most relevant tool for the user's operational request.")
+ .dynamicTools(toolIndex, {
+ topK: 6,
+ threshold: 0.7,
+ })
+ .defaultMaxTurns(4)
+ .build();
+```
+
+Static tools can still be registered when they should always be available.
+
+```ts
+const agent = new AgentBuilder("operations", model)
+ .tool(createThinkTool())
+ .dynamicTools(toolIndex, { topK: 6, threshold: 0.7 })
+ .build();
+```
+
+## Validate Tool Selection
+
+Test the catalog before testing model behavior. Search the index with representative prompts and assert that the expected tool ids appear.
+
+```ts
+const matches = await toolIndex.searchIds({
+ query: "refund order A-100 because it was duplicated",
+ topK: 6,
+ threshold: 0.7,
+});
+
+expect(matches.map((match) => match.id)).toContain("issue_refund");
+```
+
+Then test the selected tool directly through the catalog.
+
+```ts
+const result = await toolCatalog.call(
+ "issue_refund",
+ JSON.stringify({
+ orderId: "A-100",
+ amount: 25,
+ reason: "duplicate_charge",
+ }),
+);
+
+expect(JSON.parse(result).status).toBe("refunded");
+```
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| model cannot find the right tool | improve tool name, description, schema text, or `content` embedding text |
+| too many unrelated tools are sent | raise `threshold`, lower `topK`, or split catalogs by domain |
+| needed tools are missing | lower `threshold`, add domain words to descriptions, or keep critical tools static |
+| permission leak | enforce user and tenant checks inside every tool |
+| catalog changes at runtime | rebuild the index or use a new indexed catalog for future agents |
+
+## Test Checklist
+
+- Search the index with common prompts and assert expected tool ids.
+- Call high-risk tools directly through `ToolSet.call(...)`.
+- Verify each tool still enforces permissions with fake users and tenants.
+- Use Studio traces to inspect which dynamic tools were sent during real runs.
+- Add evals for prompts where tool selection is part of the expected behavior.
+
+## Related Docs
+
+- [Tool Sets](/docs/guides/tools/tool-sets)
+- [Tools and Services](/docs/best-practices/common-patterns/tools-and-services)
+- [Research Agent](/docs/best-practices/real-cases/research-agent)
+
diff --git a/apps/docs/content/docs/best-practices/tool-patterns/meta.json b/apps/docs/content/docs/best-practices/tool-patterns/meta.json
new file mode 100644
index 00000000..737ffcda
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/tool-patterns/meta.json
@@ -0,0 +1,6 @@
+{
+ "title": "Tool Patterns",
+ "defaultOpen": false,
+ "collapsible": true,
+ "pages": ["dynamic-tool-catalogs", "tool-validation-and-contracts", "side-effect-tools"]
+}
diff --git a/apps/docs/content/docs/best-practices/tool-patterns/side-effect-tools.mdx b/apps/docs/content/docs/best-practices/tool-patterns/side-effect-tools.mdx
new file mode 100644
index 00000000..759ac469
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/tool-patterns/side-effect-tools.mdx
@@ -0,0 +1,135 @@
+---
+title: Side Effect Tools
+description: Safely expose writes, external actions, approvals, idempotency, and audit records.
+---
+
+Side-effect tools change product state: refunds, deletes, emails, status transitions, webhooks, exports, or external API calls. Treat them as product operations first and model-callable tools second.
+
+## Scenario
+
+A backoffice agent can resolve tickets and issue refunds. The model can decide that a refund is appropriate, but product code must enforce permissions, approval policy, idempotency, transactions, and audit records.
+
+## When to Use It
+
+Use this pattern for any tool that:
+
+- writes to your database
+- calls an external write API
+- sends a message or notification
+- changes a workflow state
+- triggers a job
+- exposes restricted data as a side effect
+
+## Architecture Shape
+
+| Layer | Responsibility |
+| --- | --- |
+| tool schema | require bounded, explicit operation inputs |
+| approval metadata or hook | decide whether a human must approve |
+| product service | enforce permissions, transaction, idempotency, and audit |
+| runner | attach actor, tenant, trace, and operation context |
+| storage | persist operation status and audit evidence |
+
+## Code Example
+
+```ts
+import { createTool } from "@anvia/core";
+import { z } from "zod";
+
+export function createRefundTool(scope: RefundToolScope) {
+ return createTool({
+ name: "issue_refund",
+ description: "Issue an approved refund for a paid order.",
+ input: z.object({
+ orderId: z.string().min(1),
+ amount: z.number().positive(),
+ reason: z.string().min(1),
+ }),
+ output: z.object({
+ status: z.enum(["refunded", "blocked"]),
+ operationId: z.string().optional(),
+ reason: z.string().optional(),
+ }),
+ approval: {
+ when: ({ args }) => args.amount >= 100,
+ reason: "Refunds of 100 or more require reviewer approval.",
+ rejectMessage: "Refund was not approved.",
+ },
+ async execute({ orderId, amount, reason }) {
+ const operationId = `refund:${scope.tenantId}:${orderId}:${amount}`;
+
+ return scope.billing.issueRefund({
+ actorId: scope.userId,
+ tenantId: scope.tenantId,
+ orderId,
+ amount,
+ reason,
+ operationId,
+ });
+ },
+ });
+}
+```
+
+Approval metadata is useful for Studio and approval-capable runtimes. Use a hook when approval depends on request-local state or spans multiple tools.
+
+```ts
+import { createHook } from "@anvia/core";
+
+const approvalHook = createHook({
+ async onToolCall({ toolName, tool }) {
+ if (!["issue_refund", "close_account"].includes(toolName)) {
+ return tool.run();
+ }
+
+ const approved = await approvals.waitForDecision({
+ actorId: scope.userId,
+ tenantId: scope.tenantId,
+ toolName,
+ });
+
+ return approved ? tool.run() : tool.cancel("Operation was not approved.");
+ },
+});
+```
+
+## Idempotency and Audit
+
+The model should not invent idempotency keys. Generate them from product state or pass an operation id from the caller.
+
+```ts
+await audit.write({
+ actorId: scope.userId,
+ tenantId: scope.tenantId,
+ action: "refund.issued",
+ targetId: orderId,
+ operationId,
+ traceName: "backoffice-resolution",
+});
+```
+
+Use traces to debug agent behavior. Use audit records for product accountability.
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| duplicate write after retry | idempotency key or durable operation record |
+| model bypasses policy | enforce permissions in service code |
+| approval waits forever | app-owned approval timeout or asynchronous workflow |
+| audit only exists in trace | write product audit records in service code |
+| side effect is hard to test | split tool adapter from service method and fake the service |
+
+## Test Checklist
+
+- Test permission denied before the write executes.
+- Test approval required and rejected paths.
+- Test idempotency by calling the service twice with the same operation id.
+- Test audit records are written for successful side effects.
+- Test runner behavior when the tool throws a transient service error.
+
+## Related Docs
+
+- [Production Guardrails](/docs/best-practices/common-patterns/production-guardrails)
+- [Human in the Loop](/docs/guides/human-in-the-loop)
+- [Backoffice Agent](/docs/best-practices/real-cases/backoffice-agent)
diff --git a/apps/docs/content/docs/best-practices/tool-patterns/tool-validation-and-contracts.mdx b/apps/docs/content/docs/best-practices/tool-patterns/tool-validation-and-contracts.mdx
new file mode 100644
index 00000000..f262e06d
--- /dev/null
+++ b/apps/docs/content/docs/best-practices/tool-patterns/tool-validation-and-contracts.mdx
@@ -0,0 +1,163 @@
+---
+title: Tool Validation and Contracts
+description: Design tool schemas, outputs, and test boundaries that hold up in production.
+---
+
+Tool contracts are the strongest deterministic boundary in an agent harness. Use schemas to validate arguments and outputs, return typed product states for expected outcomes, and test tools directly before model runs.
+
+## Scenario
+
+A model can choose when to call a tool, but the tool owns the contract. The model should not be able to pass arbitrary unvalidated data into your service layer, and downstream code should not need to parse vague natural-language tool results.
+
+## When to Use It
+
+Use this pattern for every tool that reads product state, writes product state, or returns values used by downstream code.
+
+## Architecture Shape
+
+| Layer | Responsibility |
+| --- | --- |
+| Zod input schema | validate model arguments before execution |
+| Zod output schema | validate tool result before it is serialized |
+| tool `execute` | call product services and return typed states |
+| runner | map expected states and thrown errors to product responses |
+| tests | call tools directly with valid and invalid arguments |
+
+## Code Example
+
+```ts
+import { createTool } from "@anvia/core";
+import { z } from "zod";
+
+const lookupOrderOutput = z.discriminatedUnion("status", [
+ z.object({
+ status: z.literal("found"),
+ orderId: z.string(),
+ fulfillmentStatus: z.enum(["processing", "shipped", "delivered"]),
+ }),
+ z.object({
+ status: z.literal("not_found"),
+ orderId: z.string(),
+ }),
+ z.object({
+ status: z.literal("blocked"),
+ reason: z.literal("access_denied"),
+ }),
+]);
+
+export function createLookupOrderTool(scope: OrderToolScope) {
+ return createTool({
+ name: "lookup_order",
+ description: "Look up one order owned by the current customer.",
+ input: z.object({
+ orderId: z.string().min(1),
+ }),
+ output: lookupOrderOutput,
+ async execute({ orderId }) {
+ const allowed = await scope.orders.canRead({
+ userId: scope.userId,
+ tenantId: scope.tenantId,
+ orderId,
+ });
+
+ if (!allowed) {
+ return { status: "blocked" as const, reason: "access_denied" as const };
+ }
+
+ const order = await scope.orders.find(orderId);
+
+ if (!order) {
+ return { status: "not_found" as const, orderId };
+ }
+
+ return {
+ status: "found" as const,
+ orderId,
+ fulfillmentStatus: order.fulfillmentStatus,
+ };
+ },
+ });
+}
+```
+
+## Expected States vs Errors
+
+| Situation | Return state | Throw |
+| --- | --- | --- |
+| record not found | yes | no |
+| user lacks access and the model can continue safely | yes | no |
+| malformed model arguments | schema handles it | no |
+| database unavailable | no | yes |
+| invariant violated | no | yes |
+| downstream service timeout | no | yes |
+
+Expected states are useful model input. Unexpected failures belong to the runner, logs, retries, or product error boundary.
+
+## Direct Tool Tests
+
+```ts
+const tool = createLookupOrderTool({
+ userId: "user_123",
+ tenantId: "tenant_123",
+ orders: fakeOrders,
+});
+
+const result = await tool.call({ orderId: "A-100" });
+
+expect(result).toEqual({
+ status: "found",
+ orderId: "A-100",
+ fulfillmentStatus: "shipped",
+});
+```
+
+Use `ToolSet.call(...)` when you want to exercise JSON parsing and serialized output.
+
+```ts
+const tools = ToolSet.fromTools([tool]);
+
+await expect(
+ tools.call("lookup_order", JSON.stringify({ orderId: "" })),
+).rejects.toThrow();
+```
+
+## Runner Error Mapping
+
+The runner should decide which failures become user-facing product errors.
+
+```ts
+try {
+ const response = await agent.prompt(message).send();
+ return { ok: true as const, output: response.output };
+} catch (error) {
+ if (isTemporaryStorageError(error)) {
+ return { ok: false as const, error: "temporarily_unavailable" };
+ }
+
+ throw error;
+}
+```
+
+## Failure Modes
+
+| Failure | Fix |
+| --- | --- |
+| model keeps passing invalid arguments | tighten description, schema descriptions, or ask for missing data first |
+| downstream code parses prose | return structured tool output or agent output schema |
+| permission errors leak details | return a compact `blocked` state or generic product error |
+| tests only cover provider runs | add direct tool and runner tests with fakes |
+
+## Test Checklist
+
+- Test valid inputs, invalid inputs, and missing required fields.
+- Test permission allowed and denied paths.
+- Test expected states such as `not_found` and `blocked`.
+- Test unexpected service failures at the runner boundary.
+- Inspect tool result text in traces for readability.
+
+## Related Docs
+
+- [Creating Tools](/docs/guides/tools/creating-tools)
+- [Tool Schemas](/docs/guides/tools/tool-schemas)
+- [Testing and Observability](/docs/best-practices/common-patterns/testing-and-observability)
+
diff --git a/apps/docs/content/docs/changelog/anthropic.mdx b/apps/docs/content/docs/changelog/anthropic.mdx
new file mode 100644
index 00000000..c5a351de
--- /dev/null
+++ b/apps/docs/content/docs/changelog/anthropic.mdx
@@ -0,0 +1,63 @@
+---
+title: "@anvia/anthropic"
+description: "Release notes for @anvia/anthropic."
+---
+
+# `@anvia/anthropic`
+
+Anthropic provider adapter for Anvia.
+
+Source: [`packages/providers/anthropic/CHANGELOG.md`](https://github.com/anvia-hq/anvia/blob/main/packages/providers/anthropic/CHANGELOG.md)
+
+## 0.3.1
+
+### Patch Changes
+
+- c9728d4: Update upstream runtime dependencies to their latest compatible releases.
+
+## 0.3.0
+
+### Minor Changes
+
+- e84d775: Clean up the `@anvia/core` public import surface by keeping common app-authoring APIs on the root export, moving advanced APIs to focused subpaths, and exposing runtime agent internals through `@anvia/core/internal/agent` for Anvia integration packages.
+
+### Patch Changes
+
+- Updated dependencies [e84d775]
+ - @anvia/core@0.4.0
+
+## 0.2.0
+
+### Minor Changes
+
+- 09c70f5: Add first-class multimodal tool result support.
+
+ Tools can now return `ToolResultContent[]` directly, or use `ToolOutput.content(...)`, and agent execution will pass structured text/image tool results to model turns instead of JSON-stringifying them. Tool middleware, hooks, observers, stream events, and Studio transcript surfaces keep the existing display string while exposing optional structured result content.
+
+ OpenAI Responses and Anthropic now serialize multimodal tool result images as provider-visible image blocks. Text-only provider fallbacks render image results as media-type placeholders instead of raw base64.
+
+ Update provider and tracing wrapper dependencies to the latest checked upstream releases.
+
+### Patch Changes
+
+- Updated dependencies [09c70f5]
+ - @anvia/core@0.3.0
+
+## 0.1.10
+
+### Patch Changes
+
+- 49e43a3: Update upstream runtime dependencies for Anthropic, Gemini, OpenAI, and Studio.
+
+## 0.1.9
+
+### Patch Changes
+
+- 896ae21: Update upstream provider and runtime dependencies.
+
+## 0.1.8
+
+### Patch Changes
+
+- 1ad360d: Fix Anthropic-compatible streaming tool inputs and update provider dependencies.
+
diff --git a/apps/docs/content/docs/changelog/chroma.mdx b/apps/docs/content/docs/changelog/chroma.mdx
new file mode 100644
index 00000000..9dd1a330
--- /dev/null
+++ b/apps/docs/content/docs/changelog/chroma.mdx
@@ -0,0 +1,43 @@
+---
+title: "@anvia/chroma"
+description: "Release notes for @anvia/chroma."
+---
+
+# `@anvia/chroma`
+
+ChromaDB vector store adapter for Anvia.
+
+Source: [`packages/vector-stores/chroma/CHANGELOG.md`](https://github.com/anvia-hq/anvia/blob/main/packages/vector-stores/chroma/CHANGELOG.md)
+
+## 0.2.0
+
+### Minor Changes
+
+- e84d775: Clean up the `@anvia/core` public import surface by keeping common app-authoring APIs on the root export, moving advanced APIs to focused subpaths, and exposing runtime agent internals through `@anvia/core/internal/agent` for Anvia integration packages.
+
+### Patch Changes
+
+- Updated dependencies [e84d775]
+ - @anvia/core@0.4.0
+
+## 0.1.4
+
+### Patch Changes
+
+- Updated dependencies [b12932d]
+ - @anvia/core@0.3.1
+
+## 0.1.3
+
+### Patch Changes
+
+- Updated dependencies [09c70f5]
+ - @anvia/core@0.3.0
+
+## 0.1.2
+
+### Patch Changes
+
+- Updated dependencies [a0a5def]
+ - @anvia/core@0.2.4
+
diff --git a/apps/docs/content/docs/changelog/core.mdx b/apps/docs/content/docs/changelog/core.mdx
new file mode 100644
index 00000000..76f644be
--- /dev/null
+++ b/apps/docs/content/docs/changelog/core.mdx
@@ -0,0 +1,43 @@
+---
+title: "@anvia/core"
+description: "Release notes for @anvia/core."
+---
+
+# `@anvia/core`
+
+Core runtime primitives for context-aware Anvia agents.
+
+Source: [`packages/core/CHANGELOG.md`](https://github.com/anvia-hq/anvia/blob/main/packages/core/CHANGELOG.md)
+
+## 0.4.0
+
+### Minor Changes
+
+- e84d775: Clean up the `@anvia/core` public import surface by keeping common app-authoring APIs on the root export, moving advanced APIs to focused subpaths, and exposing runtime agent internals through `@anvia/core/internal/agent` for Anvia integration packages.
+
+## 0.3.1
+
+### Patch Changes
+
+- b12932d: Update upstream dependencies for PDF loading, globbing, Langfuse tracing, and pgvector support.
+
+ The PDF loader now destroys the `pdfjs-dist` loading task after reading pages, matching the v6 cleanup API.
+
+## 0.3.0
+
+### Minor Changes
+
+- 09c70f5: Add first-class multimodal tool result support.
+
+ Tools can now return `ToolResultContent[]` directly, or use `ToolOutput.content(...)`, and agent execution will pass structured text/image tool results to model turns instead of JSON-stringifying them. Tool middleware, hooks, observers, stream events, and Studio transcript surfaces keep the existing display string while exposing optional structured result content.
+
+ OpenAI Responses and Anthropic now serialize multimodal tool result images as provider-visible image blocks. Text-only provider fallbacks render image results as media-type placeholders instead of raw base64.
+
+ Update provider and tracing wrapper dependencies to the latest checked upstream releases.
+
+## 0.2.4
+
+### Patch Changes
+
+- a0a5def: Preserve accumulated streamed tool arguments when a provider final response contains an empty tool input.
+
diff --git a/apps/docs/content/docs/changelog/fastembed.mdx b/apps/docs/content/docs/changelog/fastembed.mdx
new file mode 100644
index 00000000..69f9b821
--- /dev/null
+++ b/apps/docs/content/docs/changelog/fastembed.mdx
@@ -0,0 +1,43 @@
+---
+title: "@anvia/fastembed"
+description: "Release notes for @anvia/fastembed."
+---
+
+# `@anvia/fastembed`
+
+FastEmbed embedding model adapter for Anvia.
+
+Source: [`packages/embeddings/fastembed/CHANGELOG.md`](https://github.com/anvia-hq/anvia/blob/main/packages/embeddings/fastembed/CHANGELOG.md)
+
+## 0.2.0
+
+### Minor Changes
+
+- e84d775: Clean up the `@anvia/core` public import surface by keeping common app-authoring APIs on the root export, moving advanced APIs to focused subpaths, and exposing runtime agent internals through `@anvia/core/internal/agent` for Anvia integration packages.
+
+### Patch Changes
+
+- Updated dependencies [e84d775]
+ - @anvia/core@0.4.0
+
+## 0.1.4
+
+### Patch Changes
+
+- Updated dependencies [b12932d]
+ - @anvia/core@0.3.1
+
+## 0.1.3
+
+### Patch Changes
+
+- Updated dependencies [09c70f5]
+ - @anvia/core@0.3.0
+
+## 0.1.2
+
+### Patch Changes
+
+- Updated dependencies [a0a5def]
+ - @anvia/core@0.2.4
+
diff --git a/apps/docs/content/docs/changelog/gemini.mdx b/apps/docs/content/docs/changelog/gemini.mdx
new file mode 100644
index 00000000..7824fd66
--- /dev/null
+++ b/apps/docs/content/docs/changelog/gemini.mdx
@@ -0,0 +1,61 @@
+---
+title: "@anvia/gemini"
+description: "Release notes for @anvia/gemini."
+---
+
+# `@anvia/gemini`
+
+Gemini provider adapter for Anvia.
+
+Source: [`packages/providers/gemini/CHANGELOG.md`](https://github.com/anvia-hq/anvia/blob/main/packages/providers/gemini/CHANGELOG.md)
+
+## 0.2.1
+
+### Patch Changes
+
+- c9728d4: Update upstream runtime dependencies to their latest compatible releases.
+
+## 0.2.0
+
+### Minor Changes
+
+- e84d775: Clean up the `@anvia/core` public import surface by keeping common app-authoring APIs on the root export, moving advanced APIs to focused subpaths, and exposing runtime agent internals through `@anvia/core/internal/agent` for Anvia integration packages.
+
+### Patch Changes
+
+- Updated dependencies [e84d775]
+ - @anvia/core@0.4.0
+
+## 0.1.10
+
+### Patch Changes
+
+- 09c70f5: Add first-class multimodal tool result support.
+
+ Tools can now return `ToolResultContent[]` directly, or use `ToolOutput.content(...)`, and agent execution will pass structured text/image tool results to model turns instead of JSON-stringifying them. Tool middleware, hooks, observers, stream events, and Studio transcript surfaces keep the existing display string while exposing optional structured result content.
+
+ OpenAI Responses and Anthropic now serialize multimodal tool result images as provider-visible image blocks. Text-only provider fallbacks render image results as media-type placeholders instead of raw base64.
+
+ Update provider and tracing wrapper dependencies to the latest checked upstream releases.
+
+- Updated dependencies [09c70f5]
+ - @anvia/core@0.3.0
+
+## 0.1.9
+
+### Patch Changes
+
+- 49e43a3: Update upstream runtime dependencies for Anthropic, Gemini, OpenAI, and Studio.
+
+## 0.1.8
+
+### Patch Changes
+
+- 896ae21: Update upstream provider and runtime dependencies.
+
+## 0.1.7
+
+### Patch Changes
+
+- 1ad360d: Fix Anthropic-compatible streaming tool inputs and update provider dependencies.
+
diff --git a/apps/docs/content/docs/changelog/index.mdx b/apps/docs/content/docs/changelog/index.mdx
new file mode 100644
index 00000000..c66b2e56
--- /dev/null
+++ b/apps/docs/content/docs/changelog/index.mdx
@@ -0,0 +1,73 @@
+---
+title: Package Changelog
+description: Release history for Anvia packages.
+---
+
+Anvia package release notes are generated from the package changelog files maintained by Changesets.
+
+Developers should keep writing release notes with `pnpm changeset`. The docs pages in this section are generated from `packages/**/CHANGELOG.md`.
+
+