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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,10 @@ export default function Page() {
description: language.t("command.terminal.new.description"),
category: language.t("command.category.terminal"),
keybind: "ctrl+alt+t",
onSelect: () => terminal.new(),
onSelect: () => {
if (terminal.all().length > 0) terminal.new()
view().terminal.open()
},
},
{
id: "steps.toggle",
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,7 @@ export namespace ACP {
}
}

async setSessionModel(params: SetSessionModelRequest) {
async unstable_setSessionModel(params: SetSessionModelRequest) {
const session = this.sessionManager.get(params.sessionId)

const model = Provider.parseModel(params.modelId)
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,11 @@ export namespace ProviderTransform {
result["reasoningSummary"] = "auto"
}
}

if (input.model.providerID === "venice") {
result["promptCacheKey"] = input.sessionID
}

return result
}

Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/session/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export namespace LLM {
topP: params.topP,
topK: params.topK,
providerOptions: ProviderTransform.providerOptions(input.model, params.options),
activeTools: Object.keys(tools).filter((x) => x !== "invalid" && x !== "_noop"),
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
tools,
maxOutputTokens,
abortSignal: input.abort,
Expand Down
75 changes: 55 additions & 20 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,40 @@ export namespace MessageV2 {

export function toModelMessages(input: WithParts[], model: Provider.Model): ModelMessage[] {
const result: UIMessage[] = []
const toolNames = new Set<string>()

const toModelOutput = (output: unknown) => {
if (typeof output === "string") {
return { type: "text", value: output }
}

if (typeof output === "object") {
const outputObject = output as {
text: string
attachments?: Array<{ mime: string; url: string }>
}
const attachments = (outputObject.attachments ?? []).filter((attachment) => {
return attachment.url.startsWith("data:") && attachment.url.includes(",")
})

return {
type: "content",
value: [
{ type: "text", text: outputObject.text },
...attachments.map((attachment) => ({
type: "media",
mediaType: attachment.mime,
data: iife(() => {
const commaIndex = attachment.url.indexOf(",")
return commaIndex === -1 ? attachment.url : attachment.url.slice(commaIndex + 1)
}),
})),
],
}
}

return { type: "json", value: output as never }
}

for (const msg of input) {
if (msg.parts.length === 0) continue
Expand Down Expand Up @@ -505,31 +539,24 @@ export namespace MessageV2 {
type: "step-start",
})
if (part.type === "tool") {
toolNames.add(part.tool)
if (part.state.status === "completed") {
if (part.state.attachments?.length) {
result.push({
id: Identifier.ascending("message"),
role: "user",
parts: [
{
type: "text",
text: `The tool ${part.tool} returned the following attachments:`,
},
...part.state.attachments.map((attachment) => ({
type: "file" as const,
url: attachment.url,
mediaType: attachment.mime,
filename: attachment.filename,
})),
],
})
}
const outputText = part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output
const attachments = part.state.time.compacted ? [] : (part.state.attachments ?? [])
const output =
attachments.length > 0
? {
text: outputText,
attachments,
}
: outputText

assistantMessage.parts.push({
type: ("tool-" + part.tool) as `tool-${string}`,
state: "output-available",
toolCallId: part.callID,
input: part.state.input,
output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
output,
...(differentModel ? {} : { callProviderMetadata: part.metadata }),
})
}
Expand Down Expand Up @@ -568,7 +595,15 @@ export namespace MessageV2 {
}
}

return convertToModelMessages(result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")))
const tools = Object.fromEntries(Array.from(toolNames).map((toolName) => [toolName, { toModelOutput }]))

return convertToModelMessages(
result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")),
{
//@ts-expect-error (convertToModelMessages expects a ToolSet but only actually needs tools[name]?.toModelOutput)
tools,
},
)
}

export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
Expand Down
14 changes: 1 addition & 13 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,12 +722,6 @@ export namespace SessionPrompt {
)
return result
},
toModelOutput(result) {
return {
type: "text",
value: result.output,
}
},
})
}

Expand Down Expand Up @@ -819,12 +813,6 @@ export namespace SessionPrompt {
content: result.content, // directly return content to preserve ordering when outputting to model
}
}
item.toModelOutput = (result) => {
return {
type: "text",
value: result.output,
}
}
tools[key] = item
}

Expand Down Expand Up @@ -1263,7 +1251,7 @@ export namespace SessionPrompt {
sessionID: userMessage.info.sessionID,
type: "text",
text: `<system-reminder>
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
## Plan File Info:
${exists ? `A plan file already exists at ${plan}. You can read it and make incremental edits using the edit tool.` : `No plan file exists yet. You should create your plan at ${plan} using the write tool.`}
Expand Down
51 changes: 51 additions & 0 deletions packages/opencode/test/acp/agent-interface.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, test } from "bun:test"
import { ACP } from "../../src/acp/agent"
import type { Agent as ACPAgent } from "@agentclientprotocol/sdk"

/**
* Type-level test: This line will fail to compile if ACP.Agent
* doesn't properly implement the ACPAgent interface.
*
* The SDK checks for methods like `agent.unstable_setSessionModel` at runtime
* and throws "Method not found" if they're missing. TypeScript allows optional
* interface methods to be omitted, but the SDK still expects them.
*
* @see https://github.com/agentclientprotocol/typescript-sdk/commit/7072d3f
*/
type _AssertAgentImplementsACPAgent = ACP.Agent extends ACPAgent ? true : never
const _typeCheck: _AssertAgentImplementsACPAgent = true

/**
* Runtime verification that optional methods the SDK expects are actually implemented.
* The SDK's router checks `if (!agent.methodName)` and throws MethodNotFound if missing.
*/
describe("acp.agent interface compliance", () => {
// Extract method names from the ACPAgent interface type
type ACPAgentMethods = keyof ACPAgent

// Methods that the SDK's router explicitly checks for at runtime
const sdkCheckedMethods: ACPAgentMethods[] = [
// Required
"initialize",
"newSession",
"prompt",
"cancel",
// Optional but checked by SDK router
"loadSession",
"setSessionMode",
"authenticate",
// Unstable - SDK checks these with unstable_ prefix
"unstable_listSessions",
"unstable_forkSession",
"unstable_resumeSession",
"unstable_setSessionModel",
]

test("Agent implements all SDK-checked methods", () => {
for (const method of sdkCheckedMethods) {
expect(typeof ACP.Agent.prototype[method as keyof typeof ACP.Agent.prototype], `Missing method: ${method}`).toBe(
"function",
)
}
})
})
24 changes: 9 additions & 15 deletions packages/opencode/test/session/message-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ describe("session.message-v2.toModelMessage", () => {
])
})

test("converts assistant tool completion into tool-call + tool-result messages and emits attachment message", () => {
test("converts assistant tool completion into tool-call + tool-result messages with attachments", () => {
const userID = "m-user"
const assistantID = "m-assistant"

Expand Down Expand Up @@ -304,7 +304,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "file",
mime: "image/png",
filename: "attachment.png",
url: "https://example.com/attachment.png",
url: "data:image/png;base64,Zm9v",
},
],
},
Expand All @@ -319,18 +319,6 @@ describe("session.message-v2.toModelMessage", () => {
role: "user",
content: [{ type: "text", text: "run tool" }],
},
{
role: "user",
content: [
{ type: "text", text: "The tool bash returned the following attachments:" },
{
type: "file",
mediaType: "image/png",
filename: "attachment.png",
data: "https://example.com/attachment.png",
},
],
},
{
role: "assistant",
content: [
Expand All @@ -352,7 +340,13 @@ describe("session.message-v2.toModelMessage", () => {
type: "tool-result",
toolCallId: "call-1",
toolName: "bash",
output: { type: "text", value: "ok" },
output: {
type: "content",
value: [
{ type: "text", text: "ok" },
{ type: "media", mediaType: "image/png", data: "Zm9v" },
],
},
providerOptions: { openai: { tool: "meta" } },
},
],
Expand Down
Loading