Runner Interface
The CLI is the primary shipped runner. It is not the only runner. AgentXchain also ships a declared library boundary for CI, hosted, or programmatic runners that need to drive the governed state machine without shelling out to agentxchain step.
If you want the constitutional model, start with Protocol v7. If you want the conformance corpus for protocol implementations, read Protocol Implementor Guide. If you want to build a runner, this page is the boundary that matters.
If you want the adoption path instead of the raw API list, use Build Your Own Runner.
Installation
npm install agentxchain
import { loadContext, assignTurn } from 'agentxchain/runner-interface';
import { dispatchLocalCli } from 'agentxchain/adapter-interface';
import { runLoop } from 'agentxchain/run-loop';
The package exports are the external contract. Repo source paths are implementation detail.
Versioned contract
The runner interface is a declared ES module exported as agentxchain/runner-interface.
| Field | Current value | Meaning |
|---|---|---|
RUNNER_INTERFACE_VERSION | 0.2 | Current versioned boundary for runner authors |
Versioning is not decorative. Adding, removing, or breaking runner operations changes the interface version.
Boundary split
Do not blur these surfaces:
agentxchain/runner-interfaceis the governed state-machine boundary.agentxchain/adapter-interfaceis the shipped adapter-dispatch boundary.agentxchain/run-loopis the higher-level runner composition layer.
If your runner uses the first-party adapters, import them through agentxchain/adapter-interface. If you implement your own dispatch, you may ignore that export entirely.
Lifecycle operations
These are the state-machine operations a runner uses to drive governed execution:
| Export | Purpose |
|---|---|
loadContext(dir?) | Load project root, config, and validation |
loadState(root, config) | Load current governed state |
initRun(root, config) | Initialize a run from idle state |
reactivateRun(root, state, details?) | Reactivate a blocked or paused run |
assignTurn(root, config, roleId) | Assign the next governed turn |
acceptTurn(root, config, opts?) | Accept a staged turn result |
rejectTurn(root, config, result, reason, opts?) | Reject a staged turn result |
approvePhaseGate(root, config) | Approve a pending phase transition |
approveCompletionGate(root, config) | Approve a pending run completion |
markRunBlocked(root, details) | Persist a blocked state explicitly |
escalate(root, config, details) | Raise an operator escalation |
assignTurn() success returns { ok, state, turn }. That top-level turn is part of the contract. Runner consumers should not have to rediscover the assigned turn by traversing state.active_turns.
Support operations
These operations are not the lifecycle itself, but real runners need them:
| Export | Purpose |
|---|---|
writeDispatchBundle(root, state, config, opts?) | Write repo-native dispatch artifacts for an active turn |
getTurnStagingResultPath(turnId) | Return the canonical .agentxchain/staging/<turn_id>/turn-result.json path |
runHooks(root, hooksConfig, phase, payload, opts?) | Execute hook phases |
emitNotifications(root, config, state, event, payload?, turn?) | Emit notification events |
acquireLock(root) | Acquire the acceptance lock |
releaseLock(root) | Release the acceptance lock |
getActiveTurns(state) | Return the active turn map |
getActiveTurnCount(state) | Return the current active-turn count |
getActiveTurn(state) | Return the current active turn when one exists |
getMaxConcurrentTurns(config) | Read the configured concurrency limit |
getTurnStagingResultPath() is in the declared boundary on purpose. If a runner must stage a turn result for acceptTurn(), it should not import turn-paths.js directly just to discover the canonical path.
acceptTurn() is destructive for transient turn artifacts. After the acceptance journal commits, AgentXchain removes the accepted turn's .agentxchain/dispatch/turns/<turn_id>/ bundle and .agentxchain/staging/<turn_id>/ directory. Runners that need those files for auditing or upload must inspect or copy them before acceptance.
Canonical runner loop
The happy-path runner loop is:
loadContext/loadState
-> initRun or reactivateRun
-> assignTurn
-> writeDispatchBundle or runner-specific dispatch
-> stage turn result at getTurnStagingResultPath(turn.turn_id)
-> acceptTurn or rejectTurn
-> approvePhaseGate or approveCompletionGate when pending
Runner-specific dispatch is intentionally outside agentxchain/runner-interface. The CLI can use manual, local_cli, api_proxy, mcp, or remote_agent through agentxchain/adapter-interface. A CI runner may stage a known-good result directly. A hosted runner may call network workers. The protocol cares about state transitions and artifacts, not how you invoked the agent runtime.
Shipped runner proofs
AgentXchain ships three runner proof tiers, each defending a different failure boundary:
| Tier | Script | What it proves |
|---|---|---|
| 1. Single-turn primitive | examples/ci-runner-proof/run-one-turn.mjs | A single governed turn can be assigned, dispatched, staged, and accepted through the raw runner-interface operations. |
| 2. Multi-turn primitive | examples/ci-runner-proof/run-to-completion.mjs | A full governed lifecycle (pm → dev → qa) can be driven through the raw runner-interface, including gate approvals, rejection, and retry on the same turn_id. |
| 3. Run-loop composition | examples/ci-runner-proof/run-with-run-loop.mjs | The runLoop library composes runner-interface operations into reusable continuous execution — gate pauses, rejection/retry, and typed stop reasons — without re-implementing lifecycle logic. |
All three repo-native proofs:
- exercise the local source boundary continuously inside this repository's CI
- execute governed turns programmatically without shelling out to
agentxchain - stage turn results at the canonical staging path
- validate state-machine artifacts and cleanup behavior
- run in the local release gate via
cli/scripts/prepublish-gate.sh
The point is not convenience. The point is proof that governed execution is not locked to the CLI shell.
Tiers 1–2 validate the primitive operations. Tier 3 validates that runLoop composes them correctly. Neither layer replaces the other — they are complementary proof boundaries.
External-consumer starter
If you are outside this repo, do not cargo-cult the repo-native proof imports. Use the installed-package starter in examples/external-runner-starter/ and import from:
agentxchain/adapter-interfacewhen you want the shipped adaptersagentxchain/runner-interfaceagentxchain/run-loop
That starter exists to prove the packaged boundary, while the CI proofs above exist to prove the repository implementation.
What is not in this interface
These concerns are intentionally excluded:
- CLI parsing and output formatting
- dashboard serving
- export and report generation
- intake workflow commands
- custom adapter dispatch strategy beyond the shipped
agentxchain/adapter-interface
Those are runner features or adjacent product surfaces. The runner interface is the governed execution boundary.