Skip to content

About

Drive coding-agent CLIs as persistent, streaming, event-captured sessions — one interface, any agent. Claude Code and Codex today.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@inixiative/agent-session

Drive coding-agent CLIs as persistent, streaming, event-captured sessions — one interface, any agent, over interchangeable transports. Claude Code (CLI or Agent SDK) and Codex (app-server) today; Gemini and Grok planned. Also: warm primed decision sessions, subscription pools with deterministic routing, limit polling and session continuity.

Most ways to script a coding agent are one-shot (claude -p "…") and lose the process. agent-session keeps a single long-lived agent process, streams turns in over stdin, and classifies every event coming back — text, thinking, tool calls, tool results, usage — so you can drive multi-turn work and capture exactly what the agent did. Built for eval harnesses, agent comparisons, and multi-agent orchestration.

import { ClaudeCodeSession } from "@inixiative/agent-session";

const session = new ClaudeCodeSession({
  cwd: "/path/to/workdir",
  model: "sonnet",
  permissionMode: "bypassPermissions",
});
await session.start();

const result = await session.send("Build a CLI that …");
console.log(result.content);          // final text
console.log(result.tokens);           // input/output + reported cache/thinking counters
for (const e of result.events) {      // full classified event stream
  if (e.kind === "tool_use") console.log("tool:", e.toolName);
}

session.kill();

Why it's different

  • Persistent, multi-turn. One process per session; send() per turn, resolves on the runtime's result event. Not one-shot -p.
  • Full event capture. A normalized SessionEvent taxonomy (text / thinking / tool_use / tool_result / result / error / session_compact) — the same shape regardless of runtime.
  • Subscription auth, no API billing. The Claude Code adapter strips ANTHROPIC_API_KEY and authenticates via the CLI's subscription / an CLAUDE_CODE_OAUTH_TOKEN from claude setup-token — works headless and in containers.
  • fork / resume / interrupt and pre-send hooks (rewrite the outgoing message — e.g. inject just-in-time context per turn).
  • Transports with declared capabilities. How a model is reached is a pluggable transport; switching it is a routing change, not a rewrite.
  • Zero dependencies. Just Bun + the agent CLI you're driving. The Agent SDK transport takes the SDK's query from the caller.

Interface

HarnessSession is the provider-agnostic contract (start / send / kill / fork / interrupt + an event handler). Optional members where a transport supports them: transport (its descriptor), interruptNative() (acknowledged stop), readLimits() (account limits without a model turn). Account limits also arrive as rate_limit events.

Transports

import { createSession, TRANSPORTS } from "@inixiative/agent-session";

const kind = "codex-app-server";            // or claude-cli, claude-agent-sdk
if (TRANSPORTS[kind].capabilities.interrupt === "acknowledged") { /* … */ }
const session = createSession(kind, { cwd, model: "gpt-6-luna", env: { CODEX_HOME: profile } });
claude-cli codex-app-server claude-agent-sdk acp api
Status ✅ live ✅ live ✅ live stub stub
Resume / fork native / native native / – native / native – –
Interrupt acknowledged acknowledged acknowledged – –
Push – steer – – –
Approvals callback callback callback – –
Limits stream / poll ✅ / ✅ ✅ / ✅ ✅ / – – –
Primed reset fork fork – – –

Codex runs only over codex app-server --listen stdio:// (CodexAppServerSession); Codex CLI 0.155 has no mcp-server subcommand.

Full matrix, notes and what is stubbed: docs/transports-pools-routing.md.

Tool gate

onToolRequest(gate) puts a decision on each tool call. Gates compose in registration order; the first deny wins and a throwing gate denies.

const session = new ClaudeCodeSession({ cwd, permissionMode: hostMode, toolGate: true });
session.onToolRequest(async (call) => {
  if (call.stage === "pre-use") return structural(call) ?? { behavior: "allow" };
  return askTheHuman(call, call.signal); // the runtime's own mode wanted to ask
});
  • pre-use (Claude only) runs before every call in every mode, bypassPermissions included; only a deny counts. A PreToolUse callback over the control protocol; one the session cannot read is denied.
  • permission is an ask the runtime's mode raised; the answer is final (no gate: denied). Claude: --permission-prompt-tool stdio; decisionReason says why it asked, agentId marks a subagent's call. Codex app-server: command and file-change requestApproval, answered accept/decline, under appServer: codexPermissions(mode). File-change asks carry the item's toolInput.changes (path, kind, bounded diff); command asks carry networkApprovalContext. Gated threads pin approvalsReviewer: "user" and are refused if a config routes approvals elsewhere. Under the default never policy nothing is asked, so onToolRequest throws.
  • call.signal aborts when the runtime withdraws the request or the session ends (kill, exit, transport failure); a decision after that is never written. A turn timeout does not withdraw a pending Claude ask; interruptNative() does.
  • A fork starts with a snapshot of the parent's gates.

Primed decision sessions

One live, warm session per middleware role, primed once with its role instructions and stable context, reset to that primed state every cycle:

import { CodexPrimedSessions } from "@inixiative/agent-session";

const decisions = new CodexPrimedSessions({ model: "gpt-6-luna", effort: "low", cwd: privateDir });
const result = await decisions.decide(
  { key: `${threadId}:aux:domain:api`, instructions: rolePrompt, context: layerContent, hash: layerHash },
  { input: "## Message\n…", onAdmission: register },
);

Run Codex decisions on a private CODEX_HOME that holds only the login (link auth.json from the account's home; a copy forks its refresh token): Codex has no setting that drops the user's own AGENTS.md, and a decision thread that reports any instruction source is refused.

Codex hosts every key as a thread of one app-server process per account and forks the primed thread per cycle; Claude forks a persisted primed session with a pre-spawned spare. Measured on a ChatGPT login: warm Codex decisions 2.45 s median vs 3.9 s for codex exec per decision, with 34% fewer input tokens; warm Claude (haiku) decisions 1.95 s with the whole primed prefix read from cache.

Pools, routing and continuity

import { SubscriptionPool } from "@inixiative/agent-session";

const pool = new SubscriptionPool({ instances: [
  { id: "work", transport: "claude-cli", profileDirectory: "/profiles/work", organizationIds: ["org"] },
  { id: "spare", transport: "claude-cli", profileDirectory: "/profiles/spare", organizationIds: ["org"] },
] });
const { session, lease } = await pool.open({ runtime: "claude", model: "sonnet", organizationId: "org", preferredInstanceId: "work" });

Routing is deterministic (quartile-balanced, owner-first or pinned) over observed limits, leases and health; stale or unknown limits exclude an instance and every exclusion has a reason. continueOn moves a thread only between instances that share native history, and says why when it cannot.

Usage telemetry

Claude results preserve cacheRead, cacheWrite, cacheWrite5m, cacheWrite1h, and thinking alongside input/output. Input excludes the disjoint cache counters; thinking is already included in output and TTL counts are included in cacheWrite. Unreported optional counters remain absent. providerUsage retains the entire original usage object, including service tier and future tags.

result events and send().tokens carry the native turn aggregate. Session totalTokens and artifact totals sum these completed results once. usage events carry request snapshots with the original envelope (request/message IDs, model and tags) in raw; they may repeat across content blocks. Do not sum request snapshots together with result totals. Interrupted turns retain request snapshots in the artifact but do not fabricate a completed-turn total.

Roadmap

Runtime Status
Claude Code ✅ shipped
Codex app-server ✅ shipped (CodexAppServerSession, CodexPrimedSessions)
Claude Agent SDK ✅ shipped (ClaudeAgentSdkSession; caller supplies query)
ACP agents / direct API typed stubs
Gemini CLI planned
Grok CLI planned (when its headless/stream mode matures)

Opaque runtimes degrade gracefully — fewer event types, never a hard failure.

Subscription pooling goals (AS-001…005) are implemented as described in docs/transports-pools-routing.md; each ticket lists what remains.

Provenance

Extracted from inixiative/foundry, where this drives the Artificer session. Consumed by foundry, oracle, and the inixiative bench.

License

MIT

Runtime

Development and new continuation captures use Bun 1.4.2, pinned in .bun-version and package.json. Historical S0 recordings retain their original Bun 1.3.14 evidence; they are not new 1.4.2 captures.

About

Drive coding-agent CLIs as persistent, streaming, event-captured sessions — one interface, any agent. Claude Code and Codex today.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages