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
}
| 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 |
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:
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.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 |
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
// 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.