ADR-0031: Treat externally controlled chat as an application seam
ADR-0031: Treat externally controlled chat as an application seam
Context
Some product hosts already own a trusted session transport and receive complete chat snapshots from another process. Requiring those hosts to instantiate a second client controller creates two competing lifecycle owners. Copying the AgentsKit Chat presentation into each host would instead create renderer drift.
The framework needs a controlled mode that accepts host state without becoming a second controller, transport, persistence layer, or authorization boundary.
Decision
@agentskit/chat exposes a framework-neutral controlled driver. Its input is a
bounded serializable snapshot plus callbacks typed as a Pick<ChatReturn> over
the existing AgentsKit lifecycle. The snapshot shell is strict and messages are
validated by validateMemoryRecord from @agentskit/core/memory-validation
before ISO timestamps are hydrated.
The driver projects the validated state and callbacks into the existing
ChatReturn presentation contract. It does not create a controller, consume an
adapter stream, persist messages, or translate lifecycle semantics.
The React shell accepts either its existing definition-owned mode or the
controlled source. Separate acquisition components preserve React hook rules:
the definition-owned component calls upstream useChat; the controlled
component never calls it. Both feed one presentation component and reuse the
same component registry, theming, lifecycle controls, and action confirmation
coordinator.
The host remains responsible for authentication, authorization, transport, persistence, snapshot freshness, and applying every callback result to its source of truth. Invalid snapshots fail closed before presentation.
Guardrails
- Do not add a local controller, reducer, adapter stream consumer, or message store to the controlled driver.
- Do not mirror the canonical AgentsKit message schema; delegate it upstream.
- Keep host credentials, topology, business rules, and private state out of the public snapshot.
- Framework bindings may adapt reactivity but must consume this same driver and their existing presentation path.
- A missing reusable primitive must be added and released in AgentsKit before downstream integration.
Consequences
Hosts with server-owned sessions can adopt the shared application shell without creating a competing runtime. Existing hosts retain the original API and behavior. Controlled hosts must rerender with a new snapshot after callbacks; the driver deliberately performs no optimistic state mutation.
Alternatives considered
- Instantiate a hidden controller from the snapshot. Rejected because the host and client would both own lifecycle and persistence.
- Create a separate controlled React renderer. Rejected because component, accessibility, theming, and lifecycle presentation would drift.
- Accept canonical
Message[]without runtime validation. Rejected because snapshots cross an external boundary and dates are serialized. - Copy the upstream message schema locally. Rejected because it would fork the AgentsKit contract and create version drift.
Acceptance
HITL approval to implement the controlled-session slice and its upstream-first guardrails was recorded on 2026-07-14.