Merge branch 'dev' into fix/queued-message-interrupt

This commit is contained in:
John Dietrich 2025-12-05 14:50:35 -05:00 committed by GitHub
commit 215a52c6fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 444 additions and 82 deletions

View file

@ -494,14 +494,9 @@ class StringSerializeHandler extends BaseSerializeHandler {
}
if (!excludeFinalCursorPosition) {
// Get cursor position relative to viewport (1-indexed for ANSI)
// cursorY is relative to the viewport, cursorX is column position
const cursorRow = this._buffer.cursorY + 1 // 1-indexed
const cursorCol = this._buffer.cursorX + 1 // 1-indexed
// Use absolute cursor positioning (CUP - Cursor Position)
// This is more reliable than relative moves which depend on knowing
// exactly where the cursor ended up after all the content
const absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY
const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER)
const cursorCol = this._buffer.cursorX + 1
content += `\u001b[${cursorRow};${cursorCol}H`
}
@ -549,22 +544,20 @@ export class SerializeAddon implements ITerminalAddon {
return ""
}
const activeBuffer = buffer.active || buffer.normal
if (!activeBuffer) {
const normalBuffer = buffer.normal || buffer.active
const altBuffer = buffer.alternate
if (!normalBuffer) {
return ""
}
let content = options?.range
? this._serializeBufferByRange(activeBuffer, options.range, true)
: this._serializeBufferByScrollback(activeBuffer, options?.scrollback)
? this._serializeBufferByRange(normalBuffer, options.range, true)
: this._serializeBufferByScrollback(normalBuffer, options?.scrollback)
// Handle alternate buffer if active and not excluded
if (!options?.excludeAltBuffer) {
const altBuffer = buffer.alternate
if (altBuffer && buffer.active?.type === "alternate") {
const alternateContent = this._serializeBufferByScrollback(altBuffer, undefined)
content += `\u001b[?1049h\u001b[H${alternateContent}`
}
if (!options?.excludeAltBuffer && buffer.active?.type === "alternate" && altBuffer) {
const alternateContent = this._serializeBufferByScrollback(altBuffer, undefined)
content += `\u001b[?1049h\u001b[H${alternateContent}`
}
return content

View file

@ -1,6 +1,5 @@
import { init, Terminal as Term, FitAddon } from "ghostty-web"
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
import { createReconnectingWS, ReconnectingWebSocket } from "@solid-primitives/websocket"
import { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/session"
@ -11,19 +10,20 @@ export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
onSubmit?: () => void
onCleanup?: (pty: LocalPTY) => void
onConnectError?: (error: unknown) => void
}
export const Terminal = (props: TerminalProps) => {
const sdk = useSDK()
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList"])
let ws: ReconnectingWebSocket
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
let ws: WebSocket
let term: Term
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
onMount(async () => {
ws = createReconnectingWS(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
term = new Term({
cursorBlink: true,
fontSize: 14,
@ -52,19 +52,14 @@ export const Terminal = (props: TerminalProps) => {
term.open(container)
if (local.pty.buffer) {
const originalSize = { cols: term.cols, rows: term.rows }
let resized = false
if (local.pty.rows && local.pty.cols) {
term.resize(local.pty.cols, local.pty.rows)
resized = true
}
term.reset()
term.write(local.pty.buffer)
if (local.pty.scrollY) {
term.scrollToLine(local.pty.scrollY)
}
if (resized) {
term.resize(originalSize.cols, originalSize.rows)
}
}
container.focus()
@ -115,6 +110,7 @@ export const Terminal = (props: TerminalProps) => {
})
ws.addEventListener("error", (error) => {
console.error("WebSocket error:", error)
props.onConnectError?.(error)
})
ws.addEventListener("close", () => {
console.log("WebSocket disconnected")

View file

@ -26,7 +26,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
const params = useParams()
const sync = useSync()
const name = createMemo(
() => `______${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}`,
() => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}.v1`,
)
const [store, setStore] = makePersisted(
@ -201,20 +201,14 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
sdk.client.pty.create({ body: { title: `Terminal ${store.terminals.all.length + 1}` } }).then((pty) => {
const id = pty.data?.id
if (!id) return
batch(() => {
setStore("terminals", "all", [
...store.terminals.all,
{
id,
title: pty.data?.title ?? "Terminal",
// rows: pty.data?.rows ?? 24,
// cols: pty.data?.cols ?? 80,
// buffer: "",
// scrollY: 0,
},
])
setStore("terminals", "active", id)
})
setStore("terminals", "all", [
...store.terminals.all,
{
id,
title: pty.data?.title ?? "Terminal",
},
])
setStore("terminals", "active", id)
})
},
update(pty: Partial<LocalPTY> & { id: string }) {
@ -224,6 +218,24 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
body: { title: pty.title, size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined },
})
},
async clone(id: string) {
const index = store.terminals.all.findIndex((x) => x.id === id)
const pty = store.terminals.all[index]
if (!pty) return
const clone = await sdk.client.pty.create({
body: {
title: pty.title,
},
})
if (!clone.data) return
setStore("terminals", "all", index, {
...pty,
...clone.data,
})
if (store.terminals.active === pty.id) {
setStore("terminals", "active", clone.data.id)
}
},
open(id: string) {
setStore("terminals", "active", id)
},

View file

@ -22,9 +22,10 @@ export default function Layout(props: ParentProps) {
const layout = useLayout()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id) ?? sessions().at(0))
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
function navigateToSession(session: Session | undefined) {
if (!session) return
navigate(`/${params.dir}/session/${session?.id}`)
}
@ -59,6 +60,7 @@ export default function Layout(props: ParentProps) {
<Select
options={sessions()}
current={currentSession()}
placeholder="Select session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={navigateToSession}

View file

@ -84,6 +84,10 @@ export default function Page() {
}
if (event.ctrlKey && event.key.toLowerCase() === "`") {
event.preventDefault()
if (event.shiftKey) {
session.terminal.new()
return
}
layout.terminal.toggle()
return
}
@ -663,7 +667,11 @@ export default function Page() {
<For each={session.terminal.all()}>
{(terminal) => (
<Tabs.Content value={terminal.id}>
<Terminal pty={terminal} onCleanup={session.terminal.update} />
<Terminal
pty={terminal}
onCleanup={session.terminal.update}
onConnectError={() => session.terminal.clone(terminal.id)}
/>
</Tabs.Content>
)}
</For>

View file

@ -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 ?? {})
@ -222,6 +237,7 @@ export namespace Agent {
}
export async function generate(input: { description: string }) {
const cfg = await Config.get()
const defaultModel = await Provider.defaultModel()
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
const language = await Provider.getLanguage(model)
@ -229,6 +245,7 @@ export namespace Agent {
system.push(PROMPT_GENERATE)
const existing = await list()
const result = await generateObject({
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
temperature: 0.3,
prompt: [
...system.map(

View file

@ -25,6 +25,7 @@ import rosepine from "./theme/rosepine.json" with { type: "json" }
import solarized from "./theme/solarized.json" with { type: "json" }
import synthwave84 from "./theme/synthwave84.json" with { type: "json" }
import tokyonight from "./theme/tokyonight.json" with { type: "json" }
import vercel from "./theme/vercel.json" with { type: "json" }
import vesper from "./theme/vesper.json" with { type: "json" }
import zenburn from "./theme/zenburn.json" with { type: "json" }
import { useKV } from "./kv"
@ -149,6 +150,7 @@ export const DEFAULT_THEMES: Record<string, ThemeJson> = {
synthwave84,
tokyonight,
vesper,
vercel,
zenburn,
}

View file

@ -0,0 +1,245 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"background100": "#0A0A0A",
"background200": "#000000",
"gray100": "#1A1A1A",
"gray200": "#1F1F1F",
"gray300": "#292929",
"gray400": "#2E2E2E",
"gray500": "#454545",
"gray600": "#878787",
"gray700": "#8F8F8F",
"gray900": "#A1A1A1",
"gray1000": "#EDEDED",
"blue600": "#0099FF",
"blue700": "#0070F3",
"blue900": "#52A8FF",
"blue1000": "#EBF8FF",
"red700": "#E5484D",
"red900": "#FF6166",
"red1000": "#FDECED",
"amber700": "#FFB224",
"amber900": "#F2A700",
"amber1000": "#FDF4DC",
"green700": "#46A758",
"green900": "#63C46D",
"green1000": "#E6F9E9",
"teal700": "#12A594",
"teal900": "#0AC7AC",
"purple700": "#8E4EC6",
"purple900": "#BF7AF0",
"pink700": "#E93D82",
"pink900": "#F75590",
"highlightPink": "#FF0080",
"highlightPurple": "#F81CE5",
"cyan": "#50E3C2",
"lightBackground": "#FFFFFF",
"lightGray100": "#FAFAFA",
"lightGray200": "#EAEAEA",
"lightGray600": "#666666",
"lightGray1000": "#171717"
},
"theme": {
"primary": {
"dark": "blue700",
"light": "blue700"
},
"secondary": {
"dark": "blue900",
"light": "#0062D1"
},
"accent": {
"dark": "purple700",
"light": "purple700"
},
"error": {
"dark": "red700",
"light": "#DC3545"
},
"warning": {
"dark": "amber700",
"light": "#FF9500"
},
"success": {
"dark": "green700",
"light": "#388E3C"
},
"info": {
"dark": "blue900",
"light": "blue700"
},
"text": {
"dark": "gray1000",
"light": "lightGray1000"
},
"textMuted": {
"dark": "gray600",
"light": "lightGray600"
},
"background": {
"dark": "background200",
"light": "lightBackground"
},
"backgroundPanel": {
"dark": "gray100",
"light": "lightGray100"
},
"backgroundElement": {
"dark": "gray300",
"light": "lightGray200"
},
"border": {
"dark": "gray200",
"light": "lightGray200"
},
"borderActive": {
"dark": "gray500",
"light": "#999999"
},
"borderSubtle": {
"dark": "gray100",
"light": "#EAEAEA"
},
"diffAdded": {
"dark": "green900",
"light": "green700"
},
"diffRemoved": {
"dark": "red900",
"light": "red700"
},
"diffContext": {
"dark": "gray600",
"light": "lightGray600"
},
"diffHunkHeader": {
"dark": "gray600",
"light": "lightGray600"
},
"diffHighlightAdded": {
"dark": "green900",
"light": "green700"
},
"diffHighlightRemoved": {
"dark": "red900",
"light": "red700"
},
"diffAddedBg": {
"dark": "#0B1D0F",
"light": "#E6F9E9"
},
"diffRemovedBg": {
"dark": "#2A1314",
"light": "#FDECED"
},
"diffContextBg": {
"dark": "background200",
"light": "lightBackground"
},
"diffLineNumber": {
"dark": "gray600",
"light": "lightGray600"
},
"diffAddedLineNumberBg": {
"dark": "#0F2613",
"light": "#D6F5D6"
},
"diffRemovedLineNumberBg": {
"dark": "#3C1618",
"light": "#FFE5E5"
},
"markdownText": {
"dark": "gray1000",
"light": "lightGray1000"
},
"markdownHeading": {
"dark": "purple900",
"light": "purple700"
},
"markdownLink": {
"dark": "blue900",
"light": "blue700"
},
"markdownLinkText": {
"dark": "teal900",
"light": "teal700"
},
"markdownCode": {
"dark": "green900",
"light": "green700"
},
"markdownBlockQuote": {
"dark": "gray600",
"light": "lightGray600"
},
"markdownEmph": {
"dark": "amber900",
"light": "amber700"
},
"markdownStrong": {
"dark": "pink900",
"light": "pink700"
},
"markdownHorizontalRule": {
"dark": "gray500",
"light": "#999999"
},
"markdownListItem": {
"dark": "gray1000",
"light": "lightGray1000"
},
"markdownListEnumeration": {
"dark": "blue900",
"light": "blue700"
},
"markdownImage": {
"dark": "teal900",
"light": "teal700"
},
"markdownImageText": {
"dark": "cyan",
"light": "teal700"
},
"markdownCodeBlock": {
"dark": "gray1000",
"light": "lightGray1000"
},
"syntaxComment": {
"dark": "gray600",
"light": "#888888"
},
"syntaxKeyword": {
"dark": "pink900",
"light": "pink700"
},
"syntaxFunction": {
"dark": "purple900",
"light": "purple700"
},
"syntaxVariable": {
"dark": "blue900",
"light": "blue700"
},
"syntaxString": {
"dark": "green900",
"light": "green700"
},
"syntaxNumber": {
"dark": "amber900",
"light": "amber700"
},
"syntaxType": {
"dark": "teal900",
"light": "teal700"
},
"syntaxOperator": {
"dark": "pink900",
"light": "pink700"
},
"syntaxPunctuation": {
"dark": "gray1000",
"light": "lightGray1000"
}
}
}

View file

@ -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(),
@ -670,6 +676,10 @@ export namespace Config {
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
disable_paste_summary: z.boolean().optional(),
batch_tool: z.boolean().optional().describe("Enable the batch tool"),
openTelemetry: z
.boolean()
.optional()
.describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"),
primary_tools: z
.array(z.string())
.optional()

View file

@ -208,30 +208,6 @@ export namespace Server {
return c.json(info)
},
)
.put(
"/pty/:id",
describeRoute({
description: "Update PTY session",
operationId: "pty.update",
responses: {
200: {
description: "Updated session",
content: {
"application/json": {
schema: resolver(Pty.Info),
},
},
},
...errors(400),
},
}),
validator("param", z.object({ id: z.string() })),
validator("json", Pty.UpdateInput),
async (c) => {
const info = await Pty.update(c.req.valid("param").id, c.req.valid("json"))
return c.json(info)
},
)
.get(
"/pty/:id",
describeRoute({
@ -258,6 +234,30 @@ export namespace Server {
return c.json(info)
},
)
.put(
"/pty/:id",
describeRoute({
description: "Update PTY session",
operationId: "pty.update",
responses: {
200: {
description: "Updated session",
content: {
"application/json": {
schema: resolver(Pty.Info),
},
},
},
...errors(400),
},
}),
validator("param", z.object({ id: z.string() })),
validator("json", Pty.UpdateInput),
async (c) => {
const info = await Pty.update(c.req.valid("param").id, c.req.valid("json"))
return c.json(info)
},
)
.delete(
"/pty/:id",
describeRoute({
@ -295,20 +295,14 @@ export namespace Server {
},
},
},
404: {
description: "Session not found",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(404),
},
}),
validator("param", z.object({ id: z.string() })),
upgradeWebSocket((c) => {
const id = c.req.param("id")
let handler: ReturnType<typeof Pty.connect>
if (!Pty.get(id)) throw new Error("Session not found")
return {
onOpen(_event, ws) {
handler = Pty.connect(id, ws)

View file

@ -10,6 +10,7 @@ import z from "zod"
import { SessionPrompt } from "./prompt"
import { Flag } from "../flag/flag"
import { Token } from "../util/token"
import { Config } from "../config/config"
import { Log } from "../util/log"
import { ProviderTransform } from "@/provider/transform"
import { SessionProcessor } from "./processor"
@ -96,6 +97,7 @@ export namespace SessionCompaction {
abort: AbortSignal
auto: boolean
}) {
const cfg = await Config.get()
const model = await Provider.getModel(input.model.providerID, input.model.modelID)
const language = await Provider.getLanguage(model)
const system = [...SystemPrompt.compaction(model.providerID)]
@ -191,6 +193,7 @@ export namespace SessionCompaction {
},
],
}),
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
})
if (result === "continue" && input.auto) {
const continueMsg = await Session.updateMessage({

View file

@ -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"
@ -42,6 +43,7 @@ import { Command } from "../command"
import { $, fileURLToPath } from "bun"
import { ConfigMarkdown } from "../config/markdown"
import { SessionSummary } from "./summary"
import { Config } from "../config/config"
import { NamedError } from "@opencode-ai/util/error"
import { fn } from "@/util/fn"
import { SessionProcessor } from "./processor"
@ -452,7 +454,10 @@ 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,
@ -489,6 +494,7 @@ export namespace SessionPrompt {
model,
agent,
system: lastUser.system,
isLastStep,
})
const tools = await resolveTools({
agent,
@ -579,6 +585,7 @@ export namespace SessionPrompt {
stopWhen: stepCountIs(1),
temperature: params.temperature,
topP: params.topP,
toolChoice: isLastStep ? "none" : undefined,
messages: [
...system.map(
(x): ModelMessage => ({
@ -601,6 +608,14 @@ export namespace SessionPrompt {
return false
}),
),
...(isLastStep
? [
{
role: "assistant" as const,
content: MAX_STEPS,
},
]
: []),
],
tools: model.capabilities.toolcall === false ? undefined : tools,
model: wrapLanguageModel({
@ -632,6 +647,7 @@ export namespace SessionPrompt {
},
],
}),
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
})
if (result === "stop") break
continue
@ -655,7 +671,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(
...(() => {
@ -666,6 +687,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")]
@ -1437,6 +1463,7 @@ export namespace SessionPrompt {
input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
.length === 1
if (!isFirst) return
const cfg = await Config.get()
const small =
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
const language = await Provider.getLanguage(small)
@ -1483,6 +1510,7 @@ export namespace SessionPrompt {
],
headers: small.headers,
model: language,
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
})
.then((result) => {
if (result.text)

View file

@ -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.

View file

@ -1,4 +1,5 @@
import { Provider } from "@/provider/provider"
import { Config } from "@/config/config"
import { fn } from "@/util/fn"
import z from "zod"
import { Session } from "."
@ -60,6 +61,7 @@ export namespace SessionSummary {
}
async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
const cfg = await Config.get()
const messages = input.messages.filter(
(m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
)
@ -109,6 +111,7 @@ export namespace SessionSummary {
],
headers: small.headers,
model: language,
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
})
log.info("title", { title: result.text })
userMsg.summary.title = result.text
@ -150,6 +153,7 @@ export namespace SessionSummary {
},
],
headers: small.headers,
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
}).catch(() => {})
if (result) summary = result.text
}

View file

@ -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?:
@ -1317,6 +1322,10 @@ export type Config = {
* Enable the batch tool
*/
batch_tool?: boolean
/**
* Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)
*/
openTelemetry?: boolean
/**
* Tools that should only be available to primary agents.
*/
@ -1554,6 +1563,7 @@ export type Agent = {
options: {
[key: string]: unknown
}
maxSteps?: number
}
export type McpStatusConnected = {
@ -1816,9 +1826,9 @@ export type PtyConnectData = {
export type PtyConnectErrors = {
/**
* Session not found
* Not found
*/
404: boolean
404: NotFoundError
}
export type PtyConnectError = PtyConnectErrors[keyof PtyConnectErrors]

View file

@ -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.