Skip to content

The agent runtime is a protocol-level enum, so a third vendor cannot be added without changing the wire contract #443

Description

@KenTaniguchi-R

What is true today

AGENT_KINDS is a closed two-value enum declared in the shared protocol package:

// packages/shared/src/protocol.ts:192
export const AGENT_KINDS = ["claude", "codex"] as const;
export const AgentKindSchema = z.enum(AGENT_KINDS);

It is not a CLI detail. It is embedded in the agent card (packages/shared/src/card.ts:37,48), in registration (protocol.ts:200), and in stored card state (protocol.ts:260). Eighteen source files across all three packages reference agent_kind / agentKind, including apps/relay/src/index.ts and apps/relay/src/recovery.ts.

The behavior itself branches inline rather than through a seam. packages/cli/src/runner.ts:375 is a bare if (kind === "claude") { ... } with the Codex path in the else, and the surrounding code hardcodes vendor specifics: --permission-mode dontAsk --allowedTools for Claude, -c sandbox_mode for Codex, ~/.claude.json MCP discovery, $CODEX_HOME/hooks.json. setup.ts carries twelve vendor references, guard.ts and doctor.ts two each.

Adding Gemini CLI, Copilot CLI, or any other runtime therefore requires a coordinated change to the shared wire schema, the relay, and the CLI — and every existing card becomes a schema question.

The precedent

agmsg (ARCHITECTURE.md) solves exactly this with a three-axis driver model — storage × agent × delivery — where each axis has one active driver behind a fixed protocol, and the axes are independent. Their agent axis abstracts "per-runtime differences (hook formats, settings file locations, monitor tool availability)."

That is the same list our runner.ts branches on inline. It is why they support Claude Code, Codex, Gemini, Antigravity, Copilot, OpenCode, and Hermes at v1.2.0 while we support two: for them a new vendor is a new driver, not a protocol change.

The boundary worth copying is the seam and the discovery/dependency-check contract, not their bash implementation or their plugin directory layout (reserved and unimplemented in their v1 anyway).

Why this is not just breadth-chasing

docs/research/reference-implementations.md instructs us to "copy the invariant or boundary, not an implementation accidentally coupled to another runtime." A closed enum in packages/shared is the opposite: it couples the wire contract to the set of runtimes the CLI happened to support on the day it was written.

It also collides with the A2A track. An Agent Card that advertises agent_kind: "claude" | "codex" is asserting a vendor identity in a public interop surface, where the relevant fact for a caller is the declared capability, not which binary runs behind it.

Recommendation

Two separable decisions, in order:

  1. Get the vendor out of the wire contract. Either widen agent_kind to an open string validated locally, or drop it from the card entirely and let capability declarations carry what a caller actually needs. This is the breaking-change half and should be decided before more surfaces read the enum.
  2. Extract a runtime driver seam in the CLI — spawn argv, settings/permission injection, MCP discovery, hook registration, resume semantics, version probing — so runner.ts dispatches instead of branching. codexThreadingEnabled / codexToolTelemetryEnabled (runner.ts:23,36) are already per-runtime capability probes and belong to that seam.

Step 2 has standalone value even if step 1 is rejected: runner.ts is 25KB and the two vendor paths are interleaved.

Not proposing which vendors to add. This is about whether adding one is a protocol change.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions