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.

LangGraph Platform -- @ngaf/langgraph --+
                                        +-- Agent -- @ngaf/chat
AG-UI backend ------ @ngaf/ag-ui -------+
 
custom backend ------ your adapter -----+

#The Contract Surface

Import the contract from @ngaf/chat:

import type { Agent } from '@ngaf/chat';

An Agent has state signals, actions, optional runtime capabilities, and one event stream.

interface Agent {
  messages: Signal<Message[]>;
  status: Signal<'idle' | 'running' | 'error'>;
  isLoading: Signal<boolean>;
  error: Signal<unknown>;
  toolCalls: Signal<ToolCall[]>;
  state: Signal<Record<string, unknown>>;
 
  submit(input: AgentSubmitInput, opts?: AgentSubmitOptions): Promise<void>;
  stop(): Promise<void>;
  regenerate(assistantMessageIndex: number): Promise<void>;
 
  interrupt?: Signal<AgentInterrupt | undefined>;
  subagents?: Signal<Map<string, Subagent>>;
 
  events$: Observable<AgentEvent>;
}

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.

await agent.submit({ message: 'Explain this trace.' });
 
await agent.submit({
  resume: { approved: true },
  state: { reviewer: 'Ada' },
});

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:

interface AgentSubmitOptions {
  signal?: AbortSignal;
}

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.

SignalMeaning
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.

import { EMPTY } from 'rxjs';
import type { AgentEvent } from '@ngaf/chat';
 
const events$ = EMPTY as Observable<AgentEvent>;

Current event variants are deliberately narrow:

EventPurpose
state_updateSync state intended for render/generative UI stores.
customRuntime-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.

  1. User input calls submit({ message }).
  2. Adapter marks the run active through status() and isLoading().
  3. Runtime events update messages(), toolCalls(), state(), and optional signals.
  4. Chat components re-render from signals.
  5. stop() aborts the active run when supported.
  6. 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.

import { mockAgent } from '@ngaf/chat';
 
const agent = mockAgent({
  messages: [{ id: 'm1', role: 'assistant', content: 'Ready' }],
  withInterrupt: true,
});

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.