Skip to content

feat: Mirror Fantasy's streaming callbacks as Kit EventBus events (ToolCallStart, ToolCallDelta, Source, etc.) #16

Description

@ezynda3

Summary

Kit's public EventBus currently surfaces only a subset of the streaming callbacks that Fantasy's AgentStreamCall provides. Several useful Fantasy callbacks are either silently swallowed inside internal/agent/agent.go or never wired up at all. This makes it impossible for SDK consumers to build responsive UIs for certain scenarios — most notably, showing a tool call block the moment the LLM starts generating tool arguments, rather than waiting for the full argument JSON to finish streaming.

Motivation / Use Case

I'm building a web UI on top of Kit's public pkg/kit API with SSE streaming. For tools whose arguments are large (e.g. create_artifact with a full HTML document as content, or write with a large file body), the user sees nothing in the chat for several seconds while the LLM streams the tool arguments. The ToolCallEvent only fires after the arguments are fully parsed — by which time 5-10+ seconds may have passed in silence.

Fantasy already fires OnToolInputStart(id, toolName) as soon as the tool name is known, which would let me show a "running" block immediately. But Kit never passes this through.

The same gap exists for several other Fantasy callbacks that would be useful for richer UI feedback.

Fantasy callbacks vs Kit events — full mapping

✅ Currently mirrored (Kit surfaces these)

Fantasy Callback Kit Event Notes
OnReasoningDelta(id, delta) ReasoningDeltaEvent
OnReasoningEnd(id, content) ReasoningCompleteEvent
OnTextDelta(id, text) MessageUpdateEvent ✅ (via onStreamingResponse)
OnToolCall(tc) ToolCallEvent ✅ (fires after args are fully parsed)
OnToolResult(tr) ToolResultEvent
OnStepFinish(step) StepUsageEvent + ToolCallContentEvent ✅ (partially — usage + text-alongside-tools extracted)

❌ Not mirrored (Fantasy provides but Kit drops)

Fantasy Callback Suggested Kit Event Use Case
OnToolInputStart(id, toolName) ToolCallStartEvent High priority. Show a "running" tool block immediately when the LLM begins generating tool call arguments, before the full JSON is streamed. The id is the tool call ID and toolName is the tool being invoked. This is the minimum needed to eliminate the "dead air" gap for tools with large arguments.
OnToolInputDelta(id, delta) ToolCallDeltaEvent Stream tool argument fragments in real-time. Useful for live-previewing artifact content as it's generated, or showing a progress indicator with byte count.
OnToolInputEnd(id) ToolCallEndEvent Signals that tool argument streaming is complete (before execution begins). Could be used to transition a UI block from "generating args" to "executing".
OnTextStart(id) TextStartEvent Know when a new text content block begins. Useful for creating DOM containers before chunks arrive.
OnTextEnd(id) TextEndEvent Know when a text content block is finalized. Currently Kit infers this from MessageEndEvent but that fires per-message, not per-content-block.
OnReasoningStart(id, content) ReasoningStartEvent Know when reasoning begins (Kit only has delta + complete).
OnSource(source) SourceEvent Surface source/citation references from providers that support them (e.g. web search grounding).
OnWarnings(warnings) WarningsEvent Surface provider warnings (e.g. content filter triggers, deprecated features).
OnStreamFinish(usage, finishReason, metadata) StreamFinishEvent Per-stream (per-step) finish with granular usage/metadata. Currently Kit only exposes this partially through StepUsageEvent.
OnAgentStart() (maybe TurnStartEvent is close enough) Low priority — Kit's TurnStartEvent likely covers this.
OnAgentFinish(result) (maybe TurnEndEvent is close enough) Low priority — Kit's TurnEndEvent likely covers this.
OnStepStart(stepNumber) StepStartEvent Know when a new agentic step begins (useful for step counters in UI).
OnChunk(StreamPart) (raw catch-all) Low priority — most consumers would use typed events instead.
OnError(err) (partially via TurnEndEvent.Error) Low priority.
OnFinish(result) (same as OnAgentFinish) Low priority.

Proposed Changes

Minimum viable (high impact, small diff)

Add three new event types and wire them through internal/agent/agent.go:

// events.go — new types

type ToolCallStartEvent struct {
    ToolCallID string
    ToolName   string
    ToolKind   string
}

type ToolCallDeltaEvent struct {
    ToolCallID string
    Delta      string  // JSON fragment of tool arguments
}

type ToolCallEndEvent struct {
    ToolCallID string
}
// internal/agent/agent.go — wire into AgentStreamCall

OnToolInputStart: func(id, toolName string) error {
    if onToolCallStart != nil {
        onToolCallStart(id, toolName)
    }
    return nil
},
OnToolInputDelta: func(id, delta string) error {
    if onToolCallDelta != nil {
        onToolCallDelta(id, delta)
    }
    return nil
},
OnToolInputEnd: func(id string) error {
    if onToolCallEnd != nil {
        onToolCallEnd(id)
    }
    return nil
},

Full parity (longer term)

Mirror all remaining Fantasy callbacks as Kit events so SDK consumers have full observability without needing to fork Kit or drop down to Fantasy directly.

Impact

  • No breaking changes — purely additive new event types
  • Small diff — the plumbing from Fantasy → Kit agent → EventBus is mechanical
  • Unblocks real-time tool call UIs, live artifact preview, source citations, and provider warning surfacing for all SDK consumers

Environment

  • Kit: v0.56.1
  • Fantasy: v0.17.2

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions