Agents
The core primitive — an LLM with instructions, tools, and guardrails
Agents
An Agent is the fundamental building block of the SDK. It represents an LLM configured with a specific role, a set of tools, guardrails, and optional memory. Agents are composable — they can call other agents, hand off to specialists, or run in parallel.
Creating an Agent
import { Agent } from 'assistme-agent-sdk'
import { claude } from 'assistme-agent-sdk-provider-claude'
const agent = new Agent({
name: 'assistant',
model: claude('claude-sonnet-4-6'),
instructions: 'You are a helpful assistant.',
})Agent Configuration
interface AgentConfig {
/** Unique name for identification and tracing */
name: string
/** The model to use (from any provider) */
model: ModelProvider
/** System instructions that define the agent's behavior */
instructions: string | ((context: RunContext) => string | Promise<string>)
/** Tools the agent can call */
tools?: Tool[]
/** Input and output guardrails */
guardrails?: {
input?: InputGuardrail[]
output?: OutputGuardrail[]
}
/** Persistent memory configuration */
memory?: MemoryConfig
/** Other agents this agent can hand off to */
handoffs?: Agent[]
/** Lifecycle hooks */
hooks?: AgentHooks
/** Model parameters */
modelParams?: {
temperature?: number
maxTokens?: number
topP?: number
stop?: string[]
}
/** Maximum tool-call turns before stopping */
maxTurns?: number
}Dynamic Instructions
Instructions can be static strings or dynamic functions that compute instructions based on runtime context:
const agent = new Agent({
name: 'support',
model: claude('claude-sonnet-4-6'),
instructions: async (context) => {
const user = await getUser(context.userId)
const plan = user.subscription.plan
return `You are a customer support agent for ${user.name}.
They are on the ${plan} plan.
${plan === 'enterprise' ? 'Provide priority support.' : 'Follow standard procedures.'}
Current time: ${new Date().toISOString()}`
},
})Agent Cloning
Create variations of an agent without redefining everything:
const baseAgent = new Agent({
name: 'base',
model: claude('claude-sonnet-4-6'),
instructions: 'You are a helpful assistant.',
tools: [webSearch, calculator],
})
// Clone with overrides
const creativeAgent = baseAgent.clone({
name: 'creative',
modelParams: { temperature: 0.9 },
})
const preciseAgent = baseAgent.clone({
name: 'precise',
modelParams: { temperature: 0.1 },
instructions: 'You are a precise, factual assistant. Always cite sources.',
})Agents as Tools
An agent can be used as a tool by another agent, enabling hierarchical composition:
const researcher = new Agent({
name: 'researcher',
model: claude('claude-sonnet-4-6'),
instructions: 'Research topics thoroughly using web search.',
tools: [webSearch],
})
const writer = new Agent({
name: 'writer',
model: claude('claude-sonnet-4-6'),
instructions: 'Write polished articles based on research.',
tools: [
// Use the researcher as a tool
researcher.asTool({
name: 'research',
description: 'Research a topic and return findings',
}),
],
})When an agent is used as a tool, it runs in its own isolated context. The parent agent only sees the final output, not the full tool-call chain — keeping the parent's context clean.
Structured Output
Force the agent to return data in a specific schema:
const classifier = new Agent({
name: 'classifier',
model: claude('claude-sonnet-4-6'),
instructions: 'Classify the sentiment of the given text.',
output: z.object({
sentiment: z.enum(['positive', 'negative', 'neutral']),
confidence: z.number().min(0).max(1),
reasoning: z.string(),
}),
})
const result = await Runner.run(classifier, {
messages: [{ role: 'user', content: 'I love this product!' }],
})
// result.output is typed: { sentiment: 'positive', confidence: 0.95, reasoning: '...' }Agent Lifecycle
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Created │────►│ Running │────►│ Completed │
└──────────┘ │ │ └──────────────┘
│ ┌────────┐ │
│ │ Tool │ │ ┌──────────────┐
│ │ Calls │ │────►│ Handoff │
│ └────────┘ │ └──────────────┘
│ │
│ ┌────────┐ │ ┌──────────────┐
│ │Guardrail│ │────►│ Blocked │
│ │Trigger │ │ └──────────────┘
│ └────────┘ │
└──────────────┘An agent run progresses through these states:
- Created — Agent is configured but not yet running
- Running — Agent is processing, may call tools in a loop
- Completed — Agent produced a final response
- Handoff — Agent transferred control to another agent
- Blocked — A guardrail prevented the agent from proceeding
Max Turns
Prevent runaway loops by setting a maximum number of turns:
const agent = new Agent({
name: 'bounded',
model: claude('claude-sonnet-4-6'),
instructions: 'Complete the task, but do not exceed the turn limit.',
tools: [webSearch],
maxTurns: 10, // Stop after 10 turns
})When maxTurns is exceeded, the runner returns with status: 'max_turns_reached' and the partial output.
Error Handling
const result = await Runner.run(agent, {
messages: [{ role: 'user', content: 'Hello' }],
})
switch (result.status) {
case 'completed':
console.log(result.output)
break
case 'max_turns_reached':
console.log('Agent hit turn limit:', result.output)
break
case 'guardrail_blocked':
console.log('Blocked by guardrail:', result.guardrailResult.reason)
break
case 'handoff':
console.log('Handed off to:', result.handoffTarget.name)
break
case 'error':
console.error('Agent error:', result.error)
break
}Best Practices
-
Keep instructions focused — An agent with clear, specific instructions outperforms one with vague, broad instructions. If an agent needs to do many different things, consider splitting into specialized agents with handoffs.
-
Limit tools per agent — 5-10 tools is the sweet spot. Too many tools degrade model performance. Use agent composition or tool search for large tool sets.
-
Use structured output for downstream processing — When an agent's output feeds into code, always use structured output with Zod schemas.
-
Set maxTurns — Always set a reasonable
maxTurnsin production to prevent runaway costs and infinite loops. -
Name your agents — Names appear in traces and logs. Clear names make debugging significantly easier.