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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,10 +354,21 @@ export const layer = Layer.effect(
yield* ensureToolCall(value)
return

case "tool-input-delta":
// AI SDK emits a final `tool-call` with the parsed `input`; accumulating
// delta fragments into `state.raw` is redundant work for no current consumer.
case "tool-input-delta": {
const toolCall = yield* ensureToolCall(value)
const state = toolCall.part.state
if (state.status === "pending") {
state.raw += value.text
yield* session.updatePartDelta({
sessionID: toolCall.part.sessionID,
messageID: toolCall.part.messageID,
partID: toolCall.part.id,
field: "raw",
delta: value.text,
})
}
return
}

case "tool-input-end": {
const toolCall = yield* ensureToolCall(value)
Expand Down
8 changes: 4 additions & 4 deletions packages/opencode/test/lib/llm-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function chunk(input: { delta?: Record<string, unknown>; finish?: string; usage?
} satisfies Line
}

function role() {
export function role() {
return chunk({ delta: { role: "assistant" } })
}

Expand All @@ -92,11 +92,11 @@ function reasonLine(value: string) {
return chunk({ delta: { reasoning_content: value } })
}

function finishLine(reason: string, usage?: Usage) {
export function finishLine(reason: string, usage?: Usage) {
return chunk({ finish: reason, usage })
}

function toolStartLine(id: string, name: string) {
export function toolStartLine(id: string, name: string) {
return chunk({
delta: {
tool_calls: [
Expand All @@ -114,7 +114,7 @@ function toolStartLine(id: string, name: string) {
})
}

function toolArgsLine(value: string) {
export function toolArgsLine(value: string) {
return chunk({
delta: {
tool_calls: [
Expand Down
67 changes: 66 additions & 1 deletion packages/opencode/test/session/processor-effect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import * as Log from "@opencode-ai/core/util/log"
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 { raw, reply, TestLLMServer, toolStartLine, toolArgsLine, finishLine, role } from "../lib/llm-server"
import { SyncEvent } from "@/sync"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge"
Expand Down Expand Up @@ -920,3 +920,68 @@ it.live("session.processor effect tests mark interruptions aborted without manua
{ config: (url) => providerCfg(url) },
),
)

it.live("tool-input-delta events accumulate into state.raw and emit part.delta", () =>
provideTmpdirServer(
({ dir, llm }) =>
Effect.gen(function* () {
const { processors, session, provider } = yield* boot()

const id = "call_1"
const fullArgs = JSON.stringify({ content: "hello world file content here" })
const half = Math.floor(fullArgs.length / 2)

yield* llm.push(
raw({
head: [
role(),
toolStartLine(id, "write"),
toolArgsLine(fullArgs.slice(0, half)),
toolArgsLine(fullArgs.slice(half)),
],
tail: [finishLine("stop")],
}),
)

const chat = yield* session.create({})
const parent = yield* user(chat.id, "write a file")
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
const handle = yield* processors.create({
assistantMessage: msg,
sessionID: chat.id,
model: mdl,
})

const value = yield* handle.process({
user: {
id: parent.id,
sessionID: chat.id,
role: "user",
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies MessageV2.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
system: [],
messages: [{ role: "user", content: "write a file" }],
tools: {},
})

const parts = MessageV2.parts(msg.id)
const toolPart = parts.find((part): part is MessageV2.ToolPart => part.type === "tool")

expect(value).toBe("continue")
expect(toolPart).toBeDefined()
expect(toolPart?.tool).toBe("write")
if (toolPart?.state.status === "pending") {
expect(toolPart.state.raw.length).toBeGreaterThan(0)
} else if (toolPart?.state.status === "completed") {
expect(toolPart.state.input).toEqual(JSON.parse(fullArgs))
}
}),
{ config: (url) => providerCfg(url) },
),
)
Loading