Best Practices

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

NeedPattern
Understand the standard agent harness boundaryHarness Blueprint
Keep stable agent config separate from request stateAgent Structure
Wrap one route, job, or queue requestRequest Runners
Wrap product services as safe model-callable actionsTools and Services
Assemble instructions, facts, retrieval, history, and sessionsContext and Memory
Choose one agent, nested agents, or a typed pipelinePipeline
Add limits, approvals, idempotency, retries, and audit recordsProduction Guardrails
Test deterministic boundaries and inspect model behaviorTesting and Observability

Tool Patterns

Real problemPattern
The agent has dozens or hundreds of toolsDynamic Tool Catalogs
Tool arguments, outputs, and product states need contractsTool Validation and Contracts
A tool writes data, sends messages, or changes external stateSide Effect Tools

Long Process Pipelines

NeedPattern
Run Anvia pipelines outside the request path with BullMQ and RedisOverview
Enqueue validated pipeline work from an API boundaryEnqueue Pipeline Jobs
Run pipeline jobs from a separate BullMQ workerRun Pipeline Workers
Persist status, handle retries, and test long-running jobsStatus, Retries, and Testing

Multi-agent Patterns

NeedPattern
Stream coordinator output and nested specialist progressOverview
Build specialist agents and expose them as streaming toolsBuild Streaming Specialists
Group parent and child stream events in a UIConsume Nested Events
Persist, replay, and test multi-agent streamsPersistence and Testing

MCP Patterns

Real problemPattern
You need to connect and reconnect external MCP serversMCP Server Lifecycle
You need to inspect, validate, filter, or wrap MCP toolsMCP Tool Inspection
You need an agent harness that combines app tools and MCP toolsMCP Agent Harness

Knowledge Patterns

Real problemPattern
You need to build or refresh a retrieval index safelyRAG Ingestion
You need to decide how retrieval enters an agent runRAG Agent Context

Real Cases

SystemPattern
Customer support chat with account tools, retrieval, and historySupport Agent
Backoffice workflow with writes, approvals, audit, and idempotencyBackoffice Agent
Research workflow with many read-only tools and extractionResearch Agent
Coding assistant over a codebase with file, command, patch, and git boundariesCoding Agent

Quality and Observability

NeedPattern
Build regression checks for prompts, tools, and workflowsEval Strategy
Connect traces, tool calls, retrieval evidence, logs, and eval scoresTracing and Debugging

Operations

NeedPattern
Check whether a harness is ready for productionProduction 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
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.