Skip to content

agent ​

Tier 1 · engines. Depends on agent-protocol, agent-lifecycle, and @anthropic-ai/claude-agent-sdk; zod v4 is a peer.

sh
npm install @titan-design/agent zod

Example 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 ​

ts
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.

OptionMaps to
binaryCLAUDE_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
promptstdin
model--model
systemPrompt--system-prompt
outputSchema--json-schema, then a local zod parse; a mismatch is schema_invalid
inactivityTimeoutMsa wall deadline that kills the child process group
signalkills 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.

VariableActionWhy
CLAUDECODE, CLAUDE_CODE_SSE_PORT, CLAUDE_CODE_ENTRYPOINTstripa child CLI that inherits these refuses to start inside a parent session
every other CLAUDE_CODE_*stripdescribes the parent session and confuses the child's config inference
CLAUDE_CODE_OAUTH_TOKENkeepthe subscription credential
CLAUDE_CODE_USE_BEDROCK / _VERTEX / _FOUNDRYkeepa deliberate provider choice
CLAUDE_CODE_MAX_RETRIESkeep, default 3the built-in default of 10 turns an outage into a long silent hang
ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKENstrip unless allowApiKeyBillingeither one silently outranks the OAuth token and bills the API account
HTTP_PROXY, HTTPS_PROXY, ALL_PROXY (both cases)stripleak from the parent shell and re-route the child's API traffic
everything elsekeepordinary 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.

kindMeans
rate_limitedusage or burst limit; carries retryAt when the stream or the message said when
budget_exceededmaxBudgetUsd was reached
max_turnsmaxTurns was reached
schema_invalidthe SDK exhausted its structured-output retries, or the answer failed your zod schema
refusalthe model declined
auth_misconfiguredthe pre-flight or the apiKeySource check failed
runtime_erroranything else, including a dead subprocess
abortedthe caller's AbortSignal fired
inactivity_timeoutno 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.

MIT licensed