Callbacks

# Callbacks ## Event-based monitoring Subscribe to events for real-time monitoring. Each method returns an unsubscribe function: ```go unsub := host.OnToolCall(func(event kit.ToolCallEvent) { fmt.Printf("Tool: %s, Args: %s\n", event.ToolName, event.ToolArgs) }) defer unsub() unsub2 := host.OnToolResult(func(event kit.ToolResultEvent) { fmt.Printf("Result: %s (error: %v)\n", event.ToolName, event.IsError) }) defer unsub2() unsub3 := host.OnMessageUpdate(func(event kit.MessageUpdateEvent) { fmt.Print(event.Chunk) }) defer unsub3() unsub4 := host.OnResponse(func(event kit.ResponseEvent) { fmt.Println("Final response received") }) defer unsub4() unsub5 := host.OnTurnStart(func(event kit.TurnStartEvent) { fmt.Println("Turn started") }) defer unsub5() unsub6 := host.OnTurnEnd(func(event kit.TurnEndEvent) { fmt.Println("Turn ended") }) 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. ### BeforeToolCall — block tool execution ```go host.OnBeforeToolCall(kit.HookPriorityNormal, func(h kit.BeforeToolCallHook) *kit.BeforeToolCallResult { // h.ToolCallID, h.ToolName, h.ToolArgs if h.ToolName == "bash" && strings.Contains(h.ToolArgs, "rm -rf") { return &kit.BeforeToolCallResult{Block: true, Reason: "dangerous command"} } return nil // allow }) ``` ### AfterToolResult — modify tool output ```go host.OnAfterToolResult(kit.HookPriorityNormal, func(h kit.AfterToolResultHook) *kit.AfterToolResultResult { // h.ToolCallID, h.ToolName, h.ToolArgs, h.Result, h.IsError if h.ToolName == "read" { filtered := redactSecrets(h.Result) return &kit.AfterToolResultResult{Result: &filtered} } return nil }) ``` ### BeforeTurn — modify prompt, inject messages ```go host.OnBeforeTurn(kit.HookPriorityNormal, func(h kit.BeforeTurnHook) *kit.BeforeTurnResult { // h.Prompt newPrompt := h.Prompt + "\nAlways respond in JSON." return &kit.BeforeTurnResult{Prompt: &newPrompt} // Also available: SystemPrompt *string, InjectText *string }) ``` ### AfterTurn — observation only ```go host.OnAfterTurn(kit.HookPriorityNormal, func(h kit.AfterTurnHook) { // h.Response, h.Error log.Printf("Turn completed: %d chars", len(h.Response)) }) ``` ### PrepareStep — intercept messages between steps The most powerful hook — fires between steps within a multi-step agent turn, after any steering messages are injected and before messages are sent to the LLM. Can replace the entire context window. ```go host.OnPrepareStep(kit.HookPriorityNormal, func(h kit.PrepareStepHook) *kit.PrepareStepResult { // h.StepNumber — zero-based step index within the turn // h.Messages — current context window (includes any steering) // Example: transform tool results with images into user messages modified := transformImageToolResults(h.Messages) return &kit.PrepareStepResult{Messages: modified} // Return nil to pass through unchanged }) ``` Use cases: transforming tool results (e.g., image data for vision models), dynamic tool filtering per step, mid-turn context injection, custom stop conditions. ### Hook priorities ```go kit.HookPriorityHigh = 0 // runs first kit.HookPriorityNormal = 50 // default kit.HookPriorityLow = 100 // runs last ``` Lower values run first. First non-nil result wins. ## All event types | Event | Typed Subscriber | Description | |-------|-----------------|-------------| | `TurnStartEvent` | `OnTurnStart` | Agent turn started | | `TurnEndEvent` | `OnTurnEnd` | Agent turn completed | | `MessageStartEvent` | `OnMessageStart` | New assistant message begins | | `MessageUpdateEvent` | `OnMessageUpdate` | Streaming text chunk from LLM | | `MessageEndEvent` | `OnMessageEnd` | Assistant message complete | | `ToolCallStartEvent` | `OnToolCallStart` | LLM began generating tool call arguments | | `ToolCallDeltaEvent` | `OnToolCallDelta` | Streamed JSON fragment of tool call arguments | | `ToolCallEndEvent` | `OnToolCallEnd` | Tool argument streaming complete | | `ToolCallEvent` | `OnToolCall` | Tool call fully parsed, about to execute | | `ToolExecutionStartEvent` | `OnToolExecutionStart` | Tool begins executing | | `ToolExecutionEndEvent` | `OnToolExecutionEnd` | Tool finishes executing | | `ToolResultEvent` | `OnToolResult` | Tool execution completed with result | | `ToolCallContentEvent` | `OnToolCallContent` | Text content alongside tool calls | | `ToolOutputEvent` | `OnToolOutput` | Streaming output chunk from tool (e.g., bash) | | `ResponseEvent` | `OnResponse` | Final response received | | `ReasoningStartEvent` | `OnReasoningStart` | LLM begins reasoning/thinking | | `ReasoningDeltaEvent` | `OnReasoningDelta` | Streaming reasoning/thinking chunk | | `ReasoningCompleteEvent` | `OnReasoningComplete` | Reasoning/thinking finished | | `StepStartEvent` | `OnStepStart` | New LLM call begins within a turn | | `StepFinishEvent` | `OnStepFinish` | Step completes (with usage, finish reason, tool call info) | | `StepUsageEvent` | `OnStepUsage` | Per-step token usage | | `StreamFinishEvent` | `OnStreamFinish` | Per-step stream completes (with usage + finish reason) | | `TextStartEvent` | `OnTextStart` | LLM begins text content generation | | `TextEndEvent` | `OnTextEnd` | LLM finishes text content generation | | `WarningsEvent` | `OnWarnings` | LLM provider returned warnings | | `SourceEvent` | `OnSource` | LLM referenced a source (e.g., web search) | | `ErrorEvent` | `OnError` | Agent-level error during streaming | | `RetryEvent` | `OnRetry` | LLM request retried after transient error | | `CompactionEvent` | `OnCompaction` | Conversation compacted | | `SteerConsumedEvent` | `OnSteerConsumed` | Steering messages injected into turn | | `PasswordPromptEvent` | — | Sudo command needs password (respond via `ResponseCh`) | > **Note:** `OnStreaming` is a deprecated alias for `OnMessageUpdate` and will be removed in a future release. ## Subagent event monitoring Monitor real-time events from LLM-initiated subagents (when the model uses the `subagent` tool): ```go host.OnToolCall(func(e kit.ToolCallEvent) { if e.ToolName == "subagent" { host.SubscribeSubagent(e.ToolCallID, func(event kit.Event) { // Receives the same event types as Subscribe(), scoped to the child agent switch ev := event.(type) { case kit.MessageUpdateEvent: fmt.Print(ev.Chunk) case kit.ToolCallEvent: fmt.Printf("Subagent calling: %s\n", ev.ToolName) } }) } }) ``` `SubscribeSubagent` returns an unsubscribe function. Listeners are also cleaned up automatically when the subagent completes. See [Subagents](/advanced/subagents) for more details.