Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use the Agent class for multi-step tool loops, conversation history, and reusable LLM agents. Use when building agents with skills, structured output, or stateful conversations.
.claude/skills/growthxai-output-dev-agent-class/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-13 | ✗→✓ | ▲ Improved | 14% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 81% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 29% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 96% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 96% | 0% |
The Agent class extends AI SDK's ToolLoopAgent with Output prompt files and the skills system. Use it when you need multi-step tool execution, conversation history, or a reusable agent instance. For single-shot LLM calls without tools, generateText is simpler.
Output.object()conversationStoreAgent and generateTexttypescriptimport { Agent, createMemoryConversationStore, skill, Output } from '@outputai/llm'; import { z } from '@outputai/core';
Agent, createMemoryConversationStore, skill, and Output all come from @outputai/llm. Import z from @outputai/core (never from zod directly).
The prompt file is loaded and rendered at construction time. Variables, skills, and tools are fixed at construction. The agent is ready to call generate() or stream() immediately.
typescriptconst agent = new Agent( { prompt: 'writing_assistant@v1', variables: { content_type: input.contentType, focus: input.focus, content: input.content }, skills: [ audienceSkill ], output: Output.object( { schema: reviewSchema } ), maxSteps: 5 } );
| Option | Type | Default | Description | |--------|------|---------|-------------| | prompt | string | (required) | Prompt file name (e.g. 'writing_assistant@v1') | | variables | Record<string, unknown> | {} | Template variables rendered at construction | | skills | Skill[] | [] | Skill packages for the LLM (see output-dev-skill-file) | | tools | ToolSet | {} | AI SDK tools available during the loop | | maxSteps | number | 10 | Maximum tool-loop iterations | | stopWhen | StopCondition | - | Custom stop condition (overrides maxSteps) | | output | Output | - | Structured output spec (e.g. Output.object({ schema })) | | conversationStore | ConversationStore | - | Pluggable store for multi-turn history | | temperature | number | - | Override prompt file temperature | | onStepFinish | Function | - | Callback after each tool-loop step | | prepareStep | Function | - | Customize each step before execution |
Run the agent and return when complete:
typescriptconst result = await agent.generate(); console.log( result.text ); // Generated text console.log( result.output ); // Structured output (when using Output.object) console.log( result.usage ); // Token counts
The result has the same shape as generateText: text, result (alias for text), output, usage, finishReason, toolCalls, etc.
Extend the conversation with extra messages:
typescriptconst result = await agent.generate( { messages: [ { role: 'user', content: 'Focus on the introduction section.' } ] } );
Messages are appended after the initial prompt messages (and any conversation store history).
Stream the agent's response:
typescriptconst stream = await agent.stream(); for await ( const chunk of stream.textStream ) { process.stdout.write( chunk ); }
Like streamText, the stream result provides textStream and fullStream iterables, plus promise-based properties (text, usage, finishReason) that resolve on completion.
Important: stream() does not automatically append messages to the conversation store. If you use streaming with a conversation store, persist messages manually.
Use Output.object() to get typed responses:
typescriptconst reviewSchema = z.object( { issues: z.array( z.string() ).describe( 'List of issues found' ), suggestions: z.array( z.string() ).describe( 'Actionable suggestions' ), score: z.number().describe( 'Quality score 0-100' ), summary: z.string().describe( 'Brief overall assessment' ) } ); const agent = new Agent( { prompt: 'writing_assistant@v1', variables: { content_type: 'documentation', focus: 'clarity', content: markdownContent }, output: Output.object( { schema: reviewSchema } ), maxSteps: 5 } ); const { output } = await agent.generate(); // output: { issues: string[], suggestions: string[], score: number, summary: string }
Use .describe() on schema fields instead of .min()/.max() for number constraints. Anthropic does not support minimum/maximum JSON Schema constraints in tool definitions.
By default, Agent is stateless. Each generate() call starts fresh with only the initial prompt messages. Pass a conversationStore to maintain history across calls:
typescriptimport { Agent, createMemoryConversationStore } from '@outputai/llm'; const store = createMemoryConversationStore(); const chatbot = new Agent( { prompt: 'chatbot@v1', conversationStore: store } ); const r1 = await chatbot.generate( { messages: [ { role: 'user', content: 'Hello, tell me about Output.' } ] } ); // r1.text: "Output is an AI framework for..." const r2 = await chatbot.generate( { messages: [ { role: 'user', content: 'How does it handle retries?' } ] } ); // r2 sees the full conversation history from r1
For production use, implement the ConversationStore interface with your database:
typescriptinterface ConversationStore { getMessages(): ModelMessage[] | Promise<ModelMessage[]>; addMessages(messages: ModelMessage[]): void | Promise<void>; }
createMemoryConversationStore() is the built-in in-memory implementation.
In workflow steps, construct a new Agent per invocation. Variables come from the step input:
typescriptimport { step, z } from '@outputai/core'; import { Agent, Output } from '@outputai/llm'; const reviewSchema = z.object( { summary: z.string().describe( 'Brief assessment' ), issues: z.array( z.string() ).describe( 'Problems found' ), suggestions: z.array( z.string() ).describe( 'Improvements' ), score: z.number().describe( 'Quality score 0-100' ) } ); export const reviewContent = step( { name: 'reviewContent', description: 'Review technical content using Agent with structured output', inputSchema: z.object( { content: z.string().describe( 'The content to review' ), content_type: z.string().describe( 'Type of content' ), focus: z.string().describe( 'Review focus areas' ) } ), outputSchema: reviewSchema, fn: async input => { const agent = new Agent( { prompt: 'writing_assistant@v1', variables: input, output: Output.object( { schema: reviewSchema } ), maxSteps: 5 } ); const { output } = await agent.generate(); return output; } } );
This is the standard pattern. Each step invocation is independent, and Agent construction is cheap.
Combine inline skills with Agent for dynamic expertise:
typescriptimport { Agent, skill, Output } from '@outputai/llm'; const audienceSkill = skill( { name: 'audience_adaptation', description: 'Tailor feedback for the specified expertise level', instructions: `# Audience Adaptation When the target audience is specified, adjust your feedback: **Beginner**: Flag jargon as high-priority issues. **Expert**: Focus on accuracy and completeness. Always mention the audience level in your summary.` } ); const agent = new Agent( { prompt: 'writing_assistant@v1', variables: input, skills: [ audienceSkill ], output: Output.object( { schema: reviewSchema } ), maxSteps: 5 } ); const { output } = await agent.generate();
Inline skills are merged with any file-based skills from the prompt's colocated skills/ directory or frontmatter paths. See output-dev-skill-file for the full skills guide.
| | generateText | Agent | |---|---|---| | Best for | Single-shot LLM calls | Multi-step tool loops | | Tools | Supported | Supported | | Skills | Supported | Supported | | Conversation history | Manual | Built-in with conversationStore | | Reusable instance | No (function call) | Yes (construct once, call many) | | Structured output | Output.object() | Output.object() |
Start with generateText. Move to Agent when you need conversation state or a reusable instance with a fixed configuration.
typescriptimport { generateText } from '@outputai/llm'; const { result } = await generateText( { prompt: 'generate_summary@v1', variables: { company_name: input.name, website_content: input.websiteContent } } );
Agent from @outputai/llm (not from ai directly)z from @outputai/core (never from zod)prompts/ folder{{ variable }} placeholders in the promptmaxSteps is set when using skills or tools (default 10)Output.object({ schema }) uses .describe() not .min()/.max() on numbersfn (not at module level) for workflow stepsoutput-dev-skill-file - Creating skill files for agentsoutput-dev-prompt-file - Creating .prompt files used by agentsoutput-dev-step-function - Using agents in step functionsoutput-dev-types-file - Defining Zod schemas for structured outputoutput-dev-workflow-function - Orchestrating agent-powered steps| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-13 | fail→pass | 17,382 | 5,462 | -69% | 1 | 1 | 0% | 3,043 | 3,467 | +14% | 0 | 0 | — |
case-19 | fail→pass | 11,005 | 4,142 | -62% | 1 | 1 | 0% | 1,837 | 3,330 | +81% | 0 | 0 | — |
case-02 | fail→pass | 16,090 | 8,391 | -48% | 1 | 1 | 0% | 3,255 | 4,213 | +29% | 0 | 0 | — |
case-01 | fail→pass | 12,965 | 11,740 | -9% | 1 | 1 | 0% | 2,455 | 4,817 | +96% | 0 | 0 | — |
case-03 | fail→pass | 12,106 | 8,849 | -27% | 1 | 1 | 0% | 2,161 | 4,236 | +96% | 0 | 0 | — |
case-04 | pass→fail | 7,849 | 6,399 | -18% | 1 | 1 | 0% | 1,265 | 3,673 | +190% | 0 | 0 | — |
case-05 | pass→pass | 10,025 | 4,128 | -59% | 1 | 1 | 0% | 1,914 | 3,274 | +71% | 0 | 0 | — |
case-06 | pass→pass | 4,787 | 3,210 | -33% | 1 | 1 | 0% | 873 | 3,134 | +259% | 0 | 0 | — |
case-07 | fail→pass | 7,323 | 5,527 | -25% | 1 | 1 | 0% | 1,176 | 3,592 | +205% | 0 | 0 | — |
case-08 | fail→pass | 11,623 | 5,208 | -55% | 1 | 1 | 0% | 2,036 | 3,452 | +70% | 0 | 0 | — |
case-09 | fail→pass | 12,833 | 6,647 | -48% | 1 | 1 | 0% | 2,412 | 3,851 | +60% | 0 | 0 | — |
case-10 | fail→pass | 6,030 | 5,651 | -6% | 1 | 1 | 0% | 1,119 | 3,530 | +215% | 0 | 0 | — |
case-11 | fail→pass | 15,915 | 5,200 | -67% | 1 | 1 | 0% | 2,966 | 3,562 | +20% | 0 | 0 | — |
case-12 | fail→pass | 9,376 | 3,950 | -58% | 1 | 1 | 0% | 1,572 | 3,121 | +99% | 0 | 0 | — |
case-14 | fail→pass | 18,276 | 13,529 | -26% | 1 | 1 | 0% | 3,657 | 4,893 | +34% | 0 | 0 | — |
case-15 | fail→pass | 10,303 | 5,722 | -44% | 1 | 1 | 0% | 1,895 | 3,568 | +88% | 0 | 0 | — |
case-16 | pass→pass | 9,991 | 2,968 | -70% | 1 | 1 | 0% | 1,673 | 3,055 | +83% | 0 | 0 | — |
case-17 | fail→pass | 10,518 | 6,690 | -36% | 1 | 1 | 0% | 2,001 | 3,917 | +96% | 0 | 0 | — |
case-18 | pass→pass | 19,241 | 9,534 | -50% | 1 | 1 | 0% | 3,271 | 4,284 | +31% | 0 | 0 | — |
case-20 | fail→pass | 12,072 | 6,899 | -43% | 1 | 1 | 0% | 2,145 | 3,693 | +72% | 0 | 0 | — |
case-21 | pass→pass | 10,572 | 3,969 | -62% | 1 | 1 | 0% | 1,894 | 3,296 | +74% | 0 | 0 | — |
case-22 | fail→pass | 12,913 | 4,665 | -64% | 1 | 1 | 0% | 2,399 | 3,397 | +42% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +68 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.