Skip to main content
CLI-backed agent classes wrap external AI command-line tools. Each implements the AI SDK Agent interface and works anywhere Smithers accepts an agent, including <Task>. The agent spawns the CLI, passes the prompt, captures output, and returns a GenerateTextResult. For API-billed provider wrappers, see SDK Agents.

Import

import {
  ClaudeCodeAgent,
  CodexAgent,
  GeminiAgent,
  PiAgent,
  KimiAgent,
  ForgeAgent,
  AmpAgent,
  type PiAgentOptions,
  type PiExtensionUiRequest,
  type PiExtensionUiResponse,
} from "smithers-orchestrator";

Prerequisites

AgentCLI RequiredInstall
ClaudeCodeAgentclaudeClaude Code
CodexAgentcodexOpenAI Codex CLI
GeminiAgentgeminiGemini CLI
PiAgentpiPI Coding Agent
KimiAgentkimiKimi CLI
ForgeAgentforgeForge CLI
AmpAgentampAmp CLI

Quick Start

import { ClaudeCodeAgent, CodexAgent, GeminiAgent, PiAgent, KimiAgent, ForgeAgent, AmpAgent } from "smithers-orchestrator";

const claude = new ClaudeCodeAgent({ model: "claude-sonnet-4-20250514" });
const codex = new CodexAgent({ model: "gpt-4.1" });
const gemini = new GeminiAgent({ model: "gemini-2.5-pro" });
const pi = new PiAgent({ provider: "openai", model: "gpt-5.2-codex" });
const kimi = new KimiAgent({ model: "kimi-latest" });
const forge = new ForgeAgent({ model: "anthropic/claude-sonnet-4-20250514" });
const amp = new AmpAgent({ model: "claude-sonnet-4-20250514" });
{/* outputs comes from createSmithers() */}
<Task id="analysis" output={outputs.analysis} agent={claude}>
  {`Analyze the codebase and identify potential improvements.`}
</Task>

Hijack Support

All built-in CLI agents support native-session hijack via smithers hijack <runId>.
AgentHijack ModeNative Relaunch
ClaudeCodeAgentNative CLI sessionclaude --resume <session>
CodexAgentNative CLI sessioncodex resume <session> -C <cwd>
GeminiAgentNative CLI sessiongemini --resume <session>
PiAgentNative CLI sessionpi --session <session>
KimiAgentNative CLI sessionkimi --session <session> --work-dir <cwd>
ForgeAgentNative CLI sessionforge --conversation-id <id> -C <cwd>
AmpAgentNative CLI sessionamp threads continue <thread>
Behavior:
  • Live run: Smithers waits until the agent is between blocking tool calls before aborting.
  • Finished/cancelled run: Smithers reopens the latest persisted native session.
  • If the hijacked session exits successfully, the workflow resumes automatically in detached mode.
  • Cross-engine hijack is not supported.
Use smithers hijack <runId> --launch=false to inspect the resumable candidate without opening the session.

Non-Idempotent Tool Resume Warning

When a <Task> retries after a failure, previous attempts may have already executed side-effect tools (e.g., sending messages, creating PRs). Smithers detects non-idempotent tool calls from prior attempts and prepends a warning to the agent’s prompt:
Previous attempts in this task already called non-idempotent side-effect tools. Those side effects may already have happened before the interruption or retry. Do not blindly call them again. Verify external state first or continue from the prior result.
The warning includes the specific tool names and attempt numbers. It is automatically injected — no configuration is required.

Base Options

type BaseCliAgentOptions = {
  id?: string;               // Agent ID (default: random UUID)
  model?: string;            // Model name to pass to the CLI
  systemPrompt?: string;     // System prompt prepended to the user prompt
  instructions?: string;     // Alias for systemPrompt
  cwd?: string;              // Working directory for the CLI process
  env?: Record<string, string>;  // Additional environment variables
  yolo?: boolean;            // Skip permission prompts (default: true)
  timeoutMs?: number;        // Hard wall-clock timeout in milliseconds
  idleTimeoutMs?: number;    // Inactivity timeout (no stdout/stderr) in milliseconds
  maxOutputBytes?: number;   // Max output capture size
  extraArgs?: string[];      // Additional CLI arguments appended to the command
};
OptionDefaultDescription
idRandom UUIDAgent instance identifier
modelundefinedModel name passed to --model
systemPromptundefinedSystem instructions prepended to the prompt
instructionsundefinedAlias for systemPrompt
cwdTool context rootDir or process.cwd()Working directory for the spawned process
env{}Extra environment variables merged with process.env
yolotrueSkip all interactive permission prompts
timeoutMsundefinedHard wall-clock timeout; kills process after this many ms
idleTimeoutMsundefinedInactivity timeout; kills process after this many ms with no output
maxOutputBytesundefinedTruncate captured output to this size
extraArgs[]Additional CLI flags

Timeouts

  • timeoutMs: hard wall-clock cap.
  • idleTimeoutMs: inactivity cap, resets on any stdout/stderr output.
Per-call override:
await agent.generate({
  prompt: "do the thing",
  timeout: { totalMs: 15 * 60 * 1000, idleMs: 2 * 60 * 1000 },
});

ClaudeCodeAgent

Wraps claude CLI with --print mode.
const claude = new ClaudeCodeAgent({
  model: "claude-sonnet-4-20250514",
  systemPrompt: "You are a careful code reviewer.",
  timeoutMs: 30 * 60 * 1000,
  idleTimeoutMs: 2 * 60 * 1000,
});

Claude-Specific Options

type ClaudeCodeAgentOptions = BaseCliAgentOptions & {
  addDir?: string[];
  agent?: string;
  agents?: Record<string, { description?: string; prompt?: string }> | string;
  allowDangerouslySkipPermissions?: boolean;
  allowedTools?: string[];
  appendSystemPrompt?: string;
  betas?: string[];
  chrome?: boolean;
  continue?: boolean;
  dangerouslySkipPermissions?: boolean;
  debug?: boolean | string;
  debugFile?: string;
  disableSlashCommands?: boolean;
  disallowedTools?: string[];
  fallbackModel?: string;
  file?: string[];
  forkSession?: boolean;
  fromPr?: string;
  ide?: boolean;
  includePartialMessages?: boolean;
  inputFormat?: "text" | "stream-json";
  jsonSchema?: string;
  maxBudgetUsd?: number;
  mcpConfig?: string[];
  mcpDebug?: boolean;
  noChrome?: boolean;
  noSessionPersistence?: boolean;
  outputFormat?: "text" | "json" | "stream-json";
  permissionMode?: "acceptEdits" | "bypassPermissions" | "default" | "delegate" | "dontAsk" | "plan";
  pluginDir?: string[];
  replayUserMessages?: boolean;
  resume?: string;
  sessionId?: string;
  settingSources?: string;
  settings?: string;
  strictMcpConfig?: boolean;
  tools?: string[] | "default" | "";
  verbose?: boolean;
};
OptionDescription
permissionMode"bypassPermissions", "acceptEdits", "default", "delegate", "dontAsk", "plan"
allowedToolsTool name whitelist
disallowedToolsTool name blacklist
disableSlashCommandsDisable all slash commands
maxBudgetUsdSpending cap in USD
mcpConfigModel Context Protocol server configuration files
mcpDebugEnable MCP debug logging
addDirAdditional context directories
fileFiles to inject into context
fromPrPull request URL or number to use as additional context
fallbackModelModel to use if the primary model is unavailable
appendSystemPromptText appended to the system prompt
agentsMulti-agent configuration as a map of agent definitions or JSON string
betasBeta feature flags to enable
pluginDirPlugin directories for Claude Code skills
resume / sessionIdResume a previous session by ID
settings / settingSourcesOverride settings file or sources
jsonSchemaJSON schema string for structured output
includePartialMessagesStream partial assistant messages
inputFormat"text" or "stream-json" for input
outputFormat"text", "json", or "stream-json" (default: "stream-json")
When yolo is true (default), the agent passes --allow-dangerously-skip-permissions, --dangerously-skip-permissions, and --permission-mode bypassPermissions unless permissionMode is explicitly set.

PR Context

The fromPr option passes --from-pr <value> to the Claude CLI, loading the diff and metadata of the specified pull request into the conversation context. Accepts a PR URL or number:
const claude = new ClaudeCodeAgent({
  model: "claude-sonnet-4-20250514",
  fromPr: "https://github.com/org/repo/pull/42",
});
Smithers does not fetch the PR itself; the Claude CLI resolves and loads it.

CodexAgent

Wraps codex CLI using codex exec with stdin input.
const codex = new CodexAgent({
  model: "gpt-4.1",
  sandbox: "workspace-write",
  fullAuto: true,
});

Codex-Specific Options

type CodexAgentOptions = BaseCliAgentOptions & {
  config?: Record<string, string | number | boolean | object | null> | string[];
  enable?: string[];
  disable?: string[];
  image?: string[];
  oss?: boolean;
  localProvider?: string;
  sandbox?: "read-only" | "workspace-write" | "danger-full-access";
  profile?: string;
  fullAuto?: boolean;
  dangerouslyBypassApprovalsAndSandbox?: boolean;
  cd?: string;
  skipGitRepoCheck?: boolean;
  addDir?: string[];
  outputSchema?: string;
  color?: "always" | "never" | "auto";
  json?: boolean;
  outputLastMessage?: string;
};
OptionDescription
sandbox"read-only", "workspace-write", or "danger-full-access"
fullAutoFull auto mode (no confirmations)
dangerouslyBypassApprovalsAndSandboxSkip all approval prompts and sandbox restrictions
configConfiguration overrides as key-value pairs or raw strings
ossUse open-source models
localProviderLocal model provider URL
imageImage file paths to include as visual inputs
outputSchemaPath to JSON schema file for structured output
outputLastMessageFile path to write the last message (auto-generated if not set)
When yolo is true and fullAuto is not set, passes --dangerously-bypass-approvals-and-sandbox. If fullAuto is true, uses --full-auto instead. Prompt is passed via stdin using the - argument.

GeminiAgent

Wraps the gemini CLI.
const gemini = new GeminiAgent({
  model: "gemini-2.5-pro",
  sandbox: true,
  allowedTools: ["read_file", "write_file"],
});

Gemini-Specific Options

type GeminiAgentOptions = BaseCliAgentOptions & {
  debug?: boolean;
  sandbox?: boolean;
  approvalMode?: "default" | "auto_edit" | "yolo" | "plan";
  experimentalAcp?: boolean;
  allowedMcpServerNames?: string[];
  allowedTools?: string[];
  extensions?: string[];
  listExtensions?: boolean;
  resume?: string;
  listSessions?: boolean;
  deleteSession?: string;
  includeDirectories?: string[];
  screenReader?: boolean;
  outputFormat?: "text" | "json" | "stream-json";
};
OptionDescription
sandboxRun in sandbox mode
approvalMode"default", "auto_edit", "yolo", or "plan"
allowedToolsTool name whitelist
allowedMcpServerNamesMCP server name whitelist
extensionsGemini CLI extensions to load
resumeResume a previous session by ID
listSessions / deleteSessionSession management
includeDirectoriesAdditional directories to include
outputFormat"text", "json", or "stream-json" (default: "json")
When yolo is true and approvalMode is not set, passes --yolo. Prompt is passed via --prompt.

gcloud Authentication

When neither GOOGLE_API_KEY nor GEMINI_API_KEY is set, Gemini CLI uses gcloud application-default credentials. The diagnostics api_key_valid check falls back to running gcloud auth print-access-token to confirm that gcloud auth is configured. No extra options are required — the Gemini CLI picks up the credentials automatically from the environment:
gcloud auth application-default login
// No API key needed when gcloud auth is configured
const gemini = new GeminiAgent({ model: "gemini-2.5-pro" });

PiAgent

Wraps the pi CLI.
const pi = new PiAgent({
  provider: "openai",
  model: "gpt-5.2-codex",
  mode: "text",
  noSession: true,
});

PI-Specific Options

type PiAgentOptions = BaseCliAgentOptions & {
  provider?: string;
  model?: string;
  apiKey?: string;
  systemPrompt?: string;
  appendSystemPrompt?: string;
  mode?: "text" | "json" | "rpc";
  print?: boolean;
  continue?: boolean;
  resume?: boolean;
  session?: string;
  sessionDir?: string;
  noSession?: boolean;
  models?: string | string[];
  listModels?: boolean | string;
  tools?: string[];
  noTools?: boolean;
  extension?: string[];
  noExtensions?: boolean;
  skill?: string[];
  noSkills?: boolean;
  promptTemplate?: string[];
  noPromptTemplates?: boolean;
  theme?: string[];
  noThemes?: boolean;
  thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
  export?: string;
  files?: string[];
  verbose?: boolean;
  onExtensionUiRequest?: (request: PiExtensionUiRequest) =>
    | Promise<PiExtensionUiResponse | null>
    | PiExtensionUiResponse
    | null;
};
OptionDescription
providerPI provider name (--provider)
modelPI model (--model)
apiKeyPassed to --api-key (prefer env/config for secrets)
modetext, json, or rpc
printForce --print in text mode
continue / resume / sessionSession continuation controls
sessionDirCustom session directory
models / listModelsScoped model patterns and listing
extensionExtension path(s)
skillSkill path(s)
promptTemplatePrompt template path(s)
themeTheme path(s)
tools / noToolsEnable specific tools or disable built-ins
exportExport session HTML
filesFile args passed as @path (text/json modes)
onExtensionUiRequestRPC-only handler for extension UI requests
noSessionDisable session persistence (default true unless session flags set)
In text/json modes, the prompt is a positional argument and files emit as @path arguments. In rpc mode, the prompt is sent as JSON over stdin. Text mode defaults to --print without --mode; json/rpc set --mode and omit --print. For workflow hijack, Smithers automatically uses PI’s structured event stream and keeps session persistence enabled regardless of noSession.

KimiAgent

Wraps kimi CLI using --print mode.
const kimi = new KimiAgent({
  model: "kimi-latest",
  thinking: true,
  timeoutMs: 300_000,
});

Kimi-Specific Options

type KimiAgentOptions = BaseCliAgentOptions & {
  workDir?: string;
  session?: string;
  continue?: boolean;
  thinking?: boolean;
  outputFormat?: "text" | "stream-json";
  finalMessageOnly?: boolean;
  quiet?: boolean;
  agent?: "default" | "okabe";
  agentFile?: string;
  mcpConfigFile?: string[];
  mcpConfig?: string[];
  skillsDir?: string;
  maxStepsPerTurn?: number;
  maxRetriesPerStep?: number;
  maxRalphIterations?: number;
  verbose?: boolean;
  debug?: boolean;
};
OptionDescription
thinkingEnable/disable thinking mode
outputFormat"text" or "stream-json" (default: "text")
finalMessageOnlyOnly print the final assistant message
quietAlias for --print --output-format text --final-message-only
agentBuilt-in agent spec: "default" or "okabe"
agentFilePath to custom agent specification file
workDirOverride the working directory for the kimi process
session / continueSession resumption and continuation
skillsDirSkills directory path
mcpConfigFile / mcpConfigMCP config file(s) or inline config
maxStepsPerTurnMax steps in one turn
maxRetriesPerStepMax retries in one step
maxRalphIterationsExtra iterations after the first turn in Loop mode
When yolo is true (default), passes --print which implicitly adds --yolo. Prompt is passed via --prompt.

Isolated Share Directory

Kimi stores per-session metadata in ~/.kimi/ (or $KIMI_SHARE_DIR). When running parallel tasks, concurrent writes to this directory can corrupt kimi.json. KimiAgent automatically creates an isolated temporary directory per invocation, copies config.toml, credentials, device_id, and latest_version.txt from the default share dir, and sets KIMI_SHARE_DIR to the temporary copy. The directory is removed via the cleanup hook when the run completes. To opt out of isolation and use a specific directory, set KIMI_SHARE_DIR in env:
const kimi = new KimiAgent({
  model: "kimi-latest",
  env: { KIMI_SHARE_DIR: "/path/to/shared-kimi" },
});

ForgeAgent

Wraps forge CLI. Supports 300+ models via --prompt.
const forge = new ForgeAgent({
  model: "anthropic/claude-sonnet-4-20250514",
  provider: "anthropic",
  directory: "/path/to/project",
});

Forge-Specific Options

type ForgeAgentOptions = BaseCliAgentOptions & {
  directory?: string;       // -C, --directory <DIR>
  provider?: string;        // --provider <PROVIDER>
  agent?: string;           // --agent <AGENT>
  conversationId?: string;  // --conversation-id <ID>
  sandbox?: string;         // --sandbox <NAME>
  restricted?: boolean;     // -r, --restricted
  verbose?: boolean;        // --verbose
  workflow?: string;        // -w, --workflow <FILE>
  event?: string;           // -e, --event <JSON>
  conversation?: string;    // --conversation <FILE>
};
OptionDescription
directoryWorking directory (-C); defaults to cwd
providerModel provider name
agentAgent type
conversationIdResume conversation by ID
sandboxSandbox name
restrictedEnable restricted mode
workflowWorkflow file path
eventEvent JSON for workflow triggers
conversationConversation file path
Forge --prompt mode auto-approves tool use; no separate yolo flag. Prompt is passed via --prompt.

AmpAgent

Wraps amp CLI using --execute mode.
const amp = new AmpAgent({
  model: "claude-sonnet-4-20250514",
  visibility: "private",
  logLevel: "info",
});

Amp-Specific Options

type AmpAgentOptions = BaseCliAgentOptions & {
  visibility?: "private" | "public" | "workspace" | "group";
  mcpConfig?: string;
  settingsFile?: string;
  logLevel?: "error" | "warn" | "info" | "debug" | "audit";
  logFile?: string;
  dangerouslyAllowAll?: boolean;
  ide?: boolean;
  jetbrains?: boolean;
};
OptionDescription
visibilityThread visibility: "private", "public", "workspace", "group"
mcpConfigMCP configuration file path
settingsFileCustom settings file path
logLevel"error", "warn", "info", "debug", "audit"
logFileLog output file path
dangerouslyAllowAllAllow all tool calls without confirmation
When yolo is true (default) or dangerouslyAllowAll is true, passes --dangerously-allow-all. Prompt is passed via --execute. Automatically passes --no-ide, --no-jetbrains, --no-color, and --archive for headless execution.

Diagnostics

Before each run, Smithers launches a diagnostic probe concurrently with the agent process. If the agent fails, the probe’s findings are attached to the error and printed as a warning.
// Diagnostics run automatically — no configuration required.
// On failure, err.details.diagnostics contains the full DiagnosticReport.
try {
  await claude.generate({ prompt: "..." });
} catch (err) {
  // err.details.diagnostics.checks contains the individual check results
}
Each DiagnosticReport contains:
type DiagnosticReport = {
  agentId: string;        // e.g. "claude-code"
  command: string;        // e.g. "claude"
  timestamp: string;      // ISO 8601
  checks: DiagnosticCheck[];
  durationMs: number;
};

type DiagnosticCheck = {
  id: "cli_installed" | "api_key_valid" | "rate_limit_status";
  status: "pass" | "fail" | "skip" | "error";
  message: string;
  detail?: Record<string, unknown>;
  durationMs: number;
};

CLI Installed Check

The cli_installed check runs which <command> to confirm the binary is on PATH.
  • pass — binary found; detail.binaryPath contains the resolved path.
  • fail — binary not found; install the CLI listed in Prerequisites.

API Key Check

The api_key_valid check verifies the API credential for each provider.
AgentEnv var checkedMethod
ClaudeCodeAgentANTHROPIC_API_KEYFormat check (sk-ant-*); absent = subscription mode (pass)
CodexAgentOPENAI_API_KEYGET /v1/models
GeminiAgentGOOGLE_API_KEY or GEMINI_API_KEYGET /v1beta/models; falls back to gcloud auth
AmpAgentSkipped (Amp manages its own auth)

Rate Limit Check

The rate_limit_status check probes the provider’s API for current quota headroom.
  • Reads standard rate-limit headers (anthropic-ratelimit-*, x-ratelimit-*).
  • Status is skip when using gcloud auth or subscription mode.
  • If the check passed before the run but the error text contains rate-limit patterns (e.g. 429, too many requests, quota exceeded), the check is upgraded to fail post-hoc and attached to the error.

Capability Registry

Every CLI agent exposes a capabilities property that describes its tool surface. Smithers uses this at runtime to normalize tool names and verify that the agent configuration is self-consistent.
console.log(claude.capabilities);
// {
//   version: 1,
//   engine: "claude-code",
//   runtimeTools: {},
//   mcp: { bootstrap: "project-config", supportsProjectScope: true, supportsUserScope: true },
//   skills: { supportsSkills: true, installMode: "plugin", smithersSkillIds: [] },
//   humanInteraction: { supportsUiRequests: false, methods: [] },
//   builtIns: ["default", "slash-commands"]
// }

Normalization

normalizeCapabilityRegistry canonicalizes a registry before comparison or hashing: string lists are deduplicated and sorted, tool descriptor fields are trimmed, and empty optional values are removed.
import { normalizeCapabilityRegistry } from "smithers-orchestrator";

const canonical = normalizeCapabilityRegistry(agent.capabilities);
normalizeCapabilityStringList applies the same rules to any standalone string array:
import { normalizeCapabilityStringList } from "smithers-orchestrator";

normalizeCapabilityStringList(["!bash", "default", "default", " web_search "])
// ["!bash", "default", "web_search"]

Hashing

hashCapabilityRegistry produces a stable SHA-256 hex fingerprint of the normalized registry. Use it to detect configuration drift between agent invocations or CI runs.
import { hashCapabilityRegistry } from "smithers-orchestrator";

const fingerprint = hashCapabilityRegistry(agent.capabilities);
// "a3f1c9..."
The hash is also returned in getCliAgentCapabilityReport() as entry.fingerprint.

Capability Doctor

getCliAgentCapabilityDoctorReport() validates every built-in CLI agent’s registry against consistency rules and returns a report with per-agent issues:
import { getCliAgentCapabilityDoctorReport } from "smithers-orchestrator";

const report = getCliAgentCapabilityDoctorReport();
if (!report.ok) {
  console.error(formatCliAgentCapabilityDoctorReport(report));
}

Agent Contract

The agent contract describes the Smithers MCP server tool surface that is injected into an agent’s context. It is separate from the capability registry — the registry describes what the agent can do, while the contract describes what Smithers exposes to the agent.

Raw vs. Semantic Tool Surface

SmithersToolSurface is "raw" or "semantic". The semantic surface groups and renames tools to reduce noise for general-purpose agents. The raw surface exposes every tool name as-is. smithers ask builds the MCP launch configuration and probes the live tool surface internally. For direct package use, build a contract from the tools you expose to an agent:
import { createSmithersAgentContract } from "smithers-orchestrator";

const contract = createSmithersAgentContract({
  serverName: "smithers",
  toolSurface: "semantic",
  tools: [
    {
      name: "list_workflows",
      description: "List discovered local Smithers workflows.",
    },
    {
      name: "run_workflow",
      description: "Start a discovered workflow directly through the engine.",
    },
  ],
});

Prompt Guidance

contract.promptGuidance is a compact, instruction-friendly string listing available tools grouped by category. Inject it into an agent’s system prompt:
const claude = new ClaudeCodeAgent({
  systemPrompt: contract.promptGuidance,
});
Example output:
You have access to the live Smithers semantic MCP surface on server "smithers".
Only rely on the tool names listed here.
For workflow discovery and launch, use `list_workflows`, `run_workflow`.
For run inspection and control, use `cancel`, `get_run`, `list_runs`.
Potentially destructive tools: `cancel`, `run_workflow`. Confirm intent before using them.

Docs Guidance

contract.docsGuidance is a Markdown table listing every tool with its category, destructive flag, and description. Suitable for injecting into documentation or longer context windows:
console.log(contract.docsGuidance);
// ## Smithers semantic Tool Surface
// | Tool | Category | Destructive | Description |
// | --- | --- | --- | --- |
// | `list_workflows` | workflows | no | List available workflows. |
// ...

Token and Usage Tracking

Smithers extracts token usage from raw CLI output and populates the usage field of the returned GenerateTextResult. This works across all built-in agents without additional configuration.
const result = await claude.generate({ prompt: "..." });
console.log(result.usage);
// {
//   inputTokens: 1024,
//   outputTokens: 512,
//   inputTokenDetails: { cacheReadTokens: 128, cacheWriteTokens: 64 },
//   outputTokenDetails: { reasoningTokens: 0 },
//   totalTokens: 1536
// }

Usage Extraction

BaseCliAgent parses raw CLI stdout to find token counts. The extraction strategy is format-specific:
Agent / FormatSource
ClaudeCodeAgent stream-jsonmessage_start.message.usage (input) + message_delta.usage (output)
CodexAgent --jsonturn.completed.usage
GeminiAgent jsonstats.models[*].tokens
Generic NDJSONAny line with a usage object containing input_tokens / output_tokens
Cache read tokens (cache_read_input_tokens, cached_input_tokens), cache write tokens (cache_creation_input_tokens), and reasoning tokens (reasoning_tokens) are accumulated when present.

BaseCliAgent Internals

Cleanup Hook

CliCommandSpec.cleanup is an optional async () => void returned by buildCommand. It runs after the agent process exits, whether the run succeeds or fails. Use it to remove temporary files:
// KimiAgent uses this pattern internally:
return {
  command: "kimi",
  args,
  env: { KIMI_SHARE_DIR: isolatedDir },
  cleanup: async () => {
    rmSync(isolatedDir, { recursive: true, force: true });
  },
};
The cleanup runs under Effect.ensuring, so it is guaranteed to execute even when the command throws.

Stdout Error Detection

Some CLIs exit with code 0 but print an error message to stdout. The stdoutErrorPatterns field on CliCommandSpec accepts an array of RegExp patterns. If any pattern matches the cleaned stdout text (after banner stripping), the agent throws AGENT_CLI_ERROR with the matched content as the message:
return {
  command: "mycli",
  args,
  stdoutErrorPatterns: [/^Error:/m, /authentication failed/i],
};
Detection is skipped when stdout starts with { or [ (i.e., JSON output). CLI tools occasionally print version banners, update notices, or telemetry lines to stdout before the model response. The stdoutBannerPatterns field on CliCommandSpec accepts an array of RegExp patterns that are stripped from stdout before text extraction:
return {
  command: "mycli",
  args,
  stdoutBannerPatterns: [/^mycli v\d+\.\d+\.\d+.*\n/m],
  errorOnBannerOnly: true, // throw if only a banner was printed (no model response)
};

Agent Interface

All CLI agents implement two methods.

generate(options)

Runs the CLI synchronously and returns a GenerateTextResult:
const result = await claude.generate({
  prompt: "Explain the architecture of this codebase.",
});
console.log(result.text);
  1. Extracts prompt from options.prompt (string) or options.messages (array).
  2. Builds the CLI command with all configured flags.
  3. Spawns the process and captures stdout/stderr.
  4. For json/stream-json output, extracts text from the JSON payload.
  5. Returns the result as a GenerateTextResult.

stream(options)

Calls generate() internally and wraps the result as a StreamTextResult. Not truly streamed.
const stream = await claude.stream({ prompt: "Review this code." });
for await (const chunk of stream.textStream) {
  process.stdout.write(chunk);
}

Message Handling

When called with messages, agents convert them to a text prompt:
  • System messages are extracted and prepended as a system prompt.
  • User/assistant messages are formatted as ROLE: content, joined with double newlines.
  • Message system prompt is combined with any systemPrompt on the agent instance.

Example: Multi-Agent Workflow

import { ClaudeCodeAgent, CodexAgent } from "smithers-orchestrator";

const reviewer = new ClaudeCodeAgent({
  model: "claude-sonnet-4-20250514",
  systemPrompt: "You are a thorough code reviewer.",
  timeoutMs: 120_000,
});

const fixer = new CodexAgent({
  model: "gpt-4.1",
  fullAuto: true,
  timeoutMs: 180_000,
});

const { Workflow, smithers, outputs } = createSmithers({
  review: z.object({ summary: z.string() }),
  fix: z.object({ result: z.string() }),
});

export default smithers((ctx) => (
  <Workflow name="review-and-fix">
    <Task id="review" output={outputs.review} agent={reviewer}>
      {`Review the changes in this PR and identify issues.`}
    </Task>
    <Task id="fix" output={outputs.fix} agent={fixer}>
      {`Fix these issues: ${ctx.output(outputs.review, { nodeId: "review" }).summary}`}
    </Task>
  </Workflow>
));

Next Steps