summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorNathan Thomas <[email protected]>2025-12-05 10:26:44 -0800
committerGitHub <[email protected]>2025-12-05 12:26:44 -0600
commit40eb8b93e132daac1dff63e260cb61a880a2ba4d (patch)
tree47a56961d3ac0dfc8249d50997d3e66611704fef
parent6e6bd1e1710be0d1daa481c0005ddd8e6cc093c4 (diff)
downloadopencode-40eb8b93e132daac1dff63e260cb61a880a2ba4d.tar.gz
opencode-40eb8b93e132daac1dff63e260cb61a880a2ba4d.zip
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]>
-rw-r--r--packages/opencode/src/agent/agent.ts17
-rw-r--r--packages/opencode/src/config/config.ts6
-rw-r--r--packages/opencode/src/session/prompt.ts25
-rw-r--r--packages/opencode/src/session/prompt/max-steps.txt16
-rw-r--r--packages/sdk/js/src/gen/types.gen.ts6
-rw-r--r--packages/web/src/content/docs/agents.mdx22
6 files changed, 90 insertions, 2 deletions
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index ea967616b..73a7a7996 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/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 ?? {})
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 03c4a39fb..d30b20860 100644
--- a/packages/opencode/src/config/config.ts
+++ b/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(),
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 2f0bc0902..d5010bc47 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/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")]
diff --git a/packages/opencode/src/session/prompt/max-steps.txt b/packages/opencode/src/session/prompt/max-steps.txt
new file mode 100644
index 000000000..3aefa7377
--- /dev/null
+++ b/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. \ No newline at end of file
diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts
index 60a68840c..6a5375f31 100644
--- a/packages/sdk/js/src/gen/types.gen.ts
+++ b/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 = {
diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx
index 557f0ccf5..a2997515b 100644
--- a/packages/web/src/content/docs/agents.mdx
+++ b/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.