For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://modelgates.ai/docs/_mcp/server.
Next Turn Params
Why nextTurnParams?
Traditional tool execution returns results to the model, but sometimes you need more:
- Skills/Plugins: Load domain-specific instructions when a skill is activated
- Progressive Context: Build up context as tools are used
- Adaptive Behavior: Adjust model parameters based on tool results
- Clean Separation: Tools manage their own context requirements
With nextTurnParams, tools can modify any callModel parameter for the next turn.
Basic Example
import { tool } from '@modelgates/agent';import { z } from 'zod'; const expertModeTool = tool({ name: 'enable_expert_mode', description: 'Enable expert mode for detailed technical responses', inputSchema: z.object({ domain: z.string().describe('Technical domain (e.g., "kubernetes", "react")'), }), outputSchema: z.object({ enabled: z.boolean() }), nextTurnParams: { instructions: (params, context) => { const base = context.instructions ?? ''; return `${base} EXPERT MODE ENABLED for $:- Provide detailed technical explanations- Include code examples and best practices- Reference official documentation- Assume advanced knowledge`; }, temperature: () => 0.3, // More precise for technical content }, execute: async (params) => { return { enabled: true }; },});The Claude Code Skills Pattern
This example shows how to recreate Claude Code's skills system as a single encapsulated tool:
import { tool } from '@modelgates/agent';import { readFileSync } from 'fs';import { z } from 'zod'; const skillsTool = tool({ name: "skill", description: `Load a specialized skill to enhance the assistant's capabilities.Available skills: pdf-processing, data-analysis, code-review, etc.Each skill provides domain-specific instructions and capabilities.`, inputSchema: z.object({ type: z.string().describe("The skill type to load (e.g., 'pdf-processing')"), }), outputSchema: z.string(), // nextTurnParams runs after all tool calls execute, before responses go to model // Executed in order of tools array. This is where the magic happens. nextTurnParams: { input: (params, context) => { // Prevent duplicate skill loading if (JSON.stringify(context.input).includes(`Skill ${params.type} is already loaded`)) { return context.input; } // Load the skill's instructions from file system const skill = readFileSync( `~/.claude/skills/${params.type}/SKILL.md`, "utf-8" ); // Inject skill context into the conversation return [ ...context.input, { role: "user", content: `Base directory for this skill: ~/.claude/skills/${params.type}/ $`, }, ]; }, }, execute: async (params, context) => { // Check if already loaded if (JSON.stringify(context.input).includes(`Skill ${params.type} is already loaded`)) { return `Skill ${params.type} is already loaded`; } return `Launching skill ${params.type}`; },}); // Usage - the skill automatically enriches future turnsconst result = modelgates.callModel({ model: 'anthropic/claude-sonnet-4.5', input: 'Process this PDF and extract the key findings', tools: [skillsTool],});Key Benefits
- Encapsulation: Skill loading logic is entirely contained in the tool
- Idempotency: Built-in check prevents loading the same skill twice
- Clean API: Callers don't need to know about skill file locations
- Composability: Multiple skills can be loaded across turns
Execution Order
Understanding when nextTurnParams runs is crucial:
1. Model generates tool calls ↓2. All tool `execute` functions run ↓3. `nextTurnParams` functions run for each tool (in tools array order) ↓4. Modified parameters used for next model turn ↓5. Repeat until model stops calling toolsAvailable Context
nextTurnParams functions receive two arguments:
params
The validated input parameters that were passed to the tool:
nextTurnParams: { instructions: (params, context) => { // params is typed based on inputSchema console.log(params.type); // e.g., "pdf-processing" return `Handle ${params.type}`; },},context
The current request context, including:
| Property | Type | Description |
|---|---|---|
input | OpenResponsesInput | Current message history |
model | string | undefined | Current model selection |
models | string[] | undefined | Model fallback array |
instructions | string | undefined | Current system instructions |
temperature | number | undefined | Current temperature |
maxOutputTokens | number | undefined | Current max tokens |
topP | number | undefined | Current top-p sampling |
topK | number | undefined | Current top-k sampling |
Modifiable Parameters
You can modify CallModelInput parameters:
nextTurnParams: { // Modify message history input: (params, ctx) => [...ctx.input, newMessage], // Change model model: (params, ctx) => 'anthropic/claude-sonnet-4.5', // Update instructions instructions: (params, ctx) => `$\n\nNew context...`, // Adjust generation parameters temperature: (params, ctx) => 0.5, maxOutputTokens: (params, ctx) => 2000,},Patterns
Research Context Accumulation
Build up context as research progresses:
const researchTool = tool({ name: "research", inputSchema: z.object({ topic: z.string() }), outputSchema: z.object({ findings: z.array(z.string()) }), nextTurnParams: { instructions: (params, context) => { const base = context.instructions ?? ''; return `${base} Previous research on "$" found important context.Build upon these findings in your response.`; }, }, execute: async (params) => { const results = await searchDatabase(params.topic); return { findings: results }; },});Complexity-Based Model Selection
Upgrade to better models when needed:
const complexityAnalyzer = tool({ name: "analyze_complexity", inputSchema: z.object({ code: z.string() }), outputSchema: z.object({ complexity: z.enum(['low', 'medium', 'high']) }), nextTurnParams: { model: (params, context) => { // Upgrade to more capable model for complex code if (params.complexity === 'high') { return 'anthropic/claude-sonnet-4.5'; } return context.model ?? 'openai/gpt-5-nano'; }, temperature: (params, context) => { // Lower temperature for complex analysis return params.complexity === 'high' ? 0.3 : 0.7; }, }, execute: async (params) => { return analyzeCodeComplexity(params.code); },});Multi-Skill Loading
Load multiple skills at once:
const multiSkillLoader = tool({ name: 'load_skills', description: 'Load multiple skills at once', inputSchema: z.object({ skills: z.array(z.string()).describe('Array of skill names to load'), }), outputSchema: z.object({ loaded: z.array(z.string()), failed: z.array(z.object({ name: z.string(), reason: z.string() })), }), nextTurnParams: { input: (params, context) => { let newInput = context.input; for (const skillName of params.skills) { const skillPath = `~/.skills/${skillName}/SKILL.md`; if (!existsSync(skillPath)) continue; const skillMarker = `[Skill: ${skillName}]`; if (JSON.stringify(newInput).includes(skillMarker)) continue; const skillContent = readFileSync(skillPath, 'utf-8'); newInput = [ ...(Array.isArray(newInput) ? newInput : [newInput]), { role: 'user', content: `${skillMarker}\n${skillContent}` }, ]; } return newInput; }, }, execute: async ({ skills }) => { const loaded = []; const failed = []; for (const skill of skills) { if (existsSync(`~/.skills/${skill}/SKILL.md`)) { loaded.push(skill); } else { failed.push({ name: skill, reason: 'Not found' }); } } return { loaded, failed }; },});Language/Locale Switching
Adapt to user language preferences:
const languageTool = tool({ name: 'set_language', inputSchema: z.object({ language: z.enum(['en', 'es', 'fr', 'de', 'ja']), }), outputSchema: z.object({ set: z.boolean() }), nextTurnParams: { instructions: (params, context) => { const base = context.instructions ?? ''; const languageInstructions = { en: 'Respond in English.', es: 'Responde en español.', fr: 'Répondez en français.', de: 'Antworten Sie auf Deutsch.', ja: '日本語で回答してください。', }; return `${base}\n\n${languageInstructions[params.language]}`; }, }, execute: async (params) => ({ set: true }),});Best Practices
Idempotency Checks
Always check if context was already added:
nextTurnParams: { input: (params, context) => { const marker = `[Context: ${params.id}]`; // Don't add if already present if (JSON.stringify(context.input).includes(marker)) { return context.input; } return [...context.input, { role: 'user', content: `${marker}\n${newContent}`, }]; },},Type Safety
Use proper typing for context access:
nextTurnParams: { instructions: (params, context) => { // Safe access with fallback const base = context.instructions ?? 'You are a helpful assistant.'; return `${base}\n\nAdditional context: ${params.data}`; },},Minimal Modifications
Only modify what's necessary:
// Good: Minimal, targeted changenextTurnParams: { temperature: (params) => params.needsPrecision ? 0.2 : undefined,}, // Avoid: Unnecessary spreadingnextTurnParams: { temperature: (params, ctx) => { return params.needsPrecision ? 0.2 : ctx.temperature; },},See Also
- Skills Loader Example - Complete implementation
- Dynamic Parameters - Async parameter functions
- Stop Conditions - Execution control