From eb80117c8d2d8e45041b36baa06afbe727a985ed Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 8 Dec 2025 20:04:37 -0500 Subject: [PATCH 01/19] sync --- packages/opencode/src/provider/provider.ts | 2 +- packages/opencode/src/provider/transform.ts | 6 +- packages/opencode/src/session/compaction.ts | 67 ++-------- packages/opencode/src/session/llm.ts | 141 ++++++++++++++++++++ packages/opencode/src/session/message-v2.ts | 16 ++- packages/opencode/src/session/processor.ts | 5 +- packages/opencode/src/session/prompt.ts | 49 +------ 7 files changed, 170 insertions(+), 116 deletions(-) create mode 100644 packages/opencode/src/session/llm.ts diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 880238a0d9c1..b823aceac4ff 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -838,7 +838,7 @@ export namespace Provider { return info } - export async function getLanguage(model: Model) { + export async function getLanguage(model: Model): Promise { const s = await state() const key = `${model.providerID}/${model.id}` if (s.models.has(key)) return s.models.get(key)! diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 17fbf18f5fb4..fb432860a90f 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -273,8 +273,8 @@ export namespace ProviderTransform { return options } - export function providerOptions(npm: string | undefined, providerID: string, options: { [x: string]: any }) { - switch (npm) { + export function providerOptions(model: Provider.Model, options: { [x: string]: any }) { + switch (model.api.npm) { case "@ai-sdk/openai": case "@ai-sdk/azure": return { @@ -302,7 +302,7 @@ export namespace ProviderTransform { } default: return { - [providerID]: options, + [model.providerID]: options, } } } diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index de75eda6e40c..613b49b16245 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -1,4 +1,3 @@ -import { wrapLanguageModel, type ModelMessage } from "ai" import { Session } from "." import { Identifier } from "../id/id" import { Instance } from "../project/instance" @@ -12,10 +11,9 @@ 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" import { fn } from "@/util/fn" -import { mergeDeep, pipe } from "remeda" +import { Agent } from "@/agent/agent" export namespace SessionCompaction { const log = Log.create({ service: "session.compaction" }) @@ -97,9 +95,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)] const msg = (await Session.updateMessage({ id: Identifier.ascending("message"), @@ -131,44 +127,16 @@ export namespace SessionCompaction { model: model, abort: input.abort, }) + const agent = await Agent.get(input.agent) const result = await processor.process({ - onError(error) { - log.error("stream error", { - error, - }) - }, - // set to 0, we handle loop - maxRetries: 0, - providerOptions: ProviderTransform.providerOptions( - model.api.npm, - model.providerID, - pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)), - ), - headers: model.headers, - abortSignal: input.abort, - tools: model.capabilities.toolcall ? {} : undefined, + requestID: input.parentID, + agent, + abort: input.abort, + sessionID: input.sessionID, + tools: {}, + system, messages: [ - ...system.map( - (x): ModelMessage => ({ - role: "system", - content: x, - }), - ), - ...MessageV2.toModelMessage( - input.messages.filter((m) => { - if (m.info.role !== "assistant" || m.info.error === undefined) { - return true - } - if ( - MessageV2.AbortedError.isInstance(m.info.error) && - m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning") - ) { - return true - } - - return false - }), - ), + ...MessageV2.toModelMessage(input.messages), { role: "user", content: [ @@ -179,22 +147,9 @@ export namespace SessionCompaction { ], }, ], - model: wrapLanguageModel({ - model: language, - middleware: [ - { - async transformParams(args) { - if (args.type === "stream") { - // @ts-expect-error - args.params.prompt = ProviderTransform.message(args.params.prompt, model) - } - return args.params - }, - }, - ], - }), - experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry }, + model, }) + if (result === "continue" && input.auto) { const continueMsg = await Session.updateMessage({ id: Identifier.ascending("message"), diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts new file mode 100644 index 000000000000..a4996a31aa82 --- /dev/null +++ b/packages/opencode/src/session/llm.ts @@ -0,0 +1,141 @@ +import { Provider } from "@/provider/provider" +import { Log } from "@/util/log" +import { streamText, wrapLanguageModel, type ModelMessage, type StreamTextResult, type Tool, type ToolSet } from "ai" +import { mergeDeep, pipe } from "remeda" +import { ProviderTransform } from "@/provider/transform" +import { iife } from "@/util/iife" +import { Config } from "@/config/config" +import { Instance } from "@/project/instance" +import type { Agent } from "@/agent/agent" + +export namespace LLM { + const log = Log.create({ service: "llm" }) + + export const OUTPUT_TOKEN_MAX = 32_000 + + export type StreamInput = { + requestID: string + sessionID: string + model: Provider.Model + agent: Agent.Info + system: string[] + abort: AbortSignal + messages: ModelMessage[] + tools: Record + retries?: number + } + + export type StreamOutput = StreamTextResult + + export async function stream(input: StreamInput) { + const [language, cfg] = await Promise.all([Provider.getLanguage(input.model), Config.get()]) + + const [first, ...rest] = input.system + const system = [first, rest.join("\n")] + const options = pipe( + ProviderTransform.options(input.model, input.sessionID), + mergeDeep(input.model.options), + mergeDeep(input.agent.options), + ) + const maxOutputTokens = ProviderTransform.maxOutputTokens( + input.model.api.npm, + options, + input.model.limit.output, + OUTPUT_TOKEN_MAX, + ) + const temperature = input.model.capabilities.temperature + ? (input.agent.temperature ?? ProviderTransform.temperature(input.model)) + : undefined + const topP = input.agent.topP ?? ProviderTransform.topP(input.model) + + return streamText({ + onError(error) { + log.error("stream error", { + error, + }) + }, + async experimental_repairToolCall(failed) { + const lower = failed.toolCall.toolName.toLowerCase() + if (lower !== failed.toolCall.toolName && input.tools[lower]) { + log.info("repairing tool call", { + tool: failed.toolCall.toolName, + repaired: lower, + }) + return { + ...failed.toolCall, + toolName: lower, + } + } + return { + ...failed.toolCall, + input: JSON.stringify({ + tool: failed.toolCall.toolName, + error: failed.error.message, + }), + toolName: "invalid", + } + }, + temperature, + topP, + providerOptions: { + [iife(() => { + switch (input.model.api.npm) { + case "@ai-sdk/openai": + case "@ai-sdk/azure": + return `openai` + case "@ai-sdk/amazon-bedrock": + return `bedrock` + case "@ai-sdk/anthropic": + return `anthropic` + case "@ai-sdk/google": + return `google` + case "@ai-sdk/gateway": + return `gateway` + case "@openrouter/ai-sdk-provider": + return `openrouter` + default: + return input.model.providerID + } + })]: options, + }, + activeTools: Object.keys(input.tools).filter((x) => x !== "invalid"), + maxOutputTokens, + abortSignal: input.abort, + headers: { + ...(input.model.providerID.startsWith("opencode") + ? { + "x-opencode-project": Instance.project.id, + "x-opencode-session": input.sessionID, + "x-opencode-request": input.requestID, + } + : undefined), + ...input.model.headers, + }, + maxRetries: input.retries ?? 0, + messages: [ + ...system.map( + (x): ModelMessage => ({ + role: "system", + content: x, + }), + ), + ...input.messages, + ], + model: wrapLanguageModel({ + model: language, + middleware: [ + { + async transformParams(args) { + if (args.type === "stream") { + // @ts-expect-error + args.params.prompt = ProviderTransform.message(args.params.prompt, input.model) + } + return args.params + }, + }, + ], + }), + experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry }, + }) + } +} diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 50a480626eaf..ed918d5bdb56 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -411,12 +411,7 @@ export namespace MessageV2 { }) export type WithParts = z.infer - export function toModelMessage( - input: { - info: Info - parts: Part[] - }[], - ): ModelMessage[] { + export function toModelMessage(input: WithParts[]): ModelMessage[] { const result: UIMessage[] = [] for (const msg of input) { @@ -460,6 +455,15 @@ export namespace MessageV2 { } if (msg.info.role === "assistant") { + if ( + msg.info.error && + !( + MessageV2.AbortedError.isInstance(msg.info.error) && + msg.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning") + ) + ) { + continue + } const assistantMessage: UIMessage = { id: msg.info.id, role: "assistant", diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index f1f7dd0964f4..fe2756957f74 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -12,6 +12,7 @@ import { SessionRetry } from "./retry" import { SessionStatus } from "./status" import { Plugin } from "@/plugin" import type { Provider } from "@/provider/provider" +import { LLM } from "./llm" export namespace SessionProcessor { const DOOM_LOOP_THRESHOLD = 3 @@ -47,13 +48,13 @@ export namespace SessionProcessor { partFromToolCall(toolCallID: string) { return toolcalls[toolCallID] }, - async process(streamInput: StreamInput) { + async process(streamInput: LLM.StreamInput) { log.info("process") while (true) { try { let currentText: MessageV2.TextPart | undefined let reasoningMap: Record = {} - const stream = streamText(streamInput) + const stream = await LLM.stream(streamInput) for await (const value of stream.fullStream) { input.abort.throwIfAborted() diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index d5010bc47d87..e819345830c2 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -516,54 +516,6 @@ export namespace SessionPrompt { } const result = await processor.process({ - onError(error) { - log.error("stream error", { - error, - }) - }, - async experimental_repairToolCall(input) { - const lower = input.toolCall.toolName.toLowerCase() - if (lower !== input.toolCall.toolName && tools[lower]) { - log.info("repairing tool call", { - tool: input.toolCall.toolName, - repaired: lower, - }) - return { - ...input.toolCall, - toolName: lower, - } - } - return { - ...input.toolCall, - input: JSON.stringify({ - tool: input.toolCall.toolName, - error: input.error.message, - }), - toolName: "invalid", - } - }, - headers: { - ...(model.providerID.startsWith("opencode") - ? { - "x-opencode-project": Instance.project.id, - "x-opencode-session": sessionID, - "x-opencode-request": lastUser.id, - } - : undefined), - ...model.headers, - }, - // set to 0, we handle loop - maxRetries: 0, - activeTools: Object.keys(tools).filter((x) => x !== "invalid"), - maxOutputTokens: ProviderTransform.maxOutputTokens( - model.api.npm, - params.options, - model.limit.output, - OUTPUT_TOKEN_MAX, - ), - abortSignal: abort, - providerOptions: ProviderTransform.providerOptions(model.api.npm, model.providerID, params.options), - stopWhen: stepCountIs(1), temperature: params.temperature, topP: params.topP, toolChoice: isLastStep ? "none" : undefined, @@ -692,6 +644,7 @@ export namespace SessionPrompt { mergeDeep(await ToolRegistry.enabled(input.agent)), mergeDeep(input.tools ?? {}), ) + for (const item of await ToolRegistry.tools(input.model.providerID)) { if (Wildcard.all(item.id, enabledTools) === false) continue const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters)) From 5a382b31d836be951031a78f8c5cc46f6315de55 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 10 Dec 2025 00:39:24 -0500 Subject: [PATCH 02/19] sync --- .opencode/opencode.jsonc | 14 +-- packages/opencode/src/agent/agent.ts | 45 +++++++++ packages/opencode/src/session/compaction.ts | 2 +- packages/opencode/src/session/llm.ts | 26 ++++- packages/opencode/src/session/prompt.ts | 100 +++++++------------- packages/opencode/src/tool/bash.ts | 49 +++++----- packages/opencode/src/tool/registry.ts | 14 ++- 7 files changed, 140 insertions(+), 110 deletions(-) diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index dc0bee7c3f67..88adf3762b4d 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -10,17 +10,5 @@ "options": {}, }, }, - "mcp": { - "exa": { - "type": "remote", - "url": "https://mcp.exa.ai/mcp", - }, - "morph": { - "type": "local", - "command": ["bunx", "@morphllm/morphmcp"], - "environment": { - "ENABLED_TOOLS": "warp_grep", - }, - }, - }, + "mcp": {}, } diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 94127e51cebf..04f50b0316c5 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -157,6 +157,51 @@ export namespace Agent { mode: "primary", builtIn: true, }, + summary: { + name: "summary", + mode: "subagent", + options: {}, + builtIn: true, + permission: agentPermission, + prompt: `You are a title generator. You output ONLY a thread title. Nothing else. + + +Generate a brief title that would help the user find this conversation later. + +Follow all rules in +Use the so you know what a good title looks like. +Your output must be: +- A single line +- ≤50 characters +- No explanations + + + +- Focus on the main topic or question the user needs to retrieve +- Use -ing verbs for actions (Debugging, Implementing, Analyzing) +- Keep exact: technical terms, numbers, filenames, HTTP codes +- Remove: the, this, my, a, an +- Never assume tech stack +- Never use tools +- NEVER respond to questions, just generate a title for the conversation +- The title should NEVER include "summarizing" or "generating" when generating a title +- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT +- Always output something meaningful, even if the input is minimal. +- If the user message is short or conversational (e.g. “hello”, “lol”, “whats up”, “hey”): + → create a title that reflects the user’s tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.) + + + +"hey" -> Greeting +"debug 500 errors in production" → Debugging production 500 errors +"refactor user service" → Refactoring user service +"why is app.js failing" → Analyzing app.js failure +"implement rate limiting" → Implementing rate limiting +"how do I connect postgres to my API" → Connecting Postgres to API +"best practices for React hooks" → React hooks best practices +`, + tools: {}, + }, plan: { name: "plan", options: {}, diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 54fe37ed4e7d..de3c3dca3234 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -129,7 +129,7 @@ export namespace SessionCompaction { }) const agent = await Agent.get(input.agent) const result = await processor.process({ - requestID: input.parentID, + user: input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User, agent, abort: input.abort, sessionID: input.sessionID, diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index cda310e3a9ad..8277f36b1aa3 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -8,6 +8,7 @@ import { Instance } from "@/project/instance" import type { Agent } from "@/agent/agent" import type { MessageV2 } from "./message-v2" import { Plugin } from "@/plugin" +import { SystemPrompt } from "./system" export namespace LLM { const log = Log.create({ service: "llm" }) @@ -22,6 +23,7 @@ export namespace LLM { system: string[] abort: AbortSignal messages: ModelMessage[] + small?: boolean tools: Record retries?: number } @@ -29,9 +31,19 @@ export namespace LLM { export type StreamOutput = StreamTextResult export async function stream(input: StreamInput) { + const l = log + .clone() + .tag("providerID", input.model.providerID) + .tag("modelID", input.model.id) + .tag("sessionID", input.sessionID) + .tag("small", (input.small ?? false).toString()) + l.info("stream", { + modelID: input.model.id, + providerID: input.model.providerID, + }) const [language, cfg] = await Promise.all([Provider.getLanguage(input.model), Config.get()]) - const [first, ...rest] = input.system + const [first, ...rest] = [...SystemPrompt.header(input.model.providerID), ...input.system] const system = [first, rest.join("\n")] const params = await Plugin.trigger( @@ -49,13 +61,18 @@ export namespace LLM { : undefined, topP: input.agent.topP ?? ProviderTransform.topP(input.model), options: pipe( - ProviderTransform.options(input.model, input.sessionID), + mergeDeep(ProviderTransform.options(input.model, input.sessionID)), + input.small ? mergeDeep(ProviderTransform.smallOptions(input.model)) : mergeDeep({}), mergeDeep(input.model.options), mergeDeep(input.agent.options), ), }, ) + l.info("params", { + params, + }) + const maxOutputTokens = ProviderTransform.maxOutputTokens( input.model.api.npm, params.options, @@ -65,14 +82,14 @@ export namespace LLM { return streamText({ onError(error) { - log.error("stream error", { + l.error("stream error", { error, }) }, async experimental_repairToolCall(failed) { const lower = failed.toolCall.toolName.toLowerCase() if (lower !== failed.toolCall.toolName && input.tools[lower]) { - log.info("repairing tool call", { + l.info("repairing tool call", { tool: failed.toolCall.toolName, repaired: lower, }) @@ -94,6 +111,7 @@ export namespace LLM { topP: params.topP, providerOptions: ProviderTransform.providerOptions(input.model, params.options, input.messages), activeTools: Object.keys(input.tools).filter((x) => x !== "invalid"), + tools: input.tools, maxOutputTokens, abortSignal: input.abort, headers: { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 96a0435debfd..8c4f4ecbad87 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -41,6 +41,8 @@ import { fn } from "@/util/fn" import { SessionProcessor } from "./processor" import { TaskTool } from "@/tool/task" import { SessionStatus } from "./status" +import { LLM } from "./llm" +import { iife } from "@/util/iife" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false @@ -281,7 +283,6 @@ export namespace SessionPrompt { }) const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID) - const language = await Provider.getLanguage(model) const task = tasks.pop() // pending subtask @@ -427,7 +428,6 @@ 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 @@ -435,6 +435,7 @@ export namespace SessionPrompt { messages: msgs, agent, }) + const processor = SessionProcessor.create({ assistantMessage: (await Session.updateMessage({ id: Identifier.ascending("message"), @@ -467,7 +468,6 @@ export namespace SessionPrompt { model, agent, system: lastUser.system, - isLastStep, }) const tools = await resolveTools({ agent, @@ -526,13 +526,9 @@ export namespace SessionPrompt { return Provider.defaultModel() } - async function resolveSystemPrompt(input: { - system?: string - agent: Agent.Info - model: Provider.Model - isLastStep?: boolean - }) { - let system = SystemPrompt.header(input.model.providerID) + async function resolveSystemPrompt(input: { system?: string; agent: Agent.Info; model: Provider.Model }) { + using _ = log.time("system") + let system = [] system.push( ...(() => { if (input.system) return [input.system] @@ -542,14 +538,6 @@ 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")] return system } @@ -560,6 +548,7 @@ export namespace SessionPrompt { tools?: Record processor: SessionProcessor.Info }) { + using _ = log.time("resolveTools") const tools: Record = {} const enabledTools = pipe( input.agent.tools, @@ -1319,28 +1308,24 @@ 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) - const provider = await Provider.getProvider(small.providerID) - const options = pipe( - {}, - mergeDeep(ProviderTransform.options(small, input.session.id, provider?.options)), - mergeDeep(ProviderTransform.smallOptions(small)), - mergeDeep(small.options), - ) - await generateText({ - // use higher # for reasoning models since reasoning tokens eat up a lot of the budget - maxOutputTokens: small.capabilities.reasoning ? 3000 : 20, - providerOptions: ProviderTransform.providerOptions(small, options, []), + const agent = await Agent.get("summary") + if (!agent) return + const result = await LLM.stream({ + agent, + user: input.message.info as MessageV2.User, + system: [agent.prompt!], + small: true, + tools: {}, + model: await iife(async () => { + if (agent.model) return await Provider.getModel(agent.model.providerID, agent.model.modelID) + return ( + (await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID)) + ) + }), + abort: new AbortController().signal, + sessionID: input.session.id, + retries: 2, messages: [ - ...SystemPrompt.title(small.providerID).map( - (x): ModelMessage => ({ - role: "system", - content: x, - }), - ), { role: "user", content: "Generate a title for this conversation:\n", @@ -1364,32 +1349,19 @@ export namespace SessionPrompt { }, ]), ], - headers: small.headers, - model: language, - experimental_telemetry: { - isEnabled: cfg.experimental?.openTelemetry, - metadata: { - userId: cfg.username ?? "unknown", - sessionId: input.session.id, - }, - }, }) - .then((result) => { - if (result.text) - return Session.update(input.session.id, (draft) => { - const cleaned = result.text - .replace(/[\s\S]*?<\/think>\s*/g, "") - .split("\n") - .map((line) => line.trim()) - .find((line) => line.length > 0) - if (!cleaned) return - - const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned - draft.title = title - }) - }) - .catch((error) => { - log.error("failed to generate title", { error, model: small.id }) + const text = await result.text.catch((err) => log.error("failed to generate title", { error: err })) + if (text) + return Session.update(input.session.id, (draft) => { + const cleaned = text + .replace(/[\s\S]*?<\/think>\s*/g, "") + .split("\n") + .map((line) => line.trim()) + .find((line) => line.length > 0) + if (!cleaned) return + + const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned + draft.title = title }) } } diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 6b0b9d41046b..cd04814bfe59 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -50,35 +50,36 @@ const parser = lazy(async () => { return p }) -// TODO: we may wanna rename this tool so it works better on other shells - -export const BashTool = Tool.define("bash", async () => { - const shell = iife(() => { - const s = process.env.SHELL - if (s) { - const basename = path.basename(s) - if (!new Set(["fish", "nu"]).has(basename)) { - return s - } +const getShell = lazy(() => { + const s = process.env.SHELL + if (s) { + const basename = path.basename(s) + if (!new Set(["fish", "nu"]).has(basename)) { + return s } + } - if (process.platform === "darwin") { - return "/bin/zsh" - } + if (process.platform === "darwin") { + return "/bin/zsh" + } - if (process.platform === "win32") { - // Let Bun / Node pick COMSPEC (usually cmd.exe) - // or explicitly: - return process.env.COMSPEC || true - } + if (process.platform === "win32") { + // Let Bun / Node pick COMSPEC (usually cmd.exe) + // or explicitly: + return process.env.COMSPEC || true + } - const bash = Bun.which("bash") - if (bash) { - return bash - } + const bash = Bun.which("bash") + if (bash) { + return bash + } - return true - }) + return true +}) + +// TODO: we may wanna rename this tool so it works better on other shells +export const BashTool = Tool.define("bash", async () => { + const shell = getShell() log.info("bash tool using shell", { shell }) return { diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 7e440a78aa5f..647c74267153 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -21,8 +21,11 @@ import { Plugin } from "../plugin" import { WebSearchTool } from "./websearch" import { CodeSearchTool } from "./codesearch" import { Flag } from "@/flag/flag" +import { Log } from "@/util/log" export namespace ToolRegistry { + const log = Log.create({ service: "tool.registry" }) + export const state = Instance.state(async () => { const custom = [] as Tool.Info[] const glob = new Bun.Glob("tool/*.{js,ts}") @@ -119,10 +122,13 @@ export namespace ToolRegistry { } return true }) - .map(async (t) => ({ - id: t.id, - ...(await t.init()), - })), + .map(async (t) => { + using _ = log.time(t.id) + return { + id: t.id, + ...(await t.init()), + } + }), ) return result } From 8cbacb844f46c7cfa64d609c4300043cd9ddd3fe Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 10 Dec 2025 08:22:02 -0500 Subject: [PATCH 03/19] sync --- packages/opencode/src/session/processor.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index fe2756957f74..4e701e81383e 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -1,5 +1,4 @@ import { MessageV2 } from "./message-v2" -import { streamText } from "ai" import { Log } from "@/util/log" import { Identifier } from "@/id/id" import { Session } from "." @@ -21,15 +20,6 @@ export namespace SessionProcessor { export type Info = Awaited> export type Result = Awaited> - export type StreamInput = Parameters[0] - - export type TBD = { - model: { - modelID: string - providerID: string - } - } - export function create(input: { assistantMessage: MessageV2.Assistant sessionID: string From 74d1f1aa3559135112b59921bd8afa48ae55e1b0 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 11 Dec 2025 22:18:54 -0500 Subject: [PATCH 04/19] sync --- packages/opencode/src/session/prompt.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 9ae54326d8f8..2989cf997443 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1322,7 +1322,6 @@ 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 -<<<<<<< HEAD const agent = await Agent.get("summary") if (!agent) return const result = await LLM.stream({ @@ -1340,23 +1339,6 @@ export namespace SessionPrompt { abort: new AbortController().signal, sessionID: input.session.id, retries: 2, -======= - 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) - const provider = await Provider.getProvider(small.providerID) - const options = pipe( - {}, - mergeDeep(ProviderTransform.options(small, input.session.id, provider?.options)), - mergeDeep(ProviderTransform.smallOptions(small)), - mergeDeep(small.options), - ) - await generateText({ - // use higher # for reasoning models since reasoning tokens eat up a lot of the budget - maxOutputTokens: small.capabilities.reasoning ? 3000 : 20, - providerOptions: ProviderTransform.providerOptions(small, options), ->>>>>>> dev messages: [ { role: "user", From a1c20e3e000938642615039e9401e19a0b402b1c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 11 Dec 2025 22:19:17 -0500 Subject: [PATCH 05/19] sync --- packages/opencode/src/session/llm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 8277f36b1aa3..fdb011b51899 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -109,7 +109,7 @@ export namespace LLM { }, temperature: params.temperature, topP: params.topP, - providerOptions: ProviderTransform.providerOptions(input.model, params.options, input.messages), + providerOptions: ProviderTransform.providerOptions(input.model, params.options), activeTools: Object.keys(input.tools).filter((x) => x !== "invalid"), tools: input.tools, maxOutputTokens, From 167709c48e76283ad8437f38b9da36c2f0ca0555 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 12 Dec 2025 15:10:49 -0500 Subject: [PATCH 06/19] sync --- packages/opencode/src/agent/agent.ts | 14 ++--- packages/opencode/src/session/compaction.ts | 6 +- packages/opencode/src/session/llm.ts | 28 +++++++-- packages/opencode/src/session/message-v2.ts | 4 ++ packages/opencode/src/session/prompt.ts | 68 +++++++-------------- packages/opencode/src/session/system.ts | 6 +- 6 files changed, 63 insertions(+), 63 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 04f50b0316c5..442531e136e9 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -13,7 +13,7 @@ export namespace Agent { name: z.string(), description: z.string().optional(), mode: z.enum(["subagent", "primary", "all"]), - builtIn: z.boolean(), + internal: z.boolean(), topP: z.number().optional(), temperature: z.number().optional(), color: z.string().optional(), @@ -112,7 +112,7 @@ export namespace Agent { options: {}, permission: agentPermission, mode: "subagent", - builtIn: true, + internal: true, }, explore: { name: "explore", @@ -147,7 +147,7 @@ export namespace Agent { options: {}, permission: agentPermission, mode: "subagent", - builtIn: true, + internal: true, }, build: { name: "build", @@ -155,13 +155,13 @@ export namespace Agent { options: {}, permission: agentPermission, mode: "primary", - builtIn: true, + internal: true, }, summary: { name: "summary", mode: "subagent", options: {}, - builtIn: true, + internal: true, permission: agentPermission, prompt: `You are a title generator. You output ONLY a thread title. Nothing else. @@ -210,7 +210,7 @@ Your output must be: ...defaultTools, }, mode: "primary", - builtIn: true, + internal: true, }, } for (const [key, value] of Object.entries(cfg.agent ?? {})) { @@ -226,7 +226,7 @@ Your output must be: permission: agentPermission, options: {}, tools: {}, - builtIn: false, + internal: false, } const { name, diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 602b7f77b635..821ca015e444 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -91,10 +91,10 @@ export namespace SessionCompaction { providerID: string modelID: string } - agent: string abort: AbortSignal auto: boolean }) { + const agent = await Agent.get("compaction") const model = await Provider.getModel(input.model.providerID, input.model.modelID) const system = [...SystemPrompt.compaction(model.providerID)] const msg = (await Session.updateMessage({ @@ -102,7 +102,8 @@ export namespace SessionCompaction { role: "assistant", parentID: input.parentID, sessionID: input.sessionID, - mode: input.agent, + mode: "compaction", + agent: "compaction", summary: true, path: { cwd: Instance.directory, @@ -127,7 +128,6 @@ export namespace SessionCompaction { model: model, abort: input.abort, }) - const agent = await Agent.get(input.agent) const result = await processor.process({ user: input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User, agent, diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index fdb011b51899..f0f48ce6679a 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -9,6 +9,7 @@ import type { Agent } from "@/agent/agent" import type { MessageV2 } from "./message-v2" import { Plugin } from "@/plugin" import { SystemPrompt } from "./system" +import { ToolRegistry } from "@/tool/registry" export namespace LLM { const log = Log.create({ service: "llm" }) @@ -43,7 +44,12 @@ export namespace LLM { }) const [language, cfg] = await Promise.all([Provider.getLanguage(input.model), Config.get()]) - const [first, ...rest] = [...SystemPrompt.header(input.model.providerID), ...input.system] + const [first, ...rest] = [ + ...SystemPrompt.header(input.model.providerID), + ...(input.agent.prompt ?? SystemPrompt.provider(input.model)), + ...input.system, + ...(input.user.system ? [input.user.system] : []), + ] const system = [first, rest.join("\n")] const params = await Plugin.trigger( @@ -80,6 +86,8 @@ export namespace LLM { OUTPUT_TOKEN_MAX, ) + const tools = await resolveTools(input) + return streamText({ onError(error) { l.error("stream error", { @@ -88,7 +96,7 @@ export namespace LLM { }, async experimental_repairToolCall(failed) { const lower = failed.toolCall.toolName.toLowerCase() - if (lower !== failed.toolCall.toolName && input.tools[lower]) { + if (lower !== failed.toolCall.toolName && tools[lower]) { l.info("repairing tool call", { tool: failed.toolCall.toolName, repaired: lower, @@ -110,8 +118,8 @@ export namespace LLM { temperature: params.temperature, topP: params.topP, providerOptions: ProviderTransform.providerOptions(input.model, params.options), - activeTools: Object.keys(input.tools).filter((x) => x !== "invalid"), - tools: input.tools, + activeTools: Object.keys(tools).filter((x) => x !== "invalid"), + tools, maxOutputTokens, abortSignal: input.abort, headers: { @@ -151,4 +159,16 @@ export namespace LLM { experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry }, }) } + + async function resolveTools(input: Pick) { + const enabled = pipe( + input.agent.tools, + mergeDeep(await ToolRegistry.enabled(input.agent)), + mergeDeep(input.user.tools ?? {}), + ) + for (const [key, value] of Object.entries(enabled)) { + if (value === false) delete input.tools[key] + } + return input.tools + } } diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 477abe958147..76162c797805 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -348,7 +348,11 @@ export namespace MessageV2 { parentID: z.string(), modelID: z.string(), providerID: z.string(), + /** + * @deprecated + */ mode: z.string(), + agent: z.string(), path: z.object({ cwd: z.string(), root: z.string(), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 2989cf997443..997366c6caf8 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -5,24 +5,22 @@ import z from "zod" import { Identifier } from "../id/id" import { MessageV2 } from "./message-v2" import { Log } from "../util/log" -import { Flag } from "../flag/flag" import { SessionRevert } from "./revert" import { Session } from "." import { Agent } from "../agent/agent" import { Provider } from "../provider/provider" -import { generateText, type ModelMessage, type Tool as AITool, tool, jsonSchema } from "ai" +import { type Tool as AITool, tool, jsonSchema } from "ai" import { SessionCompaction } from "./compaction" import { Instance } from "../project/instance" import { Bus } from "../bus" import { ProviderTransform } from "../provider/transform" import { SystemPrompt } from "./system" 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 { clone, mergeDeep, pipe } from "remeda" +import { mergeDeep, pipe } from "remeda" import { ToolRegistry } from "../tool/registry" import { Wildcard } from "../util/wildcard" import { MCP } from "../mcp" @@ -36,7 +34,6 @@ 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" @@ -89,8 +86,8 @@ export namespace SessionPrompt { .optional(), agent: z.string().optional(), noReply: z.boolean().optional(), - system: z.string().optional(), tools: z.record(z.string(), z.boolean()).optional(), + system: z.string().optional(), parts: z.array( z.discriminatedUnion("type", [ MessageV2.TextPart.omit({ @@ -138,6 +135,20 @@ export namespace SessionPrompt { }) export type PromptInput = z.infer + export const prompt = fn(PromptInput, async (input) => { + const session = await Session.get(input.sessionID) + await SessionRevert.cleanup(session) + + const message = await createUserMessage(input) + await Session.touch(input.sessionID) + + if (input.noReply === true) { + return message + } + + return loop(input.sessionID) + }) + export async function resolvePromptParts(template: string): Promise { const parts: PromptInput["parts"] = [ { @@ -189,20 +200,6 @@ export namespace SessionPrompt { return parts } - export const prompt = fn(PromptInput, async (input) => { - const session = await Session.get(input.sessionID) - await SessionRevert.cleanup(session) - - const message = await createUserMessage(input) - await Session.touch(input.sessionID) - - if (input.noReply === true) { - return message - } - - return loop(input.sessionID) - }) - function start(sessionID: string) { const s = state() if (s[sessionID]) return @@ -296,6 +293,7 @@ export namespace SessionPrompt { parentID: lastUser.id, sessionID, mode: task.agent, + agent: task.agent, path: { cwd: Instance.directory, root: Instance.worktree, @@ -406,7 +404,6 @@ export namespace SessionPrompt { messages: msgs, parentID: lastUser.id, abort, - agent: lastUser.agent, model: { providerID: model.providerID, modelID: model.id, @@ -448,6 +445,7 @@ export namespace SessionPrompt { parentID: lastUser.id, role: "assistant", mode: agent.name, + agent: agent.name, path: { cwd: Instance.directory, root: Instance.worktree, @@ -470,11 +468,6 @@ export namespace SessionPrompt { model, abort, }) - const system = await resolveSystemPrompt({ - model, - agent, - system: lastUser.system, - }) const tools = await resolveTools({ agent, sessionID, @@ -495,7 +488,7 @@ export namespace SessionPrompt { agent, abort, sessionID, - system, + system: [...(await SystemPrompt.environment()), ...(await SystemPrompt.custom())], messages: [ ...MessageV2.toModelMessage(msgs), ...(isLastStep @@ -532,21 +525,6 @@ export namespace SessionPrompt { return Provider.defaultModel() } - async function resolveSystemPrompt(input: { system?: string; agent: Agent.Info; model: Provider.Model }) { - using _ = log.time("system") - let system = [] - system.push( - ...(() => { - if (input.system) return [input.system] - if (input.agent.prompt) return [input.agent.prompt] - return SystemPrompt.provider(input.model) - })(), - ) - system.push(...(await SystemPrompt.environment())) - system.push(...(await SystemPrompt.custom())) - return system - } - async function resolveTools(input: { agent: Agent.Info model: Provider.Model @@ -561,7 +539,6 @@ export namespace SessionPrompt { mergeDeep(await ToolRegistry.enabled(input.agent)), mergeDeep(input.tools ?? {}), ) - for (const item of await ToolRegistry.tools(input.model.providerID)) { if (Wildcard.all(item.id, enabledTools) === false) continue const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters)) @@ -625,7 +602,6 @@ export namespace SessionPrompt { }, }) } - for (const [key, item] of Object.entries(await MCP.tools())) { if (Wildcard.all(key, enabledTools) === false) continue const execute = item.execute @@ -704,7 +680,6 @@ export namespace SessionPrompt { created: Date.now(), }, tools: input.tools, - system: input.system, agent: agent.name, model: input.model ?? agent.model ?? (await lastModel(input.sessionID)), } @@ -995,7 +970,7 @@ export namespace SessionPrompt { synthetic: true, }) } - const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.mode === "plan") + const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") if (wasPlan && input.agent.name === "build") { userMessage.parts.push({ id: Identifier.ascending("part"), @@ -1057,6 +1032,7 @@ export namespace SessionPrompt { sessionID: input.sessionID, parentID: userMsg.id, mode: input.agent, + agent: input.agent, cost: 0, path: { cwd: Instance.directory, diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 3146110cf3fc..8485f35f442c 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -122,7 +122,7 @@ export namespace SystemPrompt { export function compaction(providerID: string) { switch (providerID) { case "anthropic": - return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_COMPACTION] + return [PROMPT_COMPACTION] default: return [PROMPT_COMPACTION] } @@ -131,7 +131,7 @@ export namespace SystemPrompt { export function summarize(providerID: string) { switch (providerID) { case "anthropic": - return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_SUMMARIZE] + return [PROMPT_SUMMARIZE] default: return [PROMPT_SUMMARIZE] } @@ -140,7 +140,7 @@ export namespace SystemPrompt { export function title(providerID: string) { switch (providerID) { case "anthropic": - return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_TITLE] + return [PROMPT_TITLE] default: return [PROMPT_TITLE] } From f928325a1b07d0c0da0f73e8347122cdc86d369f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 12 Dec 2025 15:13:24 -0500 Subject: [PATCH 07/19] sync --- packages/opencode/src/cli/cmd/agent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index 812e97423a90..a1b29f9e98de 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -227,8 +227,8 @@ const AgentListCommand = cmd({ async fn() { const agents = await Agent.list() const sortedAgents = agents.sort((a, b) => { - if (a.builtIn !== b.builtIn) { - return a.builtIn ? -1 : 1 + if (a.internal !== b.internal) { + return a.internal ? -1 : 1 } return a.name.localeCompare(b.name) }) From 33d5809157495d32ef272852c63a0f7ea9d895d2 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 12 Dec 2025 15:17:50 -0500 Subject: [PATCH 08/19] sync --- packages/console/app/src/routes/download/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/console/app/src/routes/download/index.tsx b/packages/console/app/src/routes/download/index.tsx index 2616b7ea1342..a7dc97443530 100644 --- a/packages/console/app/src/routes/download/index.tsx +++ b/packages/console/app/src/routes/download/index.tsx @@ -8,8 +8,10 @@ import { Faq } from "~/component/faq" import desktopAppIcon from "../../asset/lander/opencode-desktop-icon.png" import { Legal } from "~/component/legal" import { config } from "~/config" +import { createMemo } from "solid-js" const getLatestRelease = query(async () => { + "use server" const response = await fetch("https://api.github.com/repos/sst/opencode/releases/latest") if (!response.ok) return null const data = await response.json() @@ -29,11 +31,11 @@ export default function Download() { const release = createAsync(() => getLatestRelease(), { deferStream: true, }) - const download = () => { - const version = release() + const download = createMemo(() => { + const version = release() ?? "v1.0.150" if (!version) return null return `https://github.com/sst/opencode/releases/download/${version}` - } + }) const handleCopyClick = (command: string) => (event: Event) => { const button = event.currentTarget as HTMLButtonElement navigator.clipboard.writeText(command) From 3955c9c1b7f94d874cba69091274197395609c95 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 12 Dec 2025 16:23:48 -0500 Subject: [PATCH 09/19] sync --- packages/opencode/src/agent/agent.ts | 15 +++++++++++- .../{session => agent}/prompt/compaction.txt | 0 packages/opencode/src/session/compaction.ts | 23 ++++++++----------- packages/opencode/src/session/prompt.ts | 4 ---- packages/opencode/src/session/summary.ts | 11 ++++----- packages/opencode/src/session/system.ts | 9 -------- 6 files changed, 28 insertions(+), 34 deletions(-) rename packages/opencode/src/{session => agent}/prompt/compaction.txt (100%) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 442531e136e9..0739dd2ad6a2 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -2,11 +2,13 @@ import { Config } from "../config/config" import z from "zod" import { Provider } from "../provider/provider" import { generateObject, type ModelMessage } from "ai" -import PROMPT_GENERATE from "./generate.txt" import { SystemPrompt } from "../session/system" import { Instance } from "../project/instance" import { mergeDeep } from "remeda" +import PROMPT_GENERATE from "./generate.txt" +import PROMPT_COMPACTION from "./prompt/compaction.txt" + export namespace Agent { export const Info = z .object({ @@ -149,6 +151,17 @@ export namespace Agent { mode: "subagent", internal: true, }, + compaction: { + name: "compaction", + mode: "primary", + internal: true, + prompt: PROMPT_COMPACTION, + tools: { + "*": false, + }, + options: {}, + permission: agentPermission, + }, build: { name: "build", tools: { ...defaultTools }, diff --git a/packages/opencode/src/session/prompt/compaction.txt b/packages/opencode/src/agent/prompt/compaction.txt similarity index 100% rename from packages/opencode/src/session/prompt/compaction.txt rename to packages/opencode/src/agent/prompt/compaction.txt diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 821ca015e444..f8ed149ba407 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -5,7 +5,6 @@ import { Identifier } from "../id/id" import { Instance } from "../project/instance" import { Provider } from "../provider/provider" import { MessageV2 } from "./message-v2" -import { SystemPrompt } from "./system" import z from "zod" import { SessionPrompt } from "./prompt" import { Flag } from "../flag/flag" @@ -87,16 +86,14 @@ export namespace SessionCompaction { parentID: string messages: MessageV2.WithParts[] sessionID: string - model: { - providerID: string - modelID: string - } abort: AbortSignal auto: boolean }) { + const userMessage = input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User const agent = await Agent.get("compaction") - const model = await Provider.getModel(input.model.providerID, input.model.modelID) - const system = [...SystemPrompt.compaction(model.providerID)] + const model = agent.model + ? await Provider.getModel(agent.model.providerID, agent.model.modelID) + : await Provider.getModel(userMessage.model.providerID, userMessage.model.modelID) const msg = (await Session.updateMessage({ id: Identifier.ascending("message"), role: "assistant", @@ -116,7 +113,7 @@ export namespace SessionCompaction { reasoning: 0, cache: { read: 0, write: 0 }, }, - modelID: input.model.modelID, + modelID: model.id, providerID: model.providerID, time: { created: Date.now(), @@ -125,16 +122,16 @@ export namespace SessionCompaction { const processor = SessionProcessor.create({ assistantMessage: msg, sessionID: input.sessionID, - model: model, + model, abort: input.abort, }) const result = await processor.process({ - user: input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User, + user: userMessage, agent, abort: input.abort, sessionID: input.sessionID, tools: {}, - system, + system: [], messages: [ ...MessageV2.toModelMessage(input.messages), { @@ -158,8 +155,8 @@ export namespace SessionCompaction { time: { created: Date.now(), }, - agent: input.agent, - model: input.model, + agent: userMessage.agent, + model: userMessage.model, }) await Session.updatePart({ id: Identifier.ascending("part"), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 997366c6caf8..553215a15a52 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -404,10 +404,6 @@ export namespace SessionPrompt { messages: msgs, parentID: lastUser.id, abort, - model: { - providerID: model.providerID, - modelID: model.id, - }, sessionID, auto: task.auto, }) diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index ab6a986862c3..4761c9d2febe 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -130,10 +130,7 @@ export namespace SessionSummary { m.info.role === "assistant" && m.parts.some((p) => p.type === "step-finish" && p.reason !== "tool-calls"), ) ) { - let summary = messages - .findLast((m) => m.info.role === "assistant") - ?.parts.findLast((p) => p.type === "text")?.text - if (!summary || diffs.length > 0) { + if (diffs.length > 0) { for (const msg of messages) { for (const part of msg.parts) { if (part.type === "tool" && part.state.status === "completed") { @@ -167,10 +164,10 @@ export namespace SessionSummary { }, }, }).catch(() => {}) - if (result) summary = result.text + if (result) { + userMsg.summary.body = result.text + } } - userMsg.summary.body = summary - log.info("body", { body: summary }) await Session.updateMessage(userMsg) } } diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 8485f35f442c..5f3cfb141131 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -119,15 +119,6 @@ export namespace SystemPrompt { return Promise.all(found).then((result) => result.filter(Boolean)) } - export function compaction(providerID: string) { - switch (providerID) { - case "anthropic": - return [PROMPT_COMPACTION] - default: - return [PROMPT_COMPACTION] - } - } - export function summarize(providerID: string) { switch (providerID) { case "anthropic": From 2fbdbe1dd14028887d24734103649335fb635881 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 12 Dec 2025 16:44:00 -0500 Subject: [PATCH 10/19] sync --- packages/opencode/src/agent/agent.ts | 19 ++++++++------- packages/opencode/src/cli/cmd/agent.ts | 4 ++-- .../cli/cmd/tui/component/dialog-agent.tsx | 2 +- .../cmd/tui/component/prompt/autocomplete.tsx | 2 +- .../src/cli/cmd/tui/context/local.tsx | 2 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 8 +++---- packages/sdk/js/src/v2/gen/types.gen.ts | 24 ++++++++++--------- 7 files changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 0739dd2ad6a2..8da8da730616 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -15,7 +15,8 @@ export namespace Agent { name: z.string(), description: z.string().optional(), mode: z.enum(["subagent", "primary", "all"]), - internal: z.boolean(), + native: z.boolean().optional(), + hidden: z.boolean().optional(), topP: z.number().optional(), temperature: z.number().optional(), color: z.string().optional(), @@ -114,7 +115,8 @@ export namespace Agent { options: {}, permission: agentPermission, mode: "subagent", - internal: true, + native: true, + hidden: true, }, explore: { name: "explore", @@ -149,12 +151,13 @@ export namespace Agent { options: {}, permission: agentPermission, mode: "subagent", - internal: true, + native: true, }, compaction: { name: "compaction", mode: "primary", - internal: true, + native: true, + hidden: true, prompt: PROMPT_COMPACTION, tools: { "*": false, @@ -168,13 +171,13 @@ export namespace Agent { options: {}, permission: agentPermission, mode: "primary", - internal: true, + native: true, }, summary: { name: "summary", mode: "subagent", options: {}, - internal: true, + native: true, permission: agentPermission, prompt: `You are a title generator. You output ONLY a thread title. Nothing else. @@ -223,7 +226,7 @@ Your output must be: ...defaultTools, }, mode: "primary", - internal: true, + native: true, }, } for (const [key, value] of Object.entries(cfg.agent ?? {})) { @@ -239,7 +242,7 @@ Your output must be: permission: agentPermission, options: {}, tools: {}, - internal: false, + native: false, } const { name, diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index a1b29f9e98de..2cbcfbfe94b5 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -227,8 +227,8 @@ const AgentListCommand = cmd({ async fn() { const agents = await Agent.list() const sortedAgents = agents.sort((a, b) => { - if (a.internal !== b.internal) { - return a.internal ? -1 : 1 + if (a.native !== b.native) { + return a.native ? -1 : 1 } return a.name.localeCompare(b.name) }) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx index 65aaeb22bf98..97f0c0ad95bb 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx @@ -12,7 +12,7 @@ export function DialogAgent() { return { value: item.name, title: item.name, - description: item.builtIn ? "native" : item.description, + description: item.internal ? "native" : item.description, } }), ) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index c40aa114ac83..37e6ccda5de0 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -184,7 +184,7 @@ export function Autocomplete(props: { const agents = createMemo(() => { const agents = sync.data.agent return agents - .filter((agent) => !agent.builtIn && agent.mode !== "primary") + .filter((agent) => !agent.hidden && agent.mode !== "primary") .map( (agent): AutocompleteOption => ({ display: "@" + agent.name, diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 6cc97e04167e..f04b79685c11 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -52,7 +52,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) const agent = iife(() => { - const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent")) + const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden)) const [agentStore, setAgentStore] = createStore<{ current: string }>({ diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 90df76c22349..16fe07ae4a8e 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -1203,10 +1203,10 @@ export class Session extends HeyApiClient { } agent?: string noReply?: boolean - system?: string tools?: { [key: string]: boolean } + system?: string parts?: Array }, options?: Options, @@ -1222,8 +1222,8 @@ export class Session extends HeyApiClient { { in: "body", key: "model" }, { in: "body", key: "agent" }, { in: "body", key: "noReply" }, - { in: "body", key: "system" }, { in: "body", key: "tools" }, + { in: "body", key: "system" }, { in: "body", key: "parts" }, ], }, @@ -1289,10 +1289,10 @@ export class Session extends HeyApiClient { } agent?: string noReply?: boolean - system?: string tools?: { [key: string]: boolean } + system?: string parts?: Array }, options?: Options, @@ -1308,8 +1308,8 @@ export class Session extends HeyApiClient { { in: "body", key: "model" }, { in: "body", key: "agent" }, { in: "body", key: "noReply" }, - { in: "body", key: "system" }, { in: "body", key: "tools" }, + { in: "body", key: "system" }, { in: "body", key: "parts" }, ], }, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9d0bbcc92cd7..148a7b62a0d5 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -147,6 +147,7 @@ export type AssistantMessage = { modelID: string providerID: string mode: string + agent: string path: { cwd: string root: string @@ -504,13 +505,6 @@ export type EventSessionIdle = { } } -export type EventSessionCompacted = { - type: "session.compacted" - properties: { - sessionID: string - } -} - export type EventFileEdited = { type: "file.edited" properties: { @@ -545,6 +539,13 @@ export type EventTodoUpdated = { } } +export type EventSessionCompacted = { + type: "session.compacted" + properties: { + sessionID: string + } +} + export type EventCommandExecuted = { type: "command.executed" properties: { @@ -747,9 +748,9 @@ export type Event = | EventPermissionReplied | EventSessionStatus | EventSessionIdle - | EventSessionCompacted | EventFileEdited | EventTodoUpdated + | EventSessionCompacted | EventCommandExecuted | EventSessionCreated | EventSessionUpdated @@ -1734,7 +1735,8 @@ export type Agent = { name: string description?: string mode: "subagent" | "primary" | "all" - builtIn: boolean + native?: boolean + hidden?: boolean topP?: number temperature?: number color?: string @@ -2797,10 +2799,10 @@ export type SessionPromptData = { } agent?: string noReply?: boolean - system?: string tools?: { [key: string]: boolean } + system?: string parts: Array } path: { @@ -2892,10 +2894,10 @@ export type SessionPromptAsyncData = { } agent?: string noReply?: boolean - system?: string tools?: { [key: string]: boolean } + system?: string parts: Array } path: { From 9d6db6d4a6804b51a3909ca7ae480c0f189b7c98 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 12 Dec 2025 16:44:35 -0500 Subject: [PATCH 11/19] sync --- packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx index 97f0c0ad95bb..365a22445b4b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx @@ -12,7 +12,7 @@ export function DialogAgent() { return { value: item.name, title: item.name, - description: item.internal ? "native" : item.description, + description: item.native ? "native" : item.description, } }), ) From 7618267bdbd66f29d21580dfb65e269d6fb47f7d Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 12 Dec 2025 16:55:51 -0500 Subject: [PATCH 12/19] fixed --- packages/opencode/src/session/llm.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index f0f48ce6679a..2b481e5c27d9 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -38,6 +38,7 @@ export namespace LLM { .tag("modelID", input.model.id) .tag("sessionID", input.sessionID) .tag("small", (input.small ?? false).toString()) + .tag("agent", input.agent.name) l.info("stream", { modelID: input.model.id, providerID: input.model.providerID, @@ -46,11 +47,11 @@ export namespace LLM { const [first, ...rest] = [ ...SystemPrompt.header(input.model.providerID), - ...(input.agent.prompt ?? SystemPrompt.provider(input.model)), + ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)), ...input.system, ...(input.user.system ? [input.user.system] : []), ] - const system = [first, rest.join("\n")] + const system = [first, rest.join("\n")].filter((x) => x) const params = await Plugin.trigger( "chat.params", From f4e6e29372ef421894939cea8430bebd1d46d1db Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 12 Dec 2025 17:20:13 -0500 Subject: [PATCH 13/19] sync --- packages/opencode/src/session/llm.ts | 4 +++ packages/opencode/src/session/summary.ts | 35 ++++++++++-------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 2b481e5c27d9..87cb8d770c32 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -46,9 +46,13 @@ export namespace LLM { const [language, cfg] = await Promise.all([Provider.getLanguage(input.model), Config.get()]) const [first, ...rest] = [ + // header prompt for providers with auth checks ...SystemPrompt.header(input.model.providerID), + // use agent prompt otherwise provider prompt ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)), + // any custom prompt passed into this call ...input.system, + // any custom prompt from last user message ...(input.user.system ? [input.user.system] : []), ] const system = [first, rest.join("\n")].filter((x) => x) diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 4761c9d2febe..de9adfbb3ec1 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -15,6 +15,8 @@ import { Instance } from "@/project/instance" import { Storage } from "@/storage/storage" import { Bus } from "@/bus" import { mergeDeep, pipe } from "remeda" +import { LLM } from "./llm" +import { Agent } from "@/agent/agent" export namespace SessionSummary { const log = Log.create({ service: "session.summary" }) @@ -89,16 +91,12 @@ export namespace SessionSummary { const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart if (textPart && !userMsg.summary?.title) { - const result = await generateText({ - maxOutputTokens: small.capabilities.reasoning ? 1500 : 20, - providerOptions: ProviderTransform.providerOptions(small, options), + const stream = await LLM.stream({ + agent: await Agent.get("summary"), + user: userMsg, + tools: {}, + model: small, messages: [ - ...SystemPrompt.title(small.providerID).map( - (x): ModelMessage => ({ - role: "system", - content: x, - }), - ), { role: "user" as const, content: ` @@ -109,18 +107,15 @@ export namespace SessionSummary { `, }, ], - headers: small.headers, - model: language, - experimental_telemetry: { - isEnabled: cfg.experimental?.openTelemetry, - metadata: { - userId: cfg.username ?? "unknown", - sessionId: assistantMsg.sessionID, - }, - }, + small: true, + abort: new AbortController().signal, + sessionID: userMsg.sessionID, + system: [], + retries: 3, }) - log.info("title", { title: result.text }) - userMsg.summary.title = result.text + const result = await stream.text + log.info("title", { title: result }) + userMsg.summary.title = result await Session.updateMessage(userMsg) } From 9a769bfd8cbe8b8a1afed41756cdce7900c37f7b Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 12 Dec 2025 17:56:35 -0500 Subject: [PATCH 14/19] centralize title and summary prompts into agent system --- packages/opencode/src/agent/agent.ts | 75 +++++-------------- .../opencode/src/agent/prompt/explore.txt | 18 +++++ .../prompt/summary.txt} | 0 .../src/{session => agent}/prompt/title.txt | 4 +- packages/opencode/src/session/summary.ts | 63 +++++++--------- packages/opencode/src/session/system.ts | 21 +----- 6 files changed, 63 insertions(+), 118 deletions(-) create mode 100644 packages/opencode/src/agent/prompt/explore.txt rename packages/opencode/src/{session/prompt/summarize.txt => agent/prompt/summary.txt} (100%) rename packages/opencode/src/{session => agent}/prompt/title.txt (84%) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 8da8da730616..ef007df136a5 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -8,6 +8,9 @@ import { mergeDeep } from "remeda" import PROMPT_GENERATE from "./generate.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt" +import PROMPT_EXPLORE from "./prompt/explore.txt" +import PROMPT_SUMMARY from "./prompt/summary.txt" +import PROMPT_TITLE from "./prompt/title.txt" export namespace Agent { export const Info = z @@ -128,26 +131,7 @@ export namespace Agent { ...defaultTools, }, description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`, - prompt: [ - `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.`, - ``, - `Your strengths:`, - `- Rapidly finding files using glob patterns`, - `- Searching code and text with powerful regex patterns`, - `- Reading and analyzing file contents`, - ``, - `Guidelines:`, - `- Use Glob for broad file pattern matching`, - `- Use Grep for searching file contents with regex`, - `- Use Read when you know the specific file path you need to read`, - `- Use Bash for file operations like copying, moving, or listing directory contents`, - `- Adapt your search approach based on the thoroughness level specified by the caller`, - `- Return file paths as absolute paths in your final response`, - `- For clear communication, avoid using emojis`, - `- Do not create any files, or run bash commands that modify the user's system state in any way`, - ``, - `Complete the user's search request efficiently and report your findings clearly.`, - ].join("\n"), + prompt: PROMPT_EXPLORE, options: {}, permission: agentPermission, mode: "subagent", @@ -173,49 +157,24 @@ export namespace Agent { mode: "primary", native: true, }, + title: { + name: "title", + mode: "primary", + options: {}, + native: true, + hidden: true, + permission: agentPermission, + prompt: PROMPT_TITLE, + tools: {}, + }, summary: { name: "summary", - mode: "subagent", + mode: "primary", options: {}, native: true, + hidden: true, permission: agentPermission, - prompt: `You are a title generator. You output ONLY a thread title. Nothing else. - - -Generate a brief title that would help the user find this conversation later. - -Follow all rules in -Use the so you know what a good title looks like. -Your output must be: -- A single line -- ≤50 characters -- No explanations - - - -- Focus on the main topic or question the user needs to retrieve -- Use -ing verbs for actions (Debugging, Implementing, Analyzing) -- Keep exact: technical terms, numbers, filenames, HTTP codes -- Remove: the, this, my, a, an -- Never assume tech stack -- Never use tools -- NEVER respond to questions, just generate a title for the conversation -- The title should NEVER include "summarizing" or "generating" when generating a title -- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT -- Always output something meaningful, even if the input is minimal. -- If the user message is short or conversational (e.g. “hello”, “lol”, “whats up”, “hey”): - → create a title that reflects the user’s tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.) - - - -"hey" -> Greeting -"debug 500 errors in production" → Debugging production 500 errors -"refactor user service" → Refactoring user service -"why is app.js failing" → Analyzing app.js failure -"implement rate limiting" → Implementing rate limiting -"how do I connect postgres to my API" → Connecting Postgres to API -"best practices for React hooks" → React hooks best practices -`, + prompt: PROMPT_SUMMARY, tools: {}, }, plan: { diff --git a/packages/opencode/src/agent/prompt/explore.txt b/packages/opencode/src/agent/prompt/explore.txt new file mode 100644 index 000000000000..5761077cbd88 --- /dev/null +++ b/packages/opencode/src/agent/prompt/explore.txt @@ -0,0 +1,18 @@ +You are a file search specialist. You excel at thoroughly navigating and exploring codebases. + +Your strengths: +- Rapidly finding files using glob patterns +- Searching code and text with powerful regex patterns +- Reading and analyzing file contents + +Guidelines: +- Use Glob for broad file pattern matching +- Use Grep for searching file contents with regex +- Use Read when you know the specific file path you need to read +- Use Bash for file operations like copying, moving, or listing directory contents +- Adapt your search approach based on the thoroughness level specified by the caller +- Return file paths as absolute paths in your final response +- For clear communication, avoid using emojis +- Do not create any files, or run bash commands that modify the user's system state in any way + +Complete the user's search request efficiently and report your findings clearly. diff --git a/packages/opencode/src/session/prompt/summarize.txt b/packages/opencode/src/agent/prompt/summary.txt similarity index 100% rename from packages/opencode/src/session/prompt/summarize.txt rename to packages/opencode/src/agent/prompt/summary.txt diff --git a/packages/opencode/src/session/prompt/title.txt b/packages/opencode/src/agent/prompt/title.txt similarity index 84% rename from packages/opencode/src/session/prompt/title.txt rename to packages/opencode/src/agent/prompt/title.txt index e297dc460b1d..f67aaa95bac5 100644 --- a/packages/opencode/src/session/prompt/title.txt +++ b/packages/opencode/src/agent/prompt/title.txt @@ -22,8 +22,8 @@ Your output must be: - The title should NEVER include "summarizing" or "generating" when generating a title - DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT - Always output something meaningful, even if the input is minimal. -- If the user message is short or conversational (e.g. “hello”, “lol”, “whats up”, “hey”): - → create a title that reflects the user’s tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.) +- If the user message is short or conversational (e.g. "hello", "lol", "whats up", "hey"): + → create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.) diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index de9adfbb3ec1..83519307a32d 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -1,20 +1,19 @@ import { Provider } from "@/provider/provider" -import { Config } from "@/config/config" + import { fn } from "@/util/fn" import z from "zod" import { Session } from "." -import { generateText, type ModelMessage } from "ai" + import { MessageV2 } from "./message-v2" import { Identifier } from "@/id/id" import { Snapshot } from "@/snapshot" -import { ProviderTransform } from "@/provider/transform" -import { SystemPrompt } from "./system" + import { Log } from "@/util/log" import path from "path" import { Instance } from "@/project/instance" import { Storage } from "@/storage/storage" import { Bus } from "@/bus" -import { mergeDeep, pipe } from "remeda" + import { LLM } from "./llm" import { Agent } from "@/agent/agent" @@ -63,7 +62,6 @@ 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), ) @@ -80,22 +78,16 @@ export namespace SessionSummary { const small = (await Provider.getSmallModel(assistantMsg.providerID)) ?? (await Provider.getModel(assistantMsg.providerID, assistantMsg.modelID)) - const language = await Provider.getLanguage(small) - - const options = pipe( - {}, - mergeDeep(ProviderTransform.options(small, assistantMsg.sessionID)), - mergeDeep(ProviderTransform.smallOptions(small)), - mergeDeep(small.options), - ) 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") const stream = await LLM.stream({ - agent: await Agent.get("summary"), + agent, user: userMsg, tools: {}, - model: small, + model: agent.model ? await Provider.getModel(agent.model.providerID, agent.model.modelID) : small, + small: true, messages: [ { role: "user" as const, @@ -107,7 +99,6 @@ export namespace SessionSummary { `, }, ], - small: true, abort: new AbortController().signal, sessionID: userMsg.sessionID, system: [], @@ -133,34 +124,30 @@ export namespace SessionSummary { } } } - const result = await generateText({ - model: language, - maxOutputTokens: 100, - providerOptions: ProviderTransform.providerOptions(small, options), + const summaryAgent = await Agent.get("summary") + const stream = await LLM.stream({ + agent: summaryAgent, + user: userMsg, + tools: {}, + model: summaryAgent.model + ? await Provider.getModel(summaryAgent.model.providerID, summaryAgent.model.modelID) + : small, + small: true, messages: [ - ...SystemPrompt.summarize(small.providerID).map( - (x): ModelMessage => ({ - role: "system", - content: x, - }), - ), ...MessageV2.toModelMessage(messages), { - role: "user", + role: "user" as const, content: `Summarize the above conversation according to your system prompts.`, }, ], - headers: small.headers, - experimental_telemetry: { - isEnabled: cfg.experimental?.openTelemetry, - metadata: { - userId: cfg.username ?? "unknown", - sessionId: assistantMsg.sessionID, - }, - }, - }).catch(() => {}) + abort: new AbortController().signal, + sessionID: userMsg.sessionID, + system: [], + retries: 3, + }) + const result = await stream.text if (result) { - userMsg.summary.body = result.text + userMsg.summary.body = result } } await Session.updateMessage(userMsg) diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 5f3cfb141131..e15185b38b7e 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -14,8 +14,7 @@ import PROMPT_BEAST from "./prompt/beast.txt" import PROMPT_GEMINI from "./prompt/gemini.txt" import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt" -import PROMPT_SUMMARIZE from "./prompt/summarize.txt" -import PROMPT_TITLE from "./prompt/title.txt" + import PROMPT_CODEX from "./prompt/codex.txt" import type { Provider } from "@/provider/provider" @@ -118,22 +117,4 @@ export namespace SystemPrompt { ) return Promise.all(found).then((result) => result.filter(Boolean)) } - - export function summarize(providerID: string) { - switch (providerID) { - case "anthropic": - return [PROMPT_SUMMARIZE] - default: - return [PROMPT_SUMMARIZE] - } - } - - export function title(providerID: string) { - switch (providerID) { - case "anthropic": - return [PROMPT_TITLE] - default: - return [PROMPT_TITLE] - } - } } From cd3085802fde4e6dd877efeee5efd118e08b96e9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 12 Dec 2025 18:02:18 -0500 Subject: [PATCH 15/19] core: hide internal agents from desktop agent selector --- packages/desktop/src/context/local.tsx | 2 +- packages/opencode/src/session/prompt.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 39fd1f98744e..181a4d247470 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -78,7 +78,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) const agent = (() => { - const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent")) + const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden)) const [store, setStore] = createStore<{ current: string }>({ diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 31421f535aec..4ec01b56e824 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1295,12 +1295,12 @@ 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 agent = await Agent.get("summary") + const agent = await Agent.get("title") if (!agent) return const result = await LLM.stream({ agent, user: input.message.info as MessageV2.User, - system: [agent.prompt!], + system: [], small: true, tools: {}, model: await iife(async () => { From 57297d8b9c2c6db6be95c79c6dc4a0ace3d17406 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 12 Dec 2025 18:03:36 -0500 Subject: [PATCH 16/19] sync --- packages/opencode/src/tool/bash.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 52084eb3fbea..ff18e15e394d 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -78,11 +78,7 @@ const getShell = lazy(() => { // TODO: we may wanna rename this tool so it works better on other shells export const BashTool = Tool.define("bash", async () => { -<<<<<<< HEAD - const shell = getShell() -======= const shell = Shell.acceptable() ->>>>>>> dev log.info("bash tool using shell", { shell }) return { From 3d813f03e5cfb2a72c93804cf9712aedad029c4b Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 14 Dec 2025 16:50:59 -0500 Subject: [PATCH 17/19] sync --- packages/opencode/src/session/llm.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 9d43d908dc8f..781ff8384884 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -73,6 +73,7 @@ export namespace LLM { : undefined, topP: input.agent.topP ?? ProviderTransform.topP(input.model), options: pipe( + {}, mergeDeep(ProviderTransform.options(input.model, input.sessionID)), input.small ? mergeDeep(ProviderTransform.smallOptions(input.model)) : mergeDeep({}), mergeDeep(input.model.options), From 521fba8ce39ac45db0d82905de92adbf4a08452a Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 14 Dec 2025 16:55:03 -0500 Subject: [PATCH 18/19] tweaks --- packages/opencode/src/session/llm.ts | 24 +++++++++++++----------- packages/opencode/src/tool/bash.ts | 27 --------------------------- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 781ff8384884..97b8aae2bd8b 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -46,17 +46,19 @@ export namespace LLM { }) const [language, cfg] = await Promise.all([Provider.getLanguage(input.model), Config.get()]) - const [first, ...rest] = [ - // header prompt for providers with auth checks - ...SystemPrompt.header(input.model.providerID), - // use agent prompt otherwise provider prompt - ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)), - // any custom prompt passed into this call - ...input.system, - // any custom prompt from last user message - ...(input.user.system ? [input.user.system] : []), - ] - const system = [first, rest.join("\n")].filter((x) => x) + const system = SystemPrompt.header(input.model.providerID) + system.push( + [ + // use agent prompt otherwise provider prompt + ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)), + // any custom prompt passed into this call + ...input.system, + // any custom prompt from last user message + ...(input.user.system ? [input.user.system] : []), + ] + .filter((x) => x) + .join("\n"), + ) const params = await Plugin.trigger( "chat.params", diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index ff18e15e394d..115d8f8b29d6 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -49,33 +49,6 @@ const parser = lazy(async () => { return p }) -const getShell = lazy(() => { - const s = process.env.SHELL - if (s) { - const basename = path.basename(s) - if (!new Set(["fish", "nu"]).has(basename)) { - return s - } - } - - if (process.platform === "darwin") { - return "/bin/zsh" - } - - if (process.platform === "win32") { - // Let Bun / Node pick COMSPEC (usually cmd.exe) - // or explicitly: - return process.env.COMSPEC || true - } - - const bash = Bun.which("bash") - if (bash) { - return bash - } - - return true -}) - // TODO: we may wanna rename this tool so it works better on other shells export const BashTool = Tool.define("bash", async () => { const shell = Shell.acceptable() From b7e5cbbdc7e23f2c1bcd1c2e3dc124bb22780155 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 14 Dec 2025 21:55:45 +0000 Subject: [PATCH 19/19] chore: format code --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/sdk/js/src/v2/gen/types.gen.ts | 62 ++++----- packages/sdk/openapi.json | 165 ++++++++++++------------ 4 files changed, 119 insertions(+), 112 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 7ec4aebe518e..4a7841908c56 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -24,4 +24,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index a68dd7603942..a180408302d2 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 148a7b62a0d5..571e33ef0a4a 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -476,35 +476,6 @@ export type EventPermissionReplied = { } } -export type SessionStatus = - | { - type: "idle" - } - | { - type: "retry" - attempt: number - message: string - next: number - } - | { - type: "busy" - } - -export type EventSessionStatus = { - type: "session.status" - properties: { - sessionID: string - status: SessionStatus - } -} - -export type EventSessionIdle = { - type: "session.idle" - properties: { - sessionID: string - } -} - export type EventFileEdited = { type: "file.edited" properties: { @@ -539,6 +510,35 @@ export type EventTodoUpdated = { } } +export type SessionStatus = + | { + type: "idle" + } + | { + type: "retry" + attempt: number + message: string + next: number + } + | { + type: "busy" + } + +export type EventSessionStatus = { + type: "session.status" + properties: { + sessionID: string + status: SessionStatus + } +} + +export type EventSessionIdle = { + type: "session.idle" + properties: { + sessionID: string + } +} + export type EventSessionCompacted = { type: "session.compacted" properties: { @@ -746,10 +746,10 @@ export type Event = | EventMessagePartRemoved | EventPermissionUpdated | EventPermissionReplied - | EventSessionStatus - | EventSessionIdle | EventFileEdited | EventTodoUpdated + | EventSessionStatus + | EventSessionIdle | EventSessionCompacted | EventCommandExecuted | EventSessionCreated diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 98c8b3586a70..d3dd438f5b80 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -1997,9 +1997,6 @@ "noReply": { "type": "boolean" }, - "system": { - "type": "string" - }, "tools": { "type": "object", "propertyNames": { @@ -2009,6 +2006,9 @@ "type": "boolean" } }, + "system": { + "type": "string" + }, "parts": { "type": "array", "items": { @@ -2202,9 +2202,6 @@ "noReply": { "type": "boolean" }, - "system": { - "type": "string" - }, "tools": { "type": "object", "propertyNames": { @@ -2214,6 +2211,9 @@ "type": "boolean" } }, + "system": { + "type": "string" + }, "parts": { "type": "array", "items": { @@ -5193,6 +5193,9 @@ "mode": { "type": "string" }, + "agent": { + "type": "string" + }, "path": { "type": "object", "properties": { @@ -5251,6 +5254,7 @@ "modelID", "providerID", "mode", + "agent", "path", "cost", "tokens" @@ -6152,6 +6156,72 @@ }, "required": ["type", "properties"] }, + "Event.file.edited": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "file.edited" + }, + "properties": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + }, + "required": ["file"] + } + }, + "required": ["type", "properties"] + }, + "Todo": { + "type": "object", + "properties": { + "content": { + "description": "Brief description of the task", + "type": "string" + }, + "status": { + "description": "Current status of the task: pending, in_progress, completed, cancelled", + "type": "string" + }, + "priority": { + "description": "Priority level of the task: high, medium, low", + "type": "string" + }, + "id": { + "description": "Unique identifier for the todo item", + "type": "string" + } + }, + "required": ["content", "status", "priority", "id"] + }, + "Event.todo.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "todo.updated" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "todos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": ["sessionID", "todos"] + } + }, + "required": ["type", "properties"] + }, "SessionStatus": { "anyOf": [ { @@ -6255,72 +6325,6 @@ }, "required": ["type", "properties"] }, - "Event.file.edited": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "file.edited" - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - } - }, - "required": ["file"] - } - }, - "required": ["type", "properties"] - }, - "Todo": { - "type": "object", - "properties": { - "content": { - "description": "Brief description of the task", - "type": "string" - }, - "status": { - "description": "Current status of the task: pending, in_progress, completed, cancelled", - "type": "string" - }, - "priority": { - "description": "Priority level of the task: high, medium, low", - "type": "string" - }, - "id": { - "description": "Unique identifier for the todo item", - "type": "string" - } - }, - "required": ["content", "status", "priority", "id"] - }, - "Event.todo.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "todo.updated" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "todos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - } - } - }, - "required": ["sessionID", "todos"] - } - }, - "required": ["type", "properties"] - }, "Event.command.executed": { "type": "object", "properties": { @@ -6887,19 +6891,19 @@ "$ref": "#/components/schemas/Event.permission.replied" }, { - "$ref": "#/components/schemas/Event.session.status" + "$ref": "#/components/schemas/Event.file.edited" }, { - "$ref": "#/components/schemas/Event.session.idle" + "$ref": "#/components/schemas/Event.todo.updated" }, { - "$ref": "#/components/schemas/Event.session.compacted" + "$ref": "#/components/schemas/Event.session.status" }, { - "$ref": "#/components/schemas/Event.file.edited" + "$ref": "#/components/schemas/Event.session.idle" }, { - "$ref": "#/components/schemas/Event.todo.updated" + "$ref": "#/components/schemas/Event.session.compacted" }, { "$ref": "#/components/schemas/Event.command.executed" @@ -8916,7 +8920,10 @@ "type": "string", "enum": ["subagent", "primary", "all"] }, - "builtIn": { + "native": { + "type": "boolean" + }, + "hidden": { "type": "boolean" }, "topP": { @@ -8997,7 +9004,7 @@ "maximum": 9007199254740991 } }, - "required": ["name", "mode", "builtIn", "permission", "tools", "options"] + "required": ["name", "mode", "permission", "tools", "options"] }, "MCPStatusConnected": { "type": "object",