Envelope Structure#

Envelope: Structured JSON object transmitted through message queues (RabbitMQ, SQS), containing routing information and application data.

Payload: Application-specific data within envelope, processed by actors.

{
  "id": "unique-message-id",
  "parent_id": "original-message-id",
  "route": {
    "prev": ["prep"],
    "curr": "infer",
    "next": ["post"]
  },
  "headers": {
    "trace_id": "abc-123",
    "priority": "high"
  },
  "status": {
    "phase": "pending",
    "actor": "infer",
    "attempt": 1,
    "max_attempts": 1,
    "created_at": "2025-11-18T12:00:00Z",
    "updated_at": "2025-11-18T12:00:00Z",
    "deadline_at": "2025-11-18T12:05:00Z"
  },
  "payload": {
    "text": "Hello world"
  }
}

Fields:

  • id (required): Unique envelope identifier
  • parent_id (optional): Parent envelope ID for fanout children (see Fan-Out section)
  • route (required): Actor routing state
  • prev: Actors that have already processed the envelope (read-only, maintained by runtime)
  • curr: The actor currently processing the envelope (read-only, set by runtime)
  • next: Actors yet to process the envelope (modifiable via ABI)
  • status (optional): Envelope lifecycle status, stamped by gateway on creation
  • phase: Current lifecycle phase (pending, processing, retrying, succeeded, failed, paused, canceled)
  • actor: Actor that last updated the status
  • deadline_at: Absolute deadline in RFC3339 UTC (omitted if no timeout configured)
  • payload (required): User data processed by actors
  • headers (optional): Routing metadata (trace IDs, priorities)

Sidecar-Managed Headers#

These headers are automatically managed by the sidecar and should not be overwritten by user handlers:

Header Description
traceparent W3C Trace Context parent (auto-injected when tracing enabled)
tracestate W3C Trace Context state (auto-injected when tracing enabled)

Queue Naming Convention#

All actor queues follow pattern: asya-{namespace}-{actor_name}

Examples: Namespace: example-ecommerce - Actor text-analyzer → Queue asya-example-ecommerce-text-analyzer - Actor image-processor → Queue asya-example-ecommerce-image-processor - System actors: asya-{namespace}-x-sink, asya-{namespace}-x-sump

Benefits:

  • Fine-grained IAM policies: arn:aws:sqs:*:*:asya-*
  • Clear namespace separation
  • Automated queue management by operator

Envelope Acknowledgment#

Ack: Envelope processed successfully, remove from queue - Runtime returns valid response - Sidecar routes to next actor or end queue

Nack: Envelope processing failed in sidecar, requeue - Sidecar crashes before processing - Queue automatically sends to DLQ after max retries

End Queues#

x-sink: First layer of termination. Receives ALL terminal envelopes — both succeeded and failed. Routed by sidecar when route is exhausted (success), when SLA expires, or when resiliency policy is exhausted (failure). Reports final status to gateway, dispatches to hooks.

x-sump: Second layer of termination (final terminal). Receives envelopes from x-sink after hook processing. Emits metrics and logs errors. Never reached directly from regular actor sidecars.

Important: Do not include x-sink or x-sump in route configurations - managed by sidecar.

Response Patterns#

Single Response#

Runtime returns mutated payload:

{"processed": true, "timestamp": "2025-11-18T12:00:00Z"}

Action: Sidecar creates envelope → Runtime shifts route (prev grows, curr advances) → Routes to next actor

Fan-Out (Generator/Yield)#

Handlers use yield to produce multiple outputs. Each yield sends a frame immediately to the sidecar over the Unix socket, and the sidecar creates a separate message for routing.

async def process(payload):
    for item in payload["items"]:
        yield {"processed": item}

Action: Sidecar reads each yielded frame and routes it as a separate envelope to the next actor.

Fanout ID semantics:

  • First yielded envelope retains original ID (for SSE streaming compatibility)
  • Subsequent yielded envelopes receive UUID4 IDs (globally unique, no collision across concurrent fan-outs)
  • All fanout children (index > 0) have parent_id set to original envelope ID

Example: Envelope abc-123 yields 3 items:

  • Index 0: id="abc-123", parent_id=null (original ID preserved)
  • Index 1: id="550e8400-e29b-41d4-a716-446655440000", parent_id="abc-123" (fanout child, UUID4)
  • Index 2: id="7c9e6679-7425-40de-944b-e07fc1f90ae7", parent_id="abc-123" (fanout child, UUID4)

Note: Returning a list from a handler does NOT trigger fan-out. A returned list is treated as a single payload value.

Empty Response#

Runtime returns None (null):

Action: Sidecar routes envelope to x-sink (no increment)

Error Response#

Runtime returns error object:

{
  "error": "processing_error",
  "message": "Invalid input format"
}

Action: Sidecar applies resiliency policy: retries if configured, or routes to x-sink (phase: failed) if exhausted/non-retryable

Payload Enrichment Pattern#

Recommended: Actors append results to payload instead of replacing it.

Example pipeline: ["data-loader", "recipe-generator", "llm-judge"]

// Input to data-loader
{"product_id": "123"}

// Output of data-loader → Input to recipe-generator
{
  "product_id": "123",
  "product_name": "Ice-cream Bourgignon"
}

// Output of recipe-generator → Input to llm-judge
{
  "product_id": "123",
  "product_name": "Ice-cream Bourgignon",
  "recipe": "Cook ice-cream in tomato sauce for 3 hours"
}

// Output of llm-judge → Final result
{
  "product_id": "123",
  "product_name": "Ice-cream Bourgignon",
  "recipe": "Cook ice-cream in tomato sauce for 3 hours",
  "recipe_eval": "INVALID",
  "recipe_eval_details": "Recipe is nonsense"
}

Benefits:

  • Better actor decoupling - each actor only needs specific fields
  • Full traceability - complete processing history in final payload
  • Routing flexibility - later actors can access earlier results
  • Monotonic computation - much easier to reason about and integrate with

Task Status Tracking#

When gateway is enabled, tasks have lifecycle statuses tracked throughout processing:

Status Values#

Status Description When Set
pending Task created, not yet processing Gateway creates task from MCP tool call
running Task is being processed by actors Sidecar sends first progress update
succeeded Pipeline completed successfully x-sink crew actor reports success
failed Pipeline failed with error x-sink crew actor reports failure (or gateway backstop timer)
paused Waiting for external input Sidecar detects x-asya-pause header from x-pause crew actor
canceled Task was canceled Client cancels via POST /a2a/tasks/{id}:cancel
unknown Status cannot be determined Edge cases, missing updates

Progress Reporting#

Sidecars report progress to gateway at three points per actor:

1. Received (received):

  • Envelope pulled from queue
  • Before forwarding to runtime

2. Processing (processing):

  • Envelope sent to runtime via Unix socket
  • Runtime is executing handler

3. Completed (completed):

  • Runtime returned successful response
  • Before routing to next actor

Progress calculation:

progress_percent = (len(prev) + 1) / (len(prev) + 1 + len(next)) * 100

Example: Route starting as {prev: [], curr: "prep", next: ["infer", "post"]} - Actor prep completed → 33% - Actor infer completed → 66% - Actor post completed → 100% (final status from x-sink)

Progress Update Flow#

Sidecar                    Gateway                    Client
-------                    -------                    ------
1. Receive from queue
   └─> POST /mesh/{id}/progress
       {status: "received", current_actor_idx: 0}
                           └─> Update DB: running
                           └─> SSE: progress 10%

2. Send to runtime
   └─> POST /mesh/{id}/progress
       {status: "processing", current_actor_idx: 0}
                           └─> SSE: progress 15%

3. Runtime returns
   └─> POST /mesh/{id}/progress
       {status: "completed", current_actor_idx: 0}
                           └─> SSE: progress 33%

4. Route to next actor...

Final Status Reporting#

Success path:

Actor N completes → Sidecar routes to x-sink
  → x-sink persists to S3
  → x-sink reports: POST /mesh/{id}/final
     {status: "succeeded", result: {...}}
  → Gateway updates: status=succeeded, progress=100%
  → SSE: final success event

Error path:

Runtime error → Sidecar retries per resiliency policy
  → If exhausted/non-retryable → Sidecar routes to x-sink (phase: failed)
  → x-sink reports: POST /mesh/{id}/final
     {status: "failed", error: "..."}
  → x-sink dispatches to hooks → x-sump (final terminal)
  → Gateway updates: status=failed
  → SSE: final error event

Design Principles#

  • Small payloads: Use object storage (S3, MinIO) for large data, pass references
  • Clear names: Use descriptive actor names (preprocess-text not actor1)
  • Monitor errors: Alert on x-sump queue depth
  • Version schema: Include version in payload for breaking changes