Expand description
AWS Lambda adapter for turul-a2a.
§Choosing a runner
A Lambda binary receives raw event JSON; the adapter owns
classifying that JSON and dispatching to the right A2A handler
method. Adopters pick the runner whose name matches the Lambda’s
AWS trigger topology and end main.rs with one .await? call —
lambda_runtime / lambda_http / event parsing never appear in
adopter code.
| Lambda triggers | Runner |
|---|---|
| HTTP only (Function URL / APIGW) | LambdaA2aHandler::run_http_only |
| SQS only (event source mapping) | [LambdaA2aHandler::run_sqs_only] (sqs) |
| HTTP and SQS on one function | [LambdaA2aHandler::run_http_and_sqs] (sqs) |
| not sure / just run it | LambdaA2aHandler::run |
LambdaA2aHandler::run is the default “it just works” entry
point: without the sqs feature it aliases run_http_only; with
sqs it aliases run_http_and_sqs (permissive, handles either
shape). The explicit runners are strict — a non-matching
event shape fails loudly, which is what hardened deployments
and tests want.
The one-call shortcut is LambdaA2aServerBuilder::run — it
build-then-runs in a single fluent call. Use .build() + a
handler method when the handler is needed for unit tests or
custom middleware.
§Composing with non-framework triggers
Lambdas wired to HTTP plus a third trigger the framework does not know about (EventBridge scheduler, DynamoDB stream, custom invoke) can’t use a named runner directly — the Unknown branch is adopter-owned. Reach for the public classifier + the HTTP envelope primitive instead:
lambda_runtime::run(lambda_runtime::service_fn(
move |event: lambda_runtime::LambdaEvent<serde_json::Value>| {
let handler = handler.clone();
async move {
let (value, _ctx) = event.into_parts();
match turul_a2a_aws_lambda::classify_event(&value) {
turul_a2a_aws_lambda::LambdaEvent::Http => {
handler.handle_http_event_value(value).await
}
turul_a2a_aws_lambda::LambdaEvent::Sqs => {
let sqs = serde_json::from_value(value)?;
Ok(serde_json::to_value(handler.handle_sqs(sqs).await)?)
}
turul_a2a_aws_lambda::LambdaEvent::Unknown => {
// Adopter-owned: run your scheduled sweep,
// DynamoDB stream consumer, etc.
run_adopter_specific_path(value).await
}
}
}
}
)).awaitLambdaA2aHandler::handle_http_event_value is available without
the sqs feature; handle_sqs is gated on sqs. classify_event
LambdaEventare always available.
§API Gateway path prefix
When Lambda sits behind a REST API Gateway with AWS_PROXY
integration, the event carries the full stage + resource tree in
the path (e.g. /stage/agent/message:send),
which the root-rooted A2A router would 404. Configure the prefix
to strip via LambdaA2aServerBuilder::strip_path_prefix:
LambdaA2aServerBuilder::new()
.executor(my_executor)
.storage(my_storage)
.strip_path_prefix("/stage/agent") // API GW prefix
.run()
.awaitThe strip applies to run_http_only and run_http_and_sqs.
SQS events are unaffected. Non-matching paths pass through
unchanged (router still 404s on genuinely unknown paths —
that’s the correct failure mode).
// HTTP-only Lambda (request Lambda in two-Lambda topology):
LambdaA2aServerBuilder::new()
.executor(my_executor)
.storage(my_storage)
.with_sqs_return_immediately(queue_url, sqs) // enqueues durable jobs
.build()?
.run_http_only()
.await
// Single-function HTTP+SQS demo:
LambdaA2aServerBuilder::new()
.executor(my_executor)
.storage(my_storage)
.with_sqs_return_immediately(queue_url, sqs)
.run() // builder shortcut; dispatches HTTP + SQS
.await
// Pure SQS worker — no with_sqs_return_immediately(...) needed
// (it consumes the queue, never enqueues):
LambdaA2aServerBuilder::new()
.executor(my_executor)
.storage(my_storage)
.build()?
.run_sqs_only()
.await§Adapter internals
Thin wrapper: converts Lambda events to axum requests, delegates to the same Router, converts responses back. Adapter contract:
- Authorizer context mapped via synthetic headers with anti-spoofing.
- SSE responses are buffered: task executes, events are collected, returned as one response.
POST /message:streamexecutes the task within the Lambda invocation and returns all events.GET /tasks/{id}:subscribeis for tasks that are not in a terminal state. Within one invocation it emits the initialTasksnapshot, replays stored events viaLast-Event-ID, and closes when the task reaches a terminal state. Subscribing to an already-terminal task returnsUnsupportedOperationErrorper A2A v1.0 §3.1.6. For retrieving a terminal task’s final state useGetTask.
§Push-notification delivery — external triggers are mandatory
Push delivery on Lambda is architecturally different from the
binary server. The request Lambda installed via
LambdaA2aServerBuilder still constructs a PushDispatcher
when push_delivery_store is wired, but any tokio::spawn
continuation it emits post-return is opportunistic only — the
Lambda execution environment may be frozen indefinitely between
invocations, so nothing can depend on that continuation completing.
Correctness for push delivery on Lambda is carried by:
- The atomic pending-dispatch marker written inside the request
Lambda’s commit transaction (opt in via
StorageImpl::with_push_dispatch_enabled(true)). LambdaStreamRecoveryHandler— DynamoDB Streams trigger ona2a_push_pending_dispatches. DynamoDB backends only.LambdaScheduledRecoveryHandler— EventBridge Scheduler backstop. Required for all backends; it is the sole recovery path for SQLite / PostgreSQL / in-memory deployments.
Without at least the scheduled worker, push delivery on Lambda is
not durable — a marker written on a cold invocation may never be
consumed. The example wiring lives in
examples/lambda-stream-worker and
examples/lambda-scheduled-worker.
Lambda streaming is request-scoped (not persistent SSE connections). The durable
event store ensures events survive across invocations. Clients reconnect with
Last-Event-ID for continuation.
§Cross-instance cancellation
Lambda invocations are stateless and short-lived, so the Lambda
adapter does not run the persistent cross-instance cancel
poller that A2aServer::run() spawns. Cancellation behaviour on
the Lambda adapter:
- Marker writes —
CancelTaskon a Lambda invocation writes the cancel marker to the shared backend (DynamoDB / PostgreSQL). This works. - Propagation to a live executor on the SAME Lambda invocation —
works via the same-instance token-trip path in
core_cancel_task. - Propagation to a live executor on a DIFFERENT Lambda invocation (warm container) — not currently supported. There is no persistent poller to observe markers written by other invocations. A subsequent invocation whose handler reads the marker directly may act on it, but that is not a substitute for the server runtime’s live propagation.
The builder still requires an A2aCancellationSupervisor
implementation on the same backend so that marker writes reach
the correct backend and a future polling-adapter variant can
consume them. If a deployment passes a non-matching supervisor,
build() rejects the configuration.
Structs§
- Authorizer
Mapping - Mapping configuration for Lambda authorizer context.
- Lambda
A2aHandler - Lambda handler wrapping the axum Router.
- Lambda
A2aServer Builder - Builder for Lambda A2A handler.
- Lambda
Authorizer Middleware - Middleware that reads trusted authorizer context from x-authorizer-* headers.
- Lambda
Scheduled Recovery Config - Configuration for a scheduled-recovery sweep.
- Lambda
Scheduled Recovery Handler - Handler for scheduled push-recovery ticks.
- Lambda
Scheduled Recovery Response - Summary returned from a single scheduled-recovery tick.
- Lambda
Stream Recovery Handler - Handler for DynamoDB Stream events on
a2a_push_pending_dispatches. - NoStreaming
Layer - Tower Layer that rejects streaming paths on Lambda.
Enums§
- Lambda
Event - Coarse Lambda event-shape discriminator returned by
classify_event.
Functions§
- axum_
to_ lambda_ response - Convert an axum response to a Lambda HTTP response.
- classify_
event - Classify a raw Lambda event JSON shape.
- lambda_
to_ axum_ request - Convert a Lambda HTTP request to an axum request.