Agent Contract
The Agent contract is the spine between runtime adapters and chat UI.
@ngaf/chat owns the contract. @ngaf/langgraph and @ngaf/ag-ui produce objects that satisfy it. Chat primitives and compositions consume it without knowing which runtime is behind the stream.
This matters because the UI should not care whether a response came from LangGraph Platform, AG-UI, a local mock, or a custom HTTP service. The boundary is explicit: adapters translate runtime events into Angular signals and a small action surface.
#The Contract Surface
Import the contract from @ngaf/chat:
An Agent has state signals, actions, optional runtime capabilities, and one event stream.
The invariant is simple: durable UI state lives on signals. events$ carries things that are not already derivable from those signals.
#Submit
submit() is the only runtime-neutral way to advance the agent.
The input can carry a new user message, an interrupt resume payload, a state patch, or a combination. The adapter decides how that maps to the backend.
This matters because chat UI can send user intent without learning backend protocol details. LangGraph can turn resume into a command. An AG-UI adapter can call runAgent(). A test mock can just record the call.
The second argument is intentionally small at the contract layer:
Runtime-specific adapters may accept richer options. @ngaf/langgraph exports LangGraphSubmitOptions for LangGraph run configuration such as checkpointing, durability, multitask strategy, and stream mode. Keep app-level code on the neutral shape unless it is intentionally using LangGraph-only behavior.
#Signals
Signals are the stable read model.
| Signal | Meaning |
|---|---|
messages() | Runtime-neutral message history. |
status() | 'idle', 'running', or 'error'. |
isLoading() | Convenience boolean for active generation. |
error() | Last error payload, or null by convention. |
toolCalls() | Tool calls normalized for chat display. |
state() | Backend-defined state snapshot as a plain object. |
interrupt?.() | Current human-in-the-loop pause, if supported. |
subagents?.() | Delegated work keyed by tool-call id, if supported. |
The optional signals are important. A simple echo adapter should not fake interrupts or subagents. Components that need those concepts feature-detect the signal and render a neutral fallback when it is absent.
#Events
events$ is required, but it can be EMPTY.
Current event variants are deliberately narrow:
| Event | Purpose |
|---|---|
state_update | Sync state intended for render/generative UI stores. |
custom | Runtime-specific escape hatch with name and data. |
Do not mirror messages, status, toolCalls, interrupt, or subagents through events$. Put those on signals. Duplicating state creates ordering bugs and makes components guess which source wins.
#Adapters And Transports
@ngaf/langgraph exports agent(), provideAgent(), FetchStreamTransport, MockAgentTransport, and AgentTransport.
agent() is the Angular adapter. It connects to LangGraph, consumes stream events through a transport, and returns a LangGraphAgent. That object satisfies the neutral Agent contract and also exposes LangGraph-specific signals such as raw LangGraph messages, history, queue, branch state, and checkpoint helpers.
AgentTransport is lower level. Use it when the backend is LangGraph-compatible but the transport needs to change: custom fetch behavior, tests, local fixtures, or a nonstandard gateway.
@ngaf/ag-ui exports toAgent(), provideAgUiAgent(), injectAgUiAgent(), FakeAgent, and provideFakeAgUiAgent().
toAgent() wraps an AG-UI AbstractAgent into the same neutral contract. The AG-UI adapter reduces AG-UI events into chat signals, appends user messages on submit, calls runAgent(), and maps stop to abortRun().
#Lifecycle
The UI lifecycle is intentionally boring.
- User input calls
submit({ message }). - Adapter marks the run active through
status()andisLoading(). - Runtime events update
messages(),toolCalls(),state(), and optional signals. - Chat components re-render from signals.
stop()aborts the active run when supported.regenerate(index)rolls back from an assistant message and reruns from the preceding user message.
LangGraph adds deeper lifecycle and history surfaces. @ngaf/langgraph exposes agent().lifecycle and exports AgentLifecycle, AgentLifecycleRegistry, and the low-level AGENT_LIFECYCLE token. Those are useful for telemetry, debugging, persistence, and time-travel UI. They are not required by @ngaf/chat.
#Testing And Mocks
Use the closest mock to the boundary you are testing.
For chat components, use mockAgent() from @ngaf/chat. It gives writable signals and records submit() calls.
For LangGraph-specific code, use mockLangGraphAgent() or MockAgentTransport from @ngaf/langgraph. That lets you test queue, checkpoint, raw SDK, and transport behavior without pretending those details exist on every adapter.
For AG-UI integration, use FakeAgent or provideFakeAgUiAgent() from @ngaf/ag-ui.
This matters because the contract is the seam you can test cheaply. Most component tests should not need a network, LangGraph server, or AG-UI runtime.
#What It Is Not
The Agent contract is not a backend protocol. It does not define SSE frames, AG-UI event names, LangGraph thread state, or tool execution semantics.
It is not a message database. Persistence belongs to the runtime or application state, then gets projected back through signals.
It is not a full orchestration API. LangGraph-specific operations such as branch selection, queued runs, checkpoint history, and raw SDK messages stay on LangGraphAgent.
It is not a UI renderer. Rendering belongs to @ngaf/chat, @ngaf/render, and A2UI surfaces. The agent only exposes the state and events those renderers need.
Keep that boundary sharp. It makes adapters replaceable, tests smaller, and chat components more predictable.