Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -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 ?? {})
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
25 changes: 24 additions & 1 deletion packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -472,6 +475,7 @@ export namespace SessionPrompt {
model,
agent,
system: lastUser.system,
isLastStep,
})
const tools = await resolveTools({
agent,
Expand Down Expand Up @@ -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 => ({
Expand All @@ -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({
Expand Down Expand Up @@ -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(
...(() => {
Expand All @@ -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")]
Expand Down
16 changes: 16 additions & 0 deletions packages/opencode/src/session/prompt/max-steps.txt
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions packages/sdk/js/src/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?:
Expand All @@ -986,6 +990,7 @@ export type AgentConfig = {
}
| boolean
| ("subagent" | "primary" | "all")
| number
| {
edit?: "ask" | "allow" | "deny"
bash?:
Expand Down Expand Up @@ -1558,6 +1563,7 @@ export type Agent = {
options: {
[key: string]: unknown
}
maxSteps?: number
}

export type McpStatusConnected = {
Expand Down
22 changes: 22 additions & 0 deletions packages/web/src/content/docs/agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down