Writing an Adapter
Learn how to implement a custom Agent adapter so @ngaf/chat components work with any backend — whether that is a custom RPC service, an in-process mock, or an exotic streaming protocol.
When to Write Your Own Adapter
@ngaf/langgraph covers LangGraph backends and @ngaf/ag-ui covers any AG-UI-compatible backend. Everything else needs a hand-rolled adapter. Common scenarios:
- Custom RPC or HTTP API — your backend speaks neither LangGraph Server nor the AG-UI protocol.
- In-process logic — you want the chat UI without any network call (demos, playgrounds, offline-first apps).
- Testing — a deterministic in-process adapter is faster and more reliable than hitting a real agent in unit tests.
- Exotic transports — WebSockets, gRPC-Web, or any other streaming mechanism.
The Contract
Every @ngaf/chat primitive and composition accepts an Agent object. The type lives in @ngaf/chat and is intentionally runtime-neutral — it says nothing about HTTP, LangGraph, or any specific backend.
An Agent is a set of Angular signals (reactive state) plus an RxJS observable of events, a submit method to send a message or resume an interrupted run, and a stop method to abort the in-flight run.
Field-by-Field Reference
| Field | Type | What you supply |
|---|---|---|
messages | Signal<Message[]> | A signal of the conversation messages so far |
status | Signal<AgentStatus> | 'idle' | 'running' | 'error' |
isLoading | Signal<boolean> | true while a run is in flight |
error | Signal<unknown> | Last error, or null |
toolCalls | Signal<ToolCall[]> | Tool invocations and their results |
state | Signal<Record<string, unknown>> | Backend-defined state snapshot |
events$ | Observable<AgentEvent> | Discriminated state_update / custom events |
submit | (input, opts?) => Promise<void> | Send a message or resume |
stop | () => Promise<void> | Abort the in-flight run |
interrupt? | Signal<AgentInterrupt | undefined> | (optional) Current pause-for-input |
subagents? | Signal<Map<string, Subagent>> | (optional) Spawned subagents |
interrupt and subagents are optional. Runtimes that do not support these concepts can leave them undefined. Components that need them gracefully fall back when they are absent.
The design invariant is: state lives on signals; events$ carries only things that are not derivable from signals. If your runtime produces no custom events, set events$ to EMPTY from RxJS — the type system requires the field to be present, but nothing forces you to emit.
Worked Example: An In-Process Echo Adapter
Below is a complete EchoAgent factory — roughly 80 lines — that satisfies the full Agent contract without any network call. It demonstrates the signal pattern clearly and is a solid starting point for your own adapter.
On submit, the factory optimistically appends the user message, then after a short delay appends an assistant message that echoes the input back. There are no tool calls, no custom events, and no interrupts.
Wiring It Into a Component
The cleanest approach is to register your factory behind an Angular injection token and inject it into your component.
provideAgent() and agent() are LangGraph-specific. When you bring your own adapter, skip them entirely — inject your token directly.
Validating with the Conformance Suite
@ngaf/chat ships a conformance helper that checks every contract field and a handful of semantic invariants (for example, isLoading() must only be true when status() === 'running'). Run it against your factory in a Vitest spec:
The conformance suite verifies:
- Every required signal is present and returns the correct type.
isLoading()isfalsewhenstatus()is'idle'.events$is a valid RxJSObservable.submitandstopreturn aPromise.
There is no separate package to install — the testing entry point ships as part of @ngaf/chat.
AgentWithHistory (Optional)
If your backend supports checkpointing or thread history, extend the basic contract with AgentWithHistory:
AgentWithHistory adds a history: Signal<AgentCheckpoint[]> field. The implementation pattern is identical — add the signal to your factory return value.
Use runAgentWithHistoryConformance from @ngaf/chat/testing in your spec instead of runAgentConformance to cover the additional field.
Publishing Your Adapter
If you want to distribute your adapter as an npm package, keep the following in mind.
Peer dependencies to declare in your package.json:
The @ngaf/chat/testing entry point is part of the same package as the main entry point, so there is nothing extra to install for the conformance tests.
Naming convention: @your-org/your-backend-agent works well (e.g., @acme/supabase-realtime-agent). The -agent suffix signals that the package satisfies the Agent contract.
Angular library setup: Use Nx (nx g @nx/angular:library) or the Angular CLI (ng g library) to scaffold an Angular library with ng-packagr. Point your package.json exports at the compiled output. See the Nx Angular library guide for the full setup.
Optional: license-key gating. If you want to restrict usage to paying customers, @ngaf/licensing provides a browser-safe license verification API. Declare it as an optional peer dependency.