AI Filters & Guardrails
Inspect and block the response path — PII redaction + drift
detection. /tutorial/12-ai-filters/
When an agent asks “what’s the user’s current plan tier?”, a language model will cheerfully make one up. When it asks “what time is it?”, it’ll guess. The fix is not a better prompt — it’s a companion deterministic layer that supplies the ground truth on every turn.
FactResolver is that companion. You implement it, you declare which
keys you supply, and Atmosphere prepends the resolved bundle to the
system prompt before every dispatch.
Atmosphere ships DefaultFactResolver out of the box. It supplies
time.now (UTC ISO-8601) and time.timezone only. With no extra
wiring, every turn’s system prompt starts with:
Grounded facts (deterministic, as of this turn):- time.now: 2026-04-19T18:32:14Z- time.timezone: UTCThat alone closes the “what year is it?” class of hallucination.
Apps typically want richer facts — user name, locale, plan tier,
feature-flag values, recent audit events. Implement FactResolver
and supply any subset of keys from FactKeys plus your own app.*
keys.
public final class UserProfileFactResolver implements FactResolver {
private final ProfileService profiles; private final FeatureFlags flags; private final AuditLog audit;
@Override public FactBundle resolve(FactRequest req) { var out = new LinkedHashMap<String, Object>();
if (req.keys().contains(FactKeys.USER_NAME)) { profiles.lookup(req.userId()) .ifPresent(p -> out.put(FactKeys.USER_NAME, p.name())); } if (req.keys().contains(FactKeys.USER_LOCALE)) { out.put(FactKeys.USER_LOCALE, profiles.lookup(req.userId()) .map(Profile::locale) .orElse("en-US")); } if (req.keys().contains(FactKeys.USER_PLAN_TIER)) { out.put(FactKeys.USER_PLAN_TIER, profiles.lookup(req.userId()) .map(Profile::planTier) .orElse("free")); } // Custom app key — the framework treats unknown keys transparently. if (req.keys().contains("app.recent_order_id")) { profiles.lastOrder(req.userId()) .ifPresent(o -> out.put("app.recent_order_id", o.id())); } return new FactBundle(out); }}Three ways to install, in priority order:
@Configurationclass FactResolverConfig { @Bean FactResolver productionResolver(ProfileService profiles, FeatureFlags flags, AuditLog audit) { return new UserProfileFactResolver(profiles, flags, audit); }}AtmosphereAiAutoConfiguration.atmosphereFactResolverBridge publishes
the bean onto framework.properties() under
FactResolver.FACT_RESOLVER_PROPERTY;
AiEndpointHandler.resolveFactResolver finds it first.
Drop a file at
META-INF/services/org.atmosphere.ai.facts.FactResolver containing
your resolver’s fully-qualified class name. The handler’s
ServiceLoader.load(FactResolver.class).findFirst() picks it up.
FactResolverHolder.install(new UserProfileFactResolver(...));The process-wide holder is the lowest priority fallback — mostly
useful for test setup via @BeforeAll + FactResolverHolder.reset()
in @AfterEach.
The resolver’s bundle is rendered as a newline-delimited block and
prepended to the system prompt. A request from alice@example.com on
a paid tier sees, at the top of the system prompt:
Grounded facts (deterministic, as of this turn):- time.now: 2026-04-19T18:32:14Z- time.timezone: UTC- user.id: alice@example.com- user.name: Alice Martin- user.locale: en-US- user.plan_tier: enterprise- app.recent_order_id: ord-7821
[then whatever the developer's @AiEndpoint systemPrompt said]AI Filters & Guardrails
Inspect and block the response path — PII redaction + drift
detection. /tutorial/12-ai-filters/
Tag agent calls with business outcomes
Sibling primitive for observability tagging via SLF4J MDC.
/tutorial/27-business-metadata-observability/
DefaultFactResolver source