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) {