Agent docs

Contract: the capability manifest and the field registry

The normative shape of GET /manifest and the shared vocabulary that requires and provides name…

The normative shape of GET /manifest and the shared vocabulary that requires and provides name across modules. canon/11 says why; this says exactly what. Written 2026-08-17 at the session's default effort (high) as a first cut; the field registry in particular wants a deeper pass before the first two modules build against it (../PLAN.md, nudges).

Audience: anyone publishing a manifest, generating UI from one, or writing the composer that will read them.

Top level#

FieldTypeRequiredMeaning
modulestring matching ^[a-z][a-z0-9-]*$ (anchored; the whole value)yesstable module id, never reused for a different thing
module_versionsemver stringyeswhat this deployment promises; bumped per canon/11 versioning
contract_versionstringyeswhich Foundry contract this conforms to; 0.1 for this tree as written
build{commit: string, built_at: RFC 3339 string, branch?: string}yessame values as /build-info, whose name and version are this manifest's module and module_version
envlocal, dev, staging, prodyes
capabilitiesmap of capability id to capability objectyes, at least one
surface{routes: array of strings, page_size: {default: integer >= 1, max: integer >= 1}, callbacks?: {events: array of strings, signature: string, max_batch: integer >= 1}}yesroutes present as <METHOD> <path> exactly as module-surface.md writes them (internal routes deliberately unlisted), page sizes, callback support
identity{verifier: one of jwks, keys, remote, local; issuer: string, audience: string, scopes: array of strings}yesverifier in use (../guides/03-machine-identity.md), issuer URL, audience, scopes this module recognises
runtime{queue: string, storage: one of gcs, s3, local; in_process_supported: bool, topology: one of self-push, api-worker}yesqueue adapter, storage adapter, whether in-process is supported, dispatch topology (canon/04)
retentionmap of one of inputs, outcomes, artifacts, logs, usage, caches to {dev: duration, prod: duration}yescanon/05 classes: inputs, outcomes, artifacts, logs, usage, caches. A jobs index or any other structure rebuildable from the outcome records is not a class and declares no retention
vendorsarray of {key: string matching ^[A-Z][A-Z0-9_]*$, purpose: string, required: bool}nooutbound credentials this module needs, by env-var name (canon/09 vendors.*). Declared so an operator can see what to supply per module without reading its source; /health/deep reports each as set or absent and never the value. Absent means the module calls no vendor
deprecationsarray of {kind: one of capability, route, error_type, review_reason, state; target: string, deprecated_at: RFC 3339 string, replaced_by: string, removal_after: RFC 3339 string}noannounced before removal; target is the id, route or code being retired (canon/14, ../patterns/03)
statesarray of one of pausednooptional job states this module implements beyond the standard set (canon/03); absent means none

A capability#

Capability ids are <domain>.<verb>[.<qualifier>], lowercase, dotted: content.generate, parttype.match, attribute.normalize, image.generate, extract.pages, judge.copy. A judge is a capability. Batching is not a qualifier: a capability that prefers batches declares a run_batch entry point (runner-verbs.md) under the same id.

FieldTypeRequiredMeaning
summarystringyesone line for a person or an agent
paramsJSON Schema (draft 2020-12)yesjob-level parameters; validated on create; the UI for the capability's settings is generated from this
itemJSON Schemayesshape of items[].input; a Catalog Output Template targets this
outputJSON Schemayesshape of a result outcome's output
error_typesmap of code to {label: string, description: string}yesclosed set (the reserved code DEFERRED_TOO_LONG is declared by every capability that uses the deferred disposition, canon/03); every error outcome's error_type is one of these; label is the short human text a status pill shows, description the one-line tooltip, so an app never invents strings for module codes
review_reasonsmap of code to {label: string, description: string}no at 0.1; yes from 0.2closed set for needs_review.reason, same label and description rule; INPUT_MISSING and JUDGE_UNAVAILABLE are reserved codes every capability with a degradable requirement lists (see Requirements). skipped outcomes carry a reason from the CONTRACT-level closed set duplicate, filtered, cancelled, superseded, not from this map
confidence{self: bool, judges: array of capability ids, authoritative: string}no at 0.1; yes from 0.2authoritative is self (legal only when self is true) or a member of judges (conformance checks the membership); whether the payload self-reports, which judges it can chain, which score fills confidence
requiresarray of requirement objectsnoinputs beyond item the capability needs or can use (below)
providesarray of field idsnowhat this capability writes onto an item, from the field registry
variantsmap of variant dimension to {options: array of strings, at least one; default: string}, default one of options (conformance checks the membership)nocanon/14; research: {options: [self, extractor], default: self}
ceilings{max_items: integer >= 1, max_concurrent_jobs_per_caller: integer >= 1, max_concurrent_jobs_per_tenant: integer >= 1, max_item_bytes: integer >= 1, max_deferrals?: integer >= 1}no at 0.1; yes from 0.2what 429 and 422 mean here; max_deferrals only when the module uses the deferred disposition (canon/03)
settingsarray of stringsno at 0.1; yes from 0.2which per-job overrides this capability honours, as bare key names (max_attempts, never settings.max_attempts)
cost_model{unit: string, typical_usd: number >= 0, meters: array of strings}no at 0.1; yes from 0.2what usage events to expect and a typical cost per unit; meters lists only the capability's OWN meters (<domain>.<name>); the standard meters of usage-event.md (item, duration, plus cache.hit when cache is present and review.held when review_reasons is non-empty) are implied and are never listed here
cache{keyed_by: array of strings (item field paths), ttl: duration, bypassable: bool, tenant_scoped: bool}nocanon/06; absent means not cached; keyed_by names the item fields ADDED to an implicit key of capability id, module_version, the chosen variant and every reference_data version; tenant_scoped: false is the explicit declaration that a cache is shared across tenants and is only legal for tenant-free inputs
reference_dataarray of {name: string, version_reported_at: "/health/deep"}nosnapshots this capability loads
log{levels: array of strings, at least one; default: string}, default one of levels; info is the envelope default (canon/07) and every module lists itno at 0.1; yes from 0.2supported log_level values
facetsarray of strings (attribution keys)noa facetable key is low-cardinality by contract: a facet returns at most 50 values plus an other bucket; GET /jobs is module-level, so its facet set is the union across capabilities and ?capability= narrows it; which attribution.<key> values GET /jobs returns live counts for; user is always facetable and has the reserved {id, label} shape
outputsmap of output name to {shape: JSON Schema or file type string, version: string, reads: array of strings (outcome field paths), partial: bool, formats: array of one of csv, xlsx, jsonl} (canon/15)nodeclared transforms over this capability's outcomes (canon/15); absent means only raw outcomes. A reads entry ending in .* means every key present under that prefix, resolved per job; it is the only non-literal form and exists for capabilities whose columns a person names at run time

Requirements#

A requirement names an input the capability wants beyond the raw item, and where it may come from. It is a table so the generator builds it the way it builds the two above; the hand-written example that used to sit here is now canon/11.

FieldTypeRequiredMeaning
fieldregistry idyesa field-registry id; the schema checks it against the registry enum
needone of required, optionalyesrequired: missing means needs_review or 422, and on_missing says which
fromarray of strings, at least oneyeswho may supply it: caller (in the item input), <module>/<capability> (a named capability of a named module), or a bare <capability> meaning a capability of THIS module
on_missingstringyesone of needs_review:<CODE> (the code MUST appear in this capability's review_reasons; INPUT_MISSING is the reserved code for the plain case, JUDGE_UNAVAILABLE for a missing judge), fallback:<dimension>:<option> (a declared variant), or reject (a 422 at create time, canon/03)

Where a field lives. A registry id IS its path. part.type is read at items[].input.part.type on the way in and written at output.part.type on the way out, dotted id as nested path, so a composer holding two manifests can move a value from one module's outcome into the next module's item without opening either schema. A capability whose output schema puts a provided field anywhere else is not conforming (Banned, below).

This is the whole composability hook. A standalone caller supplies the field in the item input and never reads from. The console (or the future composer) reads from to know that a parttype.match job before this one can fill it. The field id names the value; the path rule places it.

The field registry#

A shared vocabulary of item field ids, so provides in one module and requires in another, and the console's own Field concept, name the same thing. Registry entries are <domain>.<name>, lowercase, dotted, with a type and a one-line meaning. The registry lives in this file until it outgrows it.

First cut, from what the three instances and the owner's draft actually pass around. Marked seed because the first two modules will change it:

Field idTypeMeaningSeen in
part.numberstringthe part number as the vendor writes itevery instance
part.number_normalizedstringnormalized part number for matching and joinsspeedway normalizedSku, walmart part-number join
part.brandstringbrand name as givenevery instance
part.typestringpart-type id or name in the tenant's taxonomyspeedway $derived.partType, walmart stage 2
part.type_confidencenumber0 to 1, the matcher's confidence in part.type for this item; distinct from the outcome's confidence, which is the capability's overall scorespeedway, walmart
attributes.rawmapattribute name to value as sourcedspeedway scrape, walmart scrape
attributes.normalizedmapattribute name to accepted value in the tenant's schemaspeedway normalize, walmart stage 5
attributes.evidencemapattribute to source quote and URLspeedway scrape evidence
attributes.destinationmapattribute name to value in the DESTINATION schema (spec-mapped for publish), as opposed to attributes.normalized in the tenant's schemaspeedway spec mapping, walmart stage 6
content.titlestringthe product title as generated or normalizedcontent module, walmart stage 7
part.gtinstringGTIN, UPC or EAN as the vendor writes itspeedway, walmart
part.categorystringcategory id in the DESTINATION taxonomy (the marketplace or catalog being published to), as opposed to part.type in the tenant's own taxonomywalmart stage 3, speedway publish
content.descriptionstringthe long description as generated or normalizedcontent module, walmart stage 7
content.bulletsarray of stringfeature bullets as generatedcontent module, walmart stage 7
content.researchobjectresearch findings used for copycontent module research mode
images.sourcearray of URLsource image URLs as suppliedwalmart images
images.renderedarray of artifact refsrendered images the module producedimage generation, versable-runner object-store output
fitment.acesarray of objectACES fitment applications as parsed, one object per applicationspeedway ingest
pies.attributesobjectPIES attributes as parsedspeedway ingest
source.filemap of field id to refwhich upload each value came from, per fieldspeedway per-field provenance

Each registry entry will also carry a presentation hint (type family, unit, tone) mapping onto kit cell types (text, number with unit, status tone, timestamp, link, chip list), so an app can render fields with the kit's column factories without a module ever describing layout (vb-fable, 2026-08-18; ../BACKLOG.md).

Rules for the registry:

  • A capability's provides and requires use registry ids only. A new field is a registry change, reviewed like a schema change.
  • The registry does not fix the JSON Schema of every field's value; it fixes the id, the type family, and the meaning. Capabilities that write attributes.normalized all mean the same thing by it, and their output schemas say the exact shape.
  • Tenant-specific vocabularies (a customer's taxonomy ids, their attribute names) are values, not registry ids. part.type is the field; the tenant's taxonomy is what fills it.
  • Growth: adding a registry id is a contract_version minor bump; a validator that meets an id it does not know WARNS and does not reject, so an older console keeps working against a newer module. Renaming or retyping an id is a major bump.
  • The x. prefix is reserved for ids that are deliberately not shared (one module's private field); an x. id may appear in provides and never in requires.
  • review.reason is not a registry field: why an item was held is the outcome's needs_review.reason, an app mirrors it, and no capability provides or requires it.

Surface, identity, runtime#

"surface": { "routes": ["POST /jobs", "GET /jobs", "GET /jobs/{job_id}", "GET /jobs/{job_id}/envelope",
"GET /jobs/{job_id}/outcomes", "GET /jobs/{job_id}/outcomes/{item_id}",
"GET /jobs/{job_id}/outcomes/{item_id}/log",
"GET /jobs/{job_id}/errors", "GET /jobs/{job_id}/review",
"GET /jobs/{job_id}/outputs", "GET /jobs/{job_id}/outputs/{name}",
"GET /jobs/{job_id}/stats", "POST /jobs/{job_id}/cancel",
"GET /stats", "GET /usage/events",
"GET /health", "GET /health/deep", "GET /build-info", "GET /manifest", "GET /guide"],
"page_size": { "default": 100, "max": 1000 },
"callbacks": { "events": ["job.state", "item.outcome", "usage"], "signature": "hmac-sha256", "max_batch": 500 } }
"identity": { "verifier": "jwks" | "keys" | "remote" | "local", "issuer": "https://id.versable.ai", "audience": "content",
"scopes": ["jobs:submit", "jobs:read", "jobs:cancel", "stats:read", "stats:admin"] }
"runtime": { "queue": "cloudtasks" | "arq" | "in-process", "storage": "gcs" | "s3" | "local",
"in_process_supported": true, "topology": "self-push" | "api-worker" }

Validation#

A manifest is itself validated against contracts/manifest.schema.json, generated from the field tables in this file by scripts/build-manifest-schema.py (the runner template's step 1 produced it, ../plans/runner-template.md; --check exits 2 when the file is stale). The generator reads only the Field, Type and Required columns and the registry ids; a Meaning cell is prose and binds people, not the schema. Type cells therefore use a small grammar: string, integer, number, bool, RFC 3339 string, array of <type>, map of <key> to <type>, one of a, b, c for a closed set (never a bar, which breaks the table), >= n for a bound, at least one for a non-empty array, {name: type, other?: type} for members, map of one of a, b to <type> for a map whose keys are pinned, the named type registry id (the enum of field ids in this file's registry table), and the named type duration, a string matching ^([0-9]+[hdy]|never|ttl)$ (7d, 90d, 1y, never; ttl only for the caches class), the spelling canon/11 uses. Membership constraints written in prose (default one of options, default one of levels) are conformance checks, not schema. params, item and output are asserted to be objects and no more; the kit's lintSchema below owns their vocabulary. contracts/conformance.md checks that a live module's behaviour matches its manifest: every listed route exists, every emitted code is listed, every honoured setting is listed, ceilings are enforced.

The params, item and output schemas are full JSON Schema draft 2020-12 (owner ruling 2026-08-18: "the contract stays wide"). The kit's form vocabulary is GUIDANCE for a module that wants its params rendered by SchemaForm without a hand-built form, not a gate on the manifest: the kit's lintSchema(schema) (@versable-git/ui, packages/ui/src/schema-form/lint-schema.ts, contract doc packages/ui/docs/schema-form.md, subset per versable-builder docs/plan/65-schema-form-spec.md section 6) refuses by name, at a JSON Pointer, anything outside that subset and returns {ok, plan} or {ok, refusals[]}; a module runs it when it is ready to ship a generated form, and a params schema outside the subset is rendered by a hand-built form or the JSON view, never rejected by the runner. The four V1 modules' params schemas start from the fixtures at versable-builder docs/plan/65-schema-form-fixtures/{enhancement,image-generation,attribute-normalization,part-type-matching}.params.schema.json, which are the vocabulary's worked examples; a manifest that diverges from its fixture is either a lapse or a fixture correction, and the instance doc says which. contracts/conformance.md MAY run the lint over a published manifest as an advisory row (foundry-2), never a failing one.

Two rules that hold whatever vocabulary a params schema uses, because the runner validates the submitted params against the schema on create (canon/03) and knows nothing about form-only keys: a field hidden by a form-only condition (x-when, x-tier) MUST NOT be required at the schema level unless it sits inside a discriminated oneOf branch that JSON Schema itself selects, since a hidden required field is a guaranteed 422; and x-when may reference $settings.<key> only for keys the capability lists in settings, because that object is the job's settings block, not app-private context.

Standard settings#

The per-job overrides every module names the same way, so an app carries no translation table. A capability lists the ones it honours in settings; it may add settings under its own <domain>. prefix and may not rename these.

SettingTypeMeaning
max_attemptsinteger >= 1retry budget per item (canon/03)
item_timeout_sinteger >= 1per-item wall clock before the attempt errors
callback_urlstringwhere callback_events are delivered (canon/04)
callback_eventsarray of stringswhich of the module's callback events to send
log_levelstringone of the capability's log.levels (canon/07)
cacheone of use, bypassbypass skips the result cache and is metered as a miss (canon/06)

Banned#

  • A capability id that is not <domain>.<verb>[.<qualifier>].
  • A provides or requires entry that is not a registry id.
  • A provided field whose value sits anywhere but at its id's path in output, or a required field read from anywhere but its id's path in items[].input.
  • An x. registry id in requires.
  • A meters list that repeats a standard meter.
  • A skipped outcome whose reason is outside duplicate, filtered, cancelled, superseded.
  • An error_type or review_reason emitted at runtime and absent here.
  • A settings key honoured at runtime and absent here.
  • A manifest at 0.2 or later that omits ceilings, cost_model, or retention; at 0.1 a capability may omit review_reasons, confidence, ceilings, settings, cost_model and log and its instance doc says which, so no invented number enters a machine-readable file (review v2 J4).
  • Two capabilities in one module with the same id and different schemas; that is a version bump or a new id.
@versable-git/ui · reference, canon, and method, read in place