Adapter Cookbook
This is the guide to writing your own backend bridge. koel ships three —
koel_agno,
koel_langgraph, and
koel_runtime (CopilotKit) — and they
are the worked examples this cookbook draws on.
The SPI
Everything koel consumes is an AbstractAgent:
abstract class AbstractAgent {
Stream<AgUiEvent> run(RunAgentInput input);
}
One method. If your backend already speaks native AG-UI over SSE — and most
modern ones do — you almost never implement this directly. You extend
HttpAgent, which gives you the entire transport stack (SSE parse, timeouts,
cancellation, retry/auth interceptors, chunk synthesis) for free, and override
only the two seams that differ per backend.
Extend HttpAgent, override two seams
import 'package:koel_core/koel_core.dart';
import 'package:koel_http/koel_http.dart';
class MyAgent extends HttpAgent {
MyAgent({required Uri baseURL, this.token})
: super(url: baseURL.resolve('agui'));
final String? token;
// Seam 1 — encodeBody: shape the POST body your backend expects. Start from
// super.encodeBody(input) (canonical AG-UI: threadId/runId/state/messages/
// tools/context/forwardedProps) and add/rename only what your backend needs.
Map<String, dynamic> encodeBody(RunAgentInput input) => {
...super.encodeBody(input),
'my_backend_flag': true,
};
// Seam 2 — errorClassifier: map your backend's HTTP/business failures to typed
// KoelError codes, so a 401/403/429 reaches the consumer as a typed error
// rather than a raw status.
ErrorClassifier errorClassifier() => const MyErrorClassifier();
}
That is the whole shape of AgnoAgent and CopilotRuntimeAgent: a constructor
that resolves the run URL, an encodeBody that normalizes the body, an
errorClassifier. The response path — parsing the text/event-stream into typed
AgUiEvents — is unreshaped HttpAgent behavior, so you write none of it.
When the wire is not plain SSE
If your backend speaks something other than canonical AG-UI/SSE, you implement
AbstractAgent.run directly and emit typed events yourself. Apply the
cancel-correct teardown pattern so a
consumer cancel() tears the connection down within the abort budget and can
never hang.
Auth as an interceptor, not a special case
Add auth as an AuthInterceptor (or your own Interceptor), prepended outermost
so a caller-supplied inner auth wins the merge. A blank/absent token should be a
no-op — the right default for an open local deployment. See
Interceptors.
Prove it with the conformance runner
The contract that makes a bridge a koel bridge is the conformance runner. It drives your agent against the synthesized corpus (one event of every AG-UI type) and reports which canonical types your bridge reproduces verbatim.
('vm')
(['conformance'])
library;
import 'package:koel_test/koel_test.dart';
import 'package:test/test.dart';
void main() {
test('MyAgent conformance', () async {
final agent = MyAgent(baseURL: Uri.parse('http://localhost/agui'));
final report = await const ConformanceRunner().runAgainst(agent);
// Assert the report reproduces the canonical types your backend emits.
expect(report.passed, isNotEmpty);
});
}
The 25/28 contract
An HTTP bridge reproduces 25 of the 28 AG-UI types verbatim. The three
*_CHUNK convenience shapes are normalized into their START/CONTENT/END
triplets at the transport by koel_http's default-on synthesizeChunks — so the
runner sees long form, not chunks. This is the fixed contract every native-AG-UI
adapter shares (a real backend never emits chunk shapes anyway), not a
limitation of your bridge. See Events.
Capturing fixtures
For deterministic conformance without a live backend, capture real responses
once into JSONL fixtures and replay them through a MockClient. The repo's
melos run capture-fixtures pipeline (and the agno/langgraph/runtime test
suites) are the reference; the conformance lane then runs fully offline in CI.
Checklist
- Extend
HttpAgent(native SSE) or implementAbstractAgent.run. - Override
encodeBodyfromsuper.encodeBody(input). - Override
errorClassifiermapping your failures to typedKoelErrors. - Auth via an interceptor; blank token is a no-op.
- A
@Tags(['conformance'])test drivingConformanceRunner.runAgainst. - Cancellation honored within budget (the teardown pattern).