agent
Tier 1 · engines. Depends on agent-protocol, agent-lifecycle, and @anthropic-ai/claude-agent-sdk; zod v4 is a peer.
npm install @titan-design/agent zodExample not executed here
Every other package page on this site shows output from a run. This one cannot: calling runAgent starts a real Claude Code session and spends real budget. The example below is the package's own documented usage and its types are checked by the build, but the output comments describe the contract rather than a captured run.
The problem it solves
Triggering a headless Claude Code session is one SDK call. Doing it in production means adding three things every caller ends up adding by hand: a scrubbed child environment, mandatory circuit breakers, and a failure taxonomy you can branch on.
runAgent is that wrapper. It never throws for anything the agent does; the only throws are caller mistakes caught before the session starts.
When to reach for it
You are triggering agents from a tool — a workflow step, a queue worker, a scheduled job — and you need a typed answer or a typed failure rather than a stream to babysit. For retrying, scheduling, or pooling those runs, use workflow; this package runs exactly one session.
Example
import { z } from "zod";
import { runAgent } from "@titan-design/agent";
const result = await runAgent({
prompt: "Summarise the failing tests in this repo.",
cwd: "/path/to/worktree",
maxTurns: 12,
maxBudgetUsd: 2,
outputSchema: z.object({ failing: z.array(z.string()), likelyCause: z.string() }),
});
if (result.ok) {
console.log(result.output.likelyCause, result.usage.totalCostUsd);
} else if (result.failure.kind === "rate_limited") {
scheduleRetry(result.failure.retryAt);
}Pass-through options
agents, mcpServers, hooks, allowedTools, disallowedTools, model, resumeSessionId, and settingSources go straight to the SDK. So do tools, the built-in tool set ([] runs with no tools, for a pure judgement call), and systemPrompt, which replaces the Claude Code default prompt. Leaving either unset keeps the SDK default.
The claude-print harness
harness: "claude-print" makes runAgent spawn the claude CLI in print mode instead of calling the SDK. It suits one-turn structured answers on a machine where the CLI is already logged in: the keychain login is the credential, so CLAUDE_CODE_OAUTH_TOKEN is not required. ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN are still stripped unless allowApiKeyBilling is set.
| Option | Maps to |
|---|---|
| binary | CLAUDE_BIN, else the first executable file named claude on PATH |
| every call | -p --output-format json --tools "" --strict-mcp-config --mcp-config '{"mcpServers":{}}' --setting-sources "" --max-budget-usd <maxBudgetUsd> |
maxTurns | --max-turns, at least 2 when outputSchema is set; error_max_turns is a retryable runtime_error |
prompt | stdin |
model | --model |
systemPrompt | --system-prompt |
outputSchema | --json-schema, then a local zod parse; a mismatch is schema_invalid |
inactivityTimeoutMs | a wall deadline that kills the child process group |
signal | kills the child process group and returns aborted |
Tools other than [], MCP servers, hooks, subagents, permission modes, resume and setting sources throw a TypeError before anything spawns. claudePrintCapabilities() states the same limits in the shared capability vocabulary. A measured call with no systemPrompt used about 7,600 input tokens for the default Claude Code prompt, cost $0.031 on sonnet and took 4.4 s wall time.
The JSON result's usage.input_tokens is the non-cached count only, often single digits beside thousands of cache_creation_input_tokens, so read cost from totalCostUsd or the cache fields.
Budgets are required, not defaulted
The SDK leaves maxTurns and maxBudgetUsd unlimited. Rather than pick a default nobody would notice was wrong, both are required fields, and runAgent throws a TypeError before it calls query() if either is missing, zero, negative, or not finite.
The environment scrub
prepareEnv(env, { allowApiKeyBilling }) copies the environment and hands the result to options.env. It never mutates its argument and never reads process.env except as the default first argument.
| Variable | Action | Why |
|---|---|---|
CLAUDECODE, CLAUDE_CODE_SSE_PORT, CLAUDE_CODE_ENTRYPOINT | strip | a child CLI that inherits these refuses to start inside a parent session |
every other CLAUDE_CODE_* | strip | describes the parent session and confuses the child's config inference |
CLAUDE_CODE_OAUTH_TOKEN | keep | the subscription credential |
CLAUDE_CODE_USE_BEDROCK / _VERTEX / _FOUNDRY | keep | a deliberate provider choice |
CLAUDE_CODE_MAX_RETRIES | keep, default 3 | the built-in default of 10 turns an outage into a long silent hang |
ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN | strip unless allowApiKeyBilling | either one silently outranks the OAuth token and bills the API account |
HTTP_PROXY, HTTPS_PROXY, ALL_PROXY (both cases) | strip | leak from the parent shell and re-route the child's API traffic |
| everything else | keep | ordinary process environment |
Two guards sit either side of the run. assertAuthEnvOk is the pre-flight: it refuses to start when the OAuth token is missing, or when a metered credential survived the scrub. The post-flight reads apiKeySource off the first system/init message and ends the run as auth_misconfigured if it names the env API-key path.
That post-flight check is a blacklist, not a whitelist: the SDK's ApiKeySource union grows over time, and an unknown new member is far more likely to be another benign route than a billing one.
Failures
One kind per recovery strategy, so a caller can branch instead of parsing a message.
| kind | Means |
|---|---|
rate_limited | usage or burst limit; carries retryAt when the stream or the message said when |
budget_exceeded | maxBudgetUsd was reached |
max_turns | maxTurns was reached |
schema_invalid | the SDK exhausted its structured-output retries, or the answer failed your zod schema |
refusal | the model declined |
auth_misconfigured | the pre-flight or the apiKeySource check failed |
runtime_error | anything else, including a dead subprocess |
aborted | the caller's AbortSignal fired |
inactivity_timeout | no message arrived for inactivityTimeoutMs (default 600,000) |
classifyResult(resultMessage, options?) is exported, so a product can map a result message it captured itself.
Gotchas
result.usage holds the SDK's client-side estimates, priced from a bundled table. Use them as a budget signal, never as a billing statement. The SDK's cost fields are already cumulative across a query() call, so this package reads the latest result rather than summing; modelUsage is the fallback when a crash result arrives with the total zeroed.
A subagent inherits bypassPermissions, acceptEdits and auto from the parent and cannot narrow them per-subagent. A parent running wide open gives every subagent that same reach regardless of what its AgentDefinition says. Prefer allowedTools wildcards over a permissive mode.
Two defaults are chosen for headless safety. permissionMode defaults to "dontAsk", so nothing runs that was not pre-approved. settingSources defaults to [], so no CLAUDE.md, settings file, or .mcp.json is picked up off the filesystem unless you ask.
Every run ends cleanly. The inactivity watchdog resets on each streamed message and, like the caller's AbortSignal, ends the run through the SDK's abortController and then query.close(), so no CLI subprocess is left behind.
Where it came from
brain's agent-submission spike (925 tested lines), rather than its PM-tangled production path.
Shared harness contracts
The additive HarnessAdapter, HarnessRunRequest and dispatchHarnessRun API separates Claude and Codex native options and checks declared capabilities and limits before invoking an adapter. createCodexExecAdapter({ auth: "cached-cli" }) implements supervised Codex exec with an explicit model and mandatory wall deadline. It uses codex on PATH or an explicit absolute executable, requires version >=0.162.1, and probes command inputs and the requested sandbox before every launch. Extra requiredRuntimeCapabilities refuse missing/unverified MCP, tool-policy or rollout evidence; the default help probe cannot establish those semantics. This is runtime admission, not permission enforcement. runAgent retains its existing Claude behavior.
checkCodexAvailable(options?, deps?) is the shared model-free runtime check for consumers. Default discovery uses PATH, explicit executable overrides must be absolute, and version plus required capabilities share a bounded deadline. It returns { ok: true, executablePath, versionOutput, inspection } or a categorized failure. Supply additional requirements such as output_schema for a judge. Availability does not establish login state or permission enforcement.
Resumes inspect their own flags and the sandbox values listed by exec --help: the adapter sets sandbox_mode through --config on either subcommand. Both help probes share the setup deadline; requirements follow the built arguments.
See the contract decision for the staged migration.
Durable dispatch
createClaudeCodeAdapter({ maxTurns, maxBudgetUsd }) exposes the existing Claude SDK runner through the common bounded harness API. Native circuit breakers remain mandatory; a local wall deadline aborts and closes the owned query. Generic optional limits are rejected until their units and enforcement are verified.
createDurableHarnessDispatcher(adapter, { ledger, supervisorId, leaseMs }) wraps either Claude or Codex. Each dispatch takes a caller-generated execution ID and request key and returns a durable acknowledgment plus a separate completion promise. Use the agent-lifecycle migration and ledger in product-owned authoritative state. Results must be JSON-safe.
Same-instance reconciliation can return a live completion handle while its exact owner generation remains valid. After restart, terminal ledger results are readable; uncertain native state requires recovery. A local deadline or abort without native terminal evidence is cancellation_unknown. No transport reattachment or PID-based ownership is inferred. durableHarnessRunner provides the workflow bridge.
Pricing compatibility
Existing pricing exports re-export @titan-design/agent-protocol/pricing, which owns the unchanged catalog and accounting functions. Callers retain their existing input and output types. This extraction changes neither prices nor stored costs; native account and harness behavior is unaffected.