{"id":52153308,"url":"https://github.com/plexe-ai/rya","last_synced_at":"2026-08-07T16:01:41.428Z","repository":{"id":373157033,"uuid":"1275289086","full_name":"plexe-ai/rya","owner":"plexe-ai","description":"Production backend/runtime for AI agents — coding-agent-first.","archived":false,"fork":false,"pushed_at":"2026-07-25T20:16:13.000Z","size":1099,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-25T22:09:29.431Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/plexe-ai.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-20T13:52:20.000Z","updated_at":"2026-07-25T20:16:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/plexe-ai/rya","commit_stats":null,"previous_names":["plexe-ai/rya"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/plexe-ai/rya","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plexe-ai%2Frya","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plexe-ai%2Frya/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plexe-ai%2Frya/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plexe-ai%2Frya/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/plexe-ai","download_url":"https://codeload.github.com/plexe-ai/rya/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plexe-ai%2Frya/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":36385949,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-08-06T04:43:03.162Z","status":"online","status_checked_at":"2026-08-07T02:00:06.708Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-08-07T16:01:39.497Z","updated_at":"2026-08-07T16:01:41.420Z","avatar_url":"https://github.com/plexe-ai.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cimg src=\"docs/assets/banner.svg\" alt=\"Rya - the backend your AI agents deserve\" width=\"820\"\u003e\n\n\u003cbr/\u003e\n\n[![License](https://img.shields.io/badge/license-Apache%202.0-191918?style=flat-square\u0026labelColor=37352f)](LICENSE)\n[![Python](https://img.shields.io/badge/python-3.10+-191918?style=flat-square\u0026labelColor=37352f)](pyproject.toml)\n![Self-hosted](https://img.shields.io/badge/open--core-self--hostable-191918?style=flat-square\u0026labelColor=37352f)\n![Coding-agent-first](https://img.shields.io/badge/coding--agent-first-191918?style=flat-square\u0026labelColor=37352f)\n\n**[Quickstart](#quickstart) · [Why it's different](#why-it-feels-different) · [Docs](docs/DEEP_DIVE.md) · [Deploy](deploy/AGENTS.md) · [Repository map](src/rya/AGENTS.md)**\n\n\u003cbr/\u003e\n\n\u003cimg src=\"docs/assets/demo.gif\" alt=\"A durable agent turn on Rya: streaming, a governed tool loop, recommendation cards, and a pause for human approval\" width=\"820\"\u003e\n\n\u003c/div\u003e\n\n---\n\n\u003e Durable runs, human approvals, memory, tools, guardrails, and observability -\n\u003e as primitives, not plumbing you rebuild every time. From prompt to\n\u003e production-grade agent backend in an afternoon.\n\nYou declare what an agent may do. The runtime enforces it, makes it durable, and\nstreams it live. Here is a complete agent:\n\n```python\nfrom rya import define_agent\n\nagent = define_agent()\n\n@agent.on_event\nasync def handle(ctx, event):\n    ticket = await ctx.tools.call(\"crm.lookup\", {\"email\": event.payload[\"email\"]})\n    reply  = await ctx.llm.respond(system=\"Draft a refund reply.\", input=ticket)\n\n    # pauses the run - durably, for days if needed - until a human approves\n    await ctx.approvals.request(\n        title=\"Issue refund\", body=reply.text,\n        action={\"tool\": \"refund.issue\", \"input\": {\"ticket\": ticket[\"id\"]}},\n    )\n    await ctx.channels.send(\"email\", {\"to\": ticket[\"email\"], \"body\": reply.text})\n```\n\nEvery `ctx.*` call is journaled. So this run survives a crash, resumes exactly\nwhere it paused, streams token-by-token to your UI, and leaves a full audit\ntrace - and you wrote none of that.\n\n## Quickstart\n\n```bash\nuvx rya create support-agent \u0026\u0026 cd support-agent\nrya dev --check                                           # validate + inspect. no keys, no database\nrya events send --type message.received \\\n  --payload '{\"email\":\"ada@example.com\"}'                 # run pauses for approval\nrya approvals approve \u003cid\u003e                                # resume; the email is sent\n```\n\n`rya dev` (without `--check`) starts the real thing locally: an `api` process\nand one `worker`, the same two processes as production, with the working tree as\nthe bundle.\n\nOffline it uses a mock model, so this just works. Set `ANTHROPIC_API_KEY` for\nreal Claude, `RYA_DATABASE_URL` for durable Postgres - the same agent code runs\non a laptop, a self-hosted box, and the cloud.\n\n## Why it feels different\n\n- **Approvals actually pause the process.** A human gate is not a prompt\n  convention - `ctx.approvals.request` unwinds the coroutine, persists, and\n  resumes in another process by replaying the journal. The model never sees a\n  gated tool.\n- **The model can act, sandboxed.** `ctx.llm.run` lets the model call tools in a\n  loop - and every call goes through the same permissions, scoped credentials,\n  egress firewall, and audit as your own code.\n- **Governance the runtime enforces, not the prompt.** Permission tiers,\n  server-side argument pinning, runtime kill switches, an egress firewall, and a\n  grounding gate that blocks any number the agent did not get from a tool.\n- **Durable chat, durable jobs.** Chat turns are leased and crash-reclaimed with\n  resumable token streams; the queue runs background work in any language with\n  retries and dead-letter. An interrupted turn is retried, not dropped.\n- **Coding-agent-first.** Claude Code, Codex, and Cursor drive the whole thing\n  over a CLI (`--json` everywhere), an MCP server, and skills - and `rya deploy\n  --check` is a green checklist they satisfy so they ship something safe.\n- **Yours to run.** Open-core, self-hostable, offline-capable. No SDK lock-in for\n  callers: any app talks to it over HTTP.\n\n## Ship it\n\n```bash\nrya deploy --check              # readiness gate: missing evals, ungated actions, secrets in the repo...\nrya deploy --env prod           # bundle + record an immutable version + promote\nrya rollback --env prod         # a pointer flip back\n```\n\nFrom a **client repo** — one that installed only the `rya` SDK and has no database\nor bucket access — the same pipeline runs over HTTP:\n\n```bash\nrya login https://rya.yourco.com --key rya_sk_…\nrya publish --env prod          # content-hash + upload + record + promote\n```\n\nThe platform rebuilds the hash from the bytes it received and refuses a mismatch,\nso the content is the address either way. What `publish` cannot do is attest\nreadiness — see the honesty list below.\n\nA deploy bundles your source, lockfile, manifest and SDK version into an\n**immutable, content-hashed version**, records it, and flips the environment's\ncurrent-version pointer. New runs go to the new version; in-flight runs finish\non theirs, and a version is retained while any run is still pinned to it — a\nrun can only be replayed against the code that wrote its journal.\n\n```bash\nrya versions list               # every version, newest first\nrya envs list                   # what each environment points at\nrya bundle                      # just the content hash — the CI \"did anything change\" check\n```\n\n**Gate what reaches production.** A promotion gate is a server-side admission\ncheck, not a client-side courtesy: it refuses unless *evidence* exists that the\nchecks passed against **this exact content**.\n\n```bash\nrya gate set --env prod --require-readiness --require-evals --require-provenance gitSha\nrya eval --attest               # files the result against the version under test\nrya promote --env prod --version \u003cid\u003e\n```\n\nEvidence is bound to the version, so a green eval run on a different tree cannot\nadmit this one. Rollback is deliberately never gated — a missing attestation must\nnot hold an outage open. `--force` works and is recorded against the version.\n\n**Bound what a workspace can consume.** Quotas are admission checks too, so an\nexhausted budget refuses the *next* run rather than killing one mid-journal:\n\n```bash\nrya quotas set --max-concurrent-runs 10 --max-cost-usd-per-day 25\nrya quotas show                 # consumption against each ceiling\n```\n\nThe platform runs as **two processes**, both the same image against the same\nPostgres:\n\n```bash\nrya serve      # api    — REST/WS/SSE, auth, policy, guard, vault, console, MCP\nrya worker     # worker — loads the bundle, owns the journal, executes handlers\n```\n\nThey are run modes, not microservices: one deployable, one database, no\nservice-to-service call — they coordinate through the queue. On the durable path\n(`POST /agents/{id}/turns`) the api process executes no handler code, which is\nwhat makes per-tenant isolation mean something — though two routes still bypass\nthat, see below. Deploy both with the AWS IaC in [`deploy/`](deploy/AGENTS.md) or\n`docker compose`. Serving many tenants from one deployment is an overlay on that same\nfile — `docker compose -f docker-compose.yml -f docker-compose.multitenant.yml up -d`,\nwhich rebinds 8787 to loopback and runs one least-privilege claimer per workspace\n([architecture.md](docs/architecture.md#self-host-multi-tenant)).\n\nA third mode is optional, and it is the one that means you stop declaring workers\nby hand:\n\n```bash\nrya supervisor            # watches claimable depth; starts, scales and reaps workers\nrya supervisor --plan      # what it would do, and why — the real decision, no effects\n```\n\nWithout it a worker is started by a human, a compose file or an ECS\n`DesiredCount`, so scale-to-zero is one-way: a key exits idle and stays unserved.\nWith it, work arriving is what brings the key back. Scheduling policy is ours;\nonly the launch mechanism is pluggable (`RYA_EXECUTION_DRIVER`: `local`, `docker`\nor `kubernetes`).\n\nTwo more commands exist for the hosted posture, and both are read-first:\n\n```bash\nrya posture                # is this deployment safe for untrusted tenants? all four conditions\nrya orgs budget \u003corg\u003e --usd-per-month 500   # the billing boundary above a workspace (D29)\nrya orgs reconcile         # recompute every org's rollup; run it from a cron\nrya posture --verify       # ...and probe the substrate rather than trusting its declaration\nrya keyring show           # which key provider — and therefore whether a purge can crypto-shred\nrya workspaces disable ws  # stop scheduling, refuse claims, revoke keys. Reversible\nrya workspaces purge ws    # shred the key, delete objects and rows. Not reversible\n```\n\n## Install\n\nTwo distributions, and they are **alternatives, not halves** — both own the `rya`\nimport namespace, so install one or the other:\n\n```bash\nuvx rya create my-agent                           # zero-install: scaffold + run\npip install rya                                   # client SDK: build an agent in your repo\npip install 'rya-server[api,mcp,postgres,llm]'    # the platform: serve, worker, console, store\n```\n\nA client repo needs `rya` and a deploy token. It never imports the runtime, never\nruns a server, and never knows which deployment it is running in — `ctx` is\nimplemented by the platform, at the platform's version, which is what stops\ngovernance being forked or pinned by a client. The SDK ships `ctx` type stubs so\nyour handlers still type-check. See [packaging](docs/PACKAGING.md).\n\n## Learn more\n\n- **[Repository map](src/rya/AGENTS.md)** - the codebase, module by module. Every\n  directory has an `AGENTS.md` written so a coding agent can orient fast.\n- **[Deep dive](docs/DEEP_DIVE.md)** and **[primitives](docs/primitives.md)** -\n  the full picture and every `ctx.*` primitive.\n- **[MCP setup](docs/mcp.md)** - point Claude Code / Cursor at Rya.\n- **[TypeScript SDK](docs/typescript-sdk.md)** - drive the platform from TS/JS:\n  events, resumable turn streams, approvals, and the SDK-free durable job API.\n- **[Packaging](docs/PACKAGING.md)** - `rya` vs `rya-server`, and the enforced\n  boundary between them.\n- **[End-to-end test](scripts/AGENTS.md)** - `python scripts/e2e_platform.py`\n  builds both wheels into two separate virtualenvs, authors an agent with only\n  the SDK, and runs it on a real `api` + `worker` pair: bundle handoff, promotion\n  gate, durable approval, crash-resume in a different process.\n- **[Langfuse](docs/langfuse.md)** - self-host it in one compose; every run and\n  eval score lands there, deep evals via DeepEval.\n- **[RWAP on Rya](docs/integrations/rwap.md)** - running a visual agent builder's\n  workflows on Rya's durable queue (architecture + AWS).\n\nHonest about maturity. Everything above runs today, and the durable-execution\nprimitives are correct and tested but young — not yet load-tested at high volume.\nSpecifically not done:\n\n- **No managed cloud.** Self-host it; that is also what makes self-hosting a\n  residency control.\n- **Publishing over HTTP cannot attest readiness.** `rya publish` uploads a bundle\n  to `POST /agents/{id}/versions` and needs neither the database nor the bucket, so\n  a client repo with only the SDK can ship. But the control plane does not import\n  bundles (D13), so it cannot evaluate readiness and files no attestation — the\n  response says `\"attested\": false`, and an environment gated on\n  `--require-readiness` will refuse the version. There is also no\n  `rya attest readiness`, so `rya deploy --env` from a machine with `rya-server`\n  remains the only way to satisfy that gate.\n- **The AWS mutator Lambda is a pattern, not an implementation.** It returns 501\n  by design rather than pretending; see [`deploy/aws`](deploy/aws/README.md).\n- ~~**Two routes still execute handler code in the api process.**~~ **Fixed\n  (D21).** `POST /agents/{id}/events` now writes a `queued` run — pinned to\n  whatever the environment points at — and hands it to a worker; the caller still\n  gets a run id synchronously and an over-quota call is still a 429 rather than a\n  silently failed run. `POST /approvals/{id}/approve` records the decision and\n  enqueues the resume, pinned to the run's own version. `/reject` stays\n  synchronous because it runs no tenant code at all.\n\n  That also ends the `E_JOURNAL_DRIFT` failure this entry used to describe. The\n  api imported its mounted entrypoint at startup, so once a bundle could be\n  published from elsewhere the code resuming an approval could differ from the\n  code that paused it — including by nothing more than an edit made after the api\n  booted. The resume job is pinned to `run[\"versionId\"]`, so the process\n  continuing a run is on the hash that paused it, by construction.\n\n  **One seam is deliberate and unchanged:** a bare single-tenant `rya serve` still\n  executes inline, because there the api *is* the whole deployment and silently\n  running nothing would be the worse failure. `RYA_API_INLINE_WORKER=0` (what\n  `rya dev` and compose set) turns it off, and multi-tenant never executes.\n- ~~**Crashed workers are still reported `alive`.**~~ **Fixed (Phase 3).**\n  Liveness is derived from heartbeat age, so a SIGKILLed worker comes back `lost`\n  rather than `alive` — and it is still *listed*, because an empty worker list means\n  scale-to-zero and a crash must not look like one. This was worse than a cosmetic\n  defect: `quotas` counts live workers against `maxWorkers`, so every crash leaked a\n  slot permanently.\n- **Node isolation is an accepted residual *in the default posture*.** Process\n  isolation plus RLS contains a buggy tenant, not a hostile one — workers share a\n  kernel. Phase 4 built the hostile-tenant posture (no credentials in the tenant\n  process, a gVisor sandbox, egress enforced by the network), but it is **declared,\n  not default**: `RYA_UNTRUSTED_TENANTS=1`. Without it, this bullet is what you have,\n  which is the right answer for a self-host with one tenant. `rya posture` prints\n  which one you are in.\n- **`rya worker` is one agent per process** — the api is not. `build_app` no\n  longer reads a manifest at all (D21): it learns what agents exist from published\n  versions and environment pointers, so one control plane serves as many as the\n  workspace has and `rya publish` accepts an agent it has never heard of. The\n  limit that remains is in the execution plane: `load_agent` mutates `sys.path`\n  and never unloads, so a second agent costs a second **worker** — not a second\n  api, port, database or bundle store. See\n  [docs/architecture.md](docs/architecture.md).\n\n  `rya worker --fork` (Phase 3, D27) moves the import out of the claiming process\n  into a warm interpreter it forks per run, so the long-lived process holds no\n  tenant code at all. It does not lift the one-agent limit — a fork is still one\n  agent on one version, which is the point of D3.\n\n  **Phase 5 lifted the limit on the *claimer*, and it was the configuration change\n  D27 promised.** `rya worker --scope tenant --fork` serves every agent a workspace\n  owns from one process: it reads each item's pinned version, materialises that bundle,\n  and forks an interpreter for it. Five agents with two live versions each is **one**\n  worker holding ten warm interpreters, not ten workers. A promotion costs no extra\n  process, and an approval resuming on a retired version is a fork rather than a\n  deployment. D3 is untouched: each fork still ran exactly one bundle's import.\n- **The fleet can span more than one box, and has not been run doing it.**\n  `rya supervisor` starts, scales and reaps workers on demand through the\n  `ExecutionDriver` seam, and `--all-workspaces` ticks every tenant. Phase 4 added the\n  `docker` and `kubernetes` drivers, so `local` is no longer the only one — but see the\n  gVisor caveat below. `ecs` is still unwritten.\n- **Untrusted tenancy is enforced by a refusal, not by documentation.**\n  `RYA_UNTRUSTED_TENANTS=1` makes the platform check all four of: a sandbox that\n  contains a kernel escape, a tenant process holding no credentials, egress enforced by\n  the network, and a driver that can put the broker somewhere the tenant is not. Any\n  one missing and it refuses to start, naming every unmet condition — because half a\n  security boundary is not a security boundary. The refusal is reachable from\n  `rya worker` as well as `rya supervisor`, which was a real gap until Phase 4: the\n  check existed and only the supervisor called it.\n- **What a container driver launches is a *pair*, and that took two phases to get\n  right.** Phase 5 found that the container drivers build the sandbox's environment\n  from nothing — correct for the process that imports tenant code, and impossible for\n  the process that has to open the database and *be* the broker. They were the same\n  container, so a `docker` or `kubernetes` claimer would have started, opened an empty\n  local store, and claimed nothing while looking healthy. The gate refused for a phase.\n  Phase 6 built the missing piece: `rya template-host`, a credential-free process that\n  serves warm interpreters over a socket, so the sandbox container can run tenant code\n  without the claimer having to be its parent. A launch is now a credentialed claimer\n  container beside a credential-free sandbox container sharing an in-memory volume, and\n  the credential boundary is a container boundary rather than a process one. The\n  framing turned out to be off by one: nothing was wrong with either environment\n  builder — the second container was missing.\n- **Two supervisors no longer double your fleet.** A supervisor takes a per-workspace\n  lease before it applies a plan; a second one goes passive, keeps observing, and logs\n  the plan it did not apply. That last part is deliberate: \"why is nothing scaling\" is\n  answered by reading a correct plan going unapplied, not by silence. `--no-lease` opts\n  out. Two supervisors over many tenants *split* the fleet rather than duplicating it.\n- **gVisor has now been run, and running it broke something reading it never would.**\n  `scripts/verify_gvisor.sh` puts a real `runsc` sentry under `cryptography`,\n  `pydantic-core`, `psycopg`, `yaml`, `httpx` and `os.fork`; all six work, so D23's\n  third-party-wheel question is answered. The isolation probe was not so lucky. Its\n  `/proc/version` marker was the literal `4.4.0`, copied from a fixture; a real sentry\n  says `4.19.0-gvisor`. That is not a missed signal but an inverted one — a version\n  string that is not gVisor's counts as evidence of a *host* kernel, so a genuine\n  sandbox was actively refuted and the launch gate refused it. And it refused in\n  exactly the configuration the platform ships, because the `--cap-drop=ALL` hardening\n  is what makes the other signal (`dmesg`) unreadable. **A fixture is a recording of an\n  assumption; it confirms that assumption forever.** The platform still will not claim\n  what it cannot verify: an inconclusive probe fails the launch gate.\n\n  What is still not measured is *cost*. The sentry runs nested in a privileged\n  container with `--ignore-cgroups`, because this host has no `runsc`, no passwordless\n  sudo, and AppArmor blocks unprivileged user namespaces. Correctness is unaffected —\n  the syscall interception is real — but the timing numbers keep their caveats, and\n  nothing has been measured on `x86_64` at all.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fplexe-ai%2Frya","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fplexe-ai%2Frya","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fplexe-ai%2Frya/lists"}