v1.0.0 — First stable release
Added
-
Phase 7 — distributed multi-node sessions (Tiers 0/1/2 + eager resume).
Normandy.Behaviours.SessionStore.Postgres— durable session store over
Ecto/Postgres (entries, opaque turn state, config template), with migrations
andresume_policy/config_templatecolumns. The Tier-1 durable store.Normandy.Behaviours.SessionRegistry.Horde(:via,members: :auto) and
Normandy.Agents.Turn.Supervisor.Horde— CRDT-backed distributed registry +
dynamic supervisor that route to / ownTurn.Servers across a cluster (Tier-2).Normandy.Agents.Turn.ResumeReaper— selective eager handoff on
:nodedown. BecauseHorde.DynamicSupervisordoes not redistribute a dead
node's children, the reaper restarts the eager, unregistered, non-terminal
sessions whose server died with the lost node. Lazy rehydrate (route →
whereis→ rehydrate-on-demand) needs no reaper.Normandy.Behaviours.AgentTemplate+ a persisted config template
(Normandy.Agents.Turn.ConfigTemplate): the non-secret config
(model/temperature/behaviour refs/tools) needed to reconstruct an agent on
rehydration; atemplate_providerresolves it. Credentials are never persisted.SessionStoregainedsave_config_template/3,load_config_template/2, and
list_resumable/1(eager session ids);SessionRegistrygained the optional
child_name/2({:via, …}) for atomic, supervisor-driven start that closes the
start-time race.InMemory/ETS/Nativeimpls were extended to match.Normandy.Cluster.child_specs/1— one-call wiring of the Horde registry +
supervisor + reaper (plus an optionallibclusterCluster.Supervisorwhen
:topologiesare supplied andlibclusteris loaded).- Tier model: Tier-0 in-memory/ETS single-node default (unchanged);
Tier-1 durable store + lazy rehydrate; Tier-2 distributed
registry/supervisor + eager reaper. - Drop-in backends behind the same
SessionStore/SessionRegistryseam:
SessionStore.Mnesia(OTP-native distributed store, transactional appends, no
external DB),SessionStore.Redis(Redis Streams),SessionRegistry.Redis
(:viaregistry using Redis as the name table), and the
Normandy.Cluster.setup_mnesia_store!/1/redis_child_specs/1wiring helpers.
-
Guardrails — pre-charge admission, threaded context, fail-open, semantic scope.
Normandy.Agents.BaseAgent.admit/2,3runs input guardrails as a pre-charge
filter (no turn, memory, or circuit breaker), returning
:ok | {:block, violations}instead of raising — reject disallowed input
before paying for a turn.Normandy.Guardrails.run/3threads a caller-suppliedcontextmap to guards
implementing the optionalGuard.check/3callback (check/2-only guards are
unaffected) — host data a guard needs but the framework must not interpret
(ids, locale, conversation history).- Per-guard
:on_errorpolicy::reraise(default — a config bug stays a crash),
:open(rescue the guard's raise and treat as a pass, for a guard fronting a
flaky external service),:closed(rescue and turn it into a:guard_error
violation). Only thecheckcall is rescued; a malformed return always raises. Normandy.Guardrails.Builtins.SemanticScope— a provider-agnostic hybrid scope
guard: a cheap injectedfast_pathin front of an injectedclassifier
((value, context) -> :allow | {:block, reason}); the:blockreason becomes
the violation's machine-readable:constraint. (#31)
-
Phase 6 — AgentProcess durable turn engine (
:servermode).Normandy.Coordination.AgentProcessopt-in:servermode (turn_engine: :server)
routing turns through the durableTurn.Session/Turn.Serverengine: approval
parking, passivation, and persistence.:inlineremains the default and is
byte-for-byte unchanged.AgentProcess.approve/2delivers human-approval decisions to a parked turn.- Non-blocking
:serverrun/3/cast/3: the GenServer stays responsive while
a turn is parked awaiting approval or passivated. - Store-authoritative
get_agent/1: reconstructs agent (including conversation
memory) fromSessionStorein:servermode. - Template-only
update_agent/2in:servermode: updates config template
(model/temperature/behaviours/tools); memory mutations are ignored because
SessionStoreis authoritative. - Owned-or-supplied session infra:
:store,:registry,:supervisormay be
passed tostart_link; if omitted, the process starts and owns in-memory
defaults that terminate with it.:subscriber,:handlers,
:approval_timeout_ms, and:idle_timeout_msare forwarded toTurn.Session.
-
Phase 5 — compaction wiring (
:steeringboundary).Normandy.Behaviours.Compactorbehaviour (+NoOpdefault, opt-in
WindowManagerimpl) invoked at the:steeringturn boundary when the context
window is exceeded;compactorslot onBehaviours.Config. (PR #32)
Fixed
- Flaky
Turn.Supervisor.Hordetest: astart_serverracing the:via
registration could observe a transient{:error, {:already_started, _}}; the
test now retries the start through the via race. (#36) convert_turn_output/3previously returned the empty output-schema struct for
tool-using turns with non-chat_messageoutput schemas, dropping the final-
response content. Non-chat_message-schema agents using tools were affected.Normandy.Context.TokenCounterwas unusable against the live API: every
count_message/2,3,count_conversation/2, andcount_detailed/2call sent
max_tokensin the/v1/messages/count_tokenspayload, which the endpoint
rejects (400 invalid_request_error: "max_tokens: Extra inputs are not permitted"). The field is now omitted. The default model also moved off the
retiredclaude-3-5-sonnet-20241022toclaude-haiku-4-5-20251001. The
previously-skipped token-counter tests are now enabled as:integrationtests
and pass against the live endpoint.
Migration
- No action required:
:inlineis the default and is byte-for-byte unchanged. - To adopt the durable engine:
AgentProcess.start_link(agent: config, turn_engine: :server), optionally
passing shared:store/:registry/:supervisor.