Repository navigation
[FEATURE]: Emit message.part.delta for tool-input-delta events #28800
Description
Feature hasn't been suggested before.
- I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request
Problem
When an LLM streams tool call arguments (e.g., the file content for write/edit tools), the opencode session processor silently drops the tool-input-delta events at packages/opencode/src/session/processor.ts:357:
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.
returnThis means:
- Real-time TPS tracking is unavailable during tool argument generation. TUI plugins that rely on
message.part.deltaevents (like theoc-tpsTPS tracker) show stale data or-while the AI is writing file content, because no delta events are emitted for tool parts. - Any future event-driven feature that depends on observing tool argument streaming (progress indicators, live preview, etc.) is blocked by this gap.
The tool-input-delta events exist in the LLM event pipeline (LLMEvent.toolInputDelta() at packages/llm/src/schema/events.ts:135) and contain the streaming text fragments — they're just not forwarded to the session layer.
Context
The reasoning-delta handler at lines 329–341 shows the established pattern:
case "reasoning-delta":
if (!(value.id in ctx.reasoningMap)) return
ctx.reasoningMap[value.id].text += value.text
yield* session.updatePartDelta({
sessionID: ctx.reasoningMap[value.id].sessionID,
messageID: ctx.reasoningMap[value.id].messageID,
partID: ctx.reasoningMap[value.id].id,
field: "text",
delta: value.text,
})
returnThe tool-input-delta case could follow the same approach since:
- The tool part is already created by
ensureToolCall()duringtool-input-startwithstate: { status: "pending", input: {}, raw: "" }. readToolCall(value.id)can retrieve the existing part.updateToolCall(value.id, ...)can accumulate the delta intopart.state.raw.session.updatePartDelta(...)can emit the event with an appropriate field name.
Additionally, the comment "accumulating delta fragments into state.raw is redundant work for no current consumer" is inaccurate — state.raw is already consumed by:
packages/app/src/components/session/session-context-breakdown.ts:29— readspart.state.raw.lengthfor token countingpackages/opencode/src/cli/cmd/run/subagent-data.ts:202— serializespart.state.rawin subagent datapackages/opencode/src/cli/cmd/export.ts:100— includespart.state.rawin export output
And the ToolStatePending type definition at packages/sdk/js/src/v2/gen/types.gen.ts:573 already includes a raw: string field, confirming this was the intended design.
Request
Make the tool-input-delta handler emit message.part.delta events and accumulate the delta text into part.state.raw, mirroring the reasoning-delta pattern. This would enable real-time TPS tracking, live progress indicators, and other event-driven features during tool argument generation.
Proposed Solution
In packages/opencode/src/session/processor.ts, replace the no-op tool-input-delta case with something like:
case "tool-input-delta": {
const match = yield* readToolCall(value.id)
if (!match) return
const part = yield* session.updatePart({
...match.part,
state: { ...match.part.state, status: "pending" as const, raw: match.part.state.raw + value.text },
})
yield* session.updatePartDelta({
sessionID: match.part.sessionID,
messageID: match.part.messageID,
partID: match.part.id,
field: "tool_input",
delta: value.text,
})
return
}The field name "tool_input" distinguishes tool-argument deltas from text/reasoning deltas in downstream consumers.
Open questions
- Should the delta field be
"tool_input"or something more generic (e.g., just"text"like reasoning-delta)? A distinct name makes it easier for consumers to differentiate, but"text"simplifies the assumption that anymessage.part.deltawithfield === "text"represents token generation. - Are there any performance concerns with firing per-character deltas for large tool inputs (e.g., writing a 10k-line file)? The existing
text-deltaandreasoning-deltahandlers fire at the same granularity, so this should be consistent. - Should
state.rawaccumulation be toggleable via a config flag to avoid overhead for consumers that don't need it?
Additional Notes
The LLM.Event.ToolInputDelta schema at packages/llm/src/schema/events.ts:135 already contains the delta text and tool name:
export const ToolInputDelta = Schema.Struct({
type: Schema.tag("tool-input-delta"),
id: ToolCallID,
name: Schema.String,
text: Schema.String,
});All necessary data is available — only the processing logic in processor.ts needs to change. The SDK types, event schemas, and LLM event pipeline already support this feature.
Activity
github-actions commentedon Jul 22, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.