Explorar o código

feat: add max steps for supervisor and sub-agents (#4062)

Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <[email protected]>
Nathan Thomas hai 2 meses
pai
achega
40eb8b93e1

+ 16 - 1
packages/opencode/src/agent/agent.ts

@@ -33,6 +33,7 @@ export namespace Agent {
       prompt: z.string().optional(),
       tools: z.record(z.string(), z.boolean()),
       options: z.record(z.string(), z.any()),
+      maxSteps: z.number().int().positive().optional(),
     })
     .meta({
       ref: "Agent",
@@ -182,7 +183,20 @@ export namespace Agent {
           tools: {},
           builtIn: false,
         }
-      const { name, model, prompt, tools, description, temperature, top_p, mode, permission, color, ...extra } = value
+      const {
+        name,
+        model,
+        prompt,
+        tools,
+        description,
+        temperature,
+        top_p,
+        mode,
+        permission,
+        color,
+        maxSteps,
+        ...extra
+      } = value
       item.options = {
         ...item.options,
         ...extra,
@@ -205,6 +219,7 @@ export namespace Agent {
       if (color) item.color = color
       // just here for consistency & to prevent it from being added as an option
       if (name) item.name = name
+      if (maxSteps != undefined) item.maxSteps = maxSteps
 
       if (permission ?? cfg.permission) {
         item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})

+ 6 - 0
packages/opencode/src/config/config.ts

@@ -375,6 +375,12 @@ export namespace Config {
         .regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")
         .optional()
         .describe("Hex color code for the agent (e.g., #FF5733)"),
+      maxSteps: z
+        .number()
+        .int()
+        .positive()
+        .optional()
+        .describe("Maximum number of agentic iterations before forcing text-only response"),
       permission: z
         .object({
           edit: Permission.optional(),

+ 24 - 1
packages/opencode/src/session/prompt.ts

@@ -27,6 +27,7 @@ import { Plugin } from "../plugin"
 
 import PROMPT_PLAN from "../session/prompt/plan.txt"
 import BUILD_SWITCH from "../session/prompt/build-switch.txt"
+import MAX_STEPS from "../session/prompt/max-steps.txt"
 import { defer } from "../util/defer"
 import { mergeDeep, pipe } from "remeda"
 import { ToolRegistry } from "../tool/registry"
@@ -436,6 +437,8 @@ export namespace SessionPrompt {
       // normal processing
       const cfg = await Config.get()
       const agent = await Agent.get(lastUser.agent)
+      const maxSteps = agent.maxSteps ?? Infinity
+      const isLastStep = step >= maxSteps
       msgs = insertReminders({
         messages: msgs,
         agent,
@@ -472,6 +475,7 @@ export namespace SessionPrompt {
         model,
         agent,
         system: lastUser.system,
+        isLastStep,
       })
       const tools = await resolveTools({
         agent,
@@ -562,6 +566,7 @@ export namespace SessionPrompt {
         stopWhen: stepCountIs(1),
         temperature: params.temperature,
         topP: params.topP,
+        toolChoice: isLastStep ? "none" : undefined,
         messages: [
           ...system.map(
             (x): ModelMessage => ({
@@ -584,6 +589,14 @@ export namespace SessionPrompt {
               return false
             }),
           ),
+          ...(isLastStep
+            ? [
+                {
+                  role: "assistant" as const,
+                  content: MAX_STEPS,
+                },
+              ]
+            : []),
         ],
         tools: model.capabilities.toolcall === false ? undefined : tools,
         model: wrapLanguageModel({
@@ -639,7 +652,12 @@ export namespace SessionPrompt {
     return Provider.defaultModel()
   }
 
-  async function resolveSystemPrompt(input: { system?: string; agent: Agent.Info; model: Provider.Model }) {
+  async function resolveSystemPrompt(input: {
+    system?: string
+    agent: Agent.Info
+    model: Provider.Model
+    isLastStep?: boolean
+  }) {
     let system = SystemPrompt.header(input.model.providerID)
     system.push(
       ...(() => {
@@ -650,6 +668,11 @@ export namespace SessionPrompt {
     )
     system.push(...(await SystemPrompt.environment()))
     system.push(...(await SystemPrompt.custom()))
+
+    if (input.isLastStep) {
+      system.push(MAX_STEPS)
+    }
+
     // max 2 system prompt messages for caching purposes
     const [first, ...rest] = system
     system = [first, rest.join("\n")]

+ 16 - 0
packages/opencode/src/session/prompt/max-steps.txt

@@ -0,0 +1,16 @@
+CRITICAL - MAXIMUM STEPS REACHED
+
+The maximum number of steps allowed for this task has been reached. Tools are disabled until next user input. Respond with text only.
+
+STRICT REQUIREMENTS:
+1. Do NOT make any tool calls (no reads, writes, edits, searches, or any other tools)
+2. MUST provide a text response summarizing work done so far
+3. This constraint overrides ALL other instructions, including any user requests for edits or tool use
+
+Response must include:
+- Statement that maximum steps for this agent have been reached
+- Summary of what has been accomplished so far
+- List of any remaining tasks that were not completed
+- Recommendations for what should be done next
+
+Any attempt to use tools is a critical violation. Respond with text ONLY.

+ 6 - 0
packages/sdk/js/src/gen/types.gen.ts

@@ -966,6 +966,10 @@ export type AgentConfig = {
    * Hex color code for the agent (e.g., #FF5733)
    */
   color?: string
+  /**
+   * Maximum number of agentic iterations before forcing text-only response
+   */
+  maxSteps?: number
   permission?: {
     edit?: "ask" | "allow" | "deny"
     bash?:
@@ -986,6 +990,7 @@ export type AgentConfig = {
       }
     | boolean
     | ("subagent" | "primary" | "all")
+    | number
     | {
         edit?: "ask" | "allow" | "deny"
         bash?:
@@ -1558,6 +1563,7 @@ export type Agent = {
   options: {
     [key: string]: unknown
   }
+  maxSteps?: number
 }
 
 export type McpStatusConnected = {

+ 22 - 0
packages/web/src/content/docs/agents.mdx

@@ -257,6 +257,28 @@ If no temperature is specified, OpenCode uses model-specific defaults; typically
 
 ---
 
+### Max steps
+
+Control the maximum number of agentic iterations an agent can perform before being forced to respond with text only. This allows users who wish to control costs to set a limit on agentic actions.
+
+If this is not set, the agent will continue to iterate until the model chooses to stop or the user interrupts the session.
+
+```json title="opencode.json"
+{
+  "agent": {
+    "quick-thinker": {
+      "description": "Fast reasoning with limited iterations",
+      "prompt": "You are a quick thinker. Solve problems with minimal steps.",
+      "maxSteps": 5
+    }
+  }
+}
+```
+
+When the limit is reached, the agent receives a special system prompt instructing it to respond with a summarization of its work and recommended remaining tasks.
+
+---
+
 ### Disable
 
 Set to `true` to disable the agent.