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
| 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 for writes, approvals, idempotency, and audit records.
- Use Backoffice Agent for admin workflows with approval-heavy operations.
- Use Production Readiness Checklist before deploying a harness.
