# ctx.agent()

> Signature and result shapes for ctx.agent(name): generate and stream options, the fields Helix adds, the session interface, and every error.

`ctx.agent(name)` returns a client for an agent declared with `defineAgent` under `functions/_agents/`. `generate` runs the agent loop to completion or to an approval pause; `stream` runs it as a stream; `session` and `sessions` read conversation history.

:::note{title="Version"}
Needs `@trayai/helix-sdk` and `@trayai/helix-cli` newer than 1.6.0.
:::

## Signature

```typescript
agent(name: string): HelixAgent

interface HelixAgent {
  generate(options: AgentCallOptions): Promise<AgentResult>;
  stream(options: AgentCallOptions): Promise<AgentStreamResult>;

  // The agent's parts, as AI SDK values, for a loop of your own.
  readonly model: LanguageModel;      // primary model wrapped with its fallback chain
  readonly tools: ToolSet;            // compiled tools, including agent_<name> delegates
  readonly instructions: string;      // resolved system prompt

  session(sessionId?: string): AgentSession;
  sessions(params?: { first?: number; cursor?: string }): Promise<AgentSessionsList>;
}
```

`name` must match a loaded agent. An unknown name throws when `ctx.agent(name)` is called, naming the agents that are loaded.

## AgentCallOptions

```typescript
interface AgentCallOptions {
  prompt?: string;                 // one user message, or
  messages?: ModelMessage[];       // explicit messages for this turn
  sessionId?: string;              // continue a session; omit to create one
  approvals?: AgentApprovalDecision[];

  stopWhen?: StopCondition<ToolSet> | StopCondition<ToolSet>[];
  prepareStep?: PrepareStepFunction<ToolSet>;
  activeTools?: string[];
  toolChoice?: ToolChoice<ToolSet>;
  providerOptions?: Record<string, unknown>;
}

interface AgentApprovalDecision {
  id: string;        // from AgentPendingApproval.id
  approved: boolean;
  reason?: string;   // reaches the model
}
```

| Option | Notes |
|---|---|
| `prompt` / `messages` | One of the two for a new turn. `generate` also accepts `approvals` alone; `stream` requires one of these |
| `sessionId` | Any string. An id the store has not seen starts a new session under that id. An id that belongs to another user's session never reaches it: see [two scopes](/documentation/guides/agent-sessions/#two-scopes) |
| `approvals` | `generate` only, and on its own: no `prompt` or `messages` in the same call. Must answer every approval the session has pending. The approved tools run, the model continues, and the next call can carry new input |
| `stopWhen` | Replaces the agent's `limits` for this call. Default is `[stepCountIs(limits.maxIterations), totalToolCallsIs(limits.maxToolCalls)]` |
| `prepareStep`, `activeTools`, `toolChoice`, `providerOptions` | Passed to the AI SDK unchanged |

## AgentResult

The AI SDK `generateText` result (`text`, `steps`, `finishReason`, `usage`, `response`, and the rest) with four fields added:

```typescript
interface AgentResult extends GenerateTextResult {
  sessionId: string;
  status: 'completed' | 'awaiting-approval';
  pendingApprovals?: AgentPendingApproval[];   // present when status is 'awaiting-approval'
  helix: {
    cost: { amount: number; currency: 'USD' };
    model: string;                              // model that produced the final answer
  };
}

interface AgentPendingApproval {
  id: string;          // echo back as approvals[].id
  toolName: string;
  toolCallId: string;
  input: unknown;      // validated input the tool would run with
}
```

`helix.cost` sums every step of this agent's own loop, each priced at the rate of the model that answered that step, so a run partly served by a fallback is priced correctly. An unrecognized model prices to `0`. Runs the agent delegated to are **not** included: a delegate's model calls are its own run, and only its final text comes back to the parent.

`status` reports one thing: whether the run is waiting on an approval. A run that stopped because it hit `limits.maxIterations` or `limits.maxToolCalls` still reports `'completed'`. Check `finishReason` and `steps.length` to tell a finished answer from a run that ran out of steps.

## AgentStreamResult

```typescript
type AgentStreamResult = StreamTextResult & { sessionId: string };
```

The AI SDK `streamText` result: call `.toUIMessageStreamResponse()`, `.toTextStreamResponse()`, or read `.textStream`. `stream` resolves after prior history has been loaded, before the first token. The turn is written to the session when the stream finishes. That write happens after your code has returned the stream, so a failure there cannot reach you as an error: it is logged, and the turn is not saved. A stream that is aborted, for example because the client disconnected, is not saved either. In both cases the next call continues from the history before that turn. If a tool needing approval fires mid-stream, the stream ends and the pause is recorded in the session; read `session(sessionId).pendingApprovals()` and resume with `generate`.

## AgentSession

```typescript
interface AgentSession {
  readonly id: string;
  messages(opts?: { limit?: number }): Promise<ModelMessage[]>;   // oldest first; limit keeps the last N
  page(opts?: AgentSessionPageOptions): Promise<AgentSessionPage>; // one page of a long conversation
  append(...messages: ModelMessage[]): Promise<void>;
  clear(): Promise<void>;                                         // delete every message and pending approval
  truncated(): Promise<boolean>;                                  // has the store dropped older messages?
  pendingApprovals(): Promise<PendingApproval[]>;
  addPendingApprovals(approvals: PendingApproval[]): Promise<void>;
  resolveApproval(approvalId: string): Promise<'resolved' | 'already-resolved'>;
}

interface AgentSessionPageOptions {
  limit?: number;            // 1 to 500, default 50
  cursor?: string;           // nextCursor of the previous page
  order?: 'asc' | 'desc';    // default 'asc'; 'desc' starts at the latest message
}

interface AgentSessionPage {
  messages: ModelMessage[];  // in the requested order
  nextCursor: string | null; // null at the end
}

interface PendingApproval {
  approvalId: string;    // the same value as AgentPendingApproval.id
  toolName: string;
  toolCallId: string;
  input: unknown;
  requestedAt: string;   // ISO 8601
}
```

`session(sessionId)` opens without running the agent. `session()` with no id mints a new one. `messages()` and `page()` return the stored conversation, which is all of it: the model is sent a window of recent messages, see [agent sessions](/documentation/guides/agent-sessions/). `clear()` is permanent and returns once everything is deleted. `append`, `addPendingApprovals`, and `resolveApproval` are what the runtime itself uses; calling them from your code is supported but bypasses the loop's bookkeeping.

## sessions()

```typescript
interface AgentSessionsList {
  sessions: AgentSessionSummary[];   // most recent activity first
  nextCursor: string | null;
}

interface AgentSessionSummary {
  id: string;
  createdAt: number;        // epoch milliseconds
  lastActivityAt: number;
  messageCount: number;
  pendingApprovals: number;
}
```

Only for agents with `memory.scope: 'user'`, and only the calling user's sessions. `first` defaults to 20.

## Errors

Errors carry a `name` that says what kind they are. The classes are not exported from `@trayai/helix-sdk`, so match on `error.name` rather than `instanceof`:

| `error.name` | Meaning |
|---|---|
| `AgentCallerError` | The call was wrong: a bad option, an unknown agent or approval id, a missing user. The message says what to fix |
| `SessionExpiredError` | The session expired with approvals still pending. The message contains `session-expired` |
| `FallbackExhaustedError` | Every model in the agent's fallback chain failed. `errors` holds each attempt's error, primary first |
| `SdkError` | A platform failure. `detail.kind` identifies it |

```typescript
try {
  return await ctx.agent('support-agent').generate({ prompt: ctx.input.message });
} catch (error) {
  if (error instanceof Error && error.name === 'AgentCallerError') {
    throw ctx.error(400, error.message);
  }
  throw error;
}
```

| Condition | What is thrown |
|---|---|
| `ctx.agent(name)` for a name no loaded agent has | `AgentCallerError` listing the loaded agent names |
| `generate` with no `prompt`, `messages`, or `approvals` | `AgentCallerError`: nothing to do |
| `stream` with `approvals` | `AgentCallerError`: resume paused runs with `generate()` |
| `approvals` naming an id never requested in the session | `AgentCallerError` naming the unknown ids |
| `approvals` sent together with `prompt` or `messages` | `AgentCallerError`: send approvals on their own |
| `approvals` that leave some of the session's pending requests unanswered | `AgentCallerError` naming the unanswered ids |
| `approvals` for a session with no history and nothing pending | `AgentCallerError`: the session does not exist, has expired, or is another user's |
| A user-scoped agent invoked where `userId` on `ctx` is null | `AgentCallerError`: user-scoped agents need a user execution |
| `page()` with a `limit` outside 1 to 500, an unknown `order`, or a cursor that is not from a previous page | `AgentCallerError` naming the option |
| `sessions()` on a project-scoped agent | `AgentCallerError`: the agent must declare `memory.scope: "user"` |
| `sessions()` with the DynamoDB session store | `AgentCallerError`: not supported on that store |
| On the platform store, writing to a session id that belongs to another user | `AgentCallerError`: not found, the same answer as a session that does not exist |
| Session expired with approvals still pending | `SessionExpiredError` naming the session |
| Every model in the fallback chain failed | `FallbackExhaustedError`, with each attempt's error in `errors` |
| A delegate under `agents` holds a `needsApproval` tool | A plain `Error` at first run of the parent |
| `agentSessions.auth` names an alias not in `authentications` | `SdkError.unknownAlias` |
| The session store is unreachable or rejects a write | `SdkError.agentSessionsFailed` (`agent_sessions_failed`) |
| Tool input fails its schema on a direct call | `SdkError.inputValidationFailed` |

An agent with no `fallbacks` has a chain of one, so a model or provider error surfaces as the AI SDK throws it.

## Availability

| | `defineFunction` | `defineSchedule` |
|---|:---:|:---:|
| `ctx.agent(name)` | ✓ | ✓ |
| Agents with `memory.scope: 'user'` | ✓ | ✗ (no user identity) |

## Example

```typescript
// functions/api/v1/support.post.ts
import { z } from 'zod';
import { defineFunction } from '@trayai/helix-sdk';

export const input = z.object({
  sessionId: z.string().optional(),
  // A turn is either a new message or a set of approval decisions, never both.
  message: z.string().min(1).optional(),
  approvals: z.array(z.object({ id: z.string(), approved: z.boolean(), reason: z.string().optional() })).optional(),
});

export default defineFunction<typeof input>(async (ctx) => {
  const result = await ctx.agent('support-agent').generate(
    ctx.input.approvals
      ? { sessionId: ctx.input.sessionId, approvals: ctx.input.approvals }
      : { sessionId: ctx.input.sessionId, prompt: ctx.input.message ?? '' },
  );

  ctx.log.info('agent run', { steps: result.steps.length, cost: result.helix.cost.amount });

  return {
    sessionId: result.sessionId,
    status: result.status,
    text: result.text,
    pendingApprovals: result.pendingApprovals ?? [],
  };
});
```

Usage, patterns, approvals, and sessions are covered in the [agents guides](/documentation/guides/agents-overview/). Definition shapes are in the [agent definition reference](/documentation/reference/agents/).

---

Canonical: https://helix.tray.ai/documentation/reference/context/agent/
Any link on this page is available as markdown by appending .md to its URL.
Full corpus: https://helix.tray.ai/documentation/llms-full.txt