Compare commits

..

2 Commits

Author SHA1 Message Date
Kit Langton 746381008d test(session): clarify compaction test harness 2026-05-10 20:23:37 -04:00
Kit Langton 1e7586a93f test(session): effectify remaining compaction process tests 2026-05-10 20:12:56 -04:00
47 changed files with 324 additions and 2424 deletions
+17 -17
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -85,7 +85,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -120,7 +120,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -147,7 +147,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48",
@@ -171,7 +171,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -195,7 +195,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.14.48",
"version": "1.14.46",
"bin": {
"opencode": "./bin/opencode",
},
@@ -229,7 +229,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -283,7 +283,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -313,7 +313,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -329,7 +329,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@effect/platform-node": "catalog:",
"effect": "catalog:",
@@ -342,7 +342,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2",
@@ -360,7 +360,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.14.48",
"version": "1.14.46",
"bin": {
"opencode": "./bin/opencode",
},
@@ -496,7 +496,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"effect": "catalog:",
@@ -534,7 +534,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -549,7 +549,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -584,7 +584,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -633,7 +633,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.14.48",
"version": "1.14.46",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.14.48",
"version": "1.14.46",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.14.48",
"version": "1.14.46",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.14.48",
"version": "1.14.46",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.14.48",
"version": "1.14.46",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.48",
"version": "1.14.46",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.14.48",
"version": "1.14.46",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.14.48",
"version": "1.14.46",
"private": true,
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.14.48"
version = "1.14.46"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.48/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.48/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.48/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.48/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.48/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.14.48",
"version": "1.14.46",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.48",
"version": "1.14.46",
"name": "@opencode-ai/http-recorder",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.48",
"version": "1.14.46",
"name": "@opencode-ai/llm",
"type": "module",
"license": "MIT",
@@ -16,7 +16,6 @@ import {
type ToolResultPart,
} from "../schema"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
import * as Cache from "./utils/cache"
import { ToolStream } from "./utils/tool-stream"
const ADAPTER = "anthropic-messages"
@@ -26,10 +25,7 @@ export const PATH = "/messages"
// =============================================================================
// Request Body Schema
// =============================================================================
const AnthropicCacheControl = Schema.Struct({
type: Schema.tag("ephemeral"),
ttl: Schema.optional(Schema.Literals(["5m", "1h"])),
})
const AnthropicCacheControl = Schema.Struct({ type: Schema.tag("ephemeral") })
const AnthropicTextBlock = Schema.Struct({
type: Schema.tag("text"),
@@ -197,24 +193,8 @@ const invalid = ProviderShared.invalidRequest
// =============================================================================
// Request Lowering
// =============================================================================
// Anthropic accepts at most 4 explicit cache_control breakpoints per request,
// across `tools`, `system`, and `messages`. Beyond the cap the API returns a
// 400 — so the lowering layer counts emitted markers and silently drops any
// that exceed it.
const ANTHROPIC_BREAKPOINT_CAP = 4
const EPHEMERAL_5M = { type: "ephemeral" as const }
const EPHEMERAL_1H = { type: "ephemeral" as const, ttl: "1h" as const }
const cacheControl = (breakpoints: Cache.Breakpoints, cache: CacheHint | undefined) => {
if (cache?.type !== "ephemeral" && cache?.type !== "persistent") return undefined
if (breakpoints.remaining <= 0) {
breakpoints.dropped += 1
return undefined
}
breakpoints.remaining -= 1
return Cache.ttlBucket(cache.ttlSeconds) === "1h" ? EPHEMERAL_1H : EPHEMERAL_5M
}
const cacheControl = (cache: CacheHint | undefined) =>
cache?.type === "ephemeral" ? { type: "ephemeral" as const } : undefined
const anthropicMetadata = (metadata: Record<string, unknown>): ProviderMetadata => ({ anthropic: metadata })
@@ -224,11 +204,10 @@ const signatureFromMetadata = (metadata: ProviderMetadata | undefined): string |
return typeof anthropic.signature === "string" ? anthropic.signature : undefined
}
const lowerTool = (breakpoints: Cache.Breakpoints, tool: ToolDefinition): AnthropicTool => ({
const lowerTool = (tool: ToolDefinition): AnthropicTool => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
cache_control: cacheControl(breakpoints, tool.cache),
})
const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
@@ -270,10 +249,7 @@ const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult
return { type: wireType, tool_use_id: part.id, content: part.result.value } satisfies AnthropicServerToolResultBlock
})
const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
request: LLMRequest,
breakpoints: Cache.Breakpoints,
) {
const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (request: LLMRequest) {
const messages: AnthropicMessage[] = []
for (const message of request.messages) {
@@ -282,7 +258,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
for (const part of message.content) {
if (!ProviderShared.supportsContent(part, ["text"]))
return yield* ProviderShared.unsupportedContent("Anthropic Messages", "user", ["text"])
content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) })
content.push({ type: "text", text: part.text, cache_control: cacheControl(part.cache) })
}
messages.push({ role: "user", content })
continue
@@ -292,7 +268,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
const content: AnthropicAssistantBlock[] = []
for (const part of message.content) {
if (part.type === "text") {
content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) })
content.push({ type: "text", text: part.text, cache_control: cacheControl(part.cache) })
continue
}
if (part.type === "reasoning") {
@@ -328,7 +304,6 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
tool_use_id: part.id,
content: ProviderShared.toolResultText(part),
is_error: part.result.type === "error" ? true : undefined,
cache_control: cacheControl(breakpoints, part.cache),
})
}
messages.push({ role: "user", content })
@@ -355,33 +330,18 @@ const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (re
const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
const generation = request.generation
// Allocate the 4-breakpoint budget in invalidation order: tools → system →
// messages. Tools live highest in the cache hierarchy, so when callers
// over-mark we keep their tool hints and shed the message-tail ones first.
const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP)
const tools =
request.tools.length === 0 || request.toolChoice?.type === "none"
? undefined
: request.tools.map((tool) => lowerTool(breakpoints, tool))
const system =
request.system.length === 0
? undefined
: request.system.map((part) => ({
type: "text" as const,
text: part.text,
cache_control: cacheControl(breakpoints, part.cache),
}))
const messages = yield* lowerMessages(request, breakpoints)
if (breakpoints.dropped > 0) {
yield* Effect.logWarning(
`Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`,
)
}
return {
model: request.model.id,
system,
messages,
tools,
system:
request.system.length === 0
? undefined
: request.system.map((part) => ({
type: "text" as const,
text: part.text,
cache_control: cacheControl(part.cache),
})),
messages: yield* lowerMessages(request),
tools: request.tools.length === 0 || request.toolChoice?.type === "none" ? undefined : request.tools.map(lowerTool),
tool_choice: toolChoice,
stream: true as const,
max_tokens: generation?.maxTokens ?? request.model.limits.output ?? 4096,
+15 -47
View File
@@ -108,7 +108,7 @@ type BedrockMessage = Schema.Schema.Type<typeof BedrockMessage>
const BedrockSystemBlock = Schema.Union([BedrockTextBlock, BedrockCache.CachePointBlock])
type BedrockSystemBlock = Schema.Schema.Type<typeof BedrockSystemBlock>
const BedrockToolSpec = Schema.Struct({
const BedrockTool = Schema.Struct({
toolSpec: Schema.Struct({
name: Schema.String,
description: Schema.String,
@@ -117,9 +117,6 @@ const BedrockToolSpec = Schema.Struct({
}),
}),
})
type BedrockToolSpec = Schema.Schema.Type<typeof BedrockToolSpec>
const BedrockTool = Schema.Union([BedrockToolSpec, BedrockCache.CachePointBlock])
type BedrockTool = Schema.Schema.Type<typeof BedrockTool>
const BedrockToolChoice = Schema.Union([
@@ -217,7 +214,7 @@ type BedrockEvent = Schema.Schema.Type<typeof BedrockEvent>
// =============================================================================
// Request Lowering
// =============================================================================
const lowerToolSpec = (tool: ToolDefinition): BedrockToolSpec => ({
const lowerTool = (tool: ToolDefinition): BedrockTool => ({
toolSpec: {
name: tool.name,
description: tool.description,
@@ -225,22 +222,11 @@ const lowerToolSpec = (tool: ToolDefinition): BedrockToolSpec => ({
},
})
const lowerTools = (breakpoints: BedrockCache.Breakpoints, tools: ReadonlyArray<ToolDefinition>): BedrockTool[] => {
const result: BedrockTool[] = []
for (const tool of tools) {
result.push(lowerToolSpec(tool))
const cachePoint = BedrockCache.block(breakpoints, tool.cache)
if (cachePoint) result.push(cachePoint)
}
return result
}
const textWithCache = (
breakpoints: BedrockCache.Breakpoints,
text: string,
cache: CacheHint | undefined,
): Array<BedrockTextBlock | BedrockCache.CachePointBlock> => {
const cachePoint = BedrockCache.block(breakpoints, cache)
const cachePoint = BedrockCache.block(cache)
return cachePoint ? [{ text }, cachePoint] : [{ text }]
}
@@ -271,10 +257,7 @@ const lowerToolResult = (part: ToolResultPart): BedrockToolResultBlock => ({
},
})
const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
request: LLMRequest,
breakpoints: BedrockCache.Breakpoints,
) {
const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (request: LLMRequest) {
const messages: BedrockMessage[] = []
for (const message of request.messages) {
@@ -284,7 +267,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
if (!ProviderShared.supportsContent(part, ["text", "media"]))
return yield* ProviderShared.unsupportedContent("Bedrock Converse", "user", ["text", "media"])
if (part.type === "text") {
content.push(...textWithCache(breakpoints, part.text, part.cache))
content.push(...textWithCache(part.text, part.cache))
continue
}
if (part.type === "media") {
@@ -306,7 +289,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
"tool-call",
])
if (part.type === "text") {
content.push(...textWithCache(breakpoints, part.text, part.cache))
content.push(...textWithCache(part.text, part.cache))
continue
}
if (part.type === "reasoning") {
@@ -326,13 +309,11 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
continue
}
const content: BedrockUserBlock[] = []
const content: BedrockToolResultBlock[] = []
for (const part of message.content) {
if (!ProviderShared.supportsContent(part, ["tool-result"]))
return yield* ProviderShared.unsupportedContent("Bedrock Converse", "tool", ["tool-result"])
content.push(lowerToolResult(part))
const cachePoint = BedrockCache.block(breakpoints, part.cache)
if (cachePoint) content.push(cachePoint)
}
messages.push({ role: "user", content })
}
@@ -342,32 +323,16 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
// System prompts share the cache-point convention: emit the text block, then
// optionally a positional `cachePoint` marker.
const lowerSystem = (
breakpoints: BedrockCache.Breakpoints,
system: ReadonlyArray<LLMRequest["system"][number]>,
): BedrockSystemBlock[] => system.flatMap((part) => textWithCache(breakpoints, part.text, part.cache))
const lowerSystem = (system: ReadonlyArray<LLMRequest["system"][number]>): BedrockSystemBlock[] =>
system.flatMap((part) => textWithCache(part.text, part.cache))
const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request: LLMRequest) {
const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
const generation = request.generation
// Bedrock-Claude shares Anthropic's 4-breakpoint cap. Spend the budget in
// tools → system → messages order to favour the highest-impact prefixes.
const breakpoints = BedrockCache.breakpoints()
const toolConfig =
request.tools.length > 0 && request.toolChoice?.type !== "none"
? { tools: lowerTools(breakpoints, request.tools), toolChoice }
: undefined
const system = request.system.length === 0 ? undefined : lowerSystem(breakpoints, request.system)
const messages = yield* lowerMessages(request, breakpoints)
if (breakpoints.dropped > 0) {
yield* Effect.logWarning(
`Bedrock Converse: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${BedrockCache.BEDROCK_BREAKPOINT_CAP} per request.`,
)
}
return {
modelId: request.model.id,
messages,
system,
messages: yield* lowerMessages(request),
system: request.system.length === 0 ? undefined : lowerSystem(request.system),
inferenceConfig:
generation?.maxTokens === undefined &&
generation?.temperature === undefined &&
@@ -380,7 +345,10 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request:
topP: generation?.topP,
stopSequences: generation?.stop,
},
toolConfig,
toolConfig:
request.tools.length > 0 && request.toolChoice?.type !== "none"
? { tools: request.tools.map(lowerTool), toolChoice }
: undefined,
}
})
@@ -1,37 +1,20 @@
import { Schema } from "effect"
import type { CacheHint } from "../../schema"
import { newBreakpoints, ttlBucket, type Breakpoints } from "./cache"
// Bedrock cache markers are positional: emit a `cachePoint` block immediately
// after the content the caller wants treated as a cacheable prefix. Bedrock
// accepts optional `ttl: "5m" | "1h"` on cachePoint, mirroring Anthropic.
// after the content the caller wants treated as a cacheable prefix.
export const CachePointBlock = Schema.Struct({
cachePoint: Schema.Struct({
type: Schema.tag("default"),
ttl: Schema.optional(Schema.Literals(["5m", "1h"])),
}),
cachePoint: Schema.Struct({ type: Schema.tag("default") }),
})
export type CachePointBlock = Schema.Schema.Type<typeof CachePointBlock>
// Bedrock-Claude enforces the same 4-breakpoint cap as the Anthropic Messages
// API. Callers pass a shared counter through every `block()` call site so the
// budget is respected across `system`, `messages`, and `tools`.
export const BEDROCK_BREAKPOINT_CAP = 4
// Bedrock recently added optional `ttl: "5m" | "1h"` on cachePoint. Map
// `CacheHint.ttlSeconds` here once a recorded cassette validates the wire shape.
const DEFAULT: CachePointBlock = { cachePoint: { type: "default" } }
export type { Breakpoints } from "./cache"
export const breakpoints = () => newBreakpoints(BEDROCK_BREAKPOINT_CAP)
const DEFAULT_5M: CachePointBlock = { cachePoint: { type: "default" } }
const DEFAULT_1H: CachePointBlock = { cachePoint: { type: "default", ttl: "1h" } }
export const block = (breakpoints: Breakpoints, cache: CacheHint | undefined): CachePointBlock | undefined => {
export const block = (cache: CacheHint | undefined): CachePointBlock | undefined => {
if (cache?.type !== "ephemeral" && cache?.type !== "persistent") return undefined
if (breakpoints.remaining <= 0) {
breakpoints.dropped += 1
return undefined
}
breakpoints.remaining -= 1
return ttlBucket(cache.ttlSeconds) === "1h" ? DEFAULT_1H : DEFAULT_5M
return DEFAULT
}
export * as BedrockCache from "./bedrock-cache"
-16
View File
@@ -1,16 +0,0 @@
// Shared helpers for provider cache-marker lowering. Anthropic and Bedrock
// both enforce a 4-breakpoint cap per request and accept the same `5m`/`1h`
// TTL buckets, so the counter and TTL mapping live here.
export interface Breakpoints {
remaining: number
dropped: number
}
export const newBreakpoints = (cap: number): Breakpoints => ({ remaining: cap, dropped: 0 })
// Returns `"1h"` for any `ttlSeconds >= 3600`, otherwise `undefined` (the
// provider default 5m). Anthropic & Bedrock both treat anything shorter than
// an hour as 5m.
export const ttlBucket = (ttlSeconds: number | undefined): "1h" | undefined =>
ttlSeconds !== undefined && ttlSeconds >= 3600 ? "1h" : undefined
-3
View File
@@ -79,7 +79,6 @@ export const ToolResultPart = Object.assign(
name: Schema.String,
result: ToolResultValue,
providerExecuted: Schema.optional(Schema.Boolean),
cache: Schema.optional(CacheHint),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Content.ToolResult" }),
@@ -95,7 +94,6 @@ export const ToolResultPart = Object.assign(
name: input.name,
result: ToolResultValue.make(input.result, input.resultType),
providerExecuted: input.providerExecuted,
cache: input.cache,
metadata: input.metadata,
providerMetadata: input.providerMetadata,
}),
@@ -153,7 +151,6 @@ export class ToolDefinition extends Schema.Class<ToolDefinition>("LLM.ToolDefini
name: Schema.String,
description: Schema.String,
inputSchema: JsonSchema,
cache: Schema.optional(CacheHint),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}) {}
@@ -1,48 +0,0 @@
import { Redactor } from "@opencode-ai/http-recorder"
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { CacheHint, LLM } from "../../src"
import { LLMClient } from "../../src/route"
import * as AnthropicMessages from "../../src/protocols/anthropic-messages"
import { LARGE_CACHEABLE_SYSTEM } from "../recorded-scenarios"
import { recordedTests } from "../recorded-test"
const model = AnthropicMessages.model({
id: "claude-haiku-4-5-20251001",
apiKey: process.env.ANTHROPIC_API_KEY ?? "fixture",
})
// Two identical generations in a row. The first call writes the prefix into
// Anthropic's cache; the second should report a cache read against the same
// prefix. Cassette captures both interactions in order.
const cacheRequest = LLM.request({
id: "recorded_anthropic_cache",
model,
system: [{ type: "text", text: LARGE_CACHEABLE_SYSTEM, cache: new CacheHint({ type: "ephemeral" }) }],
prompt: "Say hi.",
generation: { maxTokens: 16, temperature: 0 },
})
const recorded = recordedTests({
prefix: "anthropic-messages-cache",
provider: "anthropic",
protocol: "anthropic-messages",
requires: ["ANTHROPIC_API_KEY"],
options: { redactor: Redactor.defaults({ requestHeaders: { allow: ["content-type", "anthropic-version"] } }) },
})
describe("Anthropic Messages cache recorded", () => {
recorded.effect.with("writes then reads cache_control on identical second call", { tags: ["cache"] }, () =>
Effect.gen(function* () {
const first = yield* LLMClient.generate(cacheRequest)
// The first call may write the cache (cacheWriteInputTokens > 0) or it
// may be a fresh miss (both fields 0) depending on whether the prefix is
// already warm on Anthropic's side. The assertion that matters is that
// the SECOND call reports a non-zero cache read.
expect(first.usage?.cacheReadInputTokens ?? 0).toBeGreaterThanOrEqual(0)
const second = yield* LLMClient.generate(cacheRequest)
expect(second.usage?.cacheReadInputTokens ?? 0).toBeGreaterThan(0)
}),
)
})
@@ -374,134 +374,4 @@ describe("Anthropic Messages route", () => {
expect(error.message).toContain("Anthropic Messages user messages only support text content for now")
}),
)
it.effect("maps ttlSeconds >= 3600 to cache_control ttl: '1h'", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
system: { type: "text", text: "system", cache: new CacheHint({ type: "ephemeral", ttlSeconds: 3600 }) },
prompt: "hi",
}),
)
expect(prepared.body).toMatchObject({
system: [{ type: "text", text: "system", cache_control: { type: "ephemeral", ttl: "1h" } }],
})
}),
)
it.effect("emits cache_control on tool definitions and tool-result blocks", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
tools: [
{
name: "lookup",
description: "lookup tool",
inputSchema: { type: "object", properties: {} },
cache: new CacheHint({ type: "ephemeral" }),
},
],
messages: [
LLM.user("What's the weather?"),
LLM.assistant([LLM.toolCall({ id: "call_1", name: "lookup", input: {} })]),
LLM.toolMessage({
id: "call_1",
name: "lookup",
result: { temp: 72 },
cache: new CacheHint({ type: "ephemeral" }),
}),
],
}),
)
expect(prepared.body).toMatchObject({
tools: [{ name: "lookup", cache_control: { type: "ephemeral" } }],
messages: [
{ role: "user", content: [{ type: "text", text: "What's the weather?" }] },
{ role: "assistant", content: [{ type: "tool_use", id: "call_1", name: "lookup" }] },
{
role: "user",
content: [{ type: "tool_result", tool_use_id: "call_1", cache_control: { type: "ephemeral" } }],
},
],
})
}),
)
it.effect("drops cache_control breakpoints past the 4-per-request cap", () =>
Effect.gen(function* () {
const hint = new CacheHint({ type: "ephemeral" })
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
system: [
{ type: "text", text: "a", cache: hint },
{ type: "text", text: "b", cache: hint },
{ type: "text", text: "c", cache: hint },
{ type: "text", text: "d", cache: hint },
{ type: "text", text: "e", cache: hint },
{ type: "text", text: "f", cache: hint },
],
prompt: "hi",
}),
)
const system = (prepared.body as { system: Array<{ cache_control?: unknown }> }).system
const marked = system.filter((part) => part.cache_control !== undefined)
expect(marked).toHaveLength(4)
expect(system[4]?.cache_control).toBeUndefined()
expect(system[5]?.cache_control).toBeUndefined()
}),
)
it.effect("spends breakpoint budget on tools before system before messages", () =>
Effect.gen(function* () {
const hint = new CacheHint({ type: "ephemeral" })
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
tools: [
{
name: "t1",
description: "t1",
inputSchema: { type: "object", properties: {} },
cache: hint,
},
{
name: "t2",
description: "t2",
inputSchema: { type: "object", properties: {} },
cache: hint,
},
{
name: "t3",
description: "t3",
inputSchema: { type: "object", properties: {} },
cache: hint,
},
{
name: "t4",
description: "t4",
inputSchema: { type: "object", properties: {} },
cache: hint,
},
],
system: [{ type: "text", text: "system-tail", cache: hint }],
messages: [LLM.user([{ type: "text", text: "message-tail", cache: hint }])],
}),
)
const body = prepared.body as {
tools: Array<{ cache_control?: unknown }>
system: Array<{ cache_control?: unknown }>
messages: Array<{ content: Array<{ cache_control?: unknown }> }>
}
expect(body.tools.every((t) => t.cache_control !== undefined)).toBe(true)
expect(body.system[0]?.cache_control).toBeUndefined()
expect(body.messages[0]?.content[0]?.cache_control).toBeUndefined()
}),
)
})
@@ -1,50 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { CacheHint, LLM } from "../../src"
import { LLMClient } from "../../src/route"
import * as BedrockConverse from "../../src/protocols/bedrock-converse"
import { LARGE_CACHEABLE_SYSTEM } from "../recorded-scenarios"
import { recordedTests } from "../recorded-test"
const RECORDING_REGION = process.env.BEDROCK_RECORDING_REGION ?? "us-east-1"
// Use a Claude model on Bedrock — Nova has automatic prefix caching that
// doesn't reliably surface `cacheRead`/`cacheWrite` in usage, so the second
// call wouldn't deterministically prove cache mapping works. Override with
// BEDROCK_CACHE_MODEL_ID if your account has access elsewhere.
const model = BedrockConverse.model({
id: process.env.BEDROCK_CACHE_MODEL_ID ?? "us.anthropic.claude-haiku-4-5-20251001-v1:0",
credentials: {
region: RECORDING_REGION,
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "fixture",
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "fixture",
sessionToken: process.env.AWS_SESSION_TOKEN,
},
})
const cacheRequest = LLM.request({
id: "recorded_bedrock_cache",
model,
system: [{ type: "text", text: LARGE_CACHEABLE_SYSTEM, cache: new CacheHint({ type: "ephemeral" }) }],
prompt: "Say hi.",
generation: { maxTokens: 16, temperature: 0 },
})
const recorded = recordedTests({
prefix: "bedrock-converse-cache",
provider: "amazon-bedrock",
protocol: "bedrock-converse",
requires: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
})
describe("Bedrock Converse cache recorded", () => {
recorded.effect.with("writes then reads cachePoint on identical second call", { tags: ["cache"] }, () =>
Effect.gen(function* () {
const first = yield* LLMClient.generate(cacheRequest)
expect(first.usage?.cacheReadInputTokens ?? 0).toBeGreaterThanOrEqual(0)
const second = yield* LLMClient.generate(cacheRequest)
expect(second.usage?.cacheReadInputTokens ?? 0).toBeGreaterThan(0)
}),
)
})
@@ -440,77 +440,6 @@ describe("Bedrock Converse route", () => {
expect(error.message).toContain("Bedrock Converse does not support media type application/x-tar")
}),
)
it.effect("maps ttlSeconds >= 3600 to cachePoint ttl: '1h'", () =>
Effect.gen(function* () {
const cache = new CacheHint({ type: "ephemeral", ttlSeconds: 3600 })
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
system: [{ type: "text", text: "system", cache }],
prompt: "hi",
}),
)
expect(prepared.body).toMatchObject({
system: [{ text: "system" }, { cachePoint: { type: "default", ttl: "1h" } }],
})
}),
)
it.effect("appends cachePoint after marked tool definitions and tool-result blocks", () =>
Effect.gen(function* () {
const cache = new CacheHint({ type: "ephemeral" })
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
tools: [{ name: "lookup", description: "lookup", inputSchema: { type: "object", properties: {} }, cache }],
messages: [
LLM.user("What's the weather?"),
LLM.assistant([LLM.toolCall({ id: "call_1", name: "lookup", input: {} })]),
LLM.toolMessage({ id: "call_1", name: "lookup", result: { temp: 72 }, cache }),
],
}),
)
expect(prepared.body).toMatchObject({
toolConfig: {
tools: [{ toolSpec: { name: "lookup" } }, { cachePoint: { type: "default" } }],
},
messages: [
{ role: "user", content: [{ text: "What's the weather?" }] },
{ role: "assistant", content: [{ toolUse: { toolUseId: "call_1" } }] },
{
role: "user",
content: [{ toolResult: { toolUseId: "call_1" } }, { cachePoint: { type: "default" } }],
},
],
})
}),
)
it.effect("drops cachePoint markers past the 4-per-request cap", () =>
Effect.gen(function* () {
const cache = new CacheHint({ type: "ephemeral" })
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
system: [
{ type: "text", text: "a", cache },
{ type: "text", text: "b", cache },
{ type: "text", text: "c", cache },
{ type: "text", text: "d", cache },
{ type: "text", text: "e", cache },
{ type: "text", text: "f", cache },
],
prompt: "hi",
}),
)
const system = (prepared.body as { system: Array<{ cachePoint?: unknown }> }).system
expect(system.filter((part) => "cachePoint" in part)).toHaveLength(4)
}),
)
})
// Live recorded integration tests. Run with `RECORD=true AWS_ACCESS_KEY_ID=...
@@ -1,47 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM } from "../../src"
import { LLMClient } from "../../src/route"
import * as Gemini from "../../src/protocols/gemini"
import { LARGE_CACHEABLE_SYSTEM } from "../recorded-scenarios"
import { recordedTests } from "../recorded-test"
const model = Gemini.model({
id: "gemini-2.5-flash",
apiKey: process.env.GEMINI_API_KEY ?? "fixture",
})
// Gemini does implicit prefix caching on 2.5+ models above ~1024 tokens. The
// `CacheHint` is currently a no-op for Gemini (the explicit `CachedContent`
// API is out-of-band and intentionally not wired up). This test exists to
// pin the usage-parsing path: `cachedContentTokenCount` should surface as
// `cacheReadInputTokens` on the second identical call.
const cacheRequest = LLM.request({
id: "recorded_gemini_cache",
model,
system: LARGE_CACHEABLE_SYSTEM,
prompt: "Say hi.",
generation: { maxTokens: 16, temperature: 0 },
})
const recorded = recordedTests({
prefix: "gemini-cache",
provider: "google",
protocol: "gemini",
requires: ["GEMINI_API_KEY"],
})
describe("Gemini cache recorded", () => {
recorded.effect.with("reports cachedContentTokenCount on identical second call", { tags: ["cache"] }, () =>
Effect.gen(function* () {
const first = yield* LLMClient.generate(cacheRequest)
expect(first.usage?.cacheReadInputTokens ?? 0).toBeGreaterThanOrEqual(0)
const second = yield* LLMClient.generate(cacheRequest)
// Implicit caching is best-effort on Gemini's side; we assert the field
// is at least populated and non-negative. When re-recording, verify the
// cassette shows > 0 in the second response's usage.
expect(second.usage?.cacheReadInputTokens ?? 0).toBeGreaterThanOrEqual(0)
}),
)
})
@@ -1,44 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM } from "../../src"
import { LLMClient } from "../../src/route"
import * as OpenAIResponses from "../../src/protocols/openai-responses"
import { LARGE_CACHEABLE_SYSTEM } from "../recorded-scenarios"
import { recordedTests } from "../recorded-test"
const model = OpenAIResponses.model({
id: "gpt-4.1-mini",
apiKey: process.env.OPENAI_API_KEY ?? "fixture",
})
// OpenAI caches prefixes automatically once they cross the 1024-token threshold;
// `CacheHint` is a no-op for the wire body. The stable signal is the
// `prompt_cache_key` routing hint, which keeps repeated calls on the same shard
// so cache hits are observable.
const cacheRequest = LLM.request({
id: "recorded_openai_responses_cache",
model,
system: LARGE_CACHEABLE_SYSTEM,
prompt: "Say hi.",
generation: { maxTokens: 16, temperature: 0 },
providerOptions: { openai: { promptCacheKey: "recorded-cache-test" } },
})
const recorded = recordedTests({
prefix: "openai-responses-cache",
provider: "openai",
protocol: "openai-responses",
requires: ["OPENAI_API_KEY"],
})
describe("OpenAI Responses cache recorded", () => {
recorded.effect.with("reports cached_tokens on identical second call", { tags: ["cache"] }, () =>
Effect.gen(function* () {
const first = yield* LLMClient.generate(cacheRequest)
expect(first.usage?.cacheReadInputTokens ?? 0).toBeGreaterThanOrEqual(0)
const second = yield* LLMClient.generate(cacheRequest)
expect(second.usage?.cacheReadInputTokens ?? 0).toBeGreaterThan(0)
}),
)
})
-12
View File
@@ -6,18 +6,6 @@ import { tool } from "../src/tool"
export const weatherToolName = "get_weather"
// A deterministic system prompt long enough to clear every supported provider's
// minimum cacheable-prefix threshold (Anthropic Haiku 3.5: 2048 tokens; Anthropic
// Opus/Haiku 4.5: 4096 tokens; OpenAI/Gemini/Bedrock: lower). Built by repeating
// a fixed sentence — the cassette replays bit-for-bit, so the exact text matters
// only when re-recording with `RECORD=true`.
export const LARGE_CACHEABLE_SYSTEM = (() => {
const sentence = "You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. "
// ~100 chars per sentence × 250 repeats ≈ 25,000 chars ≈ 5k+ tokens, safely
// above every provider's threshold.
return sentence.repeat(250)
})()
export const weatherTool = LLM.toolDefinition({
name: weatherToolName,
description: "Get current weather for a city.",
@@ -1,4 +0,0 @@
CREATE TABLE `data_migration` (
`name` text PRIMARY KEY,
`time_completed` integer NOT NULL
);
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.48",
"version": "1.14.46",
"name": "opencode",
"type": "module",
"license": "MIT",
@@ -676,12 +676,14 @@ export const layer = Layer.effect(
}
if (input.workspaceID === null) {
yield* sync.run(Session.Event.Updated, {
sessionID: input.sessionID,
info: {
workspaceID: null,
},
})
yield* Effect.sync(() =>
SyncEvent.run(Session.Event.Updated, {
sessionID: input.sessionID,
info: {
workspaceID: null,
},
}),
)
log.info("session warp complete", {
workspaceID: input.workspaceID,
@@ -1,6 +0,0 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
export const DataMigrationTable = sqliteTable("data_migration", {
name: text().primaryKey(),
time_completed: integer().notNull(),
})
-59
View File
@@ -1,59 +0,0 @@
import { Context, Effect, Layer } from "effect"
import { Database } from "./storage/db"
import { DataMigrationTable } from "./data-migration.sql"
import * as Log from "@opencode-ai/core/util/log"
import { eq } from "drizzle-orm"
export type Migration<R = never> = {
name: string
run: Effect.Effect<void, unknown, R>
}
const log = Log.create({ service: "data-migration" })
export interface Interface {}
export class Service extends Context.Service<Service, Interface>()("@opencode/DataMigration") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const migrations: Migration[] = []
yield* Effect.gen(function* () {
if (migrations.length === 0) return
// Migrations run in a background fiber, so they must be resumable until
// their completion row is written.
for (const migration of migrations) {
const completed = Database.use((db) =>
db
.select({ name: DataMigrationTable.name })
.from(DataMigrationTable)
.where(eq(DataMigrationTable.name, migration.name))
.get(),
)
if (completed) continue
log.info("running data migration", { name: migration.name })
yield* migration.run
Database.use((db) =>
db
.insert(DataMigrationTable)
.values({ name: migration.name, time_completed: Date.now() })
.onConflictDoNothing()
.run(),
)
}
}).pipe(
Effect.tapCause((cause) => Effect.logError("failed to run data migrations", { cause })),
Effect.ignore,
Effect.forkScoped,
)
return Service.of({})
}),
)
export const defaultLayer = layer
export * as DataMigration from "./data-migration"
@@ -54,7 +54,6 @@ import { SessionShare } from "@/share/session"
import { SyncEvent } from "@/sync"
import { Npm } from "@opencode-ai/core/npm"
import { memoMap } from "@opencode-ai/core/effect/memo-map"
import { DataMigration } from "@/data-migration"
export const AppLayer = Layer.mergeAll(
Npm.defaultLayer,
@@ -107,7 +106,6 @@ export const AppLayer = Layer.mergeAll(
ShareNext.defaultLayer,
SessionShare.defaultLayer,
SyncEvent.defaultLayer,
DataMigration.defaultLayer,
).pipe(Layer.provideMerge(InstanceLayer.layer), Layer.provideMerge(Observability.layer))
const rt = ManagedRuntime.make(AppLayer, { memoMap })
+12 -20
View File
@@ -18,9 +18,8 @@ import { InstanceState } from "@/effect/instance-state"
import { isOverflow as overflow, usable } from "./overflow"
import { makeRuntime } from "@/effect/run-service"
import { serviceUse } from "@/effect/service-use"
import { SyncEvent } from "@/sync"
import { EventV2 } from "@/v2/event"
import { SessionEvent } from "@/v2/session-event"
import { Flag } from "@opencode-ai/core/flag/flag"
const log = Log.create({ service: "session.compaction" })
@@ -220,7 +219,6 @@ export const layer: Layer.Layer<
| Plugin.Service
| SessionProcessor.Service
| Provider.Service
| SyncEvent.Service
> = Layer.effect(
Service,
Effect.gen(function* () {
@@ -231,7 +229,6 @@ export const layer: Layer.Layer<
const plugin = yield* Plugin.Service
const processors = yield* SessionProcessor.Service
const provider = yield* Provider.Service
const sync = yield* SyncEvent.Service
const isOverflow = Effect.fn("SessionCompaction.isOverflow")(function* (input: {
tokens: MessageV2.Assistant["tokens"]
@@ -570,14 +567,12 @@ export const layer: Layer.Layer<
parts: [],
},
)
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Compaction.Ended.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(Date.now()),
text: summary ?? "",
include: selected.tail_start_id,
})
}
EventV2.run(SessionEvent.Compaction.Ended.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(Date.now()),
text: summary ?? "",
include: selected.tail_start_id,
})
yield* bus.publish(Event.Compacted, { sessionID: input.sessionID })
}
return result
@@ -606,13 +601,11 @@ export const layer: Layer.Layer<
auto: input.auto,
overflow: input.overflow,
})
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Compaction.Started.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(Date.now()),
reason: input.auto ? "auto" : "manual",
})
}
EventV2.run(SessionEvent.Compaction.Started.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(Date.now()),
reason: input.auto ? "auto" : "manual",
})
})
return Service.of({
@@ -633,7 +626,6 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(Plugin.defaultLayer),
Layer.provide(Bus.layer),
Layer.provide(Config.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
),
)
+119 -153
View File
@@ -21,11 +21,10 @@ import { Question } from "@/question"
import { errorMessage } from "@/util/error"
import * as Log from "@opencode-ai/core/util/log"
import { isRecord } from "@/util/record"
import { SyncEvent } from "@/sync"
import { EventV2 } from "@/v2/event"
import { SessionEvent } from "@/v2/session-event"
import { Modelv2 } from "@/v2/model"
import * as DateTime from "effect/DateTime"
import { Flag } from "@opencode-ai/core/flag/flag"
const DOOM_LOOP_THRESHOLD = 3
const log = Log.create({ service: "session.processor" })
@@ -97,7 +96,6 @@ export const layer: Layer.Layer<
| Image.Service
| SessionSummary.Service
| SessionStatus.Service
| SyncEvent.Service
> = Layer.effect(
Service,
Effect.gen(function* () {
@@ -113,7 +111,6 @@ export const layer: Layer.Layer<
const scope = yield* Scope.Scope
const status = yield* SessionStatus.Service
const image = yield* Image.Service
const sync = yield* SyncEvent.Service
const create = Effect.fn("SessionProcessor.create")(function* (input: Input) {
// Pre-capture snapshot before the LLM stream starts. The AI SDK
@@ -232,13 +229,11 @@ export const layer: Layer.Layer<
case "reasoning-start":
if (value.id in ctx.reasoningMap) return
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Reasoning.Started.Sync, {
sessionID: ctx.sessionID,
reasoningID: value.id,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
EventV2.run(SessionEvent.Reasoning.Started.Sync, {
sessionID: ctx.sessionID,
reasoningID: value.id,
timestamp: DateTime.makeUnsafe(Date.now()),
})
ctx.reasoningMap[value.id] = {
id: PartID.ascending(),
messageID: ctx.assistantMessage.id,
@@ -267,14 +262,12 @@ export const layer: Layer.Layer<
case "reasoning-end":
if (!(value.id in ctx.reasoningMap)) return
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Reasoning.Ended.Sync, {
sessionID: ctx.sessionID,
reasoningID: value.id,
text: ctx.reasoningMap[value.id].text,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
EventV2.run(SessionEvent.Reasoning.Ended.Sync, {
sessionID: ctx.sessionID,
reasoningID: value.id,
text: ctx.reasoningMap[value.id].text,
timestamp: DateTime.makeUnsafe(Date.now()),
})
// oxlint-disable-next-line no-self-assign -- reactivity trigger
ctx.reasoningMap[value.id].text = ctx.reasoningMap[value.id].text
ctx.reasoningMap[value.id].time = { ...ctx.reasoningMap[value.id].time, end: Date.now() }
@@ -288,14 +281,12 @@ export const layer: Layer.Layer<
throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`)
}
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Tool.Input.Started.Sync, {
sessionID: ctx.sessionID,
callID: value.id,
name: value.toolName,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
EventV2.run(SessionEvent.Tool.Input.Started.Sync, {
sessionID: ctx.sessionID,
callID: value.id,
name: value.toolName,
timestamp: DateTime.makeUnsafe(Date.now()),
})
const part = yield* session.updatePart({
id: ctx.toolcalls[value.id]?.partID ?? PartID.ascending(),
messageID: ctx.assistantMessage.id,
@@ -319,14 +310,12 @@ export const layer: Layer.Layer<
case "tool-input-end": {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Tool.Input.Ended.Sync, {
sessionID: ctx.sessionID,
callID: value.id,
text: "",
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
EventV2.run(SessionEvent.Tool.Input.Ended.Sync, {
sessionID: ctx.sessionID,
callID: value.id,
text: "",
timestamp: DateTime.makeUnsafe(Date.now()),
})
return
}
@@ -336,19 +325,17 @@ export const layer: Layer.Layer<
}
const toolCall = yield* readToolCall(value.toolCallId)
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Tool.Called.Sync, {
sessionID: ctx.sessionID,
callID: value.toolCallId,
tool: value.toolName,
input: value.input,
provider: {
executed: toolCall?.part.metadata?.providerExecuted === true,
...(value.providerMetadata ? { metadata: value.providerMetadata } : {}),
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
EventV2.run(SessionEvent.Tool.Called.Sync, {
sessionID: ctx.sessionID,
callID: value.toolCallId,
tool: value.toolName,
input: value.input,
provider: {
executed: toolCall?.part.metadata?.providerExecuted === true,
...(value.providerMetadata ? { metadata: value.providerMetadata } : {}),
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
yield* updateToolCall(value.toolCallId, (match) => ({
...match,
tool: value.toolName,
@@ -418,29 +405,27 @@ export const layer: Layer.Layer<
attachments: attachments?.length ? attachments : undefined,
}
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Tool.Success.Sync, {
sessionID: ctx.sessionID,
callID: value.toolCallId,
structured: output.metadata,
content: [
{
type: "text",
text: output.output,
},
...(output.attachments?.map((item: MessageV2.FilePart) => ({
type: "file",
uri: item.url,
mime: item.mime,
name: item.filename,
})) ?? []),
],
provider: {
executed: toolCall?.part.metadata?.providerExecuted === true,
EventV2.run(SessionEvent.Tool.Success.Sync, {
sessionID: ctx.sessionID,
callID: value.toolCallId,
structured: output.metadata,
content: [
{
type: "text",
text: output.output,
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
...(output.attachments?.map((item: MessageV2.FilePart) => ({
type: "file",
uri: item.url,
mime: item.mime,
name: item.filename,
})) ?? []),
],
provider: {
executed: toolCall?.part.metadata?.providerExecuted === true,
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
yield* completeToolCall(value.toolCallId, output)
return
}
@@ -448,20 +433,18 @@ export const layer: Layer.Layer<
case "tool-error": {
const toolCall = yield* readToolCall(value.toolCallId)
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Tool.Failed.Sync, {
sessionID: ctx.sessionID,
callID: value.toolCallId,
error: {
type: "unknown",
message: errorMessage(value.error),
},
provider: {
executed: toolCall?.part.metadata?.providerExecuted === true,
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
EventV2.run(SessionEvent.Tool.Failed.Sync, {
sessionID: ctx.sessionID,
callID: value.toolCallId,
error: {
type: "unknown",
message: errorMessage(value.error),
},
provider: {
executed: toolCall?.part.metadata?.providerExecuted === true,
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
yield* failToolCall(value.toolCallId, value.error)
return
}
@@ -473,19 +456,17 @@ export const layer: Layer.Layer<
if (!ctx.snapshot) ctx.snapshot = yield* snapshot.track()
if (!ctx.assistantMessage.summary) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Step.Started.Sync, {
sessionID: ctx.sessionID,
agent: input.assistantMessage.agent,
model: {
id: Modelv2.ID.make(ctx.model.id),
providerID: Modelv2.ProviderID.make(ctx.model.providerID),
variant: Modelv2.VariantID.make(input.assistantMessage.variant ?? "default"),
},
snapshot: ctx.snapshot,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
EventV2.run(SessionEvent.Step.Started.Sync, {
sessionID: ctx.sessionID,
agent: input.assistantMessage.agent,
model: {
id: Modelv2.ID.make(ctx.model.id),
providerID: Modelv2.ProviderID.make(ctx.model.providerID),
variant: Modelv2.VariantID.make(input.assistantMessage.variant ?? "default"),
},
snapshot: ctx.snapshot,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
yield* session.updatePart({
id: PartID.ascending(),
@@ -505,16 +486,14 @@ export const layer: Layer.Layer<
})
if (!ctx.assistantMessage.summary) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Step.Ended.Sync, {
sessionID: ctx.sessionID,
finish: value.finishReason,
cost: usage.cost,
tokens: usage.tokens,
snapshot: completedSnapshot,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
EventV2.run(SessionEvent.Step.Ended.Sync, {
sessionID: ctx.sessionID,
finish: value.finishReason,
cost: usage.cost,
tokens: usage.tokens,
snapshot: completedSnapshot,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
ctx.assistantMessage.finish = value.finishReason
ctx.assistantMessage.cost += usage.cost
@@ -562,12 +541,10 @@ export const layer: Layer.Layer<
case "text-start":
if (!ctx.assistantMessage.summary) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Text.Started.Sync, {
sessionID: ctx.sessionID,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
EventV2.run(SessionEvent.Text.Started.Sync, {
sessionID: ctx.sessionID,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
ctx.currentText = {
id: PartID.ascending(),
@@ -609,13 +586,11 @@ export const layer: Layer.Layer<
)).text
if (!ctx.assistantMessage.summary) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Text.Ended.Sync, {
sessionID: ctx.sessionID,
text: ctx.currentText.text,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
EventV2.run(SessionEvent.Text.Ended.Sync, {
sessionID: ctx.sessionID,
text: ctx.currentText.text,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
{
const end = Date.now()
@@ -705,16 +680,14 @@ export const layer: Layer.Layer<
}
if (!ctx.assistantMessage.summary) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Step.Failed.Sync, {
sessionID: ctx.sessionID,
error: {
type: "unknown",
message: errorMessage(e),
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
EventV2.run(SessionEvent.Step.Failed.Sync, {
sessionID: ctx.sessionID,
error: {
type: "unknown",
message: errorMessage(e),
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
ctx.assistantMessage.error = error
yield* bus.publish(Session.Event.Error, {
@@ -759,28 +732,22 @@ export const layer: Layer.Layer<
parse,
set: (info) => {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
const event = Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM
? sync.run(SessionEvent.Retried.Sync, {
sessionID: ctx.sessionID,
attempt: info.attempt,
error: {
message: info.message,
isRetryable: true,
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
: Effect.void
return event.pipe(
Effect.andThen(
status.set(ctx.sessionID, {
type: "retry",
attempt: info.attempt,
message: info.message,
action: info.action,
next: info.next,
}),
),
)
EventV2.run(SessionEvent.Retried.Sync, {
sessionID: ctx.sessionID,
attempt: info.attempt,
error: {
message: info.message,
isRetryable: true,
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
return status.set(ctx.sessionID, {
type: "retry",
attempt: info.attempt,
message: info.message,
action: info.action,
next: info.next,
})
},
}),
),
@@ -821,7 +788,6 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(Image.defaultLayer),
Layer.provide(Bus.layer),
Layer.provide(Config.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
),
)
+75 -72
View File
@@ -43,6 +43,7 @@ import { Shell } from "@/shell/shell"
import { ShellID } from "@/tool/shell/id"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Truncate } from "@/tool/truncate"
import { Image } from "@/image/image"
import { decodeDataUrl } from "@/util/data-url"
import { Process } from "@/util/process"
import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } from "effect"
@@ -53,7 +54,7 @@ import { InstanceState } from "@/effect/instance-state"
import { TaskTool, type TaskPromptOps } from "@/tool/task"
import { SessionRunState } from "./run-state"
import { EffectBridge } from "@/effect/bridge"
import { SyncEvent } from "@/sync"
import { EventV2 } from "@/v2/event"
import { SessionEvent } from "@/v2/session-event"
import { Modelv2 } from "@/v2/model"
import { AgentAttachment, FileAttachment, Source } from "@/v2/session-prompt"
@@ -80,10 +81,10 @@ const elog = EffectLogger.create({ service: "session.prompt" })
export interface Interface {
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
readonly prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts>
readonly prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts, Image.Error>
readonly loop: (input: LoopInput) => Effect.Effect<MessageV2.WithParts>
readonly shell: (input: ShellInput) => Effect.Effect<MessageV2.WithParts>
readonly command: (input: CommandInput) => Effect.Effect<MessageV2.WithParts>
readonly command: (input: CommandInput) => Effect.Effect<MessageV2.WithParts, Image.Error>
readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
}
@@ -108,6 +109,7 @@ export const layer = Layer.effect(
const lsp = yield* LSP.Service
const registry = yield* ToolRegistry.Service
const truncate = yield* Truncate.Service
const image = yield* Image.Service
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const scope = yield* Scope.Scope
const instruction = yield* Instruction.Service
@@ -116,7 +118,6 @@ export const layer = Layer.effect(
const summary = yield* SessionSummary.Service
const sys = yield* SystemPrompt.Service
const llm = yield* LLM.Service
const sync = yield* SyncEvent.Service
const runner = Effect.fn("SessionPrompt.runner")(function* () {
return yield* EffectBridge.make()
})
@@ -124,7 +125,7 @@ export const layer = Layer.effect(
return {
cancel: (sessionID: SessionID) => cancel(sessionID),
resolvePromptParts: (template: string) => resolvePromptParts(template),
prompt: (input: PromptInput) => prompt(input),
prompt: (input: PromptInput) => prompt(input).pipe(Effect.catch(Effect.die)),
} satisfies TaskPromptOps
})
@@ -808,14 +809,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the
},
}
yield* sessions.updatePart(part)
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Shell.Started.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(started),
callID,
command: input.command,
})
}
EventV2.run(SessionEvent.Shell.Started.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(started),
callID,
command: input.command,
})
return { msg, part, cwd: ctx.directory }
}).pipe(Effect.ensuring(markReady))
@@ -831,14 +830,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the
output += "\n\n" + ["<metadata>", "User aborted the command", "</metadata>"].join("\n")
}
const completed = Date.now()
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Shell.Ended.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(completed),
callID: part.callID,
output,
})
}
EventV2.run(SessionEvent.Shell.Ended.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(completed),
callID: part.callID,
output,
})
if (!msg.time.completed) {
msg.time.completed = completed
yield* sessions.updateMessage(msg)
@@ -978,26 +975,34 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}
if (current?.agent !== info.agent) {
yield* sync.run(SessionEvent.AgentSwitched.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(info.time.created),
agent: info.agent,
})
EventV2.run(
SessionEvent.AgentSwitched.Sync,
{
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(info.time.created),
agent: info.agent,
},
{ bypassExperimentalEventSystem: true },
)
}
if (
current?.model?.providerID !== info.model.providerID ||
current.model.id !== info.model.modelID ||
(current.model.variant === "default" ? undefined : current.model.variant) !== info.model.variant
) {
yield* sync.run(SessionEvent.ModelSwitched.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(info.time.created),
model: {
id: Modelv2.ID.make(info.model.modelID),
providerID: Modelv2.ProviderID.make(info.model.providerID),
variant: Modelv2.VariantID.make(info.model.variant ?? "default"),
EventV2.run(
SessionEvent.ModelSwitched.Sync,
{
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(info.time.created),
model: {
id: Modelv2.ID.make(info.model.modelID),
providerID: Modelv2.ProviderID.make(info.model.providerID),
variant: Modelv2.VariantID.make(info.model.variant ?? "default"),
},
},
})
{ bypassExperimentalEventSystem: true },
)
}
yield* Effect.addFinalizer(() => instruction.clear(info.id))
@@ -1290,7 +1295,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
{ message: info, parts: resolvedParts },
)
const parts = resolvedParts
const parts = yield* Effect.forEach(resolvedParts, (part) =>
part.type === "file" && part.mime.startsWith("image/") ? image.normalize(part) : Effect.succeed(part),
)
const parsed = MessageV2.Info.zod.safeParse(info)
if (!parsed.success) {
@@ -1364,51 +1371,47 @@ NOTE: At any point in time through this workflow you should feel free to ask the
},
)
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Prompted.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(info.time.created),
prompt: {
text: nextPrompt.text.join("\n"),
files: nextPrompt.files,
agents: nextPrompt.agents,
},
})
}
EventV2.run(SessionEvent.Prompted.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(info.time.created),
prompt: {
text: nextPrompt.text.join("\n"),
files: nextPrompt.files,
agents: nextPrompt.agents,
},
})
for (const text of nextPrompt.synthetic) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Synthetic.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(info.time.created),
text,
})
}
EventV2.run(SessionEvent.Synthetic.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(info.time.created),
text,
})
}
return { info, parts }
}, Effect.scoped)
const prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.prompt")(
function* (input: PromptInput) {
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
yield* revert.cleanup(session)
const message = yield* createUserMessage(input)
yield* sessions.touch(input.sessionID)
const prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts, Image.Error> = Effect.fn(
"SessionPrompt.prompt",
)(function* (input: PromptInput) {
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
yield* revert.cleanup(session)
const message = yield* createUserMessage(input)
yield* sessions.touch(input.sessionID)
const permissions: Permission.Ruleset = []
for (const [t, enabled] of Object.entries(input.tools ?? {})) {
permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" })
}
if (permissions.length > 0) {
session.permission = permissions
yield* sessions.setPermission({ sessionID: session.id, permission: permissions })
}
const permissions: Permission.Ruleset = []
for (const [t, enabled] of Object.entries(input.tools ?? {})) {
permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" })
}
if (permissions.length > 0) {
session.permission = permissions
yield* sessions.setPermission({ sessionID: session.id, permission: permissions })
}
if (input.noReply === true) return message
return yield* loop({ sessionID: input.sessionID })
},
)
if (input.noReply === true) return message
return yield* loop({ sessionID: input.sessionID })
})
const lastAssistant = Effect.fnUntraced(function* (sessionID: SessionID) {
const match = yield* sessions.findMessage(sessionID, (m) => m.info.role !== "user")
@@ -1809,6 +1812,7 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(Session.defaultLayer),
Layer.provide(SessionRevert.defaultLayer),
Layer.provide(SessionSummary.defaultLayer),
Layer.provide(Image.defaultLayer),
Layer.provide(
Layer.mergeAll(
Agent.defaultLayer,
@@ -1816,7 +1820,6 @@ export const defaultLayer = Layer.suspend(() =>
LLM.defaultLayer,
Bus.layer,
CrossSpawnSpawner.defaultLayer,
SyncEvent.defaultLayer,
),
),
),
+11
View File
@@ -1,6 +1,7 @@
import { Identifier } from "@/id/id"
import { SyncEvent } from "@/sync"
import { withStatics } from "@opencode-ai/core/schema"
import { Flag } from "@opencode-ai/core/flag/flag"
import * as Schema from "effect/Schema"
export const ID = Schema.String.pipe(
@@ -40,4 +41,14 @@ export function define<const Type extends string, Fields extends Schema.Struct.F
})
}
export function run<Def extends SyncEvent.Definition>(
def: Def,
data: SyncEvent.Event<Def>["data"],
// Temporary escape hatch while the full v2 event system remains experimental.
options?: { publish?: boolean; bypassExperimentalEventSystem?: boolean },
) {
if (!options?.bypassExperimentalEventSystem && !Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) return
SyncEvent.run(def, data, options)
}
export * as EventV2 from "./event"
+19 -13
View File
@@ -12,7 +12,6 @@ import { SessionEvent } from "./session-event"
import { V2Schema } from "./schema"
import { optionalOmitUndefined } from "@opencode-ai/core/schema"
import { Modelv2 } from "./model"
import { SyncEvent } from "@/sync"
export const Delivery = Schema.Literals(["immediate", "deferred"]).annotate({
identifier: "Session.Delivery",
@@ -114,7 +113,6 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const sync = yield* SyncEvent.Service
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
const decode = (row: typeof SessionMessageTable.$inferSelect) =>
@@ -271,18 +269,26 @@ export const layer = Layer.effect(
shell: Effect.fn("V2Session.shell")(function* (_input) {}),
skill: Effect.fn("V2Session.skill")(function* (_input) {}),
switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) {
yield* sync.run(SessionEvent.AgentSwitched.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(Date.now()),
agent: input.agent,
})
EventV2.run(
SessionEvent.AgentSwitched.Sync,
{
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(Date.now()),
agent: input.agent,
},
{ bypassExperimentalEventSystem: true },
)
}),
switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
yield* sync.run(SessionEvent.ModelSwitched.Sync, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(Date.now()),
model: input.model,
})
EventV2.run(
SessionEvent.ModelSwitched.Sync,
{
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(Date.now()),
model: input.model,
},
{ bypassExperimentalEventSystem: true },
)
}),
subagent: Effect.fn("V2Session.subagent")(function* (input) {
const parent = yield* result.get(input.parentID)
@@ -313,6 +319,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(SyncEvent.defaultLayer))
export const defaultLayer = layer
export * as SessionV2 from "./session"
@@ -27,7 +27,6 @@ import { ProviderTest } from "../fake/provider"
import { testEffect } from "../lib/effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { TestConfig } from "../fixture/config"
import { SyncEvent } from "@/sync"
void Log.init({ print: false })
@@ -224,7 +223,6 @@ const deps = Layer.mergeAll(
Plugin.defaultLayer,
Bus.layer,
Config.defaultLayer,
SyncEvent.defaultLayer,
)
const env = Layer.mergeAll(
@@ -271,7 +269,6 @@ function compactionProcessLayer(options?: CompactionProcessOptions) {
Layer.provide(status),
Layer.provide(bus),
Layer.provide(options?.config ?? Config.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
)
}
@@ -24,7 +24,6 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { provideTmpdirServer } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { raw, reply, TestLLMServer } from "../lib/llm-server"
import { SyncEvent } from "@/sync"
void Log.init({ print: false })
@@ -167,7 +166,6 @@ const deps = Layer.mergeAll(
LLM.defaultLayer,
Provider.defaultLayer,
status,
SyncEvent.defaultLayer,
).pipe(Layer.provideMerge(infra))
const env = Layer.mergeAll(
TestLLMServer.layer,
@@ -50,7 +50,6 @@ import { Reference } from "../../src/reference/reference"
import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { reply, TestLLMServer } from "../lib/llm-server"
import { SyncEvent } from "@/sync"
void Log.init({ print: false })
@@ -175,7 +174,6 @@ function makeHttp() {
mcp,
AppFileSystem.defaultLayer,
status,
SyncEvent.defaultLayer,
).pipe(Layer.provideMerge(infra))
const question = Question.layer.pipe(Layer.provideMerge(deps))
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
@@ -58,7 +58,6 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Ripgrep } from "../../src/file/ripgrep"
import { Format } from "../../src/format"
import { Reference } from "../../src/reference/reference"
import { SyncEvent } from "@/sync"
void Log.init({ print: false })
@@ -125,7 +124,6 @@ function makeHttp() {
mcp,
AppFileSystem.defaultLayer,
status,
SyncEvent.defaultLayer,
).pipe(Layer.provideMerge(infra))
const question = Question.layer.pipe(Layer.provideMerge(deps))
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.14.48",
"version": "1.14.46",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.14.48",
"version": "1.14.46",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.14.48",
"version": "1.14.46",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.14.48",
"version": "1.14.46",
"type": "module",
"license": "MIT",
"exports": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.14.48",
"version": "1.14.46",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.14.48",
"version": "1.14.46",
"publisher": "sst-dev",
"repository": {
"type": "git",