Custom adapter (Python)
Custom adapter (Python)
Build a custom adapter with astropods-adapter-core
astropods-adapter-core is the framework-agnostic Python core. Implement the AgentAdapter protocol, hand it to serve(), and the bundled MessagingBridge handles the gRPC streaming loop, audio, feedback, 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
Requires Python 3.10+.
Minimal adapter
serve(adapter) connects to localhost:9090 (or GRPC_SERVER_ADDR) and blocks until SIGINT / SIGTERM.
Lifecycle
AgentAdapter protocol
The interface is a typing.Protocol — duck-typed. Any class with these members satisfies it.
StreamHooks
Call these from inside stream() and stream_audio(). They translate directly into outbound AgentResponse messages on the gRPC stream.
on_status_update takes a dict with a "status" key. Valid values: "THINKING", "SEARCHING", "GENERATING", "PROCESSING", "ANALYZING", "CUSTOM". For CUSTOM, include "custom_message":
StreamOptions
The bridge passes this to every stream() / stream_audio() call.
AudioInput
Passed to stream_audio().
VoiceProvider
Optional protocol for STT. Pass an instance to your adapter, then call its listen() method in stream_audio().
FeedbackEvent
Passed to on_feedback(). kind is a stable string discriminator — no proto imports needed.
FeedbackEvent.kind values
on_feedback may be sync or async — the bridge probes with hasattr and dispatches. It does not await the result, so don’t block on slow I/O. Push to a queue or trigger an async write and return.
serve(adapter, options?)
Thin wrapper that handles process lifecycle. Sets up JSON-formatted logging, runs asyncio.run(bridge.start()), and exits on SIGINT / SIGTERM.
ServeOptions.server_address defaults to os.environ.get("GRPC_SERVER_ADDR", "localhost:9090").
Worked example: a plain Anthropic agent
No framework — just calling Anthropic’s streaming messages API.
Worked example: audio with STT
Rules of thumb
- Call exactly one of
on_finish()oron_error()per request. Skipping either leaves the user staring at a half-rendered reply. - Catch your own exceptions inside
stream(). If you let the coroutine raise, the user sees nothing. - Don’t block in
on_feedback()— push to a queue or trigger async work and return. - Use
options.conversation_id(notplatform_context.thread_id) as your memory key. - Configure OTEL manually before calling
serve()if you want traces. Framework adapters auto-configure this; the raw core does not.
Exported symbols
See Messaging SDK (Python) for the underlying proto shapes referenced by StreamHooks and PlatformContext.