Skip to content

v1.0.0 — First stable release

Choose a tag to compare

@thetonymaster thetonymaster released this 19 Jun 05:13
42c530f

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
      and resume_policy / config_template columns. 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 / own Turn.Servers across a cluster (Tier-2).
    • Normandy.Agents.Turn.ResumeReaper — selective eager handoff on
      :nodedown. Because Horde.DynamicSupervisor does 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; a template_provider resolves it. Credentials are never persisted.
    • SessionStore gained save_config_template/3, load_config_template/2, and
      list_resumable/1 (eager session ids); SessionRegistry gained the optional
      child_name/2 ({:via, …}) for atomic, supervisor-driven start that closes the
      start-time race. InMemory / ETS / Native impls were extended to match.
    • Normandy.Cluster.child_specs/1 — one-call wiring of the Horde registry +
      supervisor + reaper (plus an optional libcluster Cluster.Supervisor when
      :topologies are supplied and libcluster is 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 / SessionRegistry seam:
      SessionStore.Mnesia (OTP-native distributed store, transactional appends, no
      external DB), SessionStore.Redis (Redis Streams), SessionRegistry.Redis
      (:via registry using Redis as the name table), and the
      Normandy.Cluster.setup_mnesia_store!/1 / redis_child_specs/1 wiring helpers.
  • Guardrails — pre-charge admission, threaded context, fail-open, semantic scope.

    • Normandy.Agents.BaseAgent.admit/2,3 runs 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/3 threads a caller-supplied context map to guards
      implementing the optional Guard.check/3 callback (check/2-only guards are
      unaffected) — host data a guard needs but the framework must not interpret
      (ids, locale, conversation history).
    • Per-guard :on_error policy: :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 the check call is rescued; a malformed return always raises.
    • Normandy.Guardrails.Builtins.SemanticScope — a provider-agnostic hybrid scope
      guard: a cheap injected fast_path in front of an injected classifier
      ((value, context) -> :allow | {:block, reason}); the :block reason becomes
      the violation's machine-readable :constraint. (#31)
  • Phase 6 — AgentProcess durable turn engine (:server mode).

    • Normandy.Coordination.AgentProcess opt-in :server mode (turn_engine: :server)
      routing turns through the durable Turn.Session/Turn.Server engine: approval
      parking, passivation, and persistence. :inline remains the default and is
      byte-for-byte unchanged.
    • AgentProcess.approve/2 delivers human-approval decisions to a parked turn.
    • Non-blocking :server run/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) from SessionStore in :server mode.
    • Template-only update_agent/2 in :server mode: updates config template
      (model/temperature/behaviours/tools); memory mutations are ignored because
      SessionStore is authoritative.
    • Owned-or-supplied session infra: :store, :registry, :supervisor may be
      passed to start_link; if omitted, the process starts and owns in-memory
      defaults that terminate with it. :subscriber, :handlers,
      :approval_timeout_ms, and :idle_timeout_ms are forwarded to Turn.Session.
  • Phase 5 — compaction wiring (:steering boundary).

    • Normandy.Behaviours.Compactor behaviour (+ NoOp default, opt-in
      WindowManager impl) invoked at the :steering turn boundary when the context
      window is exceeded; compactor slot on Behaviours.Config. (PR #32)

Fixed

  • Flaky Turn.Supervisor.Horde test: a start_server racing the :via
    registration could observe a transient {:error, {:already_started, _}}; the
    test now retries the start through the via race. (#36)
  • convert_turn_output/3 previously returned the empty output-schema struct for
    tool-using turns with non-chat_message output schemas, dropping the final-
    response content. Non-chat_message-schema agents using tools were affected.
  • Normandy.Context.TokenCounter was unusable against the live API: every
    count_message/2,3, count_conversation/2, and count_detailed/2 call sent
    max_tokens in the /v1/messages/count_tokens payload, 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
    retired claude-3-5-sonnet-20241022 to claude-haiku-4-5-20251001. The
    previously-skipped token-counter tests are now enabled as :integration tests
    and pass against the live endpoint.

Migration

  • No action required: :inline is 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.