Skip to content

Commit 25a17a2

Browse files
committed
feat: add max steps for supervisor and sub-agents
We want a way to limit the number of steps allow agents in the app can take. This is primarily useful for sub-agents, but it applies to the supervisor as well. The user can define a single maxSteps value or optionally define a maxSteps value for each sub-agent.
1 parent 1747979 commit 25a17a2

File tree

4 files changed

+62
-1
lines changed

4 files changed

+62
-1
lines changed

packages/opencode/src/agent/agent.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export namespace Agent {
1616
builtIn: z.boolean(),
1717
topP: z.number().optional(),
1818
temperature: z.number().optional(),
19+
maxSteps: z.number().int().positive().optional(),
1920
permission: z.object({
2021
edit: Config.Permission,
2122
bash: z.record(z.string(), Config.Permission),
@@ -39,6 +40,8 @@ export namespace Agent {
3940
const state = Instance.state(async () => {
4041
const cfg = await Config.get()
4142
const defaultTools = cfg.tools ?? {}
43+
const DEFAULT_MAX_STEPS = 25
44+
const globalMaxSteps = cfg.experimental?.maxSteps ?? DEFAULT_MAX_STEPS
4245
const defaultPermission: Info["permission"] = {
4346
edit: "allow",
4447
bash: {
@@ -108,6 +111,7 @@ export namespace Agent {
108111
permission: agentPermission,
109112
mode: "subagent",
110113
builtIn: true,
114+
maxSteps: globalMaxSteps,
111115
},
112116
build: {
113117
name: "build",
@@ -116,6 +120,7 @@ export namespace Agent {
116120
permission: agentPermission,
117121
mode: "primary",
118122
builtIn: true,
123+
maxSteps: globalMaxSteps,
119124
},
120125
plan: {
121126
name: "plan",
@@ -126,6 +131,7 @@ export namespace Agent {
126131
},
127132
mode: "primary",
128133
builtIn: true,
134+
maxSteps: globalMaxSteps,
129135
},
130136
}
131137
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
@@ -142,8 +148,9 @@ export namespace Agent {
142148
options: {},
143149
tools: {},
144150
builtIn: false,
151+
maxSteps: globalMaxSteps,
145152
}
146-
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
153+
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, maxSteps, ...extra } = value
147154
item.options = {
148155
...item.options,
149156
...extra,
@@ -163,6 +170,7 @@ export namespace Agent {
163170
if (temperature != undefined) item.temperature = temperature
164171
if (top_p != undefined) item.topP = top_p
165172
if (mode) item.mode = mode
173+
if (maxSteps != undefined) item.maxSteps = maxSteps
166174
// just here for consistency & to prevent it from being added as an option
167175
if (name) item.name = name
168176

packages/opencode/src/config/config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,12 @@ export namespace Config {
355355
disable: z.boolean().optional(),
356356
description: z.string().optional().describe("Description of when to use the agent"),
357357
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
358+
maxSteps: z
359+
.number()
360+
.int()
361+
.positive()
362+
.optional()
363+
.describe("Maximum number of steps this agent can take before stopping gracefully"),
358364
permission: z
359365
.object({
360366
edit: Permission.optional(),
@@ -602,6 +608,14 @@ export namespace Config {
602608
})
603609
.optional(),
604610
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
611+
maxSteps: z
612+
.number()
613+
.int()
614+
.positive()
615+
.optional()
616+
.describe(
617+
"Maximum number of steps (iterations) agents can take before stopping gracefully. Steps accumulate across parent and child sessions. Default: 25",
618+
),
605619
disable_paste_summary: z.boolean().optional(),
606620
})
607621
.optional(),

packages/opencode/src/session/prompt.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export namespace SessionPrompt {
109109
noReply: z.boolean().optional(),
110110
system: z.string().optional(),
111111
tools: z.record(z.string(), z.boolean()).optional(),
112+
currentSteps: z.number().int().nonnegative().optional(),
112113
parts: z.array(
113114
z.discriminatedUnion("type", [
114115
MessageV2.TextPart.omit({
@@ -178,6 +179,10 @@ export namespace SessionPrompt {
178179

179180
using abort = lock(input.sessionID)
180181

182+
const currentSteps = input.currentSteps ?? 0
183+
const maxSteps = agent.maxSteps ?? Infinity
184+
const stepRef = { current: 0 }
185+
181186
const system = await resolveSystemPrompt({
182187
providerID: model.providerID,
183188
modelID: model.info.id,
@@ -201,6 +206,8 @@ export namespace SessionPrompt {
201206
providerID: model.providerID,
202207
tools: input.tools,
203208
processor,
209+
currentSteps,
210+
stepRef,
204211
})
205212

206213
const params = await Plugin.trigger(
@@ -235,6 +242,34 @@ export namespace SessionPrompt {
235242
(messages) => insertReminders({ messages, agent }),
236243
)
237244
step++
245+
stepRef.current = step
246+
247+
// Check if we've exceeded maxSteps
248+
if (currentSteps + step > maxSteps) {
249+
const userMsgID = msgs.findLast((m) => m.info.role === "user")?.info.id!
250+
const assistantMsg = await processor.next(userMsgID)
251+
252+
// Create a graceful stop message
253+
await Session.updatePart({
254+
id: Identifier.ascending("part"),
255+
messageID: assistantMsg.id,
256+
sessionID: assistantMsg.sessionID,
257+
type: "text",
258+
text: `Maximum steps (${maxSteps}) reached. The agent has completed ${currentSteps + step} iterations across all main and sub-agent sessions. Consider increasing the maxSteps limit in your configuration if more steps are needed.`,
259+
time: {
260+
start: Date.now(),
261+
end: Date.now(),
262+
},
263+
})
264+
265+
assistantMsg.time.completed = Date.now()
266+
await Session.updateMessage(assistantMsg)
267+
await processor.end()
268+
269+
const parts = await MessageV2.parts(assistantMsg.id)
270+
return { info: assistantMsg, parts }
271+
}
272+
238273
await processor.next(msgs.findLast((m) => m.info.role === "user")?.info.id!)
239274
if (step === 1) {
240275
state().track(
@@ -510,6 +545,8 @@ export namespace SessionPrompt {
510545
providerID: string
511546
tools?: Record<string, boolean>
512547
processor: Processor
548+
currentSteps: number
549+
stepRef: { current: number }
513550
}) {
514551
const tools: Record<string, AITool> = {}
515552
const enabledTools = pipe(
@@ -544,6 +581,7 @@ export namespace SessionPrompt {
544581
extra: {
545582
modelID: input.modelID,
546583
providerID: input.providerID,
584+
currentSteps: input.currentSteps + input.stepRef.current,
547585
},
548586
agent: input.agent.name,
549587
metadata: async (val) => {

packages/opencode/src/tool/task.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export const TaskTool = Tool.define("task", async () => {
7373
providerID: model.providerID,
7474
},
7575
agent: agent.name,
76+
currentSteps: (ctx.extra as any)?.currentSteps ?? 0,
7677
tools: {
7778
todowrite: false,
7879
todoread: false,

0 commit comments

Comments
 (0)