Languages: English | 简体中文
Launch and track local Claude Code, Codex CLI, OpenCode, Cursor, Pi, and Antigravity coding-agent sessions from Python: pure launch plans, audited runs, and per-session stdio MCP wiring. Also ships an agent-client-protocol (ACP) adapter.
This is a Python library + JSON CLI + ACP adapter — not a TUI, not a session manager, not a history parser (history observation is delegated to SessKit).
Status: 0.1.2 (see docs/INTEGRATION.md and docs/CONTRACT.md). Per-runtime limits are listed below.
Install the fixed version from PyPI in your project's virtual environment.
Requires Python 3.10–3.14 (3.12 recommended). Default tmux backend runs on macOS and Linux; Windows/WSL are not supported. A Homebrew python3 may be 3.15, which the optional agent-client-protocol SDK excludes — use a 3.10–3.14 interpreter for the acp extra.
VERSION=0.1.2
SESSKIT=https://github.com/x0c/sesskit/releases/download/v0.3.2/sesskit-0.3.2-py3-none-any.whl
# Core only: plans, store, correlation, JSON CLI (stdlib only).
python -m pip install "agent-session-control==${VERSION}"
# Observing controller (+ SessKit native-history observation).
# SessKit is not on PyPI: pass its fixed wheel explicitly with the extra.
# (tool.uv.sources is uv-only and is not wheel metadata.)
python -m pip install "sesskit @ ${SESSKIT}" "agent-session-control[history]==${VERSION}"
# ACP adapter over the observing controller (default full setup).
python -m pip install "sesskit @ ${SESSKIT}" "agent-session-control[history,acp]==${VERSION}"
python -c "import agent_session_control; print(agent_session_control.__version__)"Need a virtual environment first? Run python3 -m venv .venv && . .venv/bin/activate.
The same wheel and source archive are also available on GitHub with SHA256 checksums.
- Pure launch plans (
agent_session_control.runtime):build_create_planandbuild_resume_planfor the six built-in runtimes (claude,codex,opencode,cursor,pi,antigravity) plus explicit third-partyRuntimeProfileregistration. Plans are immutable argv arrays — building one never launches a process, mutates storage, edits global config, or sends a model request. Caller defaults are preserved (no model flag, no approval relaxation unless requested). Unsupported options fail before launch instead of silently degrading. - Per-session stdio MCP (
mcp_servers=): verified launch-only channels forclaude/codex/opencode, each proven end-to-end with a disposable stdio fixture against the real installed CLI (no global config touched). Secrets never travel on argv; live values travel only inplan.env, redacted from every display projection.cursor,pi, andantigravityread MCP servers only from persistent config files, so per-launch servers are refused asunsupported(no silent downgrade). - Durable audited runs (
agent_session_control.store):RunStorewith same-transaction actor audit, immutable idempotency keys, cross-process admission throughUNIQUEconstraints, per-namespace database files, configurable root and supervisor identity. Reads never create storage. - Evidence correlation (
agent_session_control.correlation):marker,wire_prompt,own_message,correlate,build_result. Only native user-message evidence establishes input recording. Missing usage staysnull; repeated usage observations are deduplicated, never summed into a fake billing total.completedmeans the correlated turn ended, never that a task passed acceptance. - Shared models and port (
agent_session_control.models):SessionHandle,ControlEvent,RunResult, prompt-block helpers (canonicaltext+resource_link), and the asyncSessionControllerport consumed by the engineLocalController, the JSON CLI and the ACP adapter. - Stable errors (
agent_session_control.errors):ControlErrorwith stable codes, observable fields and exit codes, plus the{ok, data, error, meta}envelope helpers used by the CLI.
Core imports need only the standard library. Optional extras:
history: SessKit>=0.3.2for native-history observation.acp: officialagent-client-protocol >=0.12.1,<0.13for the ACP adapter.
from agent_session_control import build_create_plan, build_resume_plan, runtime_profile
# Approval is never relaxed by default: auto_approve=False. Pass
# auto_approve=True only when the host's explicit policy allows it.
plan = build_create_plan("codex", "/path/to/project")
print(plan.argv) # ('codex', ...)
resume = build_resume_plan("claude", {"id": "<exact-native-id>", "cwd": "/path/to/project"})
print(resume.argv)
with_mcp = build_create_plan(
"claude", "/path/to/project",
mcp_servers=[{"name": "docs", "command": "/usr/local/bin/docs-mcp", "env": {"API_KEY": "..."}}],
)
print(with_mcp.to_dict()) # redacted projection: no secret values, secret-free argv
# Live values travel only in plan.env for the child process; argv carries
# ${NAME} references (claude) or env_vars forwarding names (codex).
print(runtime_profile("cursor")) # capabilities, interrupt keys, limitationsfrom agent_session_control import RunStore
store = RunStore() # ~/.config/agent-session-control/control/runs.sqlite3
record, created = store.create("local-user", "request-key-1",
{"target": "claude:<id>", "text": "..."},
{"execution": "queued", "delivery": "accepted"})SDK lifecycle (live; spends quota, needs runtime login):
import asyncio
import tempfile
from pathlib import Path
from agent_session_control.controller import LocalController
async def main() -> None:
# Selected temp project + isolated control root: never a fixed /tmp path.
with tempfile.TemporaryDirectory(prefix="asc-sdk-") as scratch:
project = str(Path(scratch, "project"))
Path(project).mkdir(parents=True, exist_ok=True)
controller = LocalController(root=str(Path(scratch, "control")), namespace="demo")
handle = None
try:
handle = await controller.create("claude", project)
run_id = await controller.start_turn(
handle.id, [{"type": "text", "text": "Reply with ACK."}], request_id="demo-1")
assert run_id.startswith("run_"), run_id # exact run identity
result = await controller.wait(run_id, timeout=120.0)
print(result.state, result.evidence)
assert result.run_id == run_id
if result.final_response:
print(result.final_response[:200]) # returned final text
finally:
# Public-API cleanup only: releases the pane; keeps history.
if handle is not None:
await controller.close_session(handle.id)
asyncio.run(main())JSON CLI (machine envelope on stdout; ACP owns its own stdout):
python -m agent_session_control version --json
# {"ok": true, "data": {"package": "agent-session-control", "version": "0.1.2", ...}, "error": null, "meta": {...}}
python -m agent_session_control plan create --runtime codex --cwd /tmp/demo-proj --json
# {"ok": true, "data": {"dry_run": true, "status": "planned",
# "launch": {"argv": ["codex", "--no-daemon"], "cwd": "/tmp/demo-proj", "env_keys": []}}, ...}
python -m agent_session_control session create --runtime codex --cwd /tmp/demo-proj \
--dry-run --json --root /tmp/demo-control
# {"ok": true, "data": {"dry_run": true, "status": "planned", "session": {...}}, ...}ACP adapter (one runtime per invocation; protocol messages only on stdout):
python -m agent_session_control acp --runtime codex --root /tmp/demo-control --namespace demoSee docs/INTEGRATION.md for adoption recipes, isolated
roots/namespaces, and platform notes. Protocol details live in
docs/ACP.md; the full contract is docs/CONTRACT.md.
examples/basic_control.py (plans + CLI envelopes; --live for a real
turn) and examples/native_smoke.py (bounded native + stdio handshake)
run these paths end to end.
| Capability | claude | codex | opencode | cursor | pi | antigravity |
|---|---|---|---|---|---|---|
| Pure plans | yes | yes | yes | yes | yes | yes |
| Owned hosting + liveness + close (no prompt) | yes | yes | yes | yes | yes | yes |
| Native turn (login/quota-gated, spends quota) | gated | gated | gated | gated | gated | gated |
| Per-session MCP launch channel | headless-verified | headless-verified | headless-verified | refused | refused | refused |
| Hosted TUI MCP delivery | pending host run | pending host run | launcher-owned only | refused (unsupported) |
refused (unsupported) |
refused (unsupported) |
| ACP serving over this runtime | yes | yes | yes | SDK/CLI only | SDK/CLI only | SDK/CLI only (agy acp native) |
Headless proof: disposable stdio fixtures against the real installed CLIs
(claude -p, codex exec, opencode --standalone run) with no global
config touched. Hosted TUI delivery shares the same argv/env channel but is
pending a host run (final hosted-MCP truth lives with the adapter/backend
owners, see docs/ACP.md, docs/BACKENDS.md). Availability probes report
executable presence only — never login, quota, or model entitlement.
Unavailable login/quota surface as explicit limits, not passes.
Companion Skill: agents driving this library should install
agent-session-control
(references/mcp.md),
shipped in the wheel at agent_session_control/skills/agent-session-control:
npx skills add x0c/[email protected] --skill agent-session-controlThe Skill teaches the existing JSON CLI lifecycle (plan/host/observe/release); it adds no new lifecycle.
| Tool | Role | Relation |
|---|---|---|
| SessKit | Native-history observation (history extra, fixed wheel v0.3.2) |
Consumed as an explicit pinned install; never vendored |
| agent-client-protocol | ACP wire protocol + Python SDK (acp extra, >=0.12.1,<0.13) |
Consumed from PyPI; adapter serves one runtime per invocation |
tmux |
Default terminal backend (macOS/Linux) | External dependency; installed per-OS in CI |
- Cursor, Pi, and Antigravity read MCP servers only from persistent config files, so per-session
mcp_servers=is refused asunsupported(no silent downgrade). Noteagy acpis provided natively by theagyCLI. - OpenCode per-session MCP takes effect only when the launcher owns the server; an ambient background service ignores it.
completedmeans the correlated turn ended, never that a task passed acceptance. Missing usage staysnull.- Live control spends model quota and needs the runtime executable plus login. Disposable project/state roots isolate control state only: hosting may still record native history under the runtime's standard locations, and
close_sessionnever deletes it.
Contributors only — not part of installation:
uv sync --group dev --all-extras
uv run --python 3.12 --group dev scripts/check.py # FULL: includes live-marked tests
uv run --python 3.12 --group dev scripts/check.py --ci # CI: -m "not live", no quota/loginRuns Ruff, Pyrefly, the pytest suite (warnings are failures),
wheel/sdist packaging, metadata checks, and clean installs (core,
history, acp, combined; file:// URIs verified under both uv and
plain pip) plus a unique-temp external consumer probe. Default CI never
spends quota; live tests run only via the manual canonical-repo job.
MIT — see LICENSE.