From 14cab2a4f84ac5228ea2d14dcedb2a39841b7481 Mon Sep 17 00:00:00 2001
From: ops
Date: Wed, 31 Dec 2025 23:46:11 +0100
Subject: [PATCH 1/4] (feat) customizable subagent config and system env prompt
---
packages/opencode/src/session/compaction.ts | 3 ++-
packages/opencode/src/session/llm.ts | 5 +++-
packages/opencode/src/session/prompt.ts | 3 ++-
packages/opencode/src/session/summary.ts | 25 +++++++++++++++++--
packages/opencode/src/session/system.ts | 27 ++++++++++++++++-----
packages/opencode/src/tool/task.ts | 10 +++++---
6 files changed, 59 insertions(+), 14 deletions(-)
diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts
index fb382530291..5095a8bb768 100644
--- a/packages/opencode/src/session/compaction.ts
+++ b/packages/opencode/src/session/compaction.ts
@@ -97,7 +97,8 @@ export namespace SessionCompaction {
auto: boolean
}) {
const userMessage = input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User
- const agent = await Agent.get("compaction")
+ const msgAgent = await Agent.get(userMessage.agent)
+ const agent = await Agent.get(msgAgent.options.agents?.compaction ?? "compaction")
const model = agent.model
? await Provider.getModel(agent.model.providerID, agent.model.modelID)
: await Provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts
index befa46fe4ac..f9052d0100b 100644
--- a/packages/opencode/src/session/llm.ts
+++ b/packages/opencode/src/session/llm.ts
@@ -65,13 +65,16 @@ export namespace LLM {
const isCodex = provider.id === "openai" && auth?.type === "oauth"
const system = []
+ const customSystem = input.agent.options.system
+ ? await SystemPrompt.environment(input.model, input.agent.options.system)
+ : input.system
system.push(
[
// use agent prompt otherwise provider prompt
// For Codex sessions, skip SystemPrompt.provider() since it's sent via options.instructions
...(input.agent.prompt ? [input.agent.prompt] : isCodex ? [] : SystemPrompt.provider(input.model)),
// any custom prompt passed into this call
- ...input.system,
+ ...customSystem,
// any custom prompt from last user message
...(input.user.system ? [input.user.system] : []),
]
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 94eabdef7f4..0d6f3bd1ab0 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -1775,7 +1775,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const subtaskParts = firstRealUser.parts.filter((p) => p.type === "subtask") as MessageV2.SubtaskPart[]
const hasOnlySubtaskParts = subtaskParts.length > 0 && firstRealUser.parts.every((p) => p.type === "subtask")
- const agent = await Agent.get("title")
+ const msgAgent = await Agent.get(firstRealUser.info.agent)
+ const agent = await Agent.get(msgAgent.options.agents?.title ?? "title")
if (!agent) return
const model = await iife(async () => {
if (agent.model) return await Provider.getModel(agent.model.providerID, agent.model.modelID)
diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts
index 91a520a9bdf..2c5a9077216 100644
--- a/packages/opencode/src/session/summary.ts
+++ b/packages/opencode/src/session/summary.ts
@@ -132,8 +132,22 @@ export namespace SessionSummary {
const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
if (textPart && !userMsg.summary?.title) {
- const agent = await Agent.get("title")
- if (!agent) return
+ const msgAgent = await Agent.get(userMsg.agent)
+ const titleAgent = msgAgent.options.agents?.title ?? "title"
+ if (titleAgent === "none") {
+ const title = textPart.text.length > 50 ? textPart.text.slice(0, 50) + "..." : textPart.text
+ userMsg.summary.title = title
+ await Session.updateMessage(userMsg)
+ await Session.update(
+ userMsg.sessionID,
+ (draft) => {
+ draft.title = title
+ },
+ { touch: false },
+ )
+ return
+ }
+ const agent = await Agent.get(titleAgent)
const stream = await LLM.stream({
agent,
user: userMsg,
@@ -163,6 +177,13 @@ export namespace SessionSummary {
log.info("title", { title: result })
userMsg.summary.title = result
await Session.updateMessage(userMsg)
+ await Session.update(
+ userMsg.sessionID,
+ (draft) => {
+ draft.title = result
+ },
+ { touch: false },
+ )
}
}
diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts
index d34a086fe44..d84fc590450 100644
--- a/packages/opencode/src/session/system.ts
+++ b/packages/opencode/src/session/system.ts
@@ -24,11 +24,20 @@ export namespace SystemPrompt {
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
}
- export async function environment(model: Provider.Model) {
+ export async function environment(
+ model: Provider.Model,
+ options?: { model_id?: boolean; env?: boolean; files?: boolean },
+ ) {
const project = Instance.project
- return [
- [
+ const parts: string[] = []
+
+ if (options?.model_id !== false)
+ parts.push(
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
+ )
+
+ if (options?.env !== false)
+ parts.push(
`Here is some useful information about the environment you are running in:`,
``,
` Working directory: ${Instance.directory}`,
@@ -36,9 +45,14 @@ export namespace SystemPrompt {
` Platform: ${process.platform}`,
` Today's date: ${new Date().toDateString()}`,
``,
+ )
+
+ // files info was disabled during permission rework
+ if (options?.files !== false && false)
+ parts.push(
``,
` ${
- project.vcs === "git" && false
+ project.vcs === "git"
? await Ripgrep.tree({
cwd: Instance.directory,
limit: 200,
@@ -46,7 +60,8 @@ export namespace SystemPrompt {
: ""
}`,
``,
- ].join("\n"),
- ]
+ )
+
+ return [parts.join("\n")]
}
}
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index ad4268b7b0d..cbed1422cb9 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -41,20 +41,24 @@ export const TaskTool = Tool.define("task", async (ctx) => {
async execute(params: z.infer, ctx) {
const config = await Config.get()
+ // Resolve subagent type through calling agent's mapping
+ const callingAgent = ctx.agent ? await Agent.get(ctx.agent) : undefined
+ const resolvedSubagentType = callingAgent?.options.agents?.[params.subagent_type] ?? params.subagent_type
+
// Skip permission check when user explicitly invoked via @ or command subtask
if (!ctx.extra?.bypassAgentCheck) {
await ctx.ask({
permission: "task",
- patterns: [params.subagent_type],
+ patterns: [resolvedSubagentType],
always: ["*"],
metadata: {
description: params.description,
- subagent_type: params.subagent_type,
+ subagent_type: resolvedSubagentType,
},
})
}
- const agent = await Agent.get(params.subagent_type)
+ const agent = await Agent.get(resolvedSubagentType)
if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
const hasTaskPermission = agent.permission.some((rule) => rule.permission === "task")
From 1f7cb0f122523ffdce078b168e3b1fb2a2ffa0af Mon Sep 17 00:00:00 2001
From: ops
Date: Wed, 28 Jan 2026 09:00:23 +0100
Subject: [PATCH 2/4] gpt subagent fix
---
packages/opencode/src/session/llm.ts | 4 +---
packages/opencode/src/session/prompt.ts | 21 +++++++++++++++++++--
packages/opencode/src/session/summary.ts | 14 --------------
packages/opencode/src/session/system.ts | 3 +--
4 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts
index f9052d0100b..516c678c052 100644
--- a/packages/opencode/src/session/llm.ts
+++ b/packages/opencode/src/session/llm.ts
@@ -65,9 +65,7 @@ export namespace LLM {
const isCodex = provider.id === "openai" && auth?.type === "oauth"
const system = []
- const customSystem = input.agent.options.system
- ? await SystemPrompt.environment(input.model, input.agent.options.system)
- : input.system
+ const customSystem = input.system
system.push(
[
// use agent prompt otherwise provider prompt
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 0d6f3bd1ab0..7cea5e3c63e 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -947,7 +947,7 @@ export namespace SessionPrompt {
]
}
break
- case "file:":
+ case "file:": {
log.info("file", { mime: part.mime })
// have to normalize, symbol search returns absolute paths
// Decode the pathname since URL constructor doesn't automatically decode it
@@ -1130,6 +1130,7 @@ export namespace SessionPrompt {
source: part.source,
},
]
+ }
}
}
@@ -1776,7 +1777,23 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const hasOnlySubtaskParts = subtaskParts.length > 0 && firstRealUser.parts.every((p) => p.type === "subtask")
const msgAgent = await Agent.get(firstRealUser.info.agent)
- const agent = await Agent.get(msgAgent.options.agents?.title ?? "title")
+ const titleAgent = msgAgent.options.agents?.title ?? "title"
+ if (titleAgent === "none") {
+ const textPart = firstRealUser.parts.find((part) => part.type === "text" && !part.synthetic) as
+ | MessageV2.TextPart
+ | undefined
+ const text = textPart?.text?.trim()
+ if (!text) return
+ const title = text.length > 50 ? text.slice(0, 50) + "..." : text
+ return Session.update(
+ input.session.id,
+ (draft) => {
+ draft.title = title
+ },
+ { touch: false },
+ )
+ }
+ const agent = await Agent.get(titleAgent)
if (!agent) return
const model = await iife(async () => {
if (agent.model) return await Provider.getModel(agent.model.providerID, agent.model.modelID)
diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts
index 2c5a9077216..2932d9154b4 100644
--- a/packages/opencode/src/session/summary.ts
+++ b/packages/opencode/src/session/summary.ts
@@ -138,13 +138,6 @@ export namespace SessionSummary {
const title = textPart.text.length > 50 ? textPart.text.slice(0, 50) + "..." : textPart.text
userMsg.summary.title = title
await Session.updateMessage(userMsg)
- await Session.update(
- userMsg.sessionID,
- (draft) => {
- draft.title = title
- },
- { touch: false },
- )
return
}
const agent = await Agent.get(titleAgent)
@@ -177,13 +170,6 @@ export namespace SessionSummary {
log.info("title", { title: result })
userMsg.summary.title = result
await Session.updateMessage(userMsg)
- await Session.update(
- userMsg.sessionID,
- (draft) => {
- draft.title = result
- },
- { touch: false },
- )
}
}
diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts
index d84fc590450..fd537657d37 100644
--- a/packages/opencode/src/session/system.ts
+++ b/packages/opencode/src/session/system.ts
@@ -47,8 +47,7 @@ export namespace SystemPrompt {
``,
)
- // files info was disabled during permission rework
- if (options?.files !== false && false)
+ if (options?.files !== false)
parts.push(
``,
` ${
From a9c24805f4d41763a6fd10baeb6586152c31e9bf Mon Sep 17 00:00:00 2001
From: ops
Date: Wed, 28 Jan 2026 16:58:50 +0100
Subject: [PATCH 3/4] resolve slop
---
packages/opencode/src/session/llm.ts | 4 +++-
packages/opencode/src/session/prompt.ts | 3 +--
packages/opencode/src/session/system.ts | 3 ++-
3 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts
index 516c678c052..f9052d0100b 100644
--- a/packages/opencode/src/session/llm.ts
+++ b/packages/opencode/src/session/llm.ts
@@ -65,7 +65,9 @@ export namespace LLM {
const isCodex = provider.id === "openai" && auth?.type === "oauth"
const system = []
- const customSystem = input.system
+ const customSystem = input.agent.options.system
+ ? await SystemPrompt.environment(input.model, input.agent.options.system)
+ : input.system
system.push(
[
// use agent prompt otherwise provider prompt
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 7cea5e3c63e..821da158cf9 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -947,7 +947,7 @@ export namespace SessionPrompt {
]
}
break
- case "file:": {
+ case "file:":
log.info("file", { mime: part.mime })
// have to normalize, symbol search returns absolute paths
// Decode the pathname since URL constructor doesn't automatically decode it
@@ -1130,7 +1130,6 @@ export namespace SessionPrompt {
source: part.source,
},
]
- }
}
}
diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts
index fd537657d37..d84fc590450 100644
--- a/packages/opencode/src/session/system.ts
+++ b/packages/opencode/src/session/system.ts
@@ -47,7 +47,8 @@ export namespace SystemPrompt {
``,
)
- if (options?.files !== false)
+ // files info was disabled during permission rework
+ if (options?.files !== false && false)
parts.push(
``,
` ${
From ee82675c31ec77d278870b88daf8190f48f67f7a Mon Sep 17 00:00:00 2001
From: ops
Date: Thu, 29 Jan 2026 02:24:51 +0100
Subject: [PATCH 4/4] agents_md option
---
packages/opencode/src/session/compaction.ts | 2 +-
packages/opencode/src/session/prompt.ts | 7 +++++--
packages/opencode/src/session/summary.ts | 2 +-
packages/opencode/src/tool/task.ts | 2 +-
4 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts
index 5095a8bb768..c251268bcae 100644
--- a/packages/opencode/src/session/compaction.ts
+++ b/packages/opencode/src/session/compaction.ts
@@ -98,7 +98,7 @@ export namespace SessionCompaction {
}) {
const userMessage = input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User
const msgAgent = await Agent.get(userMessage.agent)
- const agent = await Agent.get(msgAgent.options.agents?.compaction ?? "compaction")
+ const agent = await Agent.get(msgAgent.options.subagents?.compaction ?? "compaction")
const model = agent.model
? await Provider.getModel(agent.model.providerID, agent.model.modelID)
: await Provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 821da158cf9..1cc6ea7a852 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -602,7 +602,10 @@ export namespace SessionPrompt {
agent,
abort,
sessionID,
- system: [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())],
+ system: [
+ ...(await SystemPrompt.environment(model, agent.options)),
+ ...(agent.options?.system?.agents_md == false ? [] : await InstructionPrompt.system())
+ ],
messages: [
...MessageV2.toModelMessages(sessionMessages, model),
...(isLastStep
@@ -1776,7 +1779,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const hasOnlySubtaskParts = subtaskParts.length > 0 && firstRealUser.parts.every((p) => p.type === "subtask")
const msgAgent = await Agent.get(firstRealUser.info.agent)
- const titleAgent = msgAgent.options.agents?.title ?? "title"
+ const titleAgent = msgAgent.options.subagents?.title ?? "title"
if (titleAgent === "none") {
const textPart = firstRealUser.parts.find((part) => part.type === "text" && !part.synthetic) as
| MessageV2.TextPart
diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts
index 2932d9154b4..b02241cd6d1 100644
--- a/packages/opencode/src/session/summary.ts
+++ b/packages/opencode/src/session/summary.ts
@@ -133,7 +133,7 @@ export namespace SessionSummary {
const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
if (textPart && !userMsg.summary?.title) {
const msgAgent = await Agent.get(userMsg.agent)
- const titleAgent = msgAgent.options.agents?.title ?? "title"
+ const titleAgent = msgAgent.options.subagents?.title ?? "title"
if (titleAgent === "none") {
const title = textPart.text.length > 50 ? textPart.text.slice(0, 50) + "..." : textPart.text
userMsg.summary.title = title
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index cbed1422cb9..574a9e2df0a 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -43,7 +43,7 @@ export const TaskTool = Tool.define("task", async (ctx) => {
// Resolve subagent type through calling agent's mapping
const callingAgent = ctx.agent ? await Agent.get(ctx.agent) : undefined
- const resolvedSubagentType = callingAgent?.options.agents?.[params.subagent_type] ?? params.subagent_type
+ const resolvedSubagentType = callingAgent?.options.subagents?.[params.subagent_type] ?? params.subagent_type
// Skip permission check when user explicitly invoked via @ or command subtask
if (!ctx.extra?.bypassAgentCheck) {