Skip to content
Helix Docs

ctx.agent()

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

For developers Updated Oct 20, 2026
View as Markdown

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.

Signature

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

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
}
OptionNotes
prompt / messagesOne of the two for a new turn. generate also accepts approvals alone; stream requires one of these
sessionIdAny 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
approvalsgenerate 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
stopWhenReplaces the agent’s limits for this call. Default is [stepCountIs(limits.maxIterations), totalToolCallsIs(limits.maxToolCalls)]
prepareStep, activeTools, toolChoice, providerOptionsPassed to the AI SDK unchanged

AgentResult

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

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

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

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. 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()

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.nameMeaning
AgentCallerErrorThe call was wrong: a bad option, an unknown agent or approval id, a missing user. The message says what to fix
SessionExpiredErrorThe session expired with approvals still pending. The message contains session-expired
FallbackExhaustedErrorEvery model in the agent’s fallback chain failed. errors holds each attempt’s error, primary first
SdkErrorA platform failure. detail.kind identifies it
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;
}
ConditionWhat is thrown
ctx.agent(name) for a name no loaded agent hasAgentCallerError listing the loaded agent names
generate with no prompt, messages, or approvalsAgentCallerError: nothing to do
stream with approvalsAgentCallerError: resume paused runs with generate()
approvals naming an id never requested in the sessionAgentCallerError naming the unknown ids
approvals sent together with prompt or messagesAgentCallerError: send approvals on their own
approvals that leave some of the session’s pending requests unansweredAgentCallerError naming the unanswered ids
approvals for a session with no history and nothing pendingAgentCallerError: the session does not exist, has expired, or is another user’s
A user-scoped agent invoked where userId on ctx is nullAgentCallerError: 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 pageAgentCallerError naming the option
sessions() on a project-scoped agentAgentCallerError: the agent must declare memory.scope: "user"
sessions() with the DynamoDB session storeAgentCallerError: not supported on that store
On the platform store, writing to a session id that belongs to another userAgentCallerError: not found, the same answer as a session that does not exist
Session expired with approvals still pendingSessionExpiredError naming the session
Every model in the fallback chain failedFallbackExhaustedError, with each attempt’s error in errors
A delegate under agents holds a needsApproval toolA plain Error at first run of the parent
agentSessions.auth names an alias not in authenticationsSdkError.unknownAlias
The session store is unreachable or rejects a writeSdkError.agentSessionsFailed (agent_sessions_failed)
Tool input fails its schema on a direct callSdkError.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

defineFunctiondefineSchedule
ctx.agent(name)✓✓
Agents with memory.scope: 'user'✓✗ (no user identity)

Example

// 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. Definition shapes are in the agent definition reference.

Loading search…

Jump to a section

tab to move · esc to close