mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-13 19:20:06 +00:00
feat(events): mirror Fantasy tool input streaming callbacks as Kit events
- Add ToolCallStartEvent, ToolCallDeltaEvent, ToolCallEndEvent to SDK - Wire Fantasy OnToolInputStart/Delta/End through agent to EventBus - Add typed convenience subscribers: OnToolCallStart/Delta/End on Kit - Bridge new events to TUI via ToolCallInputStart/Delta/End app events - Extend extension system with OnToolCallInputStart/Delta/End handlers - Add extension event types, API methods, loader wiring, Yaegi symbols - Update docs: README, SDK skill, extensions skill, www/sdk, www/extensions Closes #16
This commit is contained in:
@@ -301,7 +301,7 @@ kit -e examples/extensions/minimal.go
|
||||
|
||||
### Extension Capabilities
|
||||
|
||||
**Lifecycle Events**: OnSessionStart, OnSessionShutdown, OnBeforeAgentStart, OnAgentStart, OnAgentEnd, OnToolCall, OnToolExecutionStart, OnToolOutput, OnToolExecutionEnd, OnToolResult, OnInput, OnMessageStart, OnMessageUpdate, OnMessageEnd, OnModelChange, OnContextPrepare, OnBeforeFork, OnBeforeSessionSwitch, OnBeforeCompact, OnCustomEvent, OnSubagentStart, OnSubagentChunk, OnSubagentEnd
|
||||
**Lifecycle Events**: OnSessionStart, OnSessionShutdown, OnBeforeAgentStart, OnAgentStart, OnAgentEnd, OnToolCall, OnToolCallInputStart, OnToolCallInputDelta, OnToolCallInputEnd, OnToolExecutionStart, OnToolOutput, OnToolExecutionEnd, OnToolResult, OnInput, OnMessageStart, OnMessageUpdate, OnMessageEnd, OnModelChange, OnContextPrepare, OnBeforeFork, OnBeforeSessionSwitch, OnBeforeCompact, OnCustomEvent, OnSubagentStart, OnSubagentChunk, OnSubagentEnd
|
||||
|
||||
**Custom Components**:
|
||||
- **Tools**: Add new tools the LLM can invoke
|
||||
|
||||
+48
-2
@@ -87,6 +87,19 @@ type ReasoningDeltaHandler func(delta string)
|
||||
// Called when the last reasoning token has been processed, before text streaming starts.
|
||||
type ReasoningCompleteHandler func()
|
||||
|
||||
// ToolCallStartHandler is a function type for handling the moment when the LLM
|
||||
// begins generating tool call arguments. The tool name is known but the full
|
||||
// argument JSON is still streaming.
|
||||
type ToolCallStartHandler func(toolCallID, toolName string)
|
||||
|
||||
// ToolCallDeltaHandler is a function type for handling streamed fragments of
|
||||
// tool call arguments as they arrive from the LLM.
|
||||
type ToolCallDeltaHandler func(toolCallID, delta string)
|
||||
|
||||
// ToolCallEndHandler is a function type for handling the end of tool argument
|
||||
// streaming, before the tool call is parsed and execution begins.
|
||||
type ToolCallEndHandler func(toolCallID string)
|
||||
|
||||
// ToolOutputHandler is a function type for handling streaming tool output chunks.
|
||||
// Used by tools like bash to stream output as it arrives rather than waiting
|
||||
// for the command to complete. The isStderr flag indicates if the chunk
|
||||
@@ -411,7 +424,7 @@ func (a *Agent) GenerateWithLoop(ctx context.Context, messages []fantasy.Message
|
||||
onResponse ResponseHandler, onToolCallContent ToolCallContentHandler,
|
||||
) (*GenerateWithLoopResult, error) {
|
||||
return a.GenerateWithLoopAndStreaming(ctx, messages, onToolCall, onToolExecution, onToolResult,
|
||||
onResponse, onToolCallContent, nil, nil, nil, nil, nil, nil, nil)
|
||||
onResponse, onToolCallContent, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
}
|
||||
|
||||
// GenerateWithLoopAndStreaming processes messages using the agent with streaming and callbacks.
|
||||
@@ -427,6 +440,9 @@ func (a *Agent) GenerateWithLoopAndStreaming(ctx context.Context, messages []fan
|
||||
onStepMessages StepMessagesHandler,
|
||||
onStepUsage StepUsageHandler,
|
||||
onPasswordPrompt PasswordPromptHandler,
|
||||
onToolCallStart ToolCallStartHandler,
|
||||
onToolCallDelta ToolCallDeltaHandler,
|
||||
onToolCallEnd ToolCallEndHandler,
|
||||
) (*GenerateWithLoopResult, error) {
|
||||
|
||||
// Wait for background MCP tool loading to complete and rebuild the
|
||||
@@ -462,7 +478,8 @@ func (a *Agent) GenerateWithLoopAndStreaming(ctx context.Context, messages []fan
|
||||
// Stream is required to observe tool execution in real time. The non-streaming
|
||||
// Generate path is reserved for the simple case with no callbacks at all.
|
||||
hasCallbacks := onToolCall != nil || onToolExecution != nil || onToolResult != nil ||
|
||||
onToolCallContent != nil || onStreamingResponse != nil || onReasoningDelta != nil
|
||||
onToolCallContent != nil || onStreamingResponse != nil || onReasoningDelta != nil ||
|
||||
onToolCallStart != nil || onToolCallDelta != nil || onToolCallEnd != nil
|
||||
|
||||
if a.streamingEnabled || hasCallbacks {
|
||||
// Track completed step messages so we can return partial results
|
||||
@@ -481,6 +498,35 @@ func (a *Agent) GenerateWithLoopAndStreaming(ctx context.Context, messages []fan
|
||||
Files: files,
|
||||
Messages: history,
|
||||
|
||||
// Tool input streaming callbacks — fire during tool argument generation
|
||||
OnToolInputStart: func(id, toolName string) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if onToolCallStart != nil {
|
||||
onToolCallStart(id, toolName)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
OnToolInputDelta: func(id, delta string) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if onToolCallDelta != nil {
|
||||
onToolCallDelta(id, delta)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
OnToolInputEnd: func(id string) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if onToolCallEnd != nil {
|
||||
onToolCallEnd(id)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
||||
// Reasoning/thinking streaming callback
|
||||
OnReasoningDelta: func(id, delta string) error {
|
||||
if ctx.Err() != nil {
|
||||
|
||||
@@ -888,6 +888,12 @@ func (a *App) subscribeSDKEvents(sendFn func(tea.Msg), stepUsageSeen *atomic.Boo
|
||||
switch ev := e.(type) {
|
||||
case kit.ToolCallEvent:
|
||||
sendFn(ToolCallStartedEvent{ToolCallID: ev.ToolCallID, ToolName: ev.ToolName, ToolArgs: ev.ToolArgs})
|
||||
case kit.ToolCallStartEvent:
|
||||
sendFn(ToolCallInputStartEvent{ToolCallID: ev.ToolCallID, ToolName: ev.ToolName, ToolKind: ev.ToolKind})
|
||||
case kit.ToolCallDeltaEvent:
|
||||
sendFn(ToolCallInputDeltaEvent{ToolCallID: ev.ToolCallID, Delta: ev.Delta})
|
||||
case kit.ToolCallEndEvent:
|
||||
sendFn(ToolCallInputEndEvent{ToolCallID: ev.ToolCallID})
|
||||
case kit.ToolExecutionStartEvent:
|
||||
sendFn(ToolExecutionEvent{ToolCallID: ev.ToolCallID, ToolName: ev.ToolName, ToolArgs: ev.ToolArgs, IsStarting: true})
|
||||
case kit.ToolExecutionEndEvent:
|
||||
|
||||
@@ -32,6 +32,36 @@ type ToolCallStartedEvent struct {
|
||||
ToolArgs string
|
||||
}
|
||||
|
||||
// ToolCallInputStartEvent is sent when the LLM begins generating tool call
|
||||
// arguments. The tool name is known but the full argument JSON is still being
|
||||
// streamed. UIs can use this to show a "running" indicator immediately instead
|
||||
// of waiting for the full argument JSON to finish streaming.
|
||||
type ToolCallInputStartEvent struct {
|
||||
// ToolCallID is the stable identifier for correlating tool lifecycle events.
|
||||
ToolCallID string
|
||||
// ToolName is the name of the tool being called.
|
||||
ToolName string
|
||||
// ToolKind classifies the tool: "execute", "edit", "read", "search", "agent".
|
||||
ToolKind string
|
||||
}
|
||||
|
||||
// ToolCallInputDeltaEvent is sent for each streamed fragment of tool call
|
||||
// arguments as they arrive from the LLM. Useful for live-previewing content
|
||||
// or showing a progress indicator with byte count.
|
||||
type ToolCallInputDeltaEvent struct {
|
||||
// ToolCallID is the stable identifier for correlating tool lifecycle events.
|
||||
ToolCallID string
|
||||
// Delta is a JSON fragment of tool call arguments.
|
||||
Delta string
|
||||
}
|
||||
|
||||
// ToolCallInputEndEvent is sent when tool argument streaming is complete,
|
||||
// before the tool call is parsed and execution begins.
|
||||
type ToolCallInputEndEvent struct {
|
||||
// ToolCallID is the stable identifier for correlating tool lifecycle events.
|
||||
ToolCallID string
|
||||
}
|
||||
|
||||
// ToolExecutionEvent is sent when a tool starts or finishes executing.
|
||||
// The IsStarting flag distinguishes between the start and end of execution.
|
||||
type ToolExecutionEvent struct {
|
||||
|
||||
@@ -1063,6 +1063,9 @@ type PrintBlockOpts struct {
|
||||
type API struct {
|
||||
// Event-specific registration functions (wired by the loader).
|
||||
onToolCall func(func(ToolCallEvent, Context) *ToolCallResult)
|
||||
onToolCallInputStart func(func(ToolCallInputStartEvent, Context))
|
||||
onToolCallInputDelta func(func(ToolCallInputDeltaEvent, Context))
|
||||
onToolCallInputEnd func(func(ToolCallInputEndEvent, Context))
|
||||
onToolExecStart func(func(ToolExecutionStartEvent, Context))
|
||||
onToolExecEnd func(func(ToolExecutionEndEvent, Context))
|
||||
onToolOutput func(func(ToolOutputEvent, Context))
|
||||
@@ -1099,6 +1102,26 @@ func (a *API) OnToolCall(handler func(ToolCallEvent, Context) *ToolCallResult) {
|
||||
a.onToolCall(handler)
|
||||
}
|
||||
|
||||
// OnToolCallInputStart registers a handler that fires when the LLM begins
|
||||
// generating tool call arguments. The tool name is known but the full
|
||||
// argument JSON is still being streamed. Useful for showing a "running"
|
||||
// indicator immediately without waiting for the full arguments.
|
||||
func (a *API) OnToolCallInputStart(handler func(ToolCallInputStartEvent, Context)) {
|
||||
a.onToolCallInputStart(handler)
|
||||
}
|
||||
|
||||
// OnToolCallInputDelta registers a handler that fires for each streamed
|
||||
// fragment of tool call arguments as they arrive from the LLM.
|
||||
func (a *API) OnToolCallInputDelta(handler func(ToolCallInputDeltaEvent, Context)) {
|
||||
a.onToolCallInputDelta(handler)
|
||||
}
|
||||
|
||||
// OnToolCallInputEnd registers a handler that fires when tool argument
|
||||
// streaming is complete, before the tool call is parsed and execution begins.
|
||||
func (a *API) OnToolCallInputEnd(handler func(ToolCallInputEndEvent, Context)) {
|
||||
a.onToolCallInputEnd(handler)
|
||||
}
|
||||
|
||||
// OnToolExecutionStart registers a handler for tool execution start.
|
||||
func (a *API) OnToolExecutionStart(handler func(ToolExecutionStartEvent, Context)) {
|
||||
a.onToolExecStart(handler)
|
||||
@@ -1890,6 +1913,34 @@ type ToolCallResult struct {
|
||||
|
||||
func (ToolCallResult) isResult() {}
|
||||
|
||||
// ToolCallInputStartEvent fires when the LLM begins generating tool call
|
||||
// arguments. The tool name is known but the full argument JSON is still
|
||||
// being streamed.
|
||||
type ToolCallInputStartEvent struct {
|
||||
ToolCallID string
|
||||
ToolName string
|
||||
ToolKind string // Tool classification: "execute", "edit", "read", "search", "agent"
|
||||
}
|
||||
|
||||
func (e ToolCallInputStartEvent) Type() EventType { return ToolCallInputStart }
|
||||
|
||||
// ToolCallInputDeltaEvent fires for each streamed fragment of tool call
|
||||
// arguments as they arrive from the LLM.
|
||||
type ToolCallInputDeltaEvent struct {
|
||||
ToolCallID string
|
||||
Delta string // JSON fragment of tool arguments
|
||||
}
|
||||
|
||||
func (e ToolCallInputDeltaEvent) Type() EventType { return ToolCallInputDelta }
|
||||
|
||||
// ToolCallInputEndEvent fires when tool argument streaming is complete,
|
||||
// before the tool call is parsed and execution begins.
|
||||
type ToolCallInputEndEvent struct {
|
||||
ToolCallID string
|
||||
}
|
||||
|
||||
func (e ToolCallInputEndEvent) Type() EventType { return ToolCallInputEnd }
|
||||
|
||||
// ToolExecutionStartEvent fires when a tool begins executing.
|
||||
type ToolExecutionStartEvent struct {
|
||||
ToolCallID string
|
||||
|
||||
@@ -13,6 +13,19 @@ const (
|
||||
// ToolCall fires before a tool executes. Handlers can block execution.
|
||||
ToolCall EventType = "tool_call"
|
||||
|
||||
// ToolCallInputStart fires when the LLM begins generating tool call
|
||||
// arguments. The tool name is known but the full argument JSON is still
|
||||
// being streamed.
|
||||
ToolCallInputStart EventType = "tool_call_input_start"
|
||||
|
||||
// ToolCallInputDelta fires for each streamed fragment of tool call
|
||||
// arguments as they arrive from the LLM.
|
||||
ToolCallInputDelta EventType = "tool_call_input_delta"
|
||||
|
||||
// ToolCallInputEnd fires when tool argument streaming is complete,
|
||||
// before the tool call is parsed and execution begins.
|
||||
ToolCallInputEnd EventType = "tool_call_input_end"
|
||||
|
||||
// ToolExecutionStart fires when a tool begins executing.
|
||||
ToolExecutionStart EventType = "tool_execution_start"
|
||||
|
||||
@@ -88,7 +101,8 @@ const (
|
||||
// AllEventTypes returns every supported event type.
|
||||
func AllEventTypes() []EventType {
|
||||
return []EventType{
|
||||
ToolCall, ToolExecutionStart, ToolExecutionEnd, ToolResult,
|
||||
ToolCall, ToolCallInputStart, ToolCallInputDelta, ToolCallInputEnd,
|
||||
ToolExecutionStart, ToolExecutionEnd, ToolResult,
|
||||
Input, BeforeAgentStart, AgentStart, AgentEnd,
|
||||
MessageStart, MessageUpdate, MessageEnd,
|
||||
SessionStart, SessionShutdown,
|
||||
|
||||
@@ -4,8 +4,8 @@ import "testing"
|
||||
|
||||
func TestAllEventTypes_Count(t *testing.T) {
|
||||
all := AllEventTypes()
|
||||
if len(all) != 21 {
|
||||
t.Fatalf("expected 21 event types, got %d", len(all))
|
||||
if len(all) != 24 {
|
||||
t.Fatalf("expected 24 event types, got %d", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ func TestEventType_TypeMethod(t *testing.T) {
|
||||
want EventType
|
||||
}{
|
||||
{ToolCallEvent{ToolName: "test"}, ToolCall},
|
||||
{ToolCallInputStartEvent{ToolCallID: "x", ToolName: "test"}, ToolCallInputStart},
|
||||
{ToolCallInputDeltaEvent{ToolCallID: "x", Delta: "{"}, ToolCallInputDelta},
|
||||
{ToolCallInputEndEvent{ToolCallID: "x"}, ToolCallInputEnd},
|
||||
{ToolExecutionStartEvent{ToolName: "test"}, ToolExecutionStart},
|
||||
{ToolExecutionEndEvent{ToolName: "test"}, ToolExecutionEnd},
|
||||
{ToolResultEvent{ToolName: "test"}, ToolResult},
|
||||
|
||||
@@ -429,6 +429,24 @@ func loadSingleExtension(path string) (*LoadedExtension, error) {
|
||||
return *r
|
||||
})
|
||||
},
|
||||
onToolCallInputStart: func(h func(ToolCallInputStartEvent, Context)) {
|
||||
reg(ToolCallInputStart, func(e Event, c Context) Result {
|
||||
h(e.(ToolCallInputStartEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onToolCallInputDelta: func(h func(ToolCallInputDeltaEvent, Context)) {
|
||||
reg(ToolCallInputDelta, func(e Event, c Context) Result {
|
||||
h(e.(ToolCallInputDeltaEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onToolCallInputEnd: func(h func(ToolCallInputEndEvent, Context)) {
|
||||
reg(ToolCallInputEnd, func(e Event, c Context) Result {
|
||||
h(e.(ToolCallInputEndEvent), c)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
onToolExecStart: func(h func(ToolExecutionStartEvent, Context)) {
|
||||
reg(ToolExecutionStart, func(e Event, c Context) Result {
|
||||
h(e.(ToolExecutionStartEvent), c)
|
||||
|
||||
@@ -152,6 +152,9 @@ func Symbols() interp.Exports {
|
||||
// Event structs
|
||||
"ToolCallEvent": reflect.ValueOf((*ToolCallEvent)(nil)),
|
||||
"ToolCallResult": reflect.ValueOf((*ToolCallResult)(nil)),
|
||||
"ToolCallInputStartEvent": reflect.ValueOf((*ToolCallInputStartEvent)(nil)),
|
||||
"ToolCallInputDeltaEvent": reflect.ValueOf((*ToolCallInputDeltaEvent)(nil)),
|
||||
"ToolCallInputEndEvent": reflect.ValueOf((*ToolCallInputEndEvent)(nil)),
|
||||
"ToolExecutionStartEvent": reflect.ValueOf((*ToolExecutionStartEvent)(nil)),
|
||||
"ToolExecutionEndEvent": reflect.ValueOf((*ToolExecutionEndEvent)(nil)),
|
||||
"ToolOutputEvent": reflect.ValueOf((*ToolOutputEvent)(nil)),
|
||||
|
||||
@@ -23,6 +23,14 @@ const (
|
||||
EventMessageUpdate EventType = "message_update"
|
||||
// EventMessageEnd fires when the assistant message is complete.
|
||||
EventMessageEnd EventType = "message_end"
|
||||
// EventToolCallStart fires when the LLM begins generating tool call arguments.
|
||||
// The tool name is known but arguments are still streaming.
|
||||
EventToolCallStart EventType = "tool_call_start"
|
||||
// EventToolCallDelta fires for each streamed fragment of tool call arguments.
|
||||
EventToolCallDelta EventType = "tool_call_delta"
|
||||
// EventToolCallEnd fires when tool argument streaming is complete, before
|
||||
// the tool call is parsed and execution begins.
|
||||
EventToolCallEnd EventType = "tool_call_end"
|
||||
// EventToolCall fires when a tool call has been parsed and is about to execute.
|
||||
EventToolCall EventType = "tool_call"
|
||||
// EventToolExecutionStart fires when a tool begins executing.
|
||||
@@ -216,6 +224,40 @@ type MessageEndEvent struct {
|
||||
// EventType implements Event.
|
||||
func (e MessageEndEvent) EventType() EventType { return EventMessageEnd }
|
||||
|
||||
// ToolCallStartEvent fires when the LLM begins generating tool call arguments.
|
||||
// The tool name is known at this point but the full arguments are still being
|
||||
// streamed. UIs can use this to show a "running" indicator immediately instead
|
||||
// of waiting for the full argument JSON to finish streaming.
|
||||
type ToolCallStartEvent struct {
|
||||
ToolCallID string // Stable ID for correlating tool lifecycle events
|
||||
ToolName string
|
||||
ToolKind string // Tool classification: "execute", "edit", "read", "search", "agent"
|
||||
}
|
||||
|
||||
// EventType implements Event.
|
||||
func (e ToolCallStartEvent) EventType() EventType { return EventToolCallStart }
|
||||
|
||||
// ToolCallDeltaEvent fires for each streamed fragment of tool call arguments.
|
||||
// Useful for live-previewing artifact content as it's generated, or showing a
|
||||
// progress indicator with byte count.
|
||||
type ToolCallDeltaEvent struct {
|
||||
ToolCallID string // Stable ID for correlating tool lifecycle events
|
||||
Delta string // JSON fragment of tool arguments
|
||||
}
|
||||
|
||||
// EventType implements Event.
|
||||
func (e ToolCallDeltaEvent) EventType() EventType { return EventToolCallDelta }
|
||||
|
||||
// ToolCallEndEvent fires when tool argument streaming is complete, before
|
||||
// the tool call is parsed and execution begins. UIs can use this to
|
||||
// transition from an "generating args" state to an "executing" state.
|
||||
type ToolCallEndEvent struct {
|
||||
ToolCallID string // Stable ID for correlating tool lifecycle events
|
||||
}
|
||||
|
||||
// EventType implements Event.
|
||||
func (e ToolCallEndEvent) EventType() EventType { return EventToolCallEnd }
|
||||
|
||||
// ToolCallEvent fires when a tool call has been parsed.
|
||||
type ToolCallEvent struct {
|
||||
ToolCallID string // Stable ID for correlating tool lifecycle events
|
||||
@@ -420,6 +462,39 @@ func (m *Kit) OnToolCall(handler func(ToolCallEvent)) func() {
|
||||
})
|
||||
}
|
||||
|
||||
// OnToolCallStart registers a handler that fires only for ToolCallStartEvent.
|
||||
// This fires when the LLM begins generating tool call arguments — before the
|
||||
// full argument JSON is available. Returns an unsubscribe function.
|
||||
func (m *Kit) OnToolCallStart(handler func(ToolCallStartEvent)) func() {
|
||||
return m.Subscribe(func(e Event) {
|
||||
if tcs, ok := e.(ToolCallStartEvent); ok {
|
||||
handler(tcs)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// OnToolCallDelta registers a handler that fires only for ToolCallDeltaEvent.
|
||||
// Each delta contains a JSON fragment of tool call arguments as they stream in.
|
||||
// Returns an unsubscribe function.
|
||||
func (m *Kit) OnToolCallDelta(handler func(ToolCallDeltaEvent)) func() {
|
||||
return m.Subscribe(func(e Event) {
|
||||
if tcd, ok := e.(ToolCallDeltaEvent); ok {
|
||||
handler(tcd)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// OnToolCallEnd registers a handler that fires only for ToolCallEndEvent.
|
||||
// This fires when tool argument streaming is complete, before the tool call
|
||||
// is parsed and execution begins. Returns an unsubscribe function.
|
||||
func (m *Kit) OnToolCallEnd(handler func(ToolCallEndEvent)) func() {
|
||||
return m.Subscribe(func(e Event) {
|
||||
if tce, ok := e.(ToolCallEndEvent); ok {
|
||||
handler(tce)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// OnToolResult registers a handler that fires only for ToolResultEvent.
|
||||
// Returns an unsubscribe function.
|
||||
func (m *Kit) OnToolResult(handler func(ToolResultEvent)) func() {
|
||||
|
||||
@@ -100,6 +100,38 @@ func (m *Kit) bridgeExtensions(runner *extensions.Runner) {
|
||||
})
|
||||
}
|
||||
|
||||
// Tool call input streaming events — fire as the LLM generates tool arguments.
|
||||
if runner.HasHandlers(extensions.ToolCallInputStart) {
|
||||
m.Subscribe(func(e Event) {
|
||||
if ev, ok := e.(ToolCallStartEvent); ok {
|
||||
_, _ = runner.Emit(extensions.ToolCallInputStartEvent{
|
||||
ToolCallID: ev.ToolCallID,
|
||||
ToolName: ev.ToolName,
|
||||
ToolKind: ev.ToolKind,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
if runner.HasHandlers(extensions.ToolCallInputDelta) {
|
||||
m.Subscribe(func(e Event) {
|
||||
if ev, ok := e.(ToolCallDeltaEvent); ok {
|
||||
_, _ = runner.Emit(extensions.ToolCallInputDeltaEvent{
|
||||
ToolCallID: ev.ToolCallID,
|
||||
Delta: ev.Delta,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
if runner.HasHandlers(extensions.ToolCallInputEnd) {
|
||||
m.Subscribe(func(e Event) {
|
||||
if ev, ok := e.(ToolCallEndEvent); ok {
|
||||
_, _ = runner.Emit(extensions.ToolCallInputEndEvent{
|
||||
ToolCallID: ev.ToolCallID,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if runner.HasHandlers(extensions.AgentEnd) {
|
||||
m.Subscribe(func(e Event) {
|
||||
if ev, ok := e.(TurnEndEvent); ok {
|
||||
|
||||
@@ -2020,6 +2020,25 @@ func (m *Kit) generate(ctx context.Context, messages []fantasy.Message) (*agent.
|
||||
resp := <-responseCh
|
||||
return resp.Password, resp.Cancelled
|
||||
},
|
||||
// Tool call argument streaming — fire as the LLM generates tool arguments
|
||||
func(toolCallID, toolName string) {
|
||||
m.events.emit(ToolCallStartEvent{
|
||||
ToolCallID: toolCallID,
|
||||
ToolName: toolName,
|
||||
ToolKind: toolKindFor(toolName),
|
||||
})
|
||||
},
|
||||
func(toolCallID, delta string) {
|
||||
m.events.emit(ToolCallDeltaEvent{
|
||||
ToolCallID: toolCallID,
|
||||
Delta: delta,
|
||||
})
|
||||
},
|
||||
func(toolCallID string) {
|
||||
m.events.emit(ToolCallEndEvent{
|
||||
ToolCallID: toolCallID,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ The `Init` function receives an `ext.API` object for registering handlers, and e
|
||||
|
||||
## Lifecycle Events
|
||||
|
||||
Kit provides 18 lifecycle events. Each handler receives an event struct and a `Context`.
|
||||
Kit provides 21 lifecycle events. Each handler receives an event struct and a `Context`.
|
||||
|
||||
### Session Events
|
||||
|
||||
@@ -136,6 +136,37 @@ api.OnToolResult(func(e ext.ToolResultEvent, ctx ext.Context) *ext.ToolResultRes
|
||||
})
|
||||
```
|
||||
|
||||
### Tool Call Input Streaming Events
|
||||
|
||||
These events fire during the LLM's tool argument generation phase, **before** the tool call is fully parsed and before `OnToolCall` fires. They enable UIs to show tool activity immediately rather than waiting for the full argument JSON to finish streaming.
|
||||
|
||||
```go
|
||||
// Fires when the LLM begins generating tool call arguments.
|
||||
// The tool name is known but the full argument JSON is still streaming.
|
||||
api.OnToolCallInputStart(func(e ext.ToolCallInputStartEvent, ctx ext.Context) {
|
||||
// e.ToolCallID string — stable ID for correlating tool lifecycle events
|
||||
// e.ToolName string — name of the tool being called
|
||||
// e.ToolKind string — "execute", "edit", "read", "search", "agent"
|
||||
ctx.PrintInfo("Tool starting: " + e.ToolName)
|
||||
})
|
||||
|
||||
// Fires for each streamed fragment of tool call arguments.
|
||||
// Useful for live-previewing artifact content or showing a progress indicator.
|
||||
api.OnToolCallInputDelta(func(e ext.ToolCallInputDeltaEvent, ctx ext.Context) {
|
||||
// e.ToolCallID string
|
||||
// e.Delta string — JSON fragment of tool arguments
|
||||
})
|
||||
|
||||
// Fires when tool argument streaming is complete, before the tool call
|
||||
// is parsed and execution begins. Transition UI from "generating args"
|
||||
// to "executing".
|
||||
api.OnToolCallInputEnd(func(e ext.ToolCallInputEndEvent, ctx ext.Context) {
|
||||
// e.ToolCallID string
|
||||
})
|
||||
```
|
||||
|
||||
**Full tool lifecycle order**: `OnToolCallInputStart` → `OnToolCallInputDelta` (repeated) → `OnToolCallInputEnd` → `OnToolCall` → `OnToolExecutionStart` → `OnToolOutput` (optional, repeated) → `OnToolExecutionEnd` → `OnToolResult`
|
||||
|
||||
### Input Events
|
||||
|
||||
```go
|
||||
|
||||
@@ -252,6 +252,25 @@ unsub := host.OnToolCall(func(e kit.ToolCallEvent) {
|
||||
})
|
||||
defer unsub()
|
||||
|
||||
host.OnToolCallStart(func(e kit.ToolCallStartEvent) {
|
||||
// Fires when the LLM begins generating tool call arguments.
|
||||
// e.ToolCallID, e.ToolName, e.ToolKind
|
||||
// Use this to show a "running" indicator immediately — before the
|
||||
// full argument JSON finishes streaming (eliminates "dead air").
|
||||
})
|
||||
|
||||
host.OnToolCallDelta(func(e kit.ToolCallDeltaEvent) {
|
||||
// Fires for each streamed fragment of tool call arguments.
|
||||
// e.ToolCallID, e.Delta (JSON fragment)
|
||||
// Useful for live-previewing artifact content or progress indicators.
|
||||
})
|
||||
|
||||
host.OnToolCallEnd(func(e kit.ToolCallEndEvent) {
|
||||
// Fires when tool argument streaming is complete, before execution.
|
||||
// e.ToolCallID
|
||||
// Transition UI from "generating args" to "executing".
|
||||
})
|
||||
|
||||
host.OnToolResult(func(e kit.ToolResultEvent) {
|
||||
// e.ToolCallID, e.ToolName, e.ToolKind, e.ToolArgs, e.ParsedArgs
|
||||
// e.Result, e.IsError, e.Metadata (*ToolResultMetadata)
|
||||
@@ -303,6 +322,9 @@ unsub := host.Subscribe(func(e kit.Event) {
|
||||
| `message_start` | `MessageStartEvent` | *(none)* |
|
||||
| `message_update` | `MessageUpdateEvent` | `Chunk` |
|
||||
| `message_end` | `MessageEndEvent` | `Content` |
|
||||
| `tool_call_start` | `ToolCallStartEvent` | `ToolCallID`, `ToolName`, `ToolKind` |
|
||||
| `tool_call_delta` | `ToolCallDeltaEvent` | `ToolCallID`, `Delta` |
|
||||
| `tool_call_end` | `ToolCallEndEvent` | `ToolCallID` |
|
||||
| `tool_call` | `ToolCallEvent` | `ToolCallID`, `ToolName`, `ToolKind`, `ToolArgs`, `ParsedArgs` |
|
||||
| `tool_execution_start` | `ToolExecutionStartEvent` | `ToolCallID`, `ToolName`, `ToolKind`, `ToolArgs` |
|
||||
| `tool_execution_end` | `ToolExecutionEndEvent` | `ToolCallID`, `ToolName`, `ToolKind` |
|
||||
@@ -316,6 +338,8 @@ unsub := host.Subscribe(func(e kit.Event) {
|
||||
| `steer_consumed` | `SteerConsumedEvent` | `Count` |
|
||||
| `password_prompt` | `PasswordPromptEvent` | `Prompt`, `ResponseCh` |
|
||||
|
||||
**Tool call streaming lifecycle**: `ToolCallStartEvent` → `ToolCallDeltaEvent` (repeated) → `ToolCallEndEvent` → `ToolCallEvent` → `ToolExecutionStartEvent` → `ToolOutputEvent` (optional, repeated) → `ToolExecutionEndEvent` → `ToolResultEvent`
|
||||
|
||||
**PasswordPromptEvent** (for sudo password handling):
|
||||
```go
|
||||
// PasswordPromptEvent fires when a sudo command needs a password.
|
||||
|
||||
@@ -7,7 +7,7 @@ description: All extension capabilities — lifecycle events, tools, commands, w
|
||||
|
||||
## Lifecycle events
|
||||
|
||||
Extensions can hook into 23 lifecycle events:
|
||||
Extensions can hook into 26 lifecycle events:
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
@@ -17,6 +17,9 @@ Extensions can hook into 23 lifecycle events:
|
||||
| `OnAgentStart` | Agent loop started |
|
||||
| `OnAgentEnd` | Agent loop completed |
|
||||
| `OnToolCall` | Tool call requested by the model |
|
||||
| `OnToolCallInputStart` | LLM began generating tool call arguments (tool name known, args streaming) |
|
||||
| `OnToolCallInputDelta` | Streamed JSON fragment of tool call arguments |
|
||||
| `OnToolCallInputEnd` | Tool argument streaming complete, before execution begins |
|
||||
| `OnToolExecutionStart` | Tool execution beginning |
|
||||
| `OnToolOutput` | Streaming tool output chunk (for long-running tools) |
|
||||
| `OnToolExecutionEnd` | Tool execution completed |
|
||||
|
||||
@@ -41,6 +41,32 @@ unsub6 := host.OnTurnEnd(func(event kit.TurnEndEvent) {
|
||||
defer unsub6()
|
||||
```
|
||||
|
||||
## Tool call argument streaming
|
||||
|
||||
For tools with large arguments (e.g., `write` with a full file body), the `ToolCallEvent` only fires after the full argument JSON finishes streaming — which can take 5-10+ seconds of "dead air." These three events fire during argument generation so UIs can show activity immediately:
|
||||
|
||||
```go
|
||||
host.OnToolCallStart(func(event kit.ToolCallStartEvent) {
|
||||
// Fires as soon as the LLM begins generating tool arguments.
|
||||
// event.ToolCallID, event.ToolName, event.ToolKind
|
||||
fmt.Printf("⏳ %s generating arguments...\n", event.ToolName)
|
||||
})
|
||||
|
||||
host.OnToolCallDelta(func(event kit.ToolCallDeltaEvent) {
|
||||
// Each streamed JSON fragment of the tool arguments.
|
||||
// event.ToolCallID, event.Delta
|
||||
// Useful for live-previewing content or showing byte progress.
|
||||
})
|
||||
|
||||
host.OnToolCallEnd(func(event kit.ToolCallEndEvent) {
|
||||
// Tool argument streaming complete — execution about to begin.
|
||||
// event.ToolCallID
|
||||
fmt.Printf("✓ Arguments ready, executing...\n")
|
||||
})
|
||||
```
|
||||
|
||||
**Full tool lifecycle**: `ToolCallStartEvent` → `ToolCallDeltaEvent` (repeated) → `ToolCallEndEvent` → `ToolCallEvent` → `ToolExecutionStartEvent` → `ToolOutputEvent` (optional) → `ToolExecutionEndEvent` → `ToolResultEvent`
|
||||
|
||||
## Hook system
|
||||
|
||||
Hooks can **modify or cancel** operations. Unlike events (read-only), hooks are read-write interceptors.
|
||||
@@ -104,7 +130,10 @@ Lower values run first. First non-nil result wins.
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
| `ToolCallEvent` | Tool call parsed and about to execute |
|
||||
| `ToolCallStartEvent` | LLM began generating tool call arguments (tool name known, args streaming) |
|
||||
| `ToolCallDeltaEvent` | Streamed JSON fragment of tool call arguments |
|
||||
| `ToolCallEndEvent` | Tool argument streaming complete, before execution begins |
|
||||
| `ToolCallEvent` | Tool call fully parsed and about to execute |
|
||||
| `ToolResultEvent` | Tool execution completed with result |
|
||||
| `ToolOutputEvent` | Streaming output chunk from tool (e.g., bash stdout/stderr) |
|
||||
| `MessageUpdateEvent` | Streaming text chunk from LLM |
|
||||
|
||||
Reference in New Issue
Block a user