AI developer guidance for the Asya project.
Asya is an Actor Mesh framework for running AI workloads on Kubernetes using choreography (decentralized) instead of centralized orchestration. Actors communicate by passing envelopes through message queues; routing is embedded in each envelope, not managed by a central coordinator. Implemented with Crossplane Compositions for declarative actor deployment with inline sidecar rendering.
Core components (all in src/):
- asya-sidecar (Go): envelope router injected into actor pods; Queue β Sidecar β Runtime β Sidecar β Next Queue
- asya-runtime (Python): lightweight socket server loaded via ConfigMap; executes user handler,
returns result. Source of truth:
src/asya-runtime/asya_runtime.py(single file, no deps).deploy/helm-charts/asya-crossplane/files/asya_runtime.pyis a symlink β editing the source automatically reflects in the Crossplane chart's ConfigMap. No manual sync needed. - asya-gateway (Go): optional MCP/HTTP gateway; exposes async actor pipelines as synchronous HTTP
- asya-crew (Python): system actors β
x-sink(persist results),x-sump(DLQ handling),x-pause(checkpoint envelope to S3 and signalpaused),x-resume(restore envelope from S3 and re-inject into the mesh) - asya-lab (Python): CLI tools (
asya mcp ...,asya flow ...) for debugging and flow compilation - asya-testing (Python): shared test fixtures and utilities
- asya-state-proxy (Go): optional sidecar that gives actors virtual persistent state via filesystem
emulation; actors read/write
/state/...paths, runtime intercepts Python file I/O and forwards to the proxy over Unix socket; proxy translates to actual storage backend (S3, GCS, Redis, NATS KV) with configurable LWW or CAS guarantees; actors remain stateless Deployments β no StatefulSets
Project structure:
asya/
βββ src/
β βββ asya-gateway/
β βββ asya-sidecar/
β βββ asya-runtime/
β βββ asya-crew/
β βββ asya-testing/
β βββ asya-cli/
βββ deploy/helm-charts/
β βββ asya-crew/ # pre-built generic actors (like x-sink, x-sump)
β βββ asya-crossplane/ # contains AsyncActor XRD
β βββ asya-gateway/ # sync stateful HTTP gateway exposing actors and flows as MCP tools or A2A agents
βββ testing/
β βββ component/ # docker-compose
β βββ integration/ # docker-compose
β βββ e2e/ # local kind cluster
β βββ shared/ # shared docker-compose configurations
βββ examples/
β βββ asyas/ # sample XR definitions
βββ flows/ # sample Python flows
See docs/reference/components/ for component deep-dives.
Examples (examples/):
asyas/β real-world AsyncActor CRD manifests; use as reference when writing or reviewing actor specsflows/β teaser flow DSL (Domain-Specific Language) files; comprehensive examples (including agentic flows) are in examples/end-to-end/monorepo/end-to-end/monorepo/β full working examples organized by category (control-flow, agentic, resiliency, compiler-sugar, text-improver)
Crossplane chart (deploy/helm-charts/asya-crossplane/): Deploys XRDs, Compositions, and provider configurations for AsyncActor resource management. The render-deployment composition step renders the complete pod spec (runtime container + asya-sidecar + state proxies + volumes) using values from the chart's sidecar: block.
Prerequisites: uv (curl -LsSf https://astral.sh/uv/install.sh | sh), Go 1.24+, Python 3.13+, Docker, Make
make setup # Install uv, pre-commit hooks, sync Go deps
make build # Build all components
make build-images # Build Docker images
make build-go # Build Go components only
make test-unit # Unit tests (Go + Python)
make test-component # Component tests (Docker Compose)
make test-integration # Integration tests (Docker Compose)
make test-e2e # E2E tests (Kind cluster)
make lint # Run all linters with auto-fix
make clean # Remove build artifactsPrefer make <target>. Add new Makefile targets instead of repeating raw commands.
Hierarchy:
- Unit (
make test-unit): fast, no external deps βsrc/{component}/tests/ - Component (
make test-component): single component in Docker Compose βtesting/component/{component}/ - Integration (
make test-integration): multi-component Docker Compose βtesting/integration/{suite}/ - E2E (
make test-e2e): full stack in Kind cluster βtesting/e2e/
Trust CI: Component, integration, and e2e tests cannot be parallelized across PRs. For multi-PR work, push and observe CI logs rather than running all suites locally.
Critical rules:
- Unit tests must mock all external services
- Component/integration tests run inside Docker Compose β no port-forwarding
- Only E2E tests may use
kubectl port-forward - Prefer Docker Compose over Kind; use Kind only for K8s-specific features (CRDs, KEDA, Crossplane)
E2E local debugging (only when user explicitly permits Kind cluster access):
Kind cluster recreation costs 15-25 minutes. Never make down && make up to fix a flaky test.
Use the fast-path instead:
make build-images
kind load docker-image {image}:{tag} --name asya-e2e-{profile}
helm upgrade -n {namespace} {release} deploy/helm-charts/{chart}/ --reuse-values
kubectl rollout restart -n {namespace} deployment/{name}Run with fail-fast: make trigger-tests PYTEST_OPTS="-vv -x" PROFILE="sqs-s3" 2>&1 | tee /tmp/tests.txt
Lessons from field debugging:
- Assert intent, not syntax β composition tests that match exact variable names (e.g.
$xr.spec.actor) break on refactors; assert the behavioral intent instead - XRD field removals cascade β when removing a field from an XRD, immediately grep for it in test code and helpers
- Helm test pod namespacing β test pods inherit
Release.Namespace; secrets may live in a different namespace (e.g.asya-system); verify before referencing;deploy.shis canonical for what exists in the actor namespace at test time - Chaos tests need sequential workers β pod-kill tests interfere with parallel pytest workers; use
-p no:xdistwhen debugging - CI-only failures are usually namespace/credential issues β local dev often papers over these because secrets are created ad-hoc; CI is strict about what exists where
- AsyncActor manifest field additions cascade across test files β when a new required field is added to
AsyncActor specs (e.g.
compositionSelector), grep ALL test files forkind: AsyncActorortransport:to find every inline manifest; missing even one causes failures in seemingly unrelated suites (e.g. fixingtest_crossplane_e2e.pybut missingtest_secret_injection_e2e.py)
See CONTRIBUTING.md for detailed test structure, Makefile patterns, and Docker Compose profile assembly.
{
"id": "<envelope-id>",
"parent_id": "<original-id>",
"route": {"prev": [], "curr": "q1", "next": ["q2"]},
"headers": {"trace_id": "..."},
"payload": {}
}- Runtime (not sidecar) shifts the route:
prevgrows,curradvances,nextshrinks x-sinkandx-sumpare automatic β never include them in route configs- Empty
route.currorNonereturn β sidecar routes tox-sink - Handler error β sidecar applies resiliency policy (retry or route to
x-sinkwith phase: failed) - Infrastructure error (timeout, runtime crash, parse error) β sidecar routes directly to
x-sump
- Empty
- Modify routing from a generator handler:
yield "SET", ".route.next", ["actor_a"] - Only
.route.next,.headers, and.statusare writable;.route.prev,.route.curr,.id,.parent_idare read-only
See docs/reference/specs/envelope.md.
Asya's strategic goal is to provide the full agentic tool surface that frameworks like Google ADK,
Mastra, and LangGraph provide β but on a stateless, queue-based, K8s-native mesh. Agentic flow
patterns live in examples/end-to-end/monorepo/src/agentic/.
See the framework survey in .aint/aints/agentic-umbrella/survey-agentic-frameworks.md.
Both actors and flows share a dict -> dict signature, but they are fundamentally different:
- Actor: a deployed CRD (
AsyncActor), runs as a pod with a sidecar, can yield FLY events, abort, fan-out (multiple yields), or returnNone - Flow: ephemeral β no CRD, no pod, just a Python file that describes a pipeline as familiar
Python control flow (
if/else, sequential calls). The Flow DSL compiler transforms it into a group of router actors using CPS (continuation-passing style): instead of calling the next function, each step sends a message to the next actor's queue.
Flows support fan-out/fan-in via list comprehensions, list literals, and asyncio.gather.
Individual actor calls within flows must map payload 1:1:
- β
return payload(function actor) - β
exactly one
yield payload(generator actor, no FLY yields) - β
[actor(x) for x in items]β fan-out with automatic fan-in aggregation - β
asyncio.gather(a(x), b(y))β heterogeneous fan-out with aggregation - β
yield "FLY", ...β not supported in flows (FLY is actor-only) - β fire-and-forget fan-out (multiple
yieldwithout aggregation) β actor-only - β returning
None(abort) β not supported in flows
Generator handlers communicate with the runtime via structured yields. Full spec:
docs/reference/specs/abi-protocol.md.
| Yield form | Effect |
|---|---|
yield payload |
Emit envelope downstream (to next actor in route) |
yield "GET", ".route.prev" |
Read envelope metadata |
yield "SET", ".route.next", [...] |
Overwrite routing |
yield "FLY", {...} |
Stream event upstream to gateway (ephemeral SSE, not persisted) |
FLY vs history: FLY events reach only connected SSE clients β use for streaming tokens and live
progress. For data that must survive pause/resume or be readable by downstream actors, append to
payload.a2a.task.history[] instead.
An actor signals a pause by routing to x-pause (via yield "SET", ".route.next", ["x-pause"]).
The x-pause crew actor checkpoints the full envelope (payload + route + headers) to S3 and reports
paused status to the gateway. The gateway maps paused β A2A input_required.
On resume, the client POSTs new input to the gateway, which routes to x-resume. The x-resume
actor fetches the checkpointed envelope from S3, merges the new input into the payload, and
re-dispatches to the mesh β continuing from where the pipeline stopped.
Three fixed namespaces (planned ASYA_BASE_PREFIX, not yet implemented, would be prepended to all):
| Namespace | Audience | Purpose |
|---|---|---|
/a2a/ |
External AI agents, orchestrators | Full A2A protocol (SendMessage, GetTask, Subscribe, pause/resume, push notifications) |
/mcp/ |
LLM clients, developers | MCP Streamable HTTP + SSE β tool listing and invocation via MCP protocol |
/mesh/ |
Sidecars, operators | Progress/FLY/final reporting from sidecars (POST /api/v1/mesh/{id}/events); SSE stream at GET /api/v1/mesh/{id}/events |
Special root routes (unaffected by base prefix):
/.well-known/agent.jsonβ A2A Agent Card discovery/healthβ K8s liveness/readiness probe
A2A task state mapping: pending β submitted, running β working, succeeded β completed,
paused β input_required, failed β failed, canceled β canceled, auth_required β auth_required.
All feature work must be done in a git worktree. Never implement features directly on main.
git aint pickup <ref>β creates worktree in.worktrees/<epic>/<task>.<slug>and feature branch- Work exclusively in the worktree
- Commit, push, create a PR β never merge directly
All commits must include a Signed-off-by: trailer (DCO β Developer
Certificate of Origin). CI will reject PRs with unsigned commits.
- Every commit: use
git commit --signoff(or-s) - After rebase:
git rebase --exec 'git commit --amend --no-edit --signoff' - Verify before push:
git log --format='%h %s | SOB:%(trailers:key=Signed-off-by,valueonly)' - Never use
--no-verifyto bypass pre-commit hooks
- Prefer:
make <target> - Last resort: direct commands only if no Makefile target exists
Add new Makefile targets instead of repeating raw commands.
Use Haiku/Sonnet subagents for lint errors and unit test failures β these are mechanical fixes.
Pattern: Run make lint or make test-unit β capture errors β spawn Task subagent with
model: "haiku" β pass errors and file paths β verify with re-run.
Haiku handles: formatting (ruff, gofmt, yamlfmt), import ordering, simple assertion fixes. Keep on Opus: root cause analysis, architectural decisions, complex refactoring.
Never proactively create documentation files (*.md, README.md, design docs) unless explicitly requested. Updating existing docs to reflect code changes is fine.
docs/contains only documentation content (.md files). Website assets live indocs/website/.- Mirrored guides in
usage/andsetup/must cross-link each other. docs/plans/is gitignored and must never be committed. Use aint for work tracking.
Every file in docs/ has a YAML frontmatter description field β a grep-friendly one-liner with
searchable keywords. Use this to find the right doc fast:
# Search for a topic across all doc descriptions (fastest)
grep -r "description:.*routing" docs/ --include="*.md" -l
# Browse all descriptions at a glance
grep -rn "^description:" docs/ --include="*.md"
# Find docs about a specific component
grep -r "description:.*sidecar" docs/ --include="*.md" -l
# Find docs about a concept or pattern
grep -rE "description:.*(fan-out|fan-in|streaming)" docs/ --include="*.md" -lDoc sections (docs/):
| Section | Audience | Content |
|---|---|---|
concepts/ |
Everyone | What Asya is, architectural ideas, design principles |
setup/ |
Platform engineers | Deployment, Helm config, autoscaling, observability |
usage/ |
Data scientists | Tutorials, handler patterns, agentic patterns, debugging |
reference/ |
Both | Component specs, API specs, env vars, transport/storage config |
comparisons/ |
Evaluators | Asya vs other frameworks (Temporal, LangGraph, Ray, etc.) |
contributing/ |
Contributors | Test strategies, cross-component architecture decisions |
Naming conventions: start-* = getting started tutorials, guide-* = how-to guides,
ops-* = operations/debugging, core-* = core components, lab-* = developer tools,
as-* = category comparisons, vs-* = 1:1 deep comparisons.
When you need documentation context, grep descriptions first β don't read files speculatively.
Never use transitional comments ("instead of", "increased from", "no need to"). Comments explain what/why the current code does β not how it differs from before.
Never add defaults to env vars in code or docker-compose files. All required env vars must be passed explicitly from Makefile. Fail fast on missing config.
- ${COVERAGE_DIR}:/app/.coverage:rw # good
- ${COVERAGE_DIR:-.coverage}:/app/.coverage:rw # bad β hides missing configNever use time.Sleep/time.sleep in production code. In tests, polling sleeps are allowed but must
have an inline comment explaining purpose.
In .md files: only β
β [+] / [-] / [!] / [.] text markers.
Work is not complete until git push succeeds. Do not stop before pushing.
- File issues for remaining work
- Run quality gates (if code changed): tests, lint, build
- Update issue status (close finished, update in-progress)
- Push:
git pull --rebase git aint sync git push git status # must show "up to date with origin" - Clean up stashes, prune remote branches
- Provide context for next session
Never say "ready to push when you are" β you must push.
This project uses git-aint for issue tracking. Run
git aint init to initialize (idempotent). .aint/ is a git worktree on branch aint-sync,
shared across all agents. See .aint/AGENTS.md for full usage.
@AGENTS_GUIDELINES.md