From b39cefe14ee529a0fcc049f6917c3319f9d04c39 Mon Sep 17 00:00:00 2001 From: Abhi <43648792+abhipatel12@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:28:00 -0500 Subject: [PATCH] feat(core): add default execution limits for subagents (#18274) --- docs/core/subagents.md | 4 +-- packages/core/src/agents/agentLoader.test.ts | 4 ++- packages/core/src/agents/agentLoader.ts | 10 +++++--- packages/core/src/agents/local-executor.ts | 26 ++++++++++++-------- packages/core/src/agents/types.ts | 22 ++++++++++++++--- 5 files changed, 47 insertions(+), 19 deletions(-) diff --git a/docs/core/subagents.md b/docs/core/subagents.md index 41cbbfbdb3..1725d4a0f5 100644 --- a/docs/core/subagents.md +++ b/docs/core/subagents.md @@ -146,8 +146,8 @@ it yourself; just report it. | `tools` | array | No | List of tool names this agent can use. If omitted, it may have access to a default set. | | `model` | string | No | Specific model to use (e.g., `gemini-2.5-pro`). Defaults to `inherit` (uses the main session model). | | `temperature` | number | No | Model temperature (0.0 - 2.0). | -| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. | -| `timeout_mins` | number | No | Maximum execution time in minutes. | +| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. Defaults to `15`. | +| `timeout_mins` | number | No | Maximum execution time in minutes. Defaults to `5`. | ### Optimizing your sub-agent diff --git a/packages/core/src/agents/agentLoader.test.ts b/packages/core/src/agents/agentLoader.test.ts index 7391161542..3649558b64 100644 --- a/packages/core/src/agents/agentLoader.test.ts +++ b/packages/core/src/agents/agentLoader.test.ts @@ -16,6 +16,7 @@ import { } from './agentLoader.js'; import { GEMINI_MODEL_ALIAS_PRO } from '../config/models.js'; import type { LocalAgentDefinition } from './types.js'; +import { DEFAULT_MAX_TIME_MINUTES, DEFAULT_MAX_TURNS } from './types.js'; describe('loader', () => { let tempDir: string; @@ -237,7 +238,8 @@ Body`); }, }, runConfig: { - maxTimeMinutes: 5, + maxTimeMinutes: DEFAULT_MAX_TIME_MINUTES, + maxTurns: DEFAULT_MAX_TURNS, }, inputConfig: { inputSchema: { diff --git a/packages/core/src/agents/agentLoader.ts b/packages/core/src/agents/agentLoader.ts index 1679b52fb3..d5478ddb6b 100644 --- a/packages/core/src/agents/agentLoader.ts +++ b/packages/core/src/agents/agentLoader.ts @@ -10,7 +10,11 @@ import { type Dirent } from 'node:fs'; import * as path from 'node:path'; import * as crypto from 'node:crypto'; import { z } from 'zod'; -import type { AgentDefinition } from './types.js'; +import { + type AgentDefinition, + DEFAULT_MAX_TURNS, + DEFAULT_MAX_TIME_MINUTES, +} from './types.js'; import { isValidToolName } from '../tools/tool-names.js'; import { FRONTMATTER_REGEX } from '../skills/skillLoader.js'; import { getErrorMessage } from '../utils/errors.js'; @@ -290,8 +294,8 @@ export function markdownToAgentDefinition( }, }, runConfig: { - maxTurns: markdown.max_turns, - maxTimeMinutes: markdown.timeout_mins || 5, + maxTurns: markdown.max_turns ?? DEFAULT_MAX_TURNS, + maxTimeMinutes: markdown.timeout_mins ?? DEFAULT_MAX_TIME_MINUTES, }, toolConfig: markdown.tools ? { diff --git a/packages/core/src/agents/local-executor.ts b/packages/core/src/agents/local-executor.ts index 95f3ab74c8..d384db4b99 100644 --- a/packages/core/src/agents/local-executor.ts +++ b/packages/core/src/agents/local-executor.ts @@ -41,7 +41,12 @@ import type { OutputObject, SubagentActivityEvent, } from './types.js'; -import { AgentTerminateMode, DEFAULT_QUERY_STRING } from './types.js'; +import { + AgentTerminateMode, + DEFAULT_QUERY_STRING, + DEFAULT_MAX_TURNS, + DEFAULT_MAX_TIME_MINUTES, +} from './types.js'; import { templateString } from './utils.js'; import { DEFAULT_GEMINI_MODEL, isAutoModel } from '../config/models.js'; import type { RoutingContext } from '../routing/routingStrategy.js'; @@ -406,7 +411,10 @@ export class LocalAgentExecutor { let terminateReason: AgentTerminateMode = AgentTerminateMode.ERROR; let finalResult: string | null = null; - const { maxTimeMinutes } = this.definition.runConfig; + const maxTimeMinutes = + this.definition.runConfig.maxTimeMinutes ?? DEFAULT_MAX_TIME_MINUTES; + const maxTurns = this.definition.runConfig.maxTurns ?? DEFAULT_MAX_TURNS; + const timeoutController = new AbortController(); const timeoutId = setTimeout( () => timeoutController.abort(new Error('Agent timed out.')), @@ -441,7 +449,7 @@ export class LocalAgentExecutor { while (true) { // Check for termination conditions like max turns. - const reason = this.checkTermination(startTime, turnCounter); + const reason = this.checkTermination(turnCounter, maxTurns); if (reason) { terminateReason = reason; break; @@ -499,13 +507,13 @@ export class LocalAgentExecutor { } else { // Recovery Failed. Set the final error message based on the *original* reason. if (terminateReason === AgentTerminateMode.TIMEOUT) { - finalResult = `Agent timed out after ${this.definition.runConfig.maxTimeMinutes} minutes.`; + finalResult = `Agent timed out after ${maxTimeMinutes} minutes.`; this.emitActivity('ERROR', { error: finalResult, context: 'timeout', }); } else if (terminateReason === AgentTerminateMode.MAX_TURNS) { - finalResult = `Agent reached max turns limit (${this.definition.runConfig.maxTurns}).`; + finalResult = `Agent reached max turns limit (${maxTurns}).`; this.emitActivity('ERROR', { error: finalResult, context: 'max_turns', @@ -569,7 +577,7 @@ export class LocalAgentExecutor { } // Recovery failed or wasn't possible - finalResult = `Agent timed out after ${this.definition.runConfig.maxTimeMinutes} minutes.`; + finalResult = `Agent timed out after ${maxTimeMinutes} minutes.`; this.emitActivity('ERROR', { error: finalResult, context: 'timeout', @@ -1160,12 +1168,10 @@ Important Rules: * @returns The reason for termination, or `null` if execution can continue. */ private checkTermination( - startTime: number, turnCounter: number, + maxTurns: number, ): AgentTerminateMode | null { - const { runConfig } = this.definition; - - if (runConfig.maxTurns && turnCounter >= runConfig.maxTurns) { + if (turnCounter >= maxTurns) { return AgentTerminateMode.MAX_TURNS; } diff --git a/packages/core/src/agents/types.ts b/packages/core/src/agents/types.ts index 337a837ea7..b9994d8b4a 100644 --- a/packages/core/src/agents/types.ts +++ b/packages/core/src/agents/types.ts @@ -40,6 +40,16 @@ export interface OutputObject { */ export const DEFAULT_QUERY_STRING = 'Get Started!'; +/** + * The default maximum number of conversational turns for an agent. + */ +export const DEFAULT_MAX_TURNS = 15; + +/** + * The default maximum execution time for an agent in minutes. + */ +export const DEFAULT_MAX_TIME_MINUTES = 5; + /** * Represents the validated input parameters passed to an agent upon invocation. * Used primarily for templating the system prompt. (Replaces ContextState) @@ -183,8 +193,14 @@ export interface OutputConfig { * Configures the execution environment and constraints for the agent. */ export interface RunConfig { - /** The maximum execution time for the agent in minutes. */ - maxTimeMinutes: number; - /** The maximum number of conversational turns. */ + /** + * The maximum execution time for the agent in minutes. + * If not specified, defaults to DEFAULT_MAX_TIME_MINUTES (5). + */ + maxTimeMinutes?: number; + /** + * The maximum number of conversational turns. + * If not specified, defaults to DEFAULT_MAX_TURNS (15). + */ maxTurns?: number; }