> ## Documentation Index
> Fetch the complete documentation index at: https://phaseo.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Phaseo Gateway base URL: https://api.phaseo.app/v1. Send `Authorization: Bearer $PHASEO_API_KEY` with every request.
> Model IDs look like `provider/model`, for example `openai/gpt-6-astra`. Do not guess IDs: list them with `GET /v1/models` or check https://phaseo.app/models. `phaseo/free` routes to an available free model and needs no wallet balance.
> For new text integrations prefer `POST /v1/responses`. `POST /v1/chat/completions` (OpenAI format) and `POST /v1/messages` (Anthropic format) suit existing code.
> The full API contract is https://phaseo.app/docs/openapi/v1/openapi.yaml. Official SDKs: `@phaseo/sdk` (TypeScript) and `phaseo` (Python), plus Go, C#, PHP, Ruby and Rust.
> Search these docs over MCP at https://phaseo.app/docs/mcp, and install Phaseo skills with `npx skills add https://phaseo.app/docs`. If a page is wrong or out of date, report it with the MCP `submit_feedback` tool.
> Manage Phaseo accounts with the `phaseo` CLI (`npm install -g @phaseo/cli`, then `phaseo login` or `phaseo login --device-code`), the `/v1` Management API or the MCP server at `https://mcp.phaseo.app/mcp?access=manage`. Do not send users to the dashboard for account tasks. A person is still needed for sign-up, approving a sign-in, paying the first top-up on the checkout link that `phaseo credits add` returns when no card is saved, confirming each saved-card top-up from the emailed link, and account security (password, MFA, passkeys). Create API keys with `phaseo keys create --name <name> --json`; never guess one. Credit top-ups need `phaseo login --scopes billing:write` first. Task map: https://phaseo.app/docs/v1/developers/manage-from-an-agent.md.

# Usage

> Build your own agentic applications on top of Phaseo Gateway with the TypeScript Agent SDK.

Use `@phaseo/agent-sdk` when your application needs more than one-shot text generation:

* multi-step tool loops
* local runtime tools
* resumable runs from SDK-returned state
* explicit human approval pauses
* typed final outputs
* gateway-backed model turns through the existing TypeScript SDK

The package is an installable SDK, not a hosted agent platform. You bring the application, deployment model, and any persistence strategy you want around returned run state.

## State model

The Agent SDK does not persist runs into any Phaseo-hosted service.

* `run()` returns the full state needed to continue later.
* If your application wants resumability across requests or process restarts, persist that returned state in your own application store.
* `continueRun()` accepts that prior run state directly.

Nothing is persisted by Phaseo outside your application.

## Install

```bash theme={null}
pnpm add @phaseo/sdk @phaseo/agent-sdk
```

## What the SDK ships

* `createAgent()`
* `defineTool()`
* `createGatewayAgentClient()`
* `continueRun()` for continuing from previously returned run state
* `stream()` and `continueStream()` for incremental, replayable results
* stop-condition helpers such as `stepCountIs()`, `maxCost()`, and `hasToolCall()`

## First agent

```typescript theme={null}
import {
  createAgent,
  createGatewayAgentClient,
  defineTool,
} from "@phaseo/agent-sdk";

const lookupDocs = defineTool({
  id: "lookup-docs",
  description: "Look up an internal docs page by slug.",
  parameters: {
    type: "object",
    properties: {
      slug: { type: "string" },
    },
    required: ["slug"],
    additionalProperties: false,
  },
  async execute(input: { slug: string }) {
    return {
      slug: input.slug,
      url: `https://phaseo.app/docs/v1/${input.slug}`,
    };
  },
});

const agent = createAgent({
  id: "support-docs-agent",
  model: "phaseo/free",
  instructions: "Use tools when helpful and finish with a concise answer.",
  tools: [lookupDocs],
});

const result = await agent.run({
  input: "Find the docs page for presets and explain when to use them.",
  client: createGatewayAgentClient({
    clientOptions: {
      apiKey: process.env.PHASEO_API_KEY!,
    },
  }),
});

console.log(result.output);
```

## Mental model

The runtime loop does four things:

1. sends the current message state to the model client
2. executes any returned local tool calls
3. appends tool results into the next turn
4. returns the updated run state after each completed step boundary

That gives your application a resumable loop without forcing you into a hosted orchestration product.

## Core primitives

### `createAgent()`

Use `createAgent()` to define:

* one stable `id`
* instructions
* one model or preset
* one small tool list
* optional output parsing
* optional human review rules
* optional retry and tool-execution controls

Keep the first agent narrow. One workflow and one or two tools is usually enough.

### `defineTool()`

Define local runtime tools with:

* `id`
* `description`
* optional JSON `parameters`
* optional `timeoutMs`
* `inputSchema` and `outputSchema` runtime validators
* `execute()`, `execute: false`, or human-in-the-loop callbacks
* `requireApproval`, `onError`, `nextTurnParams`, and progress events

```typescript theme={null}
const fetchTicket = defineTool({
  id: "fetch-ticket",
  description: "Load one internal support ticket.",
  parameters: {
    type: "object",
    properties: {
      ticketId: { type: "string" },
    },
    required: ["ticketId"],
    additionalProperties: false,
  },
  timeoutMs: 3_000,
  async execute(input: { ticketId: string }, context) {
    const response = await fetch(`https://internal.example/tickets/${input.ticketId}`, {
      signal: context.signal,
    });

    return await response.json();
  },
});
```

If a timeout fires, the runtime aborts `context.signal`, marks the run as `failed`, and rethrows the timeout error.

Schemas can be a function or any object exposing `parse()` or `safeParse()`. Invalid model arguments and invalid tool results fail before they cross the tool boundary.

## Approval, HITL, and manual tools

Gate a side-effecting tool per call:

```typescript theme={null}
const deploy = defineTool({
  id: "deploy",
  requireApproval: ({ environment }) => environment === "production",
  async execute(input: { environment: string }) {
    return deployRelease(input.environment);
  },
});
```

The run pauses with `run.pause.pendingToolCalls`. Resume by exact call ID so concurrent calls cannot be confused:

```typescript theme={null}
const resumed = await agent.continueRun({
  run: paused,
  client,
  approvals: [{ toolCallId: "call_deploy_42" }],
  rejections: [{ toolCallId: "call_delete_17", reason: "Not authorized" }],
});
```

Set `execute: false` for work performed by your application and provide its result through `toolOutputs`. For an interactive tool, return `null` from `onToolCalled`; after continuation, `onResponseReceived` can validate or transform the supplied human response.

## Progress-producing tools

An async generator can publish preliminary results and return one final result:

```typescript theme={null}
const indexRepository = defineTool({
  id: "index-repository",
  async *execute(input: { path: string }) {
    yield { phase: "scan" };
    yield { phase: "embed" };
    return { indexed: 248 };
  },
});
```

Progress appears as `tool.preliminary_result` events and in the step's `preliminaryResults`.

## Streaming results

`stream()` starts the same state machine with a streaming model client. Its consumers are replayable, so UI, telemetry, and persistence code can read concurrently:

```typescript theme={null}
const result = agent.stream({ input, client });

for await (const delta of result.getTextStream()) {
  process.stdout.write(delta);
}

const [text, completed] = await Promise.all([result.getText(), result.getResult()]);
```

Use `getReasoningStream()`, `getItemsStream()`, `getToolStream()`, or `getFullStream()` for more specific consumers. `cancel()` aborts the run.

### Render typed run items

`getItemsStream()` yields `AgentItem<TOutput>`, a discriminated union that is safe to switch on:

```typescript theme={null}
for await (const item of result.getItemsStream()) {
  switch (item.type) {
    case "message":
      renderAssistantMessage(item.content);
      break;
    case "reasoning":
      renderReasoning(item.text);
      break;
    case "tool_call":
      renderToolCall(item.toolCallId, item.name, item.input);
      break;
    case "tool_result":
      renderToolResult(item.toolCallId, item.output);
      break;
    case "error":
      renderError(item.message);
      break;
    case "output":
      renderFinalOutput(item.value);
      break;
  }
}
```

The same ordered item contract is available on `completed.items` after either `run()` or `stream()`. Provider output is normalized into message, reasoning, tool-call, tool-result, error, and final-output items. Provider-specific fields remain available through `rawProviderItem` on normalized provider items.

## Stop conditions and dynamic turns

Stop conditions compose as an array; the first matching condition records its reason and returns a `stopped` run:

```typescript theme={null}
import { maxCost, maxTokensUsed, stepCountIs } from "@phaseo/agent-sdk";

const agent = createAgent({
  id: "bounded-research",
  stopWhen: [stepCountIs(12), maxTokensUsed(40_000), maxCost(2)],
  model: ({ context }) => context.fast ? "phaseo/free" : "anthropic/claude-sonnet-4",
  instructions: ({ numberOfTurns }) => `Research turn ${numberOfTurns}`,
});
```

Tools can set application context with `context.setContext()` and override the immediately following turn with `nextTurnParams`.

### `createGatewayAgentClient()`

Use the gateway-backed adapter when model turns should execute through Phaseo Gateway.

It can carry gateway-native controls such as:

* `responseFormat`
* `plugins`
* `gatewayTools`
* `toolChoice`
* `webSearchOptions`
* `providerOptions`
* `promptCacheKey`
* `includeMeta`

That lets your app keep routing, search, structured outputs, and plugin defaults close to the model client instead of rebuilding raw request payloads on every run.

## Application-owned persistence

If your application needs resumability, persist the returned `AgentRunResult` directly or provide a `state` accessor with asynchronous `load(runId)` and `save(result)` methods. A continued run can then use `runId` without carrying the serialized record through every layer.

The SDK intentionally does not ship persistence adapters or a hosted state backend.

That means you can:

* keep one-shot runs entirely in-process
* serialize paused or incomplete runs into your own application records
* reload that saved run state and pass it back to `continueRun()` later

## Human review and continuation

Use `humanReview` when a run should checkpoint and wait for approval:

```typescript theme={null}
const agent = createAgent({
  id: "support-agent",
  humanReview: ({ response }) =>
    response.message.content.includes("needs approval")
      ? {
          reason: "approval_required",
          payload: { draft: response.message.content },
        }
      : null,
});
```

Continue with explicit human input:

```typescript theme={null}
const continued = await agent.continueRun({
  run: pausedResult,
  client,
  humanInput: "Approved. Continue and return the final answer.",
});
```

## Typed outputs

Use `parseOutput` when your app wants a typed final value:

```typescript theme={null}
const agent = createAgent<string, { summary: string }>({
  id: "summary-agent",
  parseOutput(text) {
    return JSON.parse(text) as { summary: string };
  },
});
```

For stricter model behavior, combine that with structured outputs on the gateway adapter:

```typescript theme={null}
const client = createGatewayAgentClient({
  clientOptions: {
    apiKey: process.env.PHASEO_API_KEY!,
  },
  responseFormat: {
    type: "json_schema",
    name: "agent_answer",
    schema: {
      type: "object",
      properties: {
        summary: { type: "string" },
      },
      required: ["summary"],
      additionalProperties: false,
    },
  },
  plugins: [{ id: "response-healing" }],
});
```

## Runtime controls

### Model retries

Use `modelRetry` when transient model failures should retry before the run is persisted as `failed`:

```typescript theme={null}
const agent = createAgent({
  id: "support-agent",
  modelRetry: {
    maxRetries: 2,
    backoffMs: 250,
  },
});
```

`maxRetries` counts extra attempts after the first model request.
The persisted step record stores the final retry count as `modelAttempts`.

### Concurrent local tools

If one model turn can safely call several independent tools, set `toolExecution.toolConcurrency`:

```typescript theme={null}
const agent = createAgent({
  id: "research-agent",
  toolExecution: {
    toolConcurrency: 3,
  },
  tools: [fetchDocs, fetchStatus, fetchIncidents],
});
```

The runtime still preserves tool-result message order.

### Preset-driven routing

Use `preset` when routing, prompt, or parameter defaults should stay managed in the dashboard instead of being hard-coded in app code:

```typescript theme={null}
const agent = createAgent({
  id: "support-triage-agent",
  preset: "support-triage",
});
```

## Event hooks

Use `onEvent` when your application wants lifecycle hooks for logs, telemetry, or internal workflows.

Current events include:

* `run.started`
* `run.resumed`
* `step.started`
* `step.completed`
* `step.failed`
* `step.cancelled`
* `model.requested`
* `model.completed`
* `model.failed`
* `tool.started`
* `tool.completed`
* `tool.failed`
* `checkpoint.saved`
* `run.waiting_for_human`
* `run.cancelled`
* `run.completed`
* `run.failed`

If one step succeeds, the runtime emits `step.completed` after the checkpointed step has been persisted.

## Error handling

Gateway failures are rethrown as `AgentGatewayError`:

```typescript theme={null}
import { AgentGatewayError } from "@phaseo/agent-sdk";

try {
  await agent.run({ input, client });
} catch (error) {
  if (error instanceof AgentGatewayError) {
    console.error(error.status, error.requestId, error.reason);
  }
  throw error;
}
```

If the failure came from the gateway, failed runs and steps also persist `errorDetails`.

## Included examples

The package currently ships these examples:

* `examples/research-brief-agent.ts`
* `examples/support-triage-agent.ts`
* `examples/coding-review-agent.ts`
* `examples/parallel-tool-agent.ts`

## Current scope

The SDK is intentionally focused on application-building primitives:

* local or app-owned checkpoint persistence
* gateway-backed model turns
* local tools
* resumable agent loops
* per-step normalized token usage, cost, warnings, finish reasons, and tool results

It does not try to be a hosted orchestration platform or ship one opinionated remote persistence backend.

## Related guides

* [Build a durable agent loop in TypeScript](https://phaseo.app/docs/v1/cookbook/agent-sdk-durable-loop.md)
* [Research with agent-backed web search](https://phaseo.app/docs/v1/cookbook/agent-sdk-research-brief.md)
* [Triage support with preset-driven agents](https://phaseo.app/docs/v1/cookbook/agent-sdk-support-triage.md)
* [Review code with local runtime tools](https://phaseo.app/docs/v1/cookbook/agent-sdk-coding-review.md)
* [Fan out local tools concurrently](https://phaseo.app/docs/v1/cookbook/agent-sdk-parallel-tools.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.