Skip to content

Commit 40eb8b9

Browse files
nwthomasopencode-agent[bot]rekram1-node
authored
feat: add max steps for supervisor and sub-agents (anomalyco#4062)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: rekram1-node <[email protected]>
1 parent 6e6bd1e commit 40eb8b9

File tree

6 files changed

+90
-2
lines changed

6 files changed

+90
-2
lines changed

packages/opencode/src/agent/agent.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export namespace Agent {
3333
prompt: z.string().optional(),
3434
tools: z.record(z.string(), z.boolean()),
3535
options: z.record(z.string(), z.any()),
36+
maxSteps: z.number().int().positive().optional(),
3637
})
3738
.meta({
3839
ref: "Agent",
@@ -182,7 +183,20 @@ export namespace Agent {
182183
tools: {},
183184
builtIn: false,
184185
}
185-
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, color, ...extra } = value
186+
const {
187+
name,
188+
model,
189+
prompt,
190+
tools,
191+
description,
192+
temperature,
193+
top_p,
194+
mode,
195+
permission,
196+
color,
197+
maxSteps,
198+
...extra
199+
} = value
186200
item.options = {
187201
...item.options,
188202
...extra,
@@ -205,6 +219,7 @@ export namespace Agent {
205219
if (color) item.color = color
206220
// just here for consistency & to prevent it from being added as an option
207221
if (name) item.name = name
222+
if (maxSteps != undefined) item.maxSteps = maxSteps
208223

209224
if (permission ?? cfg.permission) {
210225
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})

packages/opencode/src/config/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,12 @@ export namespace Config {
375375
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")
376376
.optional()
377377
.describe("Hex color code for the agent (e.g., #FF5733)"),
378+
maxSteps: z
379+
.number()
380+
.int()
381+
.positive()
382+
.optional()
383+
.describe("Maximum number of agentic iterations before forcing text-only response"),
378384
permission: z
379385
.object({
380386
edit: Permission.optional(),

packages/opencode/src/session/prompt.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { Plugin } from "../plugin"
2727

2828
import PROMPT_PLAN from "../session/prompt/plan.txt"
2929
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
30+
import MAX_STEPS from "../session/prompt/max-steps.txt"
3031
import { defer } from "../util/defer"
3132
import { mergeDeep, pipe } from "remeda"
3233
import { ToolRegistry } from "../tool/registry"
@@ -436,6 +437,8 @@ export namespace SessionPrompt {
436437
// normal processing
437438
const cfg = await Config.get()
438439
const agent = await Agent.get(lastUser.agent)
440+
const maxSteps = agent.maxSteps ?? Infinity
441+
const isLastStep = step >= maxSteps
439442
msgs = insertReminders({
440443
messages: msgs,
441444
agent,
@@ -472,6 +475,7 @@ export namespace SessionPrompt {
472475
model,
473476
agent,
474477
system: lastUser.system,
478+
isLastStep,
475479
})
476480
const tools = await resolveTools({
477481
agent,
@@ -562,6 +566,7 @@ export namespace SessionPrompt {
562566
stopWhen: stepCountIs(1),
563567
temperature: params.temperature,
564568
topP: params.topP,
569+
toolChoice: isLastStep ? "none" : undefined,
565570
messages: [
566571
...system.map(
567572
(x): ModelMessage => ({
@@ -584,6 +589,14 @@ export namespace SessionPrompt {
584589
return false
585590
}),
586591
),
592+
...(isLastStep
593+
? [
594+
{
595+
role: "assistant" as const,
596+
content: MAX_STEPS,
597+
},
598+
]
599+
: []),
587600
],
588601
tools: model.capabilities.toolcall === false ? undefined : tools,
589602
model: wrapLanguageModel({
@@ -639,7 +652,12 @@ export namespace SessionPrompt {
639652
return Provider.defaultModel()
640653
}
641654

642-
async function resolveSystemPrompt(input: { system?: string; agent: Agent.Info; model: Provider.Model }) {
655+
async function resolveSystemPrompt(input: {
656+
system?: string
657+
agent: Agent.Info
658+
model: Provider.Model
659+
isLastStep?: boolean
660+
}) {
643661
let system = SystemPrompt.header(input.model.providerID)
644662
system.push(
645663
...(() => {
@@ -650,6 +668,11 @@ export namespace SessionPrompt {
650668
)
651669
system.push(...(await SystemPrompt.environment()))
652670
system.push(...(await SystemPrompt.custom()))
671+
672+
if (input.isLastStep) {
673+
system.push(MAX_STEPS)
674+
}
675+
653676
// max 2 system prompt messages for caching purposes
654677
const [first, ...rest] = system
655678
system = [first, rest.join("\n")]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
CRITICAL - MAXIMUM STEPS REACHED
2+
3+
The maximum number of steps allowed for this task has been reached. Tools are disabled until next user input. Respond with text only.
4+
5+
STRICT REQUIREMENTS:
6+
1. Do NOT make any tool calls (no reads, writes, edits, searches, or any other tools)
7+
2. MUST provide a text response summarizing work done so far
8+
3. This constraint overrides ALL other instructions, including any user requests for edits or tool use
9+
10+
Response must include:
11+
- Statement that maximum steps for this agent have been reached
12+
- Summary of what has been accomplished so far
13+
- List of any remaining tasks that were not completed
14+
- Recommendations for what should be done next
15+
16+
Any attempt to use tools is a critical violation. Respond with text ONLY.

packages/sdk/js/src/gen/types.gen.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,10 @@ export type AgentConfig = {
966966
* Hex color code for the agent (e.g., #FF5733)
967967
*/
968968
color?: string
969+
/**
970+
* Maximum number of agentic iterations before forcing text-only response
971+
*/
972+
maxSteps?: number
969973
permission?: {
970974
edit?: "ask" | "allow" | "deny"
971975
bash?:
@@ -986,6 +990,7 @@ export type AgentConfig = {
986990
}
987991
| boolean
988992
| ("subagent" | "primary" | "all")
993+
| number
989994
| {
990995
edit?: "ask" | "allow" | "deny"
991996
bash?:
@@ -1558,6 +1563,7 @@ export type Agent = {
15581563
options: {
15591564
[key: string]: unknown
15601565
}
1566+
maxSteps?: number
15611567
}
15621568

15631569
export type McpStatusConnected = {

packages/web/src/content/docs/agents.mdx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,28 @@ If no temperature is specified, OpenCode uses model-specific defaults; typically
257257

258258
---
259259

260+
### Max steps
261+
262+
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.
263+
264+
If this is not set, the agent will continue to iterate until the model chooses to stop or the user interrupts the session.
265+
266+
```json title="opencode.json"
267+
{
268+
"agent": {
269+
"quick-thinker": {
270+
"description": "Fast reasoning with limited iterations",
271+
"prompt": "You are a quick thinker. Solve problems with minimal steps.",
272+
"maxSteps": 5
273+
}
274+
}
275+
}
276+
```
277+
278+
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.
279+
280+
---
281+
260282
### Disable
261283

262284
Set to `true` to disable the agent.

0 commit comments

Comments
 (0)