Feature 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.
return
This means:
- Real-time TPS tracking is unavailable during tool argument generation. TUI plugins that rely on
message.part.delta events (like the oc-tps TPS 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,
})
return
The tool-input-delta case could follow the same approach since:
- The tool part is already created by
ensureToolCall() during tool-input-start with state: { status: "pending", input: {}, raw: "" }.
readToolCall(value.id) can retrieve the existing part.
updateToolCall(value.id, ...) can accumulate the delta into part.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 — reads part.state.raw.length for token counting
packages/opencode/src/cli/cmd/run/subagent-data.ts:202 — serializes part.state.raw in subagent data
packages/opencode/src/cli/cmd/export.ts:100 — includes part.state.raw in 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 any message.part.delta with field === "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-delta and reasoning-delta handlers fire at the same granularity, so this should be consistent.
- Should
state.raw accumulation 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.
Feature 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/edittools), the opencode session processor silently drops thetool-input-deltaevents atpackages/opencode/src/session/processor.ts:357:This means:
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.The
tool-input-deltaevents exist in the LLM event pipeline (LLMEvent.toolInputDelta()atpackages/llm/src/schema/events.ts:135) and contain the streaming text fragments — they're just not forwarded to the session layer.Context
The
reasoning-deltahandler at lines 329–341 shows the established pattern:The
tool-input-deltacase could follow the same approach since: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.rawis redundant work for no current consumer" is inaccurate —state.rawis 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 outputAnd the
ToolStatePendingtype definition atpackages/sdk/js/src/v2/gen/types.gen.ts:573already includes araw: stringfield, confirming this was the intended design.Request
Make the
tool-input-deltahandler emitmessage.part.deltaevents and accumulate the delta text intopart.state.raw, mirroring thereasoning-deltapattern. 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-optool-input-deltacase with something like:The
fieldname"tool_input"distinguishes tool-argument deltas from text/reasoning deltas in downstream consumers.Open questions
"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.text-deltaandreasoning-deltahandlers fire at the same granularity, so this should be consistent.state.rawaccumulation be toggleable via a config flag to avoid overhead for consumers that don't need it?Additional Notes
The
LLM.Event.ToolInputDeltaschema atpackages/llm/src/schema/events.ts:135already contains the delta text and tool name:All necessary data is available — only the processing logic in
processor.tsneeds to change. The SDK types, event schemas, and LLM event pipeline already support this feature.