Rya core (Track A): retry/repair, credentials, streaming chat, Langfuse datasets - #2
Conversation
Add a secrecy output guard that rewrites configured secret-id patterns to a safe token instead of blocking the turn — the runtime form of production's scrubMasterIds(). Unlike grounding (which fails closed), secrecy scrubs and lets the redacted value through so the model keeps working. - secrecy_scrub_text / secrecy_scrub (per-leaf over dicts/lists) / secrecy_check - _compile_secrecy with a policy-keyed cache; a malformed pattern is skipped, never fatal, so one broken rule can't take down every tool call - opt-in via `secrecy.enabled` in rya.guard.yaml; applied by callers at the tool boundary and on outbound (wired in a later commit) Tests port chatstudyabroad/lib/utils.test.ts (scrubMasterIds): a Crizac master id (3-8 letters + 8+ digits) is scrubbed; numeric CAMS ids, passports, phones, years, and money are preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add upsert_connection to both FileStore and PostgresStore, keyed on (provider, owner). A login handler that re-mints a per-user bearer must not leave a stale duplicate the runtime could later inject, so the upsert rewrites the existing active connection in place — preserving id/createdAt, stamping updatedAt — rather than appending a new one. - FileStore: scan connections_dir for the existing (provider, owner, active) doc and rewrite its file; else create. - PostgresStore: SELECT ... FOR UPDATE on `owner IS NOT DISTINCT FROM %s`, then UPDATE or INSERT. Backs the ctx.connections.upsert SDK path (wired in a later commit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oy gate (A3) A per-user connection can expire mid-turn. Surface that as a clean, distinct outcome the caller can act on (log in again and retry) instead of a generic failure — mirroring production's CrizacAuthError -> reconnect prompt. No auto-refresh. - engine: an E_CONNECTION_EXPIRED RyaError sets run.status = needs_reconnect with its own run.needs_reconnect trace step (not run.failed); status added to the observability export gate. - turns: needs_reconnect added to TERMINAL_RUN_STATUSES. - api/app: needs_reconnect added to the manual run-status allow-list. - readiness: `rya deploy --check` now blocks E_PLAINTEXT_SECRET_AT_REST when a require_user provider has a stored connection sitting unencrypted at rest — seal() degrades to plaintext without key material, so this fails closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…edential enforcement (A1/A2/A5/A3)
Rewrite the _Tools.call backend path so retry, self-heal, secrecy, and adoption
all flow through one journaled step, backend-agnostic across agent/http/mock —
so a replay after an approval pause returns the memoized result and never
re-runs a retry or repair.
A1 — declarative retry + repair:
- manifest RetryDecl (max_attempts / backoff / on), with a YAML-1.1 `on: true`
rescue so an unquoted `on:` key is never a silent no-op.
- @agent.repair("<tool>") callback; a RyaRecoverableToolError self-heals once
with a patched input (tool.repair trace step). Repair (domain) and retry
(transient) are orthogonal budgets.
- _http_tool tags a 5xx (http_status) and a socket timeout (E_TIMEOUT) so the
retry classifier can see them; RyaError carries http_status.
A2 — secrecy wiring: the scrub runs on every tool result before it re-enters
the loop / journal / trace, and on every outbound message before it leaves.
ctx.guard.check_secrecy exposed for handler-side assertions.
A5 — adoption: a manifest `adopt: {field: scope.key}` copies a successful
result field into scoped memory so a later pinned tool in the same turn adopts
it (e.g. create_lead -> student_state.camsId).
A3 — per-user credential: credential resolution is scoped to egress backends
only (a local @agent.tool leaf never receives the secret, so provider: on it is
governance metadata); require_user fails closed with E_NO_IDENTITY when no
verified sub; _http_tool maps a 401 on a credentialed request to
E_CONNECTION_EXPIRED; ctx.connections.upsert mints/refreshes the caller's
connection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rya is a general-purpose agent runtime, so the core primitives and their tests should not carry a specific customer's business vocabulary. Replace Crizac/CAMS/counsellor/chatstudyabroad references (comments, test provider names, fixture field names, a secrecy-policy id) with neutral equivalents (external CRM, account id, user, opaque_master_id, session_state). Behaviour is unchanged: comment-only in src, self-contained renames in tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The governed agent loop seeded itself with only the current message, so
follow-ups ("compare those", "#2") had no earlier context to resolve
against. Add an optional keyword-only `history` param to _LLM.run that
seeds the loop with prior user/assistant turns ahead of the current
message. Blank-content and non-user/assistant rows are filtered so the
provider never receives an empty or unnormalizable message.
Backward-compatible: defaults to None (existing callers and journals
unchanged; the number/order of _step calls is unaffected). The caller
owns the window size, e.g. via ctx.sessions.history(...).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
support conversation history in
|
Handlers that need to call an upstream API directly (not through a declared `url:` tool) had no way to get at a connection's bearer - only leaf tools got credential injection. Add ctx.connections.secret(provider, scopes=...), the handler-side analogue: same per-user resolution and scope-intersection enforcement as tool injection, and the resolved secret is seeded into the redaction vault so it can never surface in a trace or log. Deliberately not journaled - a plaintext credential must never touch the run journal - so a replay re-resolves it live instead of memoizing it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
chat() gained an optional on_token callback: when set, the anthropic provider streams the response over SSE and emits assistant text deltas as they arrive, instead of blocking for the full completion. Tool-use blocks are rebuilt from input_json_delta fragments so a tool-calling step behaves identically to the non-streaming path - only the final text is additionally streamed. Providers without a streaming chat path ignore on_token and return the full result at once, so passing it is always safe. _anthropic_messages/_anthropic_tools are pulled out of _anthropic_chat so the streaming and non-streaming paths build the request body from the same code and can never diverge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…taset Rya could only push runs/scores to Langfuse; there was no way to run the agent over an existing Langfuse Dataset. Add `rya eval --langfuse-dataset <name>`: fetches the dataset's items, fires a real engine run per item (--trigger-type/--payload-defaults control how an item's `input` maps to an event), and links each run's trace back to its dataset item as a dataset-run-item under a named run - so the run shows up under Datasets -> runs in the Langfuse UI, one row per item. An item may carry a Rya `expect` block under its metadata to be scored with the same SCORERS as local eval cases (extracted local-suite scoring into _score_expect so both paths share it); a failing item still exits non-zero like the local suite, so a Langfuse dataset can gate a deploy too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
handler-side credentials, streaming Anthropic chat, Langfuse dataset evals
|
The repo had no linting at all - style and import hygiene were whatever each file happened to land on. Add ruff (one tool for both lint and format), configured entirely in pyproject.toml: line-length 120 (the tree only has 31 lines over it), target py310 to match requires-python, and the F/E/W/I/UP/B/ C4/SIM/RUF rule set. Three ignores, each with a stated reason - E501 (the formatter owns wrapping), B008 (Typer/FastAPI declare params as calls in defaults, 48 intentional hits), SIM105 - plus per-file E402 for tests/ (conftest sets sys.path before importing rya) and examples/. Config only: no source file is touched by this commit. The tree is NOT clean against it yet - `ruff check` reports 739 findings (562 safe-autofixable, mostly UP045 Optional -> `| None`) and `ruff format --check` would rewrite 88 of 136 files. Fixing those is deliberately deferred and will happen gradually, file by file as they're touched, so real changes aren't buried under thousands of lines of churn. CI is also pending: there is no .github/ in this repo, and wiring a workflow now would only fail red. Once the tree is closer to clean - or as a changed-files-only gate - lint/format/pytest should run there. docs/devex.md documents the commands and this convergence plan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ruff as linter + formatter — config only, cleanup and CI still pendingThe repo had no linting at all, so style and import hygiene were whatever each
No source file is touched by this commit — nothing in the diff is Deliberately deferredThe tree is not clean against this config yet. Current baseline:
Largest buckets: Fixing these is intentionally not in this PR. A repo-wide autofix + CI is also pending. There is no |
A real agent turn exported a trace that was hard to read: the model steps themselves were contentless, most observations were empty rows, everything was flat, and the reported latency was wrong. Root causes, all in the export path: - The governed agent loop (ctx.llm.run) emits its model steps with kind `llm.chat`, but `_LLM_KINDS` only listed `llm.respond`/`model.call`. Those steps fell through to the generic branch and were exported as contentless EVENTs — no prompt, no response, no model, no tokens. Only the single-shot sidecar (llm.respond) rendered as a GENERATION, so the interesting calls were the ones missing. The same mismatch made run_usage skip the whole compose loop, silently under-reporting tokens and cost. - Every step records its return value, but the exporter only mapped input/output for LLM and tool steps; everything else dumped it into `metadata.result`, so memory/session/ui observations rendered blank. - No parent was ever set, so observations were flat siblings of the trace. - Steps carried only `ts`, which is second-resolution, so a whole turn collapsed onto one instant and could not even be ordered reliably. - The trace output read `run["result"]`, which the engine never populates — the handler's return value lives on `run["output"]`. Changes: - Recognise `llm.chat` as an LLM call in both the exporter and usage metering, and journal the messages array + model parameters so the generation shows the real prompt it was sent. - Record a true wall-clock span (`startedAt`/`endedAt`, ms) around every journaled step. `store.now_iso()` is deliberately untouched: it has 63 call sites and its second-resolution format is compared when scheduling. - Build a real tree — run root -> agent loop -> model step -> the tools that step called. ctx.llm.run stamps a `loopId` on its steps and on the tool calls they trigger; the loop span is synthesised at export time, so no extra journaled step is created and approval-pause/resume replay is unaffected. - Give every observation input/output, and demote runtime bookkeeping (memory/session/file/connection/knowledge) to DEBUG under one collapsed `context` span, so the top level shows the agent's work rather than the runtime's. Durable operations are exported as SPANs because Langfuse only accepts an endTime on SPAN and above; instantaneous markers stay EVENTs. Failures carry level=ERROR plus a statusMessage. - Populate trace-level `output` (from run["output"]), `sessionId` (recovered from the session step, which enables Langfuse's Sessions view for multi-turn agents), `userId` (new, set from the run's identity), and tags. - Mirror the same hierarchy and real timings in the OTLP exporter so Phoenix/Tempo/Datadog show the identical shape. Runs recorded before this change have no startedAt/loopId and still export, falling back to the previous seq-derived ordering (covered by a test). Verified against a live Langfuse instance: latency now reflects reality (10.1s for a turn previously reported as 0.02s), no non-EVENT observation has null input/output, and generations carry model parameters and usage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…icy, workers Adds the store-layer primitives PLATFORM_DESIGN needs above the existing per-run journal: content-addressed version records, per-environment pointers with promote/rollback history, an append-only policy log, worker registration, and the queue/quota counters that back admission checks. Both the in-memory and Postgres stores gain the same surface so tests and production stay behavior-identical. Also declares the new platform error codes (journal drift, version/bundle/environment not-found, promotion-blocked, quota-exceeded) so they map to the right CLI exit codes instead of falling through to generic.
…otas, worker plane Implements the core PLATFORM_DESIGN modules on top of the new store primitives: - config.py: per-environment run config as data (D8) — env/secrets/model routes are resolved once from an explicit mapping, never from os.environ. - bundles.py: content-hashed deployment artifacts (D12) — sha256 over sorted (path, content) pairs plus SDK version, packed as a byte-deterministic tar.gz; local and S3-backed storage arms. - deployments.py: version/environment orchestration (D11/D12) — create, promote, rollback, retire, pinned-run tracking. - gates.py: promotion admission checks (§9) — readiness and eval attestations gate staging->prod rather than being a client-side courtesy. - quotas.py: per-workspace resource limits (D13, §11.12) — concurrent runs, runs/day, queue depth, tokens/cost per day, worker count. - worker.py: the execution plane (§5.2/§6) — per (workspace, agent, version) process that loads a pinned bundle, advertises its handler set, registers, heartbeats, and scales to zero. - cli/client.py: thin HTTP client the CLI and e2e script use to talk to a running platform server. Each module ships with its own test module (bundles, bundles+S3, config, deployments, promotion gates, quotas, worker, plus cross-cutting platform governance tests).
…the platform Connects the new store/platform modules to every surface that talks to them: - api/app.py: routes for versions/deploy, promote/rollback, promotion gate policy and attestations, quota get/set, worker registration and heartbeat, workspace usage. - cli/main.py + cli/__init__.py: `rya deploy`, `rya env promote/rollback`, `rya worker`, gate and quota subcommands, wired through the new cli/client.py HTTP client; scaffold no longer needs a tweak that platform deploy makes redundant. - guard.py: policy enforcement extended to cover the new promotion/quota policy keys (D7 — a bundle cannot write privileged platform state). - runtime/engine.py, queue.py: version-pinned execution and fair per-workspace claim ordering (the scheduling half of D13, quotas.py is the resource half). - readiness.py: the local check is now also what the server-side promotion gate runs (§9). - snapshot.py, tenancy.py, turns.py: multi-tenant/version-aware plumbing these needed to carry a version and workspace through a run. - manifest/loader.py, manifest/schema.py: drop the manifest-level `environment` field now that an environment is a deploy-time pointer, not a build-time property of the artifact (D11). - providers/*, sdk/context.py, sdk/__init__.py: consume config.py's explicit RunConfig instead of reading os.environ directly (D8). - observability/usage.py: durable per-workspace token/cost meter that quotas and gates read from. - console: surfaces versions, environments, gates and quotas in the UI. Tests updated/added alongside: test_api, test_cli, test_console, test_evals, test_guard, test_job_groups, test_llm_layer, conftest fixtures, and a new test_sdk_surface covering the public SDK import surface.
scripts/e2e_platform.py drives a running server through the CLI client (deploy, promote, run, gate, quota, worker) to catch wiring bugs that unit tests miss because they exercise each module in isolation. AGENTS.md documents the new scripts/ directory.
…latform) PLATFORM_DESIGN D16: PyPI `rya` becomes the thin client SDK; the platform ships separately as `rya-server`. Both are built from this same tree via packaging/sdk and packaging/server, each with its own pyproject.toml, LICENSE and README; packaging/surface.py checks that the SDK package only exposes the intended public surface (type stubs included). The root pyproject.toml stays the whole-tree dev install (`pip install -e .`) — see docs/PACKAGING.md for why the two published wheels are alternatives rather than co-installable halves. uv.lock is now committed so the dev environment resolves reproducibly.
Breaks the monolithic src/index.ts into focused modules (http.ts, sse.ts, queue.ts, turns.ts, errors.ts, types.ts, client.ts) so the published `rya` package and this client track the same PLATFORM_DESIGN D16 SDK/platform boundary as the Python side. Adds a real unit test suite (test/*.test.mjs) covering request handling, SSE, queue polling and error mapping, a tsconfig.check.json for standalone type-checking, and checks/usage.ts as a compile-time usage sample. docs/typescript-sdk.md documents the client for external consumers.
…ployment Reflects the deploy/worker/environment split in the AWS CloudFormation template and local docker-compose stack: separate worker service(s), the Postgres-backed store, and the environment variables the new config.py and multi-tenant worker plane expect.
Follows the manifest schema change (D11 drops the build-time `environment` field) and config.py's explicit RunConfig, plus a README note for csa-counsellor.
…nding docs PLATFORM_DESIGN.md moves from "proposal / RFC" to a record of what shipped, trimmed down to the settled decisions and how they map to the modules that now implement them (store, bundles, config, deployments, gates, quotas, worker, packaging, the TS client split). The original RFC text is preserved verbatim in PLATFORM_DESIGN.superseded.md for history. README, DEEP_DIVE, KNOWLEDGE, VISION_GAP, architecture and mcp docs are updated to describe the platform primitives (versions/environments, promotion gates, quotas, workers, the rya/rya-server package split) instead of the pre-platform single-process model.
Four conflicts, all where both branches extended the same seam: deploy/aws/template.yaml — main moved the template into the package (src/rya/deploy_assets/template.yaml) and left a symlink behind; this branch edited it in place. Took main's structure and replayed this branch's edits (worker service, RyaEnvironment/WorkerCount, fail-closed mutator) onto the packaged asset. The api container keeps both new env vars. The worker task also gets the Langfuse trio: §11.7 moved handler execution off the api process, so the worker is where spans are now produced — without it `rya deploy aws --langfuse` would stand Langfuse up with nothing writing to it. src/rya/cli/main.py — both sides added a deploy mode. Signature and body are the union: `rya deploy aws|status|destroy`, `--env <name>`, and the original `--target`. Combining an action with --env now fails E_VALIDATION instead of silently doing only the bundle promotion. src/rya/console/index.html — union of the view lists, and the Govern nav no longer duplicates id="nav3" (it is nav4, added to NAVBTNS). show() keeps this branch's superset. Overview cards keep main's terse one-liners from a1e3071 rather than reinstating the long copy; the new Deployments card matches that voice. 521 passed, 39 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`_CODE_EXIT` is the contract a coding agent branches on, so a code missing from it is not a cosmetic gap — it exits 1, which reads as "the runtime crashed" rather than "your input was refused". - E_EGRESS_BLOCKED exited 1 while its sibling E_GROUNDING_BLOCKED exits 5, so an Action Guard refusal was indistinguishable from a crash. Now 5. - E_NO_EVENT_HANDLER is a refusal to ship, not a crash. Now 7, matching E_NOT_PRODUCTION_READY. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The S3 arm was complete and tested but could not talk to MinIO, Ceph or R2.
Pointing boto3 at a custom endpoint is not enough: botocore gives
`s3.addressing_style` no environment variable at all, and the default
virtual-host form templates a custom endpoint as `{bucket}.{host}` — so
bucket `rya-bundles` at http://minio:9000 becomes http://rya-bundles.minio:9000
and fails to resolve on a container network. Declaring RYA_BUNDLES_S3_ENDPOINT
therefore also forces path-style addressing, which is why the knob must stay
empty on real AWS, where virtual-host addressing is wanted.
`files_s3.py` gets RYA_FILES_S3_ENDPOINT with the same three lines rather than
a shared helper: it is platform-only and `bundles.py` is in the SDK surface, so
a common module would drag one into the other's wheel.
`unpack()` gains an optional `max_total_bytes`, checked before a byte is
written. A local deploy unpacks an archive it just built and leaves it unset;
an endpoint accepting archives from the network cannot, because gzip's ratio is
an amplification factor and "the upload was under the limit" says nothing about
what it expands to.
`botocore` joins SDK_OPTIONAL_THIRD_PARTY — bundles.py is an SDK module, so its
deferred import of Config would otherwise fail the surface test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the gap the README named: `rya deploy --env` shipped only in
`rya-server` and wrote bundle bytes and version rows directly with local
credentials, so a client repo holding just the thin SDK had nowhere to ship to.
`POST /agents/{agent_id}/versions` takes the packed archive as a raw
`application/gzip` body with the scalars in the query string — the shape
`POST /files` already established, and one that keeps the client stdlib-only.
It rebuilds the content hash from the bytes it received rather than trusting the
sidecar, because D12's point is that the CONTENT proves the version. A mismatch
names SDK skew when that is the cause, since the digest folds in the SDK version
and identical bytes hash differently across SDKs — the generic "artifact was
modified" message would send an operator hunting tampering that never happened.
It never imports the bundle (D13), which has a consequence worth stating rather
than hiding: readiness cannot be evaluated and no attestation is filed, so the
response carries `"attested": false` and a readiness-gated environment refuses
the promotion. `rya deploy --env` remains the only attesting path.
Publishing is refused outright while the control plane is unauthenticated.
Every other route is open in that mode; this one would be anonymous code upload
to a box whose worker imports it.
`publish` and `check` are defined in the client CLI and re-registered in the
operator CLI. Both distributions own the `rya` console script, and the
recommended local loop is an editable install of the platform — a client-only
command would vanish exactly when a developer is dev-linked against a checkout.
`publish` deliberately does not check that declared tools have implementations:
a tool may be served by a built-in from `tools/registry.py`, which the SDK does
not ship, so the client cannot tell a built-in from a hole and would reject the
scaffold's own template. The worker's preflight owns that check.
`RemoteClient` now preserves the server's error code instead of collapsing every
HTTP failure to E_REMOTE, so the stable-code contract survives the network
boundary. `_load_agent` also raises the registered E_AGENT_NOT_DEFINED rather
than an undeclared E_NO_AGENT — one condition must not report two codes
depending on which side of the split noticed it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…exist Both strings reach a user directly, and `rya publish` makes the first far more likely to be seen. `gates.py` told an operator whose promotion was blocked to run `rya attest readiness --version <id>`. No such command exists in either CLI, and the HTTP publish path cannot produce that evidence at all, so the one hint shown on the most likely failure was unactionable. It now names only what exists. The MCP tool's docstring and payload said hosted deploy was "a later milestone". Shipping exists. An MCP client renders the docstring as the tool description, so that claim was being served to coding agents as current fact. Publishing stays absent from MCP on purpose — a version promotion should need a human running the command — and the strings now say that instead of implying it is unbuilt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The api and the workers are separate containers, so a container-local
`.rya/bundles` leaves a worker resolving a version whose artifact it cannot see.
MinIO is therefore a service, not an option, and `s3` (boto3) stops being an
optional extra in the runtime image — without it the first publish fails
E_BUNDLE_STORE. The bucket is created by `mkdir`, since on MinIO's filesystem
backend a top-level directory in the data dir IS a bucket; that keeps it one
service instead of two.
`worker-pinned` sits behind the `pinned` profile because it is a different
operating model rather than a better one: it serves ONE content-hashed bundle,
ignores the mounted tree, and needs a restart to pick up a re-publish. It also
crash-loops until the first promotion, since resolving an environment with no
current version is an error rather than a wait — `restart: unless-stopped` turns
that into "comes up when you publish".
The project mount becomes `${RYA_PROJECT:-…}` on all three services. One
variable, because pointing only one service somewhere new would leave the api and
the worker serving different code. `/project/.rya` becomes a named volume: the
containers run as root, so without it the worker leaves root-owned files inside
the developer's working copy that they cannot delete.
Both deploy/aws images gain `s3` too. `template.yaml` sets RYA_FILES_S3_BUCKET on
both tasks and `files_s3` needs boto3 to honour it, so Dockerfile.baked would
have failed on its first offload; Dockerfile.project only worked because the
`[bedrock]` layer happened to drag boto3 in, which would have broken silently the
day Bedrock was dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`phase_publish` is the only place in the repo where a venv already proven unable to import `rya.worker` ships code to a real platform, which is the whole claim of the HTTP path: a client repo needs a URL and a key, not database or bucket credentials. The load-bearing assertion is idempotency against the operator path. Publishing the same tree `rya deploy --env` already deployed must return the SAME version id — the two paths agree on the content address, or the hash is a label rather than an address. Also asserts the honest limit: a readiness-gated environment refuses an unattested HTTP publish while still recording the version. That needs NEW content, because the version the CLI pipeline created carries a readiness attestation from `rya deploy` and gating on it would pass for a legitimate reason and prove nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
D11 removed the field: one content-hashed bundle is promoted between environments, so anything that differs per environment is platform config. The loader already stripped the key with a warning, which is why this went unnoticed — but `readiness.py` raises it as a BLOCK, so `crizac` and `csa-counsellor` could not be deployed at all. Removing it is behaviour-preserving. All four examples now report `"ready": true` from `rya check`. AGENTS.md gains the two examples it never listed, the rule that no manifest may carry the key, and a pointer to the separate rya-examples repo for the external-author experience. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…copies The ops skill claimed "deploy" in its description and had no shipping section at all. It now covers both paths, the pinned-worker restart, and the one-agent-per-deployment limit — the three things an agent gets wrong first. Editing it exposed a trap: `skills/<name>/SKILL.md` is duplicated by hand into `src/rya/skills/skill_data.py`, and that module is the one that SHIPS (it is in the SDK wheel and `rya skills install` writes from it). They were in sync, so editing only the repo copy would have changed the documentation while every installed agent kept the old text. `tests/test_skills.py` now fails if they diverge, and also checks the frontmatter each skill is routed by and that the bodies survive the triple-quoted literal they live in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The limit lands in architecture.md, which owns topology and never stated it:
`build_app` resolves a single rya.agent.yaml at startup, so `{agent_id}` in every
route is decorative and `POST /agents/{id}/versions` refuses a bundle declaring a
different name. Serving a second agent means a second deployment, sharing one
Postgres and one bundle store. Its "Execution model (today: single worker)"
section had to go anyway — "one worker process runs all agents" was doubly wrong.
Also corrected there: the plane boundary is about IMPORTING code, not touching
state. Publish writes artifacts and flips pointers from the api process without
importing a handler, and that distinction is what makes the unattested-readiness
consequence follow rather than look like an oversight.
Reference tables brought back in step with the code across DEEP_DIVE, KNOWLEDGE,
devex and primitives: the new route and its contract, the CLI surface, the
error-code/exit-code and HTTP-status mappings (every row verified against
`_CODE_EXIT`), and the object-store and publish environment variables.
Pre-existing drift fixed while in there, most of it actively misleading:
- DEEP_DIVE's showcase manifest declared `environment: local`, i.e. it was
un-shippable; `export: langfuse` is not a manifest field; the provider list and
the 13-name `ctx` list were both short; RYA_LLM_PROVIDER does not exist.
- devex's exit-code table contradicted itself — E_ENTRYPOINT_NOT_FOUND was listed
as 3 and also matched a literal `E_*_NOT_FOUND` wildcard mapping to 4.
- KNOWLEDGE claimed "no worker on the deploy" as an open gap; the AWS template has
had a WorkerService and WorkerCount for some time.
- PLATFORM_DESIGN §7 still described `Engine._execute_action` as an ungoverned
second dispatch path and named DEEP_DIVE as overclaiming on that basis. The code
was fixed: it delegates to `ctx.tools.call_approved`, `prepare()` refuses an
approval_required tool unless approved=True, and `ctx.tools.call` never passes
it. DEEP_DIVE was right and this document was stale; §11.1 is marked done.
- VISION_GAP's control-plane section header said ❌ while its own body now says the
plane split is deployed.
- Test counts everywhere: 63 files, 562 test functions.
deploy/aws documents that `rya publish` MISFIRES SILENTLY against that stack —
no bundle bucket, a task role scoped to FilesBucket, and a worker running bare
`rya worker` — so a version records, the call returns ok, and a pinned worker
later fails E_BUNDLE_NOT_FOUND. The three-part fix is written down; it is not
built. Also that the endpoint overrides must stay empty on real AWS.
Not done, listed so the next person does not assume otherwise: KNOWLEDGE's
endpoint list and DEEP_DIVE's console-nav list are still incomplete, VISION_GAP
has four other self-contradictions its own preamble claims were fixed,
PLATFORM_DESIGN's LOC count and template.yaml citations are stale, and §2/§11
still call the client package `rya-sdk` where D16 says `rya`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-scoped from #1 to contain ONLY the Track A core-runtime primitives:
Commits (4), ordered guard → store → runtime → sdk; all changes are under
src/+ roottests/.The csa-counsellor example (Track B) is being moved to another repo, so it is intentionally not part of this PR. Supersedes #1.