Common Patterns

Production Guardrails

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.

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.

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.

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.

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();
  },
});
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.

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.

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.

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

RiskGuardrail
endless tool loop.defaultMaxTurns(...) and request .maxTurns(...)
restricted readpermission check in tool or service code
restricted writepermission check, hook, approval runtime, audit record
human approval requiredapplication-owned approval runtime called from a hook
caller needs bounded latencyapp-level timeout around the runner
duplicate side effectidempotency key or transaction in the service layer
provider or transport failureretry at the application boundary when safe
sensitive operationproduct audit record plus trace metadata