Production checklist
A pre-ship checklist for an agent that real users will reach. Work through it before the first production deploy, and again before releases that change the agent loop, memory, or security settings.
Reliability
- Durable runtime:
runtime=SqliteRuntime(...)orPostgresRuntime(...), with a fixedsession_idper job, so crashes don’t lose work. Addtuning=Tuning(checkpoint=True)to resume long runs withagent.resume(). See Checkpoints. Journal values are JSON; rows pickled by releases before 0.17 are skipped (those steps run again) unless you passserializer="pickle", which is safe only for a store no untrusted party can write. - Persistent memory: pass a URL,
memory="sqlite:./bot.db"for single-instance,memory="postgres://..."/memory="redis://..."for multi-instance. Not the default"inmemory"which loses everything on exit. - Multi-tenancy: pass
user_id=andsession_id=to everyagent.run. Memory partitions automatically; no app-side namespace plumbing. - Per-user budget caps:
BudgetConfig(per_user_max_tokens=, per_user_max_cost_usd=)so one tenant can’t exhaust another’s quota. See Per-user budget caps. - Bounded in-process state:
StandardBudgetandInMemoryMemorydefault to 100k users + 24h idle TTL. For known smaller tenant pools, lowermax_usersto reclaim memory faster. - Auto fact extraction: on by default for real models; facts the
user tells the bot persist as structured triples for future runs to
recall. Pass
auto_extract=Falseto opt out. Extraction runs in the background, so callawait agent.aclose()at shutdown to let it finish. - Budget:
StandardBudgetwithmax_tokens,max_cost_usd,max_wall_clock. Soft warnings at 80%. The wall clock totals time spent running (idle time doesn’t count); cap a single run withAgent(timeout=). - Max turns cap: default 50; lower if your tools are expensive.
- Rate limits:
rate_limiter=TokenBucketRateLimiter(...)when many tenants share an agent. See Rate limiting.
Observability
- Telemetry:
OTelTelemetrywired to your existing TracerProvider. At minimum, surfaceloom.session.duration_ms,loom.tokens.input/output,loom.cost.usd,loom.budget.exceeded,loom.auto_extract.duration_ms,loom.auto_extract.invocations(last two appear whenauto_extractis on), or the GenAIgen_ai.client.inference.*andgen_ai.execute_tool.durationmetrics if your backend charts those. Metrics carry nouser_idorsession_idtags (unbounded cardinality); per-session breakdowns come from the spans’session_id, per-user ones from the audit log. Dashboards keyed on OTel span names should usechat {model}/execute_tool {tool}/invoke_agent, or theloom.span.nameattribute. - Audit log:
FileAuditLogwith a real HMAC secret. Every tool call and run-lifecycle transition lands here, attributed byuser_id(top-level field; HMAC includes it). - Streaming: expose
stream()so a UI / log pipeline can follow the loop in real time. - Multi-tenant load test: run
bench/multi_tenant.pybefore any release that touches the agent loop, memory, or budget. Catches isolation regressions that unit tests miss.
Security
- Permission policy:
StandardPermissions(mode=Mode.DEFAULT)for interactive use;BYPASSonly in CI / sandbox. For per-tenant policy routing, usePerUserPermissions(policies=, default=). - Approval handler: when destructive tools live behind
PermissionDecision.ask_(...), wireAgent(approval_handler=callable)so the gate routes to a human, Slack, or a ticket queue. Without one,askfalls back to deny; it is never silently allowed. ReturnTrue, an action name ("allow","deny", …) or anApprovalDecision; anything else ("yes",{"approved": True}) denies. (Before 0.17 any truthy value approved.) See Approval handlers. - Filesystem sandbox: wrap any tool that touches the FS. Declare the allowed roots explicitly. See Sandboxes.
- Pre-tool hooks:
@agent.before_toolfor any tool that sends external messages (email, Slack, etc.). A hook that raises or runs past its timeout denies the call. - Secrets:
Agent(tuning=Tuning(secrets=EnvSecrets()))is the default; for vault-backed lookup pass a customSecretsadapter. Usesecrets.redact(text)before logging tool args / payloads so API keys don’t leak into the audit log.
Memory
- Embedder: a real one (
OpenAIEmbedder,VoyageEmbedder,CohereEmbedder, or a custom embedder) for production.HashEmbedderis for tests and zero-key dev only. - Auto-consolidate:
Agent(..., tuning=Tuning(auto_consolidate=True))if you want facts extracted automatically. Otherwise callawait agent.consolidate()on a cadence. - Fact store: explicit (
with_facts=Trueon the memory factory, or passfact_store=...). Don’t rely on the in-memory default in production.
Testing
- Test with ScriptedModel for deterministic multi-turn
scenarios.
EchoModelfor the simplest smoke tests. - Fake the embedder with a small class of your own that maps specific texts to specific vectors when you need to assert on ranking.
- Use the in-memory backends in tests (
InMemoryMemory,InMemoryFactStore,InMemoryAuditLog,InMemoryJournalStore) so tests are fast and hermetic. - Skip live-integration tests with env-var gates, for example
@pytest.mark.skipif(not os.environ.get("TEST_PG_DSN"), reason="no Postgres").
Last updated on