Skip to main content

Crate turul_a2a_aws_lambda

Crate turul_a2a_aws_lambda 

Source
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 triggersRunner
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 itLambdaA2aHandler::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
                }
            }
        }
    }
)).await

LambdaA2aHandler::handle_http_event_value is available without the sqs feature; handle_sqs is gated on sqs. classify_event

  • LambdaEvent are 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()
    .await

The 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:stream executes the task within the Lambda invocation and returns all events.
  • GET /tasks/{id}:subscribe is for tasks that are not in a terminal state. Within one invocation it emits the initial Task snapshot, replays stored events via Last-Event-ID, and closes when the task reaches a terminal state. Subscribing to an already-terminal task returns UnsupportedOperationError per A2A v1.0 §3.1.6. For retrieving a terminal task’s final state use GetTask.

§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:

  1. The atomic pending-dispatch marker written inside the request Lambda’s commit transaction (opt in via StorageImpl::with_push_dispatch_enabled(true)).
  2. LambdaStreamRecoveryHandler — DynamoDB Streams trigger on a2a_push_pending_dispatches. DynamoDB backends only.
  3. 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 writesCancelTask on 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§

AuthorizerMapping
Mapping configuration for Lambda authorizer context.
LambdaA2aHandler
Lambda handler wrapping the axum Router.
LambdaA2aServerBuilder
Builder for Lambda A2A handler.
LambdaAuthorizerMiddleware
Middleware that reads trusted authorizer context from x-authorizer-* headers.
LambdaScheduledRecoveryConfig
Configuration for a scheduled-recovery sweep.
LambdaScheduledRecoveryHandler
Handler for scheduled push-recovery ticks.
LambdaScheduledRecoveryResponse
Summary returned from a single scheduled-recovery tick.
LambdaStreamRecoveryHandler
Handler for DynamoDB Stream events on a2a_push_pending_dispatches.
NoStreamingLayer
Tower Layer that rejects streaming paths on Lambda.

Enums§

LambdaEvent
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.