Skip to content

Client

The agents namespace is the main entry point. It's pre-configured and ready to use. For custom defaults, create your own client with createClient().

agents.ask(agent, prompt, options?)

Send a one-shot prompt to an agent and get the complete result.

ts
import { agents } from "@pivanov/agents-wire";

const result = await agents.ask(
  "claude",
  "Fix the bug in main.ts",
  {
    cwd: "/path/to/project",
    permission: "auto-allow",
    maxCostUsd: 0.5,
  },
);

console.log(result.text);
console.log(result.cost?.totalUsd);
console.log(result.stopReason);

Returns: Promise<IAskResult>

ts
type IAskResult = {
  text: string;          // concatenated text output
  // "end-turn" | "tool-use" | "max-tokens" | "error" | "cancelled"
  stopReason: TStopReason;
  usage?: IUsageReport;  // { contextSize, contextUsed, costUsd } - not reported by all agents
  cost?: ICostSnapshot;  // SDK-side cumulative cost tracker snapshot
  sessionId?: string;
  events: TAgentEvent[]; // all events from the turn
};

Cost vs usage

result.usage comes from the agent's own report (not all agents provide it; Cursor/Copilot/Pi/Auggie are subscription-based and do not report costUsd). result.cost is the SDK's own tracker - always present, aggregated across the session.

agents.askJson(agent, prompt, schema, options?)

Send a prompt, parse the response as JSON, and validate it against a schema.

ts
import { agents } from "@pivanov/agents-wire";
import { z } from "zod";

const { data } = await agents.askJson(
  "claude",
  "List 3 colors as JSON: { colors: string[] }",
  z.object({ colors: z.array(z.string()) }),
);

// ["red", "green", "blue"]
console.log(data.colors);

Accepts Standard Schema objects (Zod, Valibot, ArkType) or a raw JSON Schema string. Returns { data: T, raw: IAskResult }. Throws JsonValidationError on parse or validation failure.

Returns: Promise<IJsonResult<T>>

ts
interface IJsonResult<T> {
  data: T;          // validated and typed result
  raw: IAskResult;  // full ask result (text, cost, events)
}

agents.stream(agent, prompt, options?)

Returns an IAgentStream - an async iterable that yields events as they arrive.

ts
const stream = agents.stream("claude", "Explain this code");

// Option A: iterate events
for await (const event of stream) {
  if (event.type === "text-delta") process.stdout.write(event.text);
}

// Option B: convenience methods (consumes the stream)
const text = await stream.text();
const result = await stream.result();

WARNING

Iteration and convenience methods are mutually exclusive. If you start iterating with for await, calling .text() / .result() will throw. Pick one approach.

agents.session(agent, options?)

Create a persistent multi-turn session. See Session.

ts
await using session = await agents.session(
  "codex",
  {
    cwd: ".",
    permission: "auto-allow",
  },
);

createClient(defaults)

Create a new client with preset defaults. All options are merged with per-call overrides.

ts
import { createClient } from "@pivanov/agents-wire";

const myClient = createClient({
  cwd: "/my/project",
  permission: "auto-allow",
  maxCostUsd: 1.00,
});

const result = await myClient.ask("claude", "What does this do?");

IAgentOptions

All methods accept these options:

OptionTypeDescription
cwdstringWorking directory for the agent process
permissionTPermissionPolicyPermission policy: "auto-allow", "auto-allow-once", "auto-reject", "stream", or custom function
modelstringAgent-specific model identifier. What this actually does varies by agent - see Model selection by agent. Forwarded to definition.launch() as IWireLaunchOptions.model; also used for pricing lookup.
effortstringReasoning effort hint. For codex, becomes -c model_reasoning_effort="X" CLI arg (works). For other agents, sent via ACP setSessionConfigOption({ configId: "reasoning_effort", value }) after newSession - best-effort; the agent may silently ignore. Common values: "low", "medium", "high".
modelPreference{ configId: string; value: string | boolean }Sent via ACP setSessionConfigOption after newSession. Best-effort: silently ignored if the agent doesn't implement that method.
systemPromptstringOverride system prompt
toolHandlerIToolHandlerRuntime tool control (allow/deny/intercept)
maxCostUsdnumberSDK-side budget limit. Throws BudgetExceededError when exceeded
mcpServersIMcpServer[]MCP servers to register with the agent
envFilter(env: NodeJS.ProcessEnv) => NodeJS.ProcessEnvStrip or transform the merged environment before spawning. Use this to inject or remove env vars without modifying process.env.
signalAbortSignalAbort signal for cancellation
onCostUpdate(cost: ICostSnapshot) => voidCalled after each turn with cost data
onWarning(message: string, cause?: unknown) => voidRoutes library warnings. Defaults to console.warn; pass () => {} to silence.
onRetry(attempt: number, error: unknown) => voidCalled before each backoff delay during auto-respawn.
onRecycle(reason: TRecycleReason) => voidCalled before each proactive recycle. Reason is "turn-limit" today.
inactivityTimeoutMsnumberInactivity watchdog timeout in ms (default: 5 minutes). Pass Infinity to disable.
autoRespawnbooleanAuto-respawn on transient failures (default: true). Set false to disable.
maxTurnsBeforeRecyclenumberProactively recycle the host after N turns to bound memory growth. Defaults to 100. Set to 0 to disable.

Session-only options

ISessionOptions extends IAgentOptions with:

OptionTypeDescription
onRetry(attempt: number, error: unknown) => voidFires each time a transient failure triggers a respawn inside one ask().
autoRespawnbooleanDefault true. Re-spawns the host with backoff [500, 1000, 2000] ms up to 3 attempts on AgentConnectionClosedError, ECONNRESET, and similar transient failures. Set false to disable.
maxTurnsBeforeRecyclenumberRecycle the host after N turns (default: 100). Set 0 to disable.

Per-ask options

session.ask(prompt, options?) accepts IAskOptions:

OptionTypeDescription
onRetry(attempt: number, error: unknown) => voidPer-ask retry observer. Fires alongside the session-level onRetry when both are set.
onCostUpdate(cost: ICostSnapshot) => voidPer-ask cost observer. Useful for request-scoped metadata.
signalAbortSignalPer-ask abort. Aborts this ask only (session stays alive).

Dual Budget System

maxCostUsd is SDK-level budget enforcement (throws BudgetExceededError) and uses the SDK cost tracker. It works for agents that report per-turn costUsd (for example Claude). Subscription agents (Cursor, Copilot, Pi, Auggie) do not report per-turn costUsd, so maxCostUsd remains 0 and will not trigger for them.

Terminal and FileSystem Handlers

Two optional handler groups let the agent call back into your process for file and terminal operations over ACP.

IAgentOptions.fileSystem

ts
interface IFileSystemHandlers {
  readTextFile?: (params: ReadTextFileRequest) => Promise<ReadTextFileResponse>;
  writeTextFile?: (params: WriteTextFileRequest) => Promise<WriteTextFileResponse>;
}

Implement these to intercept file reads and writes the agent requests. The request and response types come directly from @agentclientprotocol/sdk.

IAgentOptions.terminal

ts
interface ITerminalHandlers {
  createTerminal?: (
    params: CreateTerminalRequest,
  ) => Promise<CreateTerminalResponse>;
  terminalOutput?: (
    params: TerminalOutputRequest,
  ) => Promise<TerminalOutputResponse>;
  releaseTerminal?: (
    params: ReleaseTerminalRequest,
  ) => Promise<ReleaseTerminalResponse | undefined>;
  waitForTerminalExit?: (
    params: WaitForTerminalExitRequest,
  ) => Promise<WaitForTerminalExitResponse>;
  killTerminal?: (
    params: KillTerminalRequest,
  ) => Promise<KillTerminalResponse | undefined>;
}

All five methods are optional. Unimplemented methods are silently skipped.

INFO

Terminal and FileSystem handlers are currently primarily exercised by Claude Code. Other agents may not invoke these callbacks.

Example wiring both handlers:

ts
import { agents } from "@pivanov/agents-wire";

const result = await agents.ask(
  "claude",
  "Read README.md",
  {
    permission: "auto-allow",
    fileSystem: {
      readTextFile: async ({ path }) => ({
        content: await myStorage.read(path),
      }),
      writeTextFile: async ({ path, content }) => {
        await myStorage.write(path, content);
        return {};
      },
    },
    terminal: {
      createTerminal: async ({ id }) => {
        return { id };
      },
    },
  },
);