Custom adapter (Node)
Custom adapter (Node)
Build a custom adapter with @astropods/adapter-core
@astropods/adapter-core is the framework-agnostic core. Implement the AgentAdapter interface, hand it to serve(), and the bundled MessagingBridge handles the gRPC streaming loop, audio, feedback, reconnect, and shutdown.
Use this when there’s no first-party adapter for your framework (or no framework at all — you’re calling LLM APIs directly).
Install
Minimal adapter
serve(adapter) connects to localhost:9090 (or GRPC_SERVER_ADDR) and runs until the process exits.
Lifecycle
AgentAdapter
StreamHooks
Call these from inside stream() and streamAudio(). They translate directly into outbound AgentResponse messages on the gRPC stream.
StatusUpdate.status values: THINKING, SEARCHING, GENERATING, PROCESSING, ANALYZING, CUSTOM. Use CUSTOM with customMessage.
StreamOptions
The bridge passes this to every stream() / streamAudio() call.
AudioInput
Passed to streamAudio().
FeedbackEvent
Passed to onFeedback(). kind is a stable string discriminator so you don’t have to import proto types to switch on it.
FeedbackEvent.kind values
onFeedback() may return void or a Promise. The bridge does not await the result — so don’t block the stream on slow I/O. Push to a queue or trigger an async write and return.
Trace context
Call hooks.onTraceContext({ traceparent }) once per turn to attach the turn’s W3C trace context. The bridge stamps it on every AgentResponse it emits for the turn, so a consumer can correlate any response — and the feedback that references it — back to the trace. It’s ignored when traceparent is empty.
createTraceparent(input) formats a native trace/span ID pair into a W3C traceparent string. It returns "" for invalid or all-zero IDs, so it’s safe to pass raw OpenTelemetry span context. traceFlags accepts a number or hex string and defaults to 01 (sampled).
The first-party framework adapters already emit trace context from their root span — you only wire this up in a custom adapter. See the Messaging SDK for the underlying TraceContext wire shape.
serve(adapter, options?)
Thin wrapper around MessagingBridge that handles process lifecycle.
ServeOptions.serverAddress defaults to process.env.GRPC_SERVER_ADDR || 'localhost:9090'.
Worked example: a plain OpenAI agent
No framework — just calling OpenAI’s streaming chat API and forwarding tokens.
Rules of thumb
- Call exactly one of
onFinish()oronError()per request. Skipping either leaves the user staring at a half-rendered reply. - Catch your own exceptions inside
stream(). If you let the promise reject, the user sees nothing. - Don’t block in
onFeedback()— push to a queue or trigger async work and return. - Use
options.conversationId(notplatformContext.threadId) as your memory key. - Use only
options.attachmentsas file input for the turn. Write output files toAGENT_FILES_DIRand callonFilebeforeonFinish; see Files in chat. - Set up OTEL manually before calling
serve()if you want traces. Framework adapters auto-configure this; the raw core does not.
Re-exported types
logger is a Pino instance. Use it for adapter logs so output stays consistent with the bridge.
See Messaging SDK (Node) for the underlying proto shapes referenced by StreamHooks and PlatformContext.