Common Patterns

Harness Blueprint

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

LayerOwns
route, server action, job, or queue workertransport parsing, auth entrypoint, response shape, job retry policy
harness runnerone product workflow, request validation, scoped dependencies, trace metadata, error mapping
stable agent factoryagent id, instructions, model behavior, static defaults, reusable observers
request-scoped toolsproduct service calls, permissions, tenant filtering, side-effect safety
context assemblystatic facts, request facts, retrieval results, history, session id
storage and auditconversation history, event stream, memory store, idempotency records, approval decisions
Anvia runtimemodel-tool loop, schemas, hooks, turn limits, tool calls, streaming events, observer events
model providercompletion 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.

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

StepHarness responsibilityAnvia responsibility
validate inputparse payload, reject missing fields, normalize product idsnone
resolve app contextauthenticate, load user, tenant, feature flags, services, transactionnone
load stateread history, session state, idempotency record, relevant product recordsread memory only if configured
create runtimebuild or select agent, create request-scoped tools, add contexthold agent configuration
run promptattach trace metadata, set turn limit, call .send() or .stream()run model-tool loop
handle effectstools enforce permissions, services run transactions and audit writescall tools and return tool results to model
persist resultappend messages, event logs, summaries, metrics, user-visible outputreturn response messages and usage
map responsereturn HTTP body, job result, UI stream, or domain objectnone

Minimal Harness

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

ConcernProduction default
identitystable agent id and trace name
inputvalidate before constructing the prompt
permissionsenforce inside tools and services, not in prompt text
contextattach only scoped, relevant facts
side effectsuse idempotency keys or transactions in service code
persistencesave response.messages and any app-owned audit records
observabilityuse stable traces and safe metadata
failuresmap 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.