tool/<name>) and its actions (action/<name>). The contract is the sealed, machine-readable statement of them. It is a contract.json at the integration’s root, derived from your defineIntegration result, listing for each operation its input and output as JSON Schema, whether it reads or writes, an example of its output, and the connection it needs.
An experience never imports your code. It pins your contract, and the pinned contract is all it is typed and checked against. That is why a change to the contract has rules.
Derive the contract
First, declare a contract version in the integration’sekanos.json. The scaffold does not write one:
- a
description(required); - a typed output, so an experience can bind to it:
outputas a zod schema on a tool or an action (without one the output is opaque and an experience cannot use the operation); - whether it reads or writes:
effect: 'read' | 'write'. An operation with noeffectis treated as a write, with a warning; - an example of its output, for the harness and for tests:
outputExample, or a recorded one (below). A read operation without one is an error.
deriveContract from @ekanos/sdk/contract, and writes contract.json. Commit it: it is reviewed like code, and it ships in the published package.
The three modes
boolean
Derives in memory and fails if
contract.json differs. It writes nothing.
Use it in CI and as the integration’s prepack script: "prepack": "ekanos contract --check". The checks/contract-emit stage of ekanos validate, and
so of ekanos publish, makes the same freshness check for an integration that
declares a contract version.boolean
Fills in missing output examples under
contracts/examples/. Without --op,
it writes a placeholder synthesized from the output schema for each
operation that has no example. The command says so, and the harness serves
those placeholders in fixtures mode until you replace them. With --op, it
records a real example from a run (below).boolean
Rewrites
contract.version in ekanos.json to the smallest version that
satisfies the rules for this change.--op takes one or more operations, comma separated. --input takes JSON for a single --op; without it the input is synthesized from the input schema. A recording is checked against the output schema before anything is written, and live data may contain real records, so review the file before you commit it. A write is never recorded live. To record a write, add a scenario fixture at fixtures/<integration>/action-create-draft.default.json holding a real-shaped response, then record it with --from fixtures; nothing is called.
--check cannot be combined with --record or --bump.
Versions and compat lines
A contract has its own version,contract.version, independent of the package version, and a compat line derived from it:
For a 0.x contract the line is
0.<minor>; from 1.0 it is the major. An experience pins a line ("version": "0.2" in its ekanos.json), so everything on that line has to stay compatible with what the experience was built against. Operations carry the line in their id: acme-cms@0.2/tool/list_posts.
What is a breaking change
Every change between two contracts is classified. Consumers send inputs and read outputs, so the same edit is judged in opposite directions on each side.
Descriptions, titles, defaults, examples, and whether an object allows extra keys are ignored: documentation is not a contract, and a consumer’s tolerance of extra response keys is what makes “add an optional output field” safe.
What ekanos contract enforces
- A breaking change on the same compat line is an error. Start a new line instead: on a 0.x contract, bump the minor (
0.2.0to0.3.0); from 1.0, bump the major. - Any change needs a higher
contract.version. If the surface changed and the version did not, the command fails. If the version went down, it fails. - From 1.0, an additive change needs a minor bump. On a 0.x contract the minor is the line, so an additive change is a patch bump.
- A change to the package only (no change to the operations) leaves the contract untouched.
ekanos contract --bump works out the right bump for you and writes it to ekanos.json. A breaking change that opens a new line is allowed: that is what a line is for. Experiences pinned to the old line keep working against it.
Pins in an experience
ekanos add acme-cms@0.2 copies the contract it found into the experience’s contracts/ folder and records its digest in ekanos.lock.json. From then on:
ekanos checkfails if the pinned line has no such operation, or if the stored contract no longer matches the lock;ekanos syncaccepts a new digest within the pinned line (that is, compatible changes), andekanos sync --checkreports drift without writing;- moving to a new line is a deliberate edit to
versioninekanos.json, followed byekanos sync.