Atlas now runs on Pydantic AI (Story-1288 / ADR-117). The general telemetry lane — CloudWatch + X-Ray via the single ADOT layer (ADR-104) — sees Lambda/AgentCore runtime metrics, but it is blind to the inside of an LLM call: the exact prompt, the model + provider chosen, the token counts, the latency, the structured output. When Atlas fabricates a clause or stalls, that lane gives us a flat span and no answer.
The naïve fix — bolt prompt/response logging into the runtime by hand — detonates on two doctrines: it would re-implement an observability path the platform already standardises (ADR-104 says ONE general-tracing system), and it risks dumping privileged prompt text into the wrong place. The decision (ADR-118): a dedicated LLM lane — Pydantic Logfire — that natively instruments Pydantic AI and ships prompt/model/token/latency spans to a Logfire project, leaving the general lane untouched.
This Story is the agents-side wiring only: one env-gated init hook. No behaviour change, no new truth, no runtime-shape change.
cold start (main.py)
│ configure_observability() ← the one hook, called once
▼
LOGFIRE_TOKEN set? ──no──► NO-OP (no configure, no instrument, no egress)
│ yes local dev + CI are byte-identical here
▼
logfire.configure(token, service_name="atlas",
advanced=AdvancedOptions(base_url="https://logfire-us.pydantic.dev"),
send_to_logfire="if-token-present", console=False)
logfire.instrument_pydantic_ai() ← instruments ALL five Atlas agents
▼
every agent.run / run_stream → prompt · model · provider · tokens · latency span
▼
OTLP egress → Logfire US datacenter project
One new module (observability.py), one call site (main.py cold start), one dep (logfire). The brain's request/response contracts, events, and truth boundary are frozen.
| Piece | What & WHY |
|---|---|
| Env gate | os.getenv("LOGFIRE_TOKEN") is the only input. Absent → return early, nothing imported, nothing configured. WHY: local dev + CI must run with zero Logfire setup and be indistinguishable from a Logfire-free build (acceptance criterion + ADR-118). |
| US datacenter pin | AdvancedOptions(base_url="https://logfire-us.pydantic.dev"). WHY: Atlas's spans must land in the US project explicitly (ADR-118), not wherever the SDK's account default points. |
| Global instrumentation | logfire.instrument_pydantic_ai() — one hook covers all five Atlas agents (intake · reflect · explain · reframe · staleness) across both the Bedrock and OpenRouter provider paths (ADR-116). WHY: Pydantic AI is the inference framework (ADR-117), so instrumenting it once captures every model call without per-call code. |
| Token hygiene | Read from env only; never a source literal; never passed to a log call. A dedicated test asserts the token string is absent from captured logs. WHY: a write token is a secret (acceptance criterion). |
| Never blocks Atlas | The whole body is wrapped: any SDK/setup failure → _log.warning + return False, never a raise. WHY: ADR-110 — the brain never blocks the matter; observability is strictly best-effort. |
| Idempotency guard | Module-level _configured flag; a warm-container second cold-start hook is a no-op. WHY: AgentCore reuses warm containers; configure exactly once per process. |
console=False | No stdout exporter inside the runtime. WHY: spans go to Logfire; ordinary logs stay on the CloudWatch lane (ADR-104) — the two lanes don't cross-contaminate. |
| Deps + locks | logfire>=3.0 added to agents/atlas/pyproject.toml + requirements.in; requirements.lock (hashed) and agents/atlas/uv.lock regenerated (resolves logfire 4.37.0). |
The import logfire lives inside the token branch, not at module top. So with no token the SDK is never even imported — the no-op path costs nothing and a Logfire-free environment never trips over a missing/heavy dependency at cold start. The test injects a fake logfire into sys.modules and asserts it is never touched on the no-token path.
token = os.getenv("LOGFIRE_TOKEN")
if not token:
_log.debug("Logfire disabled: …")
return False
# logfire is never imported hereimport logfire
logfire.configure(
token=token, service_name="atlas",
advanced=logfire.AdvancedOptions(
base_url="https://logfire-us.pydantic.dev"),
send_to_logfire="if-token-present",
console=False)
logfire.instrument_pydantic_ai()The point: token absent ⇒ nothing changes; token present ⇒ spans only. A new test module pins every constraint, and the existing Atlas suite stays green.
| Invariant (Story-1291 / ADR-118 constraint) | Proof |
|---|---|
| No token → no-op (configure + instrument never called) | test_no_token_is_a_noop: a fake logfire SDK is present yet never touched; returns False. |
Token → US datacenter + service_name="atlas" + instrument | test_token_configures_us_datacenter_and_instruments: asserts base_url=https://logfire-us.pydantic.dev, service_name, send_to_logfire="if-token-present", console=False, and instrument_pydantic_ai() called once. |
| Optional deploy-env tag | test_environment_tag_forwarded_when_set: LOGFIRE_ENVIRONMENT forwarded only when set; omitted otherwise. |
| Configure once (warm-container safe) | test_configure_is_idempotent: second call is a no-op; one configure, one instrument. |
| Token never logged | test_token_is_never_logged: the secret string is absent from all captured log output. |
| Observability never blocks Atlas | test_sdk_failure_never_blocks: a throwing configure() returns False, never raises. |
| Brain doctrine (ADR-110 §6) | Logfire is egress-only — no DB/queue/bucket, no boto3 s3·sqs·dynamodb, no run-truth change. Existing event-stream / runtime / reflection tests unchanged and green. |
cd agents/atlas && pytest tests -q → 51 passed (45 prior + 6 new). ruff check agents/ → clean. Without LOGFIRE_TOKEN: Atlas behaves byte-identically to before this Story.LOGFIRE_TOKEN into the AgentCore runtime env. Until that lands, staging runs the no-op path — correct and safe. This closes the open follow-up flagged in the Story-1288 walkthrough (wiring structured LLM logging into the runtime).