Architecture

The sentinel pattern: extracting plans from any agent

By Tetherlab team
Updated 2026-05-29

The constraint

Tetherlab coordinates on a plan: a {summary, steps} object the master can match against the concept registry before the agent acts. The hard part is getting that object out of an agent the shim wraps at the process level, with no SDK and no cooperation from the agent itself.

Design principle two: the agent is unconscious of the coordination system. It sees only normal turn-taking. So the shim cannot ask the agent to call a special API. It can only read what the agent emits and write into its input.

Two strings and a round-trip

The shim coaxes the agent into a planning turn, then watches for two sentinel strings the agent emits around its plan: ::tether-plan-ready:: and ::tether-plan-end::. When it sees the open sentinel, it does one JSON round-trip to pull the structured {summary, steps} and submits it as an intent. No screen-scraping of prose, no fragile natural-language parser.

The child runs under a PTY so its normal TUI renders for the developer. Coordination happens out-of-band on a separate read channel. The two never fight over the terminal.

intent
{
  "summary": "Refactor auth layer to use JWT instead of session cookies",
  "steps": [
    "Replace cookie-based session middleware with JWT verification",
    "Deprecate the in-memory session store"
  ]
}

Same shape, different transport

A ToolProfile classifies the wrapped binary and the orchestrator dispatches on it. The plan always comes back the same shape; only the transport changes.

  • Claude Code runs under claude --session-id <UUID>. The shim tails the JSONL transcript at ~/.claude/projects/<encoded-cwd>/<UUID>.jsonl for the sentinels.
  • OpenCode runs under opencode --port=N. The shim coordinates over its local HTTP server API instead of a transcript file.
  • Anything unrecognized runs as Generic: plain PTY passthrough, no coordination. The shim stays transparent.

When the plan never arrives

Claude only writes its transcript after its first persisted turn, so the shim waits for the file to appear. The deadline is generous: DEFAULT_TRANSCRIPT_DEADLINE is 10 minutes, overridable with TETHER_TRANSCRIPT_TIMEOUT_SECS. An earlier 30-second gate fired before a human finished typing their first message, so coordination never started. The wait now also races the child, so a quit or crash unblocks at once.

Degrade, do not kill

If the deadline elapses, the shim degrades to plain passthrough: the live session keeps running, coordination just stops. A wedged plan extraction never takes the developer's session down with it.

The invariants that keep it honest

  • No agent execution starts before the master returns continue.
  • Fail closed on auth and network errors.
  • No fabricated intents: if no plan can be extracted, the run exits without submitting anything.

Coordination injects are best-effort and log-on-error, so a stalled write degrades gracefully rather than hanging the shim. The result is a coordination layer that adds nothing to a session it cannot help, and blocks the moment it cannot guarantee safety.

Next steps

See the pattern in practice in wrapping Claude Code, or read what the master does with the plan in intent-level conflict detection.

Frequently asked questions

Does the agent need to know about Tetherlab?

No. The agent sees only normal turn-taking. The shim coaxes a planning turn and reads two sentinel strings the agent emits around its plan; all system-aware behavior lives in the shim and master.

What if the agent never produces a plan?

The shim waits up to a 10-minute deadline (overridable via TETHER_TRANSCRIPT_TIMEOUT_SECS), then degrades to plain passthrough. The session keeps running; coordination simply stops. It never fabricates an intent.

Which agents are supported today?

Claude Code (via its session transcript) and OpenCode (via its HTTP server). Anything unrecognized runs as plain passthrough with no coordination. The Codex shim is on the roadmap.

/ ready to start

Wrap one agent.
See the difference.