Agent docs

The capability manifest

> Evidence: 1 file:line witnesses as of 2026-08-18.

Evidence: 1 file:line witnesses as of 2026-08-18. Confidence: derived, written from the seams and the estate's failures rather than from citations; treat each rule as a design call and argue with it. What changes it: the first module built against this doc (../PLAN.md forge-1) and its instance breakdown.

What a module says about itself, machine-readably, so an app, an agent, or a future workflow composer can discover what it does, what it needs, what it gives back, and which version answered. No instance has one. versable-runner's GET /usage, a plain-text guide generated from the live method registry and aimed at LLM agents (src/services-api/app/usage.py), is the closest thing, and it is the right instinct pointed at the wrong reader: a person or a model can read prose; code cannot.

Owner ruling 2026-08-17: composability is supported from the start even if the composer is not built, and standalone must work. The manifest is how both are true at once. It costs a module one JSON document; without it, the first attempt at composition means reading every module's source.

Audience: anyone building a module (you publish one), an app that calls modules (you read them), or the future composer.

The rule#

Every module serves GET /manifest, unauthenticated, and everything a caller needs to call it correctly is in there. A capability that is not in the manifest does not exist; a param the manifest does not list is rejected; an error code the manifest does not list is a bug.

Same stance as versable-builder's barrel: "if it is not exported there, it does not exist" (versable-builder/docs/KIT-HANDBOOK.md:10-11).

What is in it#

{
"module": "content", stable module id
"module_version": "2.3.0", semver of this deployment
"contract_version": "0.1", which Foundry contract this conforms to
"build": { "commit": "…", "built_at": "…", "branch": "main" },
name and version are module and module_version
"env": "prod",
"capabilities": {
"content.generate": {
"summary": "Marketing copy from a template, with web research",
"params": { …JSON Schema… }, validated on create
"item": { …JSON Schema… }, shape of items[].input
"output": { …JSON Schema… }, shape of a result outcome's output
"error_types": { "RESEARCH_QUALITY_LOW": { "label": "Research too thin", "description": "…" },
"TEMPLATE_INVALID": { "label": "Template invalid", "description": "…" } },
"review_reasons": { "UNVERIFIABLE_CLAIM": { "label": "Unverifiable claim", "description": "…" },
"INPUT_MISSING": { "label": "Input missing", "description": "…" } }, reserved code, declared because requires uses it
"confidence": { "self": true, "judges": ["judge.copy"], "authoritative": "self" },
"requires": [ { "field": "part.type", a field-registry id
"need": "required",
"from": ["caller", "parttype.match"],
"on_missing": "needs_review:INPUT_MISSING" } ],
"provides": [ "content.title", "content.description", "content.bullets" ],
"variants": { "research": { "options": ["self", "extractor"], "default": "self" } },
"ceilings": { "max_items": 10000, "max_concurrent_jobs_per_caller": 4,
"max_concurrent_jobs_per_tenant": 8, "max_item_bytes": 262144 },
"settings": [ "max_attempts", "item_timeout_s", "callback_url", "callback_events" ],
"cost_model": { "unit": "item", "typical_usd": 0.012, "meters": ["llm.tokens", "research.calls"] },
"log": { "levels": ["debug", "info", "warn", "error"], "default": "info" },
"facets": ["user", "workflow_run"], attribution keys GET /jobs counts live
"outputs": { "catalog-rows": { "shape": { …JSON Schema… }, one row per settled item
"version": "1", names the materialization (canon/15)
"reads": ["output.title", "output.description", "output.bullets"],
"partial": true,
"formats": ["csv", "xlsx", "jsonl"] } }
},
"judge.copy": { … a judge is a capability like any other; its item schema is another capability's output … }
},
"surface": {
"routes": [ "POST /jobs", "GET /jobs/{job_id}", "GET /jobs/{job_id}/outcomes", … ], METHOD then path, as module-surface writes them
the normative list is contracts/manifest.md
"page_size": { "default": 100, "max": 1000 },
"callbacks": { "events": ["job.state", "item.outcome", "usage"], "signature": "hmac-sha256", "max_batch": 500 }
},
"identity": { "verifier": "jwks", "issuer": "https://id.versable.ai", "audience": "content",
"scopes": ["jobs:submit", "jobs:read", "jobs:cancel", "stats:read", "stats:admin"] },
"runtime": { "queue": "cloudtasks", "storage": "gcs", "in_process_supported": true, "topology": "api-worker" },
"retention": { "inputs": { "dev": "7d", "prod": "90d" },
"outcomes": { "dev": "30d", "prod": "1y" },
"artifacts": { "dev": "7d", "prod": "90d" },
"logs": { "dev": "7d", "prod": "30d" },
"usage": { "dev": "never", "prod": "never" },
"caches": { "dev": "ttl", "prod": "ttl" } }
}

Field by field, the ones that carry weight:

  • params, item, output as JSON Schema. These are what the create route validates with and what a Catalog Output Template targets. That a per-module UI can be generated from them (customers "won't be writing json configs ever") is available, not required: the kit's SchemaForm ships in its next bump (owner ruling 2026-08-18) and renders the declared JSON Schema subset its lintSchema accepts, but the contract stays wide (full draft 2020-12; the same ruling), so a schema outside the subset is rendered by a hand-built form or the JSON view, never rejected. A module runs the lint when it wants a generated form (../contracts/manifest.md § Validation names the fixtures and the two rules that hold regardless of vocabulary). What the manifest can promise now is fields, not layout: each field the module reads or writes carries a presentation hint (type family, unit, tone) that maps onto kit cell types, and the app renders rows with the kit's column factories.
  • error_types and review_reasons as closed sets. An app builds rules and UI against codes, not messages. Each code carries the label a status pill shows and the description a tooltip shows, so an app never invents human text for a module's code. A new code is a manifest change and a version bump.
  • confidence. Whether the capability self-reports, which judges it knows how to attach, and which score is authoritative for the outcome's confidence field (03-jobs-and-state.md).
  • requires and provides. The composability hooks. requires names inputs the capability needs and where they can come from: a caller can supply part.type directly, or a parttype.match job's output can. Both names are field-registry ids, which is what makes the two agree; a requires or provides entry that is not a registry id is banned. The module does not care which; the future composer does. provides names what this capability puts on an item, in a vocabulary shared across modules (contracts/manifest.md § The field registry holds the field names). Standalone callers ignore both and pass what they have.
  • variants. Different coverage for the same purpose, selectable per job: research done by the module itself, or delegated to a prior extractor step; confidence from self or from a judge. 14-graceful-degradation.md.
  • ceilings. What 429 and 422 mean here: the job and concurrency limits that shed load, and the per-item size that gets rejected outright.
  • cost_model. What the usage events will contain and a typical cost, so a caller can estimate before submitting and an app can build a price on it (08-usage-and-credits.md).
  • facets. Which attribution keys GET /jobs returns live counts for, so an app can build a filter bar without guessing which keys are cheap to count. user is always facetable and carries the reserved {id, label} shape.
  • outputs. The transforms this capability offers over its own outcomes: a name, the outcome fields it reads, the shape it produces, and whether it will run on a job that is still going. Absent means the module serves raw outcomes and nothing else (15-outputs-and-transforms.md). A reads entry may end in .*, meaning every key under that prefix; a capability whose output columns are named by a person at run time has no other way to declare them at build time.
  • retention. How long each class of bytes survives, per environment, stated where a caller can read it rather than promised in a runbook (05-storage-and-persistence.md). A manifest without it is invalid.
  • contract_version and module_version. Which contract, which build. A caller pins the contract version it was written against and can tell from the manifest whether the module still speaks it.

Versioning#

Three numbers, three meanings:

NumberChanges whenWho reads it
contract_versionthis doc set changes a required route or shapeevery client, once
module_versionthe module ships; semver: patch for fixes, minor for a new capability or additive field, major for a breaking schema or removed capabilityapps deciding when to upgrade; the degradation rules
capability schemaa capability's params, item, or output schema changes shapecallers of that capability; the UI generated from it

Breaking route changes bump the path prefix (/v2/jobs); the old prefix keeps serving until every caller has moved, and the manifest lists both. Two majors of one module can run side by side as two deployments (14-graceful-degradation.md), each with its own manifest.

/build-info is {commit, built_at, branch?, name, version, contract_version}, snake_case, one shape for modules and apps alike (contracts/module-surface.md). Its name and version are this manifest's module and module_version, so the two agree by construction rather than by discipline. What the manifest adds is everything /build-info has no room for: which capabilities that build offers and what they accept. One route says which build is running, the other says what it promises. speedway and walmart-mvp both serve a build route (/build-info, /api/build-info); no instance serves a manifest.

For a person or an agent#

GET /guide is an optional route rendering the manifest as prose with examples: the runner's existing guide is good and agents use it. It is generated from the manifest, never hand-written, so it cannot drift. The route is named /guide and not /usage because /usage/events is the usage route; versable-runner serves the guide at /usage today, which is the collision (contracts/module-surface.md). /usage may alias /guide for one release.

Do-nots#

  • Do not accept a capability, param, or setting the manifest does not list.
  • Do not emit an error code or review reason the manifest does not list.
  • Do not hand-write the prose guide; generate it from the manifest. (versable-runner generates /usage from the registry, which is the right half of this)
  • Do not change a schema without bumping module_version, and do not remove a capability without a major bump and a period of running both.
  • Do not require a caller to read source to learn what a module needs.
@versable-git/ui · reference, canon, and method, read in place