chore: generate

This commit is contained in:
opencode-agent[bot]
2026-06-04 03:03:39 +00:00
parent 76ee87ead8
commit b0a929440b
87 changed files with 2360 additions and 1658 deletions

View File

@@ -43,7 +43,9 @@ function latestTool(assistant: SessionMessageAssistant | undefined, callID?: str
}
function latestText(assistant: SessionMessageAssistant | undefined, textID: string) {
return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID)
return assistant?.content.findLast(
(item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID,
)
}
function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) {
@@ -208,19 +210,28 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
break
case "session.next.tool.input.delta":
update(event.properties.sessionID, (draft) => {
const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
const match = latestTool(
ownedAssistant(draft, event.properties.assistantMessageID),
event.properties.callID,
)
if (match?.state.status === "pending") match.state.input += event.properties.delta
})
break
case "session.next.tool.input.ended":
update(event.properties.sessionID, (draft) => {
const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
const match = latestTool(
ownedAssistant(draft, event.properties.assistantMessageID),
event.properties.callID,
)
if (match?.state.status === "pending") match.state.input = event.properties.text
})
break
case "session.next.tool.called":
update(event.properties.sessionID, (draft) => {
const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
const match = latestTool(
ownedAssistant(draft, event.properties.assistantMessageID),
event.properties.callID,
)
if (!match) return
match.time.ran = event.properties.timestamp
match.provider = event.properties.provider
@@ -229,7 +240,10 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
break
case "session.next.tool.progress":
update(event.properties.sessionID, (draft) => {
const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
const match = latestTool(
ownedAssistant(draft, event.properties.assistantMessageID),
event.properties.callID,
)
if (match?.state.status !== "running") return
match.state.structured = event.properties.structured
match.state.content = [...event.properties.content]
@@ -237,7 +251,10 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
break
case "session.next.tool.success":
update(event.properties.sessionID, (draft) => {
const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
const match = latestTool(
ownedAssistant(draft, event.properties.assistantMessageID),
event.properties.callID,
)
if (match?.state.status !== "running") return
match.state = {
status: "completed",
@@ -252,7 +269,10 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
break
case "session.next.tool.failed":
update(event.properties.sessionID, (draft) => {
const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
const match = latestTool(
ownedAssistant(draft, event.properties.assistantMessageID),
event.properties.callID,
)
if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return
match.state = {
status: "error",
@@ -287,7 +307,8 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
if (match) {
match.text = event.properties.text
if (event.properties.providerMetadata !== undefined) match.providerMetadata = event.properties.providerMetadata
if (event.properties.providerMetadata !== undefined)
match.providerMetadata = event.properties.providerMetadata
}
})
break

View File

@@ -1087,7 +1087,8 @@ function toolOutput(content?: Array<ToolTextContent | ToolFileContent>) {
return (content ?? [])
.map((item) => {
if (item.type === "text") return item.text.trim()
const source = item.source.type === "data" ? "inline data" : item.source.type === "url" ? item.source.url : item.source.uri
const source =
item.source.type === "data" ? "inline data" : item.source.type === "url" ? item.source.url : item.source.uri
return `[file ${item.name ?? source}]`
})
.filter(Boolean)

View File

@@ -7,7 +7,15 @@ import { asSchema, type ModelMessage, type Tool } from "ai"
import { Cause, Effect, FiberSet, Queue } from "effect"
import * as Stream from "effect/Stream"
import { FetchHttpClient } from "effect/unstable/http"
import { LLMRequest, Tool as NativeTool, ToolFailure, ToolRuntime, toDefinitions, type JsonSchema, type LLMEvent } from "@opencode-ai/llm"
import {
LLMRequest,
Tool as NativeTool,
ToolFailure,
ToolRuntime,
toDefinitions,
type JsonSchema,
type LLMEvent,
} from "@opencode-ai/llm"
import type { LLMClientShape } from "@opencode-ai/llm/route"
import { LLMNative } from "./native-request"
@@ -80,17 +88,17 @@ export function stream(input: StreamInput): StreamResult {
// translation belongs here, not split across both packages.
const tools = nativeTools(input.tools, input)
const request = LLMNative.request({
model: input.model,
apiKey: current.apiKey,
baseURL: current.baseURL,
messages: ProviderTransform.message(input.messages, input.model, input.providerOptions ?? {}),
toolChoice: input.toolChoice,
temperature: input.temperature,
topP: input.topP,
topK: input.topK,
maxOutputTokens: input.maxOutputTokens,
providerOptions: ProviderTransform.providerOptions(input.model, input.providerOptions ?? {}),
headers: { ...providerHeaders(input.provider.options.headers), ...input.headers },
model: input.model,
apiKey: current.apiKey,
baseURL: current.baseURL,
messages: ProviderTransform.message(input.messages, input.model, input.providerOptions ?? {}),
toolChoice: input.toolChoice,
temperature: input.temperature,
topP: input.topP,
topK: input.topK,
maxOutputTokens: input.maxOutputTokens,
providerOptions: ProviderTransform.providerOptions(input.model, input.providerOptions ?? {}),
headers: { ...providerHeaders(input.provider.options.headers), ...input.headers },
})
const stream = Stream.scoped(
Stream.unwrap(

View File

@@ -426,7 +426,9 @@ export const layer = Layer.effect(
case "tool-input-delta":
{
const toolCall = yield* ensureToolCall(value)
const assistantMessageID = flags.experimentalEventSystem ? yield* requireV2AssistantMessage(toolCall.call) : undefined
const assistantMessageID = flags.experimentalEventSystem
? yield* requireV2AssistantMessage(toolCall.call)
: undefined
if (assistantMessageID) {
yield* events.publish(SessionEvent.Tool.Input.Delta, {
sessionID: ctx.sessionID,
@@ -589,12 +591,19 @@ export const layer = Layer.effect(
const content = [
ToolOutput.text({ type: "text", text: output.output }),
...(output.attachments?.map((item: SessionV1.FilePart) =>
ToolOutput.file({ type: "file", source: toolFileSourceFromUri(item.url), mime: item.mime, name: item.filename }),
ToolOutput.file({
type: "file",
source: toolFileSourceFromUri(item.url),
mime: item.mime,
name: item.filename,
}),
) ?? []),
]
const unsupported = content.find((item) => item.type === "file" && item.source.type !== "data")
if (unsupported?.type === "file") {
const error = new Error(`Tool attachment source "${unsupported.source.type}" must be materialized before durable V2 settlement`)
const error = new Error(
`Tool attachment source "${unsupported.source.type}" must be materialized before durable V2 settlement`,
)
yield* events.publish(SessionEvent.Tool.Failed, {
sessionID: ctx.sessionID,
assistantMessageID,
@@ -611,19 +620,20 @@ export const layer = Layer.effect(
})
yield* failToolCall(value.id, error)
return
} else yield* events.publish(SessionEvent.Tool.Success, {
sessionID: ctx.sessionID,
assistantMessageID,
callID: value.id,
structured: output.metadata,
content,
result: value.result,
provider: {
executed: value.providerExecuted === true || toolCall?.part.metadata?.providerExecuted === true,
...(value.providerMetadata ? { metadata: value.providerMetadata } : {}),
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
} else
yield* events.publish(SessionEvent.Tool.Success, {
sessionID: ctx.sessionID,
assistantMessageID,
callID: value.id,
structured: output.metadata,
content,
result: value.result,
provider: {
executed: value.providerExecuted === true || toolCall?.part.metadata?.providerExecuted === true,
...(value.providerMetadata ? { metadata: value.providerMetadata } : {}),
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
yield* completeToolCall(value.id, output)
return

View File

@@ -107,7 +107,11 @@ test("sync v2 settles pending tools when a live failure arrives", async () => {
await wait(() => {
const assistant = sync.session.message.fromSession("session-1")[0]
return assistant?.type === "assistant" && assistant.content[0]?.type === "tool" && assistant.content[0].state.status === "error"
return (
assistant?.type === "assistant" &&
assistant.content[0]?.type === "tool" &&
assistant.content[0].state.status === "error"
)
})
const assistant = sync.session.message.fromSession("session-1")[0]

View File

@@ -47,7 +47,7 @@ const appCache: Partial<Record<string, CachedApp>> = {}
export async function disposeApps() {
const apps = Object.values(appCache)
for (const key of Object.keys(appCache)) delete appCache[key]
await Promise.all(apps.flatMap((app) => app === undefined ? [] : [app.dispose()]))
await Promise.all(apps.flatMap((app) => (app === undefined ? [] : [app.dispose()])))
}
function app(modules: Runtime, options: CallOptions) {

View File

@@ -493,10 +493,12 @@ describe("session HttpApi", () => {
direction: "next",
})
const nextMessagePage = yield* request(`/api/session/${session.id}/message?cursor=${messageCursor}`, { headers })
expect((yield* json<{ items: SessionMessage.Message[] }>(nextMessagePage)).items.map((message) => message.id)).toEqual([
firstMessage.id,
])
const nextMessagePage = yield* request(`/api/session/${session.id}/message?cursor=${messageCursor}`, {
headers,
})
expect(
(yield* json<{ items: SessionMessage.Message[] }>(nextMessagePage)).items.map((message) => message.id),
).toEqual([firstMessage.id])
const legacyMessageCursor = Buffer.from(
JSON.stringify({ id: secondMessage.id, time: 1, order: "desc", direction: "next" }),
@@ -504,9 +506,9 @@ describe("session HttpApi", () => {
const legacyMessagePage = yield* request(`/api/session/${session.id}/message?cursor=${legacyMessageCursor}`, {
headers,
})
expect((yield* json<{ items: SessionMessage.Message[] }>(legacyMessagePage)).items.map((message) => message.id)).toEqual([
firstMessage.id,
])
expect(
(yield* json<{ items: SessionMessage.Message[] }>(legacyMessagePage)).items.map((message) => message.id),
).toEqual([firstMessage.id])
const messageCursorWithOrder = yield* request(
`/api/session/${session.id}/message?cursor=${messageCursor}&order=asc`,

View File

@@ -280,20 +280,18 @@ function recordedNativeLLMLayer(scenario: RecordedScenario) {
)
// Only the HTTP client is recorded; RequestExecutor and the opencode LLM stack remain real.
const recordedHttp = HttpRecorder.cassetteLayer(scenario.cassette, {
directory: FIXTURES_DIR,
mode: shouldRecord ? "record" : "replay",
metadata: {
provider: scenario.providerID,
protocol: scenario.protocol,
route: scenario.protocol,
tags: scenario.tags,
},
redactor: recordingRedactor,
})
directory: FIXTURES_DIR,
mode: shouldRecord ? "record" : "replay",
metadata: {
provider: scenario.providerID,
protocol: scenario.protocol,
route: scenario.protocol,
tags: scenario.tags,
},
redactor: recordingRedactor,
})
const recordedClient = LLMClient.layer.pipe(
Layer.provide(
Layer.mergeAll(RequestExecutor.layer.pipe(Layer.provide(recordedHttp)), WebSocketExecutor.layer),
),
Layer.provide(Layer.mergeAll(RequestExecutor.layer.pipe(Layer.provide(recordedHttp)), WebSocketExecutor.layer)),
)
return Layer.mergeAll(

View File

@@ -1004,7 +1004,8 @@ itProviderError.live("session.processor effect tests fail provider-executed erro
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
const settlements: Array<typeof SessionEvent.Tool.Failed.Type> = []
const off = yield* events.listen((event) => {
if (event.type === SessionEvent.Tool.Failed.type) settlements.push(event as typeof SessionEvent.Tool.Failed.Type)
if (event.type === SessionEvent.Tool.Failed.type)
settlements.push(event as typeof SessionEvent.Tool.Failed.Type)
return Effect.void
})
const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl })
@@ -1059,7 +1060,8 @@ itFragmentFailure.live("session.processor effect tests flush partial v2 fragment
let reasoning: string | undefined
const off = yield* events.listen((event) => {
seen.push(event.type)
if (event.type === SessionEvent.Text.Ended.type) text = (event.data as typeof SessionEvent.Text.Ended.data.Type).text
if (event.type === SessionEvent.Text.Ended.type)
text = (event.data as typeof SessionEvent.Text.Ended.data.Type).text
if (event.type === SessionEvent.Reasoning.Ended.type)
reasoning = (event.data as typeof SessionEvent.Reasoning.Ended.data.Type).text
return Effect.void