Skip to main content

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.

FieldCurrent valueMeaning
RUNNER_INTERFACE_VERSION0.2Current 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-interface is the governed state-machine boundary.
  • agentxchain/adapter-interface is the shipped adapter-dispatch boundary.
  • agentxchain/run-loop is 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:

ExportPurpose
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:

ExportPurpose
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:

TierScriptWhat it proves
1. Single-turn primitiveexamples/ci-runner-proof/run-one-turn.mjsA single governed turn can be assigned, dispatched, staged, and accepted through the raw runner-interface operations.
2. Multi-turn primitiveexamples/ci-runner-proof/run-to-completion.mjsA 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 compositionexamples/ci-runner-proof/run-with-run-loop.mjsThe 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-interface when you want the shipped adapters
  • agentxchain/runner-interface
  • agentxchain/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.