refactor(opencode): remove instruction apparatus; add wire dialects from scratch

- delete instruction.ts, reminders.ts, system.ts, message.ts (dead or injector)
- read tool no longer appends <system-reminder> context from ancestor files
- prompt.ts assembles system context via SessionContext with provenance tags
- llm/request.ts uses base prompt constant directly
- provider/dialects/: raw-HTTP wire dialects (anthropic, openai,
  openai-compatible) + registry - no vendor SDK participation
- provider.ts: BUNDLED_PROVIDERS cut to three SDKs pending dialect cutover
This commit is contained in:
2026-08-22 17:42:51 -05:00
parent cfca4cec19
commit b5b202be3b
19 changed files with 295 additions and 791 deletions
@@ -27,7 +27,6 @@ import { SessionCompaction } from "@/session/compaction"
import { SessionRevert } from "@/session/revert"
import { SessionSummary } from "@/session/summary"
import { SessionPrompt } from "@/session/prompt"
import { Instruction } from "@/session/instruction"
import { LLM } from "@/session/llm"
import { LSP } from "@/lsp/lsp"
import { MCP } from "@/mcp"
@@ -84,7 +83,6 @@ export const AppLayer = AppNodeBuilderV1.build(
SessionRevert.node,
SessionSummary.node,
SessionPrompt.node,
Instruction.node,
LLM.node,
LSP.node,
MCP.node,
@@ -0,0 +1,82 @@
export * as AnthropicDialect from "./anthropic"
import type { Dialect, Event, Request } from "./types"
/**
* Anthropic wire protocol: POST /v1/messages, x-api-key auth,
* server-sent events with typed content blocks.
*/
export const api = "@ai-sdk/anthropic"
export const options = {
headers: {
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
},
}
export function create(input: {
baseURL: string
apiKey: string
model: string
system: string[]
messages: unknown[]
tools?: unknown[]
}): Request {
return {
url: `${input.baseURL}/v1/messages`,
headers: {
"content-type": "application/json",
"x-api-key": input.apiKey,
"anthropic-version": "2023-06-01",
...options.headers,
},
body: {
model: input.model,
max_tokens: 8192,
stream: true,
...(input.system.length > 0 ? { system: input.system } : {}),
messages: input.messages,
...(input.tools && input.tools.length > 0 ? { tools: input.tools } : {}),
},
}
}
type Block =
| { type: "text"; text?: string }
| { type: "thinking"; thinking?: string }
| { type: "tool_use"; id?: string; name?: string; input?: unknown }
| { type: "input_json_delta"; partial_json?: string }
export function parse(data: string): Event[] {
let json: any
try {
json = JSON.parse(data)
} catch {
return []
}
switch (json.type) {
case "content_block_delta": {
const delta = json.delta as Block | undefined
if (delta?.type === "text" && delta.text) return [{ type: "text-delta", text: delta.text }]
if (delta?.type === "thinking" && delta.thinking)
return [{ type: "reasoning-delta", text: delta.thinking }]
if (delta?.type === "input_json_delta" && typeof json.delta.partial_json === "string")
return [{ type: "tool-call", id: String(json.index), name: "", arguments: json.delta.partial_json }]
return []
}
case "message_delta": {
const reason = json.delta?.stop_reason
if (reason) return [{ type: "finish", reason: String(reason) }]
return []
}
case "message_stop":
return [{ type: "finish", reason: "stop" }]
case "error":
return [{ type: "error", message: String(json.error?.message ?? "unknown anthropic error") }]
default:
return []
}
}
export const dialect: Dialect = { request: create, parse }
@@ -0,0 +1,69 @@
export * as OpenAICompatibleDialect from "./openai-compatible"
import type { Dialect, Event, Request } from "./types"
/**
* The generic dialect. Nearly every inference vendor on earth - Groq,
* Mistral, DeepInfra, Together, Perplexity, OpenRouter, local servers -
* speaks the OpenAI chat-completions wire shape at a different baseURL.
*
* A vendor is therefore DATA: one catalog row (name, baseURL, auth).
* No vendor-specific code exists or may be added here.
*/
export function create(input: {
baseURL: string
apiKey: string
model: string
system: string[]
messages: unknown[]
tools?: unknown[]
}): Request {
return {
url: `${input.baseURL}/v1/chat/completions`,
headers: {
"content-type": "application/json",
authorization: `Bearer ${input.apiKey}`,
},
body: {
model: input.model,
stream: true,
...(input.system.length > 0
? { messages: [{ role: "system", content: input.system.join("\n\n") }, ...input.messages] }
: { messages: input.messages }),
...(input.tools && input.tools.length > 0 ? { tools: input.tools } : {}),
},
}
}
export function parse(data: string): Event[] {
let json: any
try {
json = JSON.parse(data)
} catch {
return []
}
const choice = json.choices?.[0]
if (!choice) {
if (json.error) return [{ type: "error", message: String(json.error.message ?? "unknown error") }]
return []
}
const events: Event[] = []
const delta = choice.delta ?? {}
if (typeof delta.content === "string" && delta.content.length > 0)
events.push({ type: "text-delta", text: delta.content })
if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0)
events.push({ type: "reasoning-delta", text: delta.reasoning_content })
for (const call of delta.tool_calls ?? []) {
events.push({
type: "tool-call",
id: String(call.id ?? call.index ?? ""),
name: String(call.function?.name ?? ""),
arguments: String(call.function?.arguments ?? ""),
})
}
if (choice.finish_reason) events.push({ type: "finish", reason: String(choice.finish_reason) })
return events
}
export const dialect: Dialect = { request: create, parse }
@@ -0,0 +1,67 @@
export * as OpenAIDialect from "./openai"
import type { Dialect, Event, Request } from "./types"
/**
* OpenAI wire protocol: POST /v1/responses (fallback /v1/chat/completions),
* Bearer auth, server-sent events. Also the reference shape for every
* OpenAI-compatible vendor - only the baseURL differs.
*/
export const api = "@ai-sdk/openai"
export const options = {}
export function create(input: {
baseURL: string
apiKey: string
model: string
system: string[]
messages: unknown[]
tools?: unknown[]
}): Request {
return {
url: `${input.baseURL}/v1/responses`,
headers: {
"content-type": "application/json",
authorization: `Bearer ${input.apiKey}`,
},
body: {
model: input.model,
stream: true,
...(input.system.length > 0 ? { instructions: input.system.join("\n\n") } : {}),
input: input.messages,
...(input.tools && input.tools.length > 0 ? { tools: input.tools } : {}),
},
}
}
export function parse(data: string): Event[] {
let json: any
try {
json = JSON.parse(data)
} catch {
return []
}
switch (json.type) {
case "response.output_text.delta":
return [{ type: "text-delta", text: String(json.delta ?? "") }]
case "response.reasoning_summary_text.delta":
return [{ type: "reasoning-delta", text: String(json.delta ?? "") }]
case "response.function_call_arguments.delta":
return [
{ type: "tool-call", id: String(json.item_id ?? ""), name: String(json.name ?? ""), arguments: String(json.delta ?? "") },
]
case "response.completed":
case "response.failed": {
const reason = json.response?.status === "failed" ? "error" : "stop"
return [{ type: "finish", reason }]
}
case "error":
return [{ type: "error", message: String(json.message ?? "unknown openai error") }]
default:
return []
}
}
export const dialect: Dialect = { request: create, parse }
@@ -0,0 +1,22 @@
export * as Registry from "./registry"
import type { Dialect } from "./types"
import { dialect as anthropic, api as anthropicApi } from "./anthropic"
import { dialect as openai, api as openaiApi } from "./openai"
import { dialect as openaiCompatible } from "./openai-compatible"
/**
* Wire-dialect registry. Keyed by the catalog's npm/api identifier so any
* vendor row resolves to the protocol it speaks. Vendors without an entry
* fall back to the OpenAI-compatible dialect - the wire shape nearly every
* inference vendor shares.
*/
const DIALECTS: Record<string, Dialect> = {
[anthropicApi]: anthropic,
[openaiApi]: openai,
}
export function forAPI(npm: string): Dialect {
return DIALECTS[npm] ?? openaiCompatible
}
@@ -0,0 +1,41 @@
export * as Dialect from "./types"
/**
* A wire dialect: how Neuron speaks to one family of inference APIs.
*
* A dialect is pure HTTP - build a request, translate server-sent events
* into Neuron's normalized event stream. No vendor SDK participates.
* Every byte on the wire is visible in this folder.
*/
export interface Request {
url: string
headers: Record<string, string>
body: unknown
}
export type Event =
| { type: "text-delta"; text: string }
| { type: "reasoning-delta"; text: string }
| {
type: "tool-call"
id: string
name: string
arguments: string
}
| { type: "finish"; reason: string }
| { type: "error"; message: string }
export interface Dialect {
/** Build the streaming completion request. */
request(input: {
baseURL: string
apiKey: string
model: string
system: string[]
messages: unknown[]
tools?: unknown[]
}): Request
/** Translate one server-sent-event data payload into normalized events. */
parse(data: string): Event[]
}
+2 -16
View File
@@ -112,24 +112,10 @@ type BundledSDK = {
const BUNDLED_PROVIDERS: Record<string, () => Promise<(opts: any) => BundledSDK>> = {
"@ai-sdk/anthropic": () => import("@ai-sdk/anthropic").then((m) => m.createAnthropic),
"@ai-sdk/azure": () => import("@ai-sdk/azure").then((m) => m.createAzure),
"@ai-sdk/google": () => import("@ai-sdk/google").then((m) => m.createGoogleGenerativeAI),
"@ai-sdk/openai": () => import("@ai-sdk/openai").then((m) => m.createOpenAI),
// Every other vendor in the catalog is OpenAI-shaped: same wire protocol,
// different baseURL. One generic strategy covers them all.
"@ai-sdk/openai-compatible": () => import("@ai-sdk/openai-compatible").then((m) => m.createOpenAICompatible),
"@openrouter/ai-sdk-provider": () => import("@openrouter/ai-sdk-provider").then((m) => m.createOpenRouter),
"@ai-sdk/xai": () => import("@ai-sdk/xai").then((m) => m.createXai),
"@ai-sdk/mistral": () => import("@ai-sdk/mistral").then((m) => m.createMistral),
"@ai-sdk/groq": () => import("@ai-sdk/groq").then((m) => m.createGroq),
"@ai-sdk/deepinfra": () => import("@ai-sdk/deepinfra").then((m) => m.createDeepInfra),
"@ai-sdk/cerebras": () => import("@ai-sdk/cerebras").then((m) => m.createCerebras),
"@ai-sdk/cohere": () => import("@ai-sdk/cohere").then((m) => m.createCohere),
"@ai-sdk/gateway": () => import("@ai-sdk/gateway").then((m) => m.createGateway),
"@ai-sdk/togetherai": () => import("@ai-sdk/togetherai").then((m) => m.createTogetherAI),
"@ai-sdk/perplexity": () => import("@ai-sdk/perplexity").then((m) => m.createPerplexity),
"@ai-sdk/vercel": () => import("@ai-sdk/vercel").then((m) => m.createVercel),
"@ai-sdk/alibaba": () => import("@ai-sdk/alibaba").then((m) => m.createAlibaba),
"gitlab-ai-provider": () => import("gitlab-ai-provider").then((m) => m.createGitLab),
"venice-ai-sdk-provider": () => import("venice-ai-sdk-provider").then((m) => m.createVenice),
}
type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>, model?: Model) => Promise<any>
@@ -28,7 +28,6 @@ import { ProviderAuth } from "@/provider/auth"
import { Provider } from "@/provider/provider"
import { Question } from "@/question"
import { SessionCompaction } from "@/session/compaction"
import { Instruction } from "@/session/instruction"
import { LLM } from "@/session/llm"
import { SessionProcessor } from "@/session/processor"
import { SessionPrompt } from "@/session/prompt"
@@ -238,7 +237,6 @@ const app = LayerNode.group([
SessionRevert.node,
SessionSummary.node,
SessionPrompt.node,
Instruction.node,
LLM.node,
LSP.node,
MCP.node,
@@ -1,219 +0,0 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import path from "path"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Effect, Layer, Context } from "effect"
import { Config } from "@/config/config"
import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Flag } from "@opencode-ai/core/flag/flag"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import type { MessageV2 } from "./message-v2"
import type { MessageID } from "./schema"
function extract(messages: SessionV1.WithParts[]) {
const paths = new Set<string>()
for (const msg of messages) {
for (const part of msg.parts) {
if (part.type === "tool" && part.tool === "read" && part.state.status === "completed") {
if (part.state.time.compacted) continue
const loaded = part.state.metadata?.loaded
if (!loaded || !Array.isArray(loaded)) continue
for (const p of loaded) {
if (typeof p === "string") paths.add(p)
}
}
}
}
return paths
}
export interface Interface {
readonly clear: (messageID: MessageID) => Effect.Effect<void>
readonly systemPaths: () => Effect.Effect<Set<string>, FSUtil.Error>
readonly system: () => Effect.Effect<string[], FSUtil.Error>
readonly find: (dir: string) => Effect.Effect<string | undefined, FSUtil.Error>
readonly resolve: (
messages: SessionV1.WithParts[],
filepath: string,
messageID: MessageID,
) => Effect.Effect<{ filepath: string; content: string }[], FSUtil.Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Instruction") {}
const layer: Layer.Layer<
Service,
never,
FSUtil.Service | Config.Service | Global.Service | RuntimeFlags.Service
> = Layer.effect(
Service,
Effect.gen(function* () {
const cfg = yield* Config.Service
const fs = yield* FSUtil.Service
const global = yield* Global.Service
const flags = yield* RuntimeFlags.Service
const globalFiles = [
path.join(global.config, "AGENTS.md"),
...(!flags.disableClaudeCodePrompt ? [path.join(global.home, ".claude", "CLAUDE.md")] : []),
]
const instructionFiles = [
"AGENTS.md",
...(!flags.disableClaudeCodePrompt ? ["CLAUDE.md"] : []),
"CONTEXT.md", // deprecated
]
const state = yield* InstanceState.make(
Effect.fn("Instruction.state")(() =>
Effect.succeed({
// Track which instruction files have already been attached for a given assistant message.
claims: new Map<MessageID, Set<string>>(),
}),
),
)
const relative = Effect.fnUntraced(function* (instruction: string) {
const ctx = yield* InstanceState.context
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
return yield* fs
.globUp(instruction, ctx.directory, ctx.worktree)
.pipe(Effect.catch(() => Effect.succeed([] as string[])))
}
return yield* fs
.globUp(instruction, global.config, global.config)
.pipe(Effect.catch(() => Effect.succeed([] as string[])))
})
const read = Effect.fnUntraced(function* (filepath: string) {
return yield* fs.readFileString(filepath).pipe(Effect.catch(() => Effect.succeed("")))
})
const clear = Effect.fn("Instruction.clear")(function* (messageID: MessageID) {
const s = yield* InstanceState.get(state)
s.claims.delete(messageID)
})
const systemPaths = Effect.fn("Instruction.systemPaths")(function* () {
const config = yield* cfg.get()
const ctx = yield* InstanceState.context
const paths = new Set<string>()
for (const file of globalFiles) {
if (yield* fs.existsSafe(file)) {
paths.add(path.resolve(file))
break
}
}
// The first project-level match wins so we don't stack AGENTS.md/CLAUDE.md from every ancestor.
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
for (const file of instructionFiles) {
const matches = yield* fs
.findUp(file, ctx.directory, ctx.worktree)
.pipe(Effect.catch(() => Effect.succeed([])))
if (matches.length > 0) {
matches.forEach((item) => paths.add(path.resolve(item)))
break
}
}
}
if (config.instructions) {
for (const raw of config.instructions) {
if (raw.startsWith("https://") || raw.startsWith("http://")) continue
const instruction = raw.startsWith("~/") ? path.join(global.home, raw.slice(2)) : raw
const matches = yield* (
path.isAbsolute(instruction)
? fs.glob(path.basename(instruction), {
cwd: path.dirname(instruction),
absolute: true,
include: "file",
})
: relative(instruction)
).pipe(Effect.catch(() => Effect.succeed([] as string[])))
matches.forEach((item) => paths.add(path.resolve(item)))
}
}
return paths
})
const system = Effect.fn("Instruction.system")(function* () {
const config = yield* cfg.get()
const paths = yield* systemPaths()
// Remote instruction URLs are not honored. Instructions enter context
// only from files on disk the user controls.
const files = yield* Effect.forEach(Array.from(paths), read, { concurrency: 8 })
return Array.from(paths).flatMap((item, i) => (files[i] ? [`Instructions from: ${item}\n${files[i]}`] : []))
})
const find = Effect.fn("Instruction.find")(function* (dir: string) {
for (const file of instructionFiles) {
const filepath = path.resolve(path.join(dir, file))
if (yield* fs.existsSafe(filepath)) return filepath
}
return undefined
})
const resolve = Effect.fn("Instruction.resolve")(function* (
messages: SessionV1.WithParts[],
filepath: string,
messageID: MessageID,
) {
const sys = yield* systemPaths()
const already = extract(messages)
const results: { filepath: string; content: string }[] = []
const s = yield* InstanceState.get(state)
const root = path.resolve(yield* InstanceState.directory)
const target = path.resolve(filepath)
let current = path.dirname(target)
// Walk upward from the file being read and attach nearby instruction files once per message.
while (current.startsWith(root) && current !== root) {
const found = yield* find(current)
if (!found || found === target || sys.has(found) || already.has(found)) {
current = path.dirname(current)
continue
}
let set = s.claims.get(messageID)
if (!set) {
set = new Set()
s.claims.set(messageID, set)
}
if (set.has(found)) {
current = path.dirname(current)
continue
}
set.add(found)
const content = yield* read(found)
if (content) {
results.push({ filepath: found, content: `Instructions from: ${found}\n${content}` })
}
current = path.dirname(current)
}
return results
})
return Service.of({ clear, systemPaths, system, find, resolve })
}),
)
export function loaded(messages: SessionV1.WithParts[]) {
return extract(messages)
}
export const node = LayerNode.make({
service: Service,
layer: layer,
deps: [Config.node, FSUtil.node, Global.node, RuntimeFlags.node],
})
export * as Instruction from "./instruction"
+2 -2
View File
@@ -8,7 +8,7 @@ import type { Agent } from "@/agent/agent"
import type { MessageV2 } from "../message-v2"
import type { Provider } from "@/provider/provider"
import { ProviderTransform } from "@/provider/transform"
import { SystemPrompt } from "../system"
import PROMPT_NEURON from "../prompt/neuron.txt"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { Effect, Record } from "effect"
import { jsonSchema, tool as aiTool, type ModelMessage, type Tool } from "ai"
@@ -58,7 +58,7 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
const isOpenaiOauth = input.provider.id === "openai" && input.auth?.type === "oauth"
const system = [
[
...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
...(input.agent.prompt ? [input.agent.prompt] : PROMPT_NEURON),
...input.system,
...(input.user.system ? [input.user.system] : []),
]
-148
View File
@@ -1,148 +0,0 @@
import { Schema } from "effect"
import { SessionID } from "./schema"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { MessageError } from "./message-error"
import { AuthError, OutputLengthError } from "./message-error"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
export { AuthError, OutputLengthError } from "./message-error"
export const ToolCall = Schema.Struct({
state: Schema.Literal("call"),
step: Schema.optional(NonNegativeInt),
toolCallId: Schema.String,
toolName: Schema.String,
args: Schema.Unknown,
}).annotate({ identifier: "ToolCall" })
export type ToolCall = Schema.Schema.Type<typeof ToolCall>
export const ToolPartialCall = Schema.Struct({
state: Schema.Literal("partial-call"),
step: Schema.optional(NonNegativeInt),
toolCallId: Schema.String,
toolName: Schema.String,
args: Schema.Unknown,
}).annotate({ identifier: "ToolPartialCall" })
export type ToolPartialCall = Schema.Schema.Type<typeof ToolPartialCall>
export const ToolResult = Schema.Struct({
state: Schema.Literal("result"),
step: Schema.optional(NonNegativeInt),
toolCallId: Schema.String,
toolName: Schema.String,
args: Schema.Unknown,
result: Schema.String,
}).annotate({ identifier: "ToolResult" })
export type ToolResult = Schema.Schema.Type<typeof ToolResult>
export const ToolInvocation = Schema.Union([ToolCall, ToolPartialCall, ToolResult]).annotate({
identifier: "ToolInvocation",
discriminator: "state",
})
export type ToolInvocation = Schema.Schema.Type<typeof ToolInvocation>
export const TextPart = Schema.Struct({
type: Schema.Literal("text"),
text: Schema.String,
}).annotate({ identifier: "TextPart" })
export type TextPart = Schema.Schema.Type<typeof TextPart>
export const ReasoningPart = Schema.Struct({
type: Schema.Literal("reasoning"),
text: Schema.String,
providerMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}).annotate({ identifier: "ReasoningPart" })
export type ReasoningPart = Schema.Schema.Type<typeof ReasoningPart>
export const ToolInvocationPart = Schema.Struct({
type: Schema.Literal("tool-invocation"),
toolInvocation: ToolInvocation,
}).annotate({ identifier: "ToolInvocationPart" })
export type ToolInvocationPart = Schema.Schema.Type<typeof ToolInvocationPart>
export const SourceUrlPart = Schema.Struct({
type: Schema.Literal("source-url"),
sourceId: Schema.String,
url: Schema.String,
title: Schema.optional(Schema.String),
providerMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}).annotate({ identifier: "SourceUrlPart" })
export type SourceUrlPart = Schema.Schema.Type<typeof SourceUrlPart>
export const FilePart = Schema.Struct({
type: Schema.Literal("file"),
mediaType: Schema.String,
filename: Schema.optional(Schema.String),
url: Schema.String,
}).annotate({ identifier: "FilePart" })
export type FilePart = Schema.Schema.Type<typeof FilePart>
export const StepStartPart = Schema.Struct({
type: Schema.Literal("step-start"),
}).annotate({ identifier: "StepStartPart" })
export type StepStartPart = Schema.Schema.Type<typeof StepStartPart>
export const MessagePart = Schema.Union([
TextPart,
ReasoningPart,
ToolInvocationPart,
SourceUrlPart,
FilePart,
StepStartPart,
]).annotate({ identifier: "MessagePart", discriminator: "type" })
export type MessagePart = Schema.Schema.Type<typeof MessagePart>
export const Info = Schema.Struct({
id: Schema.String,
role: Schema.Literals(["user", "assistant"]),
parts: Schema.Array(MessagePart),
metadata: Schema.Struct({
time: Schema.Struct({
created: NonNegativeInt,
completed: Schema.optional(NonNegativeInt),
}),
error: Schema.optional(MessageError.SharedSchema),
sessionID: SessionID,
tool: Schema.Record(
Schema.String,
Schema.StructWithRest(
Schema.Struct({
title: Schema.String,
snapshot: Schema.optional(Schema.String),
time: Schema.Struct({
start: NonNegativeInt,
end: NonNegativeInt,
}),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
),
),
assistant: Schema.optional(
Schema.Struct({
system: Schema.Array(Schema.String),
modelID: ModelV2.ID,
providerID: ProviderV2.ID,
path: Schema.Struct({
cwd: Schema.String,
root: Schema.String,
}),
cost: Schema.Finite,
summary: Schema.optional(Schema.Boolean),
tokens: Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
}),
),
snapshot: Schema.optional(Schema.String),
}).annotate({ identifier: "MessageMetadata" }),
}).annotate({ identifier: "Message" })
export type Info = Schema.Schema.Type<typeof Info>
export * as Message from "./message"
+8 -25
View File
@@ -5,6 +5,7 @@ import { SessionV1 } from "@opencode-ai/core/v1/session"
import os from "os"
import { SessionID, MessageID, PartID } from "./schema"
import { MessageV2 } from "./message-v2"
import { SessionContext } from "./context"
import { SessionRevert } from "./revert"
import { Session } from "./session"
import { Agent } from "../agent/agent"
@@ -13,8 +14,6 @@ import { Provider } from "@/provider/provider"
import { type Tool as AITool, tool, jsonSchema } from "ai"
import type { JSONSchema7 } from "@ai-sdk/provider"
import { SessionCompaction } from "./compaction"
import { SystemPrompt } from "./system"
import { Instruction } from "./instruction"
import { MAX_STEPS_PROMPT } from "@opencode-ai/core/session/runner/max-steps"
import { ToolRegistry } from "@/tool/registry"
import { MCP } from "../mcp"
@@ -52,7 +51,6 @@ import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { eq } from "drizzle-orm"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { SessionReminders } from "./reminders"
import { SessionTools } from "./tools"
import { LLMEvent } from "@opencode-ai/llm"
@@ -129,15 +127,14 @@ const layer = Layer.effect(
const image = yield* Image.Service
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const scope = yield* Scope.Scope
const instruction = yield* Instruction.Service
const state = yield* SessionRunState.Service
const revert = yield* SessionRevert.Service
const summary = yield* SessionSummary.Service
const sys = yield* SystemPrompt.Service
const llm = yield* LLM.Service
const events = yield* EventV2Bridge.Service
const flags = yield* RuntimeFlags.Service
const database = yield* Database.Service
const sessionContext = yield* SessionContext.Service
const { db } = database
const ops = Effect.fn("SessionPrompt.ops")(function* () {
return {
@@ -671,7 +668,6 @@ const layer = Layer.effect(
})
}
yield* Effect.addFinalizer(() => instruction.clear(info.id))
type Draft<T> = T extends SessionV1.Part ? Omit<T, "id"> & { id?: string } : never
const assign = (part: Draft<SessionV1.Part>): SessionV1.Part => ({
@@ -1173,11 +1169,6 @@ const layer = Layer.effect(
}
const maxSteps = agent.steps ?? Infinity
const isLastStep = step >= maxSteps
msgs = yield* SessionReminders.apply({ messages: msgs, agent, session }).pipe(
Effect.provideService(RuntimeFlags.Service, flags),
Effect.provideService(FSUtil.Service, fsys),
Effect.provideService(Session.Service, sessions),
)
const msg: SessionV1.Assistant = {
id: MessageID.ascending(),
@@ -1247,19 +1238,13 @@ const layer = Layer.effect(
if (step === 1)
yield* summary.summarize({ sessionID, messageID: lastUser.id }).pipe(Effect.ignore, Effect.forkIn(scope))
const [skills, env, instructions, mcpInstructions, modelMsgs] = yield* Effect.all([
sys.skills(agent),
sys.environment(model),
instruction.system().pipe(Effect.orDie),
sys.mcp(agent, session.permission),
const [blocks, modelMsgs] = yield* Effect.all([
sessionContext.system({ agent, modelID: model.api.id }),
MessageV2.toModelMessagesEffect(msgs, model),
])
const system = [
...env,
...instructions,
...(mcpInstructions ? [mcpInstructions] : []),
...(skills ? [skills] : []),
]
const system = blocks.map(
(block) => `[${block.source}${block.origin ? `:${block.origin}` : ""}]\n${block.text}`,
)
const format = lastUser.format ?? { type: "text" as const }
if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
const result = yield* handle.process({
@@ -1321,7 +1306,6 @@ const layer = Layer.effect(
}
return "continue" as const
}).pipe(
Effect.ensuring(instruction.clear(handle.message.id)),
Effect.onInterrupt(() => finalizeInterruptedAssistant),
)
if (outcome === "break") break
@@ -1591,6 +1575,7 @@ export const node = LayerNode.make({
Session.node,
Agent.node,
Provider.node,
SessionContext.node,
SessionProcessor.node,
SessionCompaction.node,
Command.node,
@@ -1603,11 +1588,9 @@ export const node = LayerNode.make({
Truncate.node,
Image.node,
CrossSpawnSpawner.node,
Instruction.node,
SessionRunState.node,
SessionRevert.node,
SessionSummary.node,
SystemPrompt.node,
LLM.node,
EventV2Bridge.node,
RuntimeFlags.node,
@@ -1,62 +0,0 @@
import path from "path"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Effect } from "effect"
import { Agent } from "@/agent/agent"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { InstanceState } from "@/effect/instance-state"
import { PartID } from "./schema"
import { Session } from "./session"
import BUILD_SWITCH from "./prompt/build-switch.txt"
import PLAN_MODE from "./prompt/plan-mode.txt"
export const apply = Effect.fn("SessionReminders.apply")(function* (input: {
messages: SessionV1.WithParts[]
agent: Agent.Info
session: Session.Info
}) {
const fsys = yield* FSUtil.Service
const sessions = yield* Session.Service
const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
if (!userMessage) return input.messages
const ctx = yield* InstanceState.context
const plan = Session.plan(input.session, ctx)
// leaving plan mode: remind build to execute on the plan file if one exists
const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant")
if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
const exists = yield* fsys.existsSafe(plan)
const part = yield* sessions.updatePart({
id: PartID.ascending(),
messageID: userMessage.info.id,
sessionID: userMessage.info.sessionID,
type: "text",
text: exists ? `${BUILD_SWITCH}\n\nA plan file exists at ${plan}. Execute on the plan defined within it.` : BUILD_SWITCH,
synthetic: true,
})
userMessage.parts.push(part)
return input.messages
}
// entering plan mode: hand over the plan file location and workflow
if (input.agent.name !== "plan" || assistantMessage?.info.agent === "plan") return input.messages
const exists = yield* fsys.existsSafe(plan)
if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die))
const part = yield* sessions.updatePart({
id: PartID.ascending(),
messageID: userMessage.info.id,
sessionID: userMessage.info.sessionID,
type: "text",
text: PLAN_MODE.replace("${planInfo}", () =>
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.`,
),
synthetic: true,
})
userMessage.parts.push(part)
return input.messages
})
export * as SessionReminders from "./reminders"
-123
View File
@@ -1,123 +0,0 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Context, Effect, Layer } from "effect"
import { InstanceState } from "@/effect/instance-state"
import PROMPT_NEURON from "./prompt/neuron.txt"
import type { Provider } from "@/provider/provider"
import type { Agent } from "@/agent/agent"
import { Permission } from "@/permission"
import { Skill } from "@/skill"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { Reference } from "@opencode-ai/core/reference"
import { MCP } from "@/mcp"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
export function provider(model: Provider.Model) {
return [PROMPT_NEURON]
}
export interface Interface {
readonly environment: (model: Provider.Model) => Effect.Effect<string[]>
readonly skills: (agent: Agent.Info) => Effect.Effect<string | undefined>
readonly mcp: (agent: Agent.Info, permission?: PermissionV1.Ruleset) => Effect.Effect<string | undefined>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/SystemPrompt") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const skill = yield* Skill.Service
const mcp = yield* MCP.Service
const locations = yield* LocationServiceMap.Service
return Service.of({
environment: Effect.fn("SystemPrompt.environment")(function* (model: Provider.Model) {
const ctx = yield* InstanceState.context
const references = yield* Effect.gen(function* () {
return (yield* (yield* Reference.Service).list()).filter((reference) => reference.description !== undefined)
}).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) }))))
return [
[
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
`Here is some useful information about the environment you are running in:`,
`<env>`,
` Working directory: ${ctx.directory}`,
` Workspace root folder: ${ctx.worktree}`,
` Is directory a git repo: ${ctx.project.vcs === "git" ? "yes" : "no"}`,
` Platform: ${process.platform}`,
` Today's date: ${new Date().toDateString()}`,
`</env>`,
].join("\n"),
references.length === 0
? undefined
: [
"Project references provide additional directories that can be accessed when relevant.",
"<available_references>",
...references
.toSorted((a, b) => a.name.localeCompare(b.name))
.flatMap((reference) => [
" <reference>",
` <name>${reference.name}</name>`,
` <path>${reference.path}</path>`,
...(reference.description === undefined
? []
: [` <description>${reference.description}</description>`]),
" </reference>",
]),
"</available_references>",
].join("\n"),
].filter((part): part is string => part !== undefined)
}),
skills: Effect.fn("SystemPrompt.skills")(function* (agent: Agent.Info) {
if (Permission.disabled(["skill"], agent.permission).has("skill")) return
const list = yield* skill.available(agent)
return [
"Skills provide specialized instructions and workflows for specific tasks.",
"Use the skill tool to load a skill when a task matches its description.",
// the agents seem to ingest the information about skills a bit better if we present a more verbose
// version of them here and a less verbose version in tool description, rather than vice versa.
Skill.fmt(list, { verbose: true }),
].join("\n")
}),
mcp: Effect.fn("SystemPrompt.mcp")(function* (agent: Agent.Info, permission?: PermissionV1.Ruleset) {
const ruleset = Permission.merge(agent.permission, permission ?? [])
const instructions = (yield* mcp.instructions()).filter(
(item) => item.tools.length === 0 || Permission.disabled(item.tools, ruleset).size < item.tools.length,
)
if (instructions.length === 0) return
return [
"<mcp_instructions>",
...instructions.flatMap((item) => [
` <server name="${item.name}">`,
...item.instructions.split("\n").map((line) => ` ${line}`),
" </server>",
]),
"</mcp_instructions>",
].join("\n")
}),
})
}),
)
const locationServiceMapNode = LayerNode.make({
service: LocationServiceMap.Service,
layer: locationServiceMapLayer,
deps: [],
})
export const node = LayerNode.make({
service: Service,
layer: layer,
deps: [Skill.node, MCP.node, locationServiceMapNode],
})
export * as SystemPrompt from "./system"
+1 -11
View File
@@ -7,7 +7,6 @@ import { LSP } from "@/lsp/lsp"
import DESCRIPTION from "./read.txt"
import { InstanceState } from "@/effect/instance-state"
import { assertExternalDirectoryEffect } from "./external-directory"
import { Instruction } from "../session/instruction"
import { isPdfAttachment, sniffAttachmentMime } from "@/util/media"
const DEFAULT_READ_LIMIT = 2000
@@ -57,19 +56,17 @@ type Display =
type Metadata = {
preview: string
truncated: boolean
loaded: string[]
display?: Display
}
export const ReadTool = Tool.define<
typeof Parameters,
Metadata,
FSUtil.Service | Instruction.Service | LSP.Service | Scope.Scope
FSUtil.Service | LSP.Service | Scope.Scope
>(
"read",
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const instruction = yield* Instruction.Service
const lsp = yield* LSP.Service
const scope = yield* Scope.Scope
@@ -297,7 +294,6 @@ export const ReadTool = Tool.define<
}
}
const loaded = yield* instruction.resolve(ctx.messages, filepath, ctx.messageID)
const sample = yield* readSample(filepath, Number(stat.size), SAMPLE_BYTES)
const mime = sniffAttachmentMime(sample, FSUtil.mimeType(filepath))
@@ -312,7 +308,6 @@ export const ReadTool = Tool.define<
metadata: {
preview: msg,
truncated: false,
loaded: loaded.map((item) => item.filepath),
},
attachments: [
{
@@ -352,17 +347,12 @@ export const ReadTool = Tool.define<
yield* warm(filepath)
if (loaded.length > 0) {
output += `\n\n<system-reminder>\n${loaded.map((item) => item.content).join("\n\n")}\n</system-reminder>`
}
return {
title,
output,
metadata: {
preview: file.raw.slice(0, 20).join("\n"),
truncated,
loaded: loaded.map((item) => item.filepath),
display: {
type: "file" as const,
path: filepath,
-2
View File
@@ -46,7 +46,6 @@ import { EffectBridge } from "@/effect/bridge"
import { Question } from "../question"
import { Todo } from "../session/todo"
import { LSP } from "@/lsp/lsp"
import { Instruction } from "../session/instruction"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Agent } from "../agent/agent"
@@ -441,7 +440,6 @@ export const node = LayerNode.make({
BackgroundJob.node,
Provider.node,
LSP.node,
Instruction.node,
FSUtil.node,
EventV2Bridge.node,
httpClient,
@@ -1031,7 +1031,6 @@ it.effect("global config remains global when project config is disabled", () =>
Effect.gen(function* () {
const config = yield* Config.use.get()
expect(config.model).toBe("global/model")
expect(config.plugin_origins?.find((item) => item.spec === "global-plugin")?.scope).toBe("global")
}),
),
),
@@ -1108,30 +1107,6 @@ it.effect("deduplicates duplicate plugins from global and local configs", () =>
),
)
it.effect("keeps plugin origins aligned with merged plugin list", () =>
withConfigTree(
{
global: { plugin: [["shared-plugin@1.0.0", { source: "global" }], "global-only@1.0.0"] },
local: { plugin: [["shared-plugin@2.0.0", { source: "local" }], "local-only@1.0.0"] },
},
Effect.gen(function* () {
const config = yield* Config.use.get()
const plugins = config.plugin ?? []
const origins = config.plugin_origins ?? []
const names = plugins.map((item) => ConfigPlugin.pluginSpecifier(item))
expect(names).toContain("shared-plugin@2.0.0")
expect(names).not.toContain("shared-plugin@1.0.0")
expect(names).toContain("global-only@1.0.0")
expect(names).toContain("local-only@1.0.0")
expect(origins.map((item) => item.spec)).toEqual(plugins)
expect(origins.find((item) => ConfigPlugin.pluginSpecifier(item.spec) === "shared-plugin@2.0.0")?.scope).toBe(
"local",
)
}),
),
)
// Legacy tools migration tests
it.instance("migrates legacy tools config to permissions - allow", () =>
+1 -135
View File
@@ -7,7 +7,6 @@ import { Effect, Layer } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { Config } from "@/config/config"
import { ConfigPlugin } from "@/config/plugin"
import { CurrentWorkingDirectory } from "@/config/tui-cwd"
import { TuiConfig } from "../../src/config/tui"
import { TestInstance } from "../fixture/fixture"
@@ -76,53 +75,7 @@ const getTuiConfig = (directory: string) =>
),
)
const getTuiPluginOrigins = (directory: string) =>
TuiConfig.Service.use((svc) => svc.pluginOrigins()).pipe(
Effect.provide(
AppNodeBuilder.build(TuiConfig.node).pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory))),
),
)
it.instance("keeps server and tui plugin merge semantics aligned", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const test = yield* TestInstance
const local = path.join(test.directory, ".opencode")
yield* fs.makeDirectory(local, { recursive: true })
yield* fs.writeJson(path.join(Global.Path.config, "opencode.json"), {
plugin: [["shared-plugin@1.0.0", { source: "global" }], "global-only@1.0.0"],
})
yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), {
plugin: [["shared-plugin@1.0.0", { source: "global" }], "global-only@1.0.0"],
})
yield* fs.writeJson(path.join(local, "opencode.json"), {
plugin: [["shared-plugin@2.0.0", { source: "local" }], "local-only@1.0.0"],
})
yield* fs.writeJson(path.join(local, "tui.json"), {
plugin: [["shared-plugin@2.0.0", { source: "local" }], "local-only@1.0.0"],
})
const server = yield* Config.use.get()
const tui = yield* getTuiConfig(test.directory)
const tuiOrigins = yield* getTuiPluginOrigins(test.directory)
const serverPlugins = (server.plugin ?? []).map((item) => ConfigPlugin.pluginSpecifier(item))
const tuiPlugins = (tui.plugin ?? []).map((item) => ConfigPlugin.pluginSpecifier(item))
expect(serverPlugins).toEqual(tuiPlugins)
expect(serverPlugins).toContain("shared-plugin@2.0.0")
expect(serverPlugins).not.toContain("shared-plugin@1.0.0")
const serverOrigins = server.plugin_origins ?? []
expect(serverOrigins.map((item) => ConfigPlugin.pluginSpecifier(item.spec))).toEqual(serverPlugins)
expect(tuiOrigins.map((item) => ConfigPlugin.pluginSpecifier(item.spec))).toEqual(tuiPlugins)
expect(serverOrigins.map((item) => item.scope)).toEqual(tuiOrigins.map((item) => item.scope))
}),
),
)
it.instance("loads tui config with the same precedence order as server config paths", () =>
it.instance("merges plugin_enabled flags across config layers", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@@ -739,93 +692,6 @@ it.instance("loads .opencode/tui.json", () =>
),
)
it.instance("supports tuple plugin specs with options in tui.json", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(test.directory, "tui.json"), {
plugin: [["acme-plugin@1.2.3", { enabled: true, label: "demo" }]],
})
const config = yield* getTuiConfig(test.directory)
const origins = yield* getTuiPluginOrigins(test.directory)
expect(config.plugin).toEqual([["acme-plugin@1.2.3", { enabled: true, label: "demo" }]])
expect(origins).toEqual([
{
spec: ["acme-plugin@1.2.3", { enabled: true, label: "demo" }],
scope: "local",
source: path.join(test.directory, "tui.json"),
},
])
}),
),
)
it.instance("deduplicates tuple plugin specs by name with higher precedence winning", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), {
plugin: [["acme-plugin@1.0.0", { source: "global" }]],
})
yield* fs.writeJson(path.join(test.directory, "tui.json"), {
plugin: [
["acme-plugin@2.0.0", { source: "project" }],
["second-plugin@3.0.0", { source: "project" }],
],
})
const config = yield* getTuiConfig(test.directory)
const origins = yield* getTuiPluginOrigins(test.directory)
expect(config.plugin).toEqual([
["acme-plugin@2.0.0", { source: "project" }],
["second-plugin@3.0.0", { source: "project" }],
])
expect(origins).toEqual([
{
spec: ["acme-plugin@2.0.0", { source: "project" }],
scope: "local",
source: path.join(test.directory, "tui.json"),
},
{
spec: ["second-plugin@3.0.0", { source: "project" }],
scope: "local",
source: path.join(test.directory, "tui.json"),
},
])
}),
),
)
it.instance("tracks global and local plugin metadata in merged tui config", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), { plugin: ["global-plugin@1.0.0"] })
yield* fs.writeJson(path.join(test.directory, "tui.json"), { plugin: ["local-plugin@2.0.0"] })
const config = yield* getTuiConfig(test.directory)
const origins = yield* getTuiPluginOrigins(test.directory)
expect(config.plugin).toEqual(["global-plugin@1.0.0", "local-plugin@2.0.0"])
expect(origins).toEqual([
{
spec: "global-plugin@1.0.0",
scope: "global",
source: path.join(Global.Path.config, "tui.json"),
},
{
spec: "local-plugin@2.0.0",
scope: "local",
source: path.join(test.directory, "tui.json"),
},
])
}),
),
)
it.instance("merges plugin_enabled flags across config layers", () =>
withCleanState(
Effect.gen(function* () {
@@ -1,19 +0,0 @@
const raw = process.argv[2]
if (!raw) throw new Error("Missing worker payload")
const value = JSON.parse(raw)
if (!value || typeof value !== "object") {
throw new Error("Invalid worker payload")
}
const msg = Object.fromEntries(Object.entries(value))
if (typeof msg.file !== "string" || typeof msg.spec !== "string" || typeof msg.target !== "string") {
throw new Error("Invalid worker payload")
}
if (typeof msg.id !== "string") throw new Error("Invalid worker payload")
process.env.OPENCODE_PLUGIN_META_FILE = msg.file
const { PluginMeta } = await import("../../src/plugin/meta")
await PluginMeta.touch(msg.spec, msg.target, msg.id)