Documentation / SDK and API
Streaming, events and hooks
Show progress while the agent works, answer permission questions from your own interface, and plug code into the lifecycle.
SubmitMessage waits for the whole turn. To build an interface, you listen while it runs. The SDK gives you three hooks into a running agent: callbacks, a prompt bridge and lifecycle hooks.
Where each signal falls
During one turn, the loop and the session emit hooks and events at fixed points:
Hooks run your code and can stop the action. Events are for observing and are sent to your callbacks. Streaming text, tool progress and runtime events are described below, then lifecycle hooks.
Streaming text
Set a callback on the client or on a session. It receives each piece of the answer as it arrives.
session.SetResponseChunkFn(func(c sdk.ResponseChunk) {
fmt.Print(c.Delta)
})
resp, err := session.SubmitMessage(ctx, "Explain this repository")
ResponseChunk has a Type (content_block_start, content_block_delta, content_block_stop, message_delta, message_stop, error) and a Delta with the text. Thinking and tool input arrive as other delta types, described by DeltaType and PartialJSON.
Tool progress
session.SetProgressFn(func(p sdk.ToolProgress) {
fmt.Println("tool:", p)
})
Use it to show “running bash…” lines or a progress bar while a tool works.
Runtime events
One callback receives structured events for the whole turn: when it starts and ends, chunks, tool progress, permission questions, plans, tasks, goals, sub-agents and compaction.
session.SetRuntimeEventFn(func(e sdk.RuntimeEvent) {
switch e.Type {
case sdk.RuntimeEventTypeTurnCompleted:
fmt.Println("done after", e.TurnNumber, "turns")
case sdk.RuntimeEventTypeCompaction:
// the context was compacted during this turn
}
})
The runtime emits these events:
| Event | When |
|---|---|
turn.started, turn.completed, turn.failed | A turn begins and ends |
response.chunk | Each piece of the answer (the same data as the streaming callback) |
turn.stage | A phase of the turn setup, before the first chunk (preparing_turn) |
tool.progress | The stages of a tool call |
tool.permission_required | A tool needs approval, just before your prompt function is called |
prompt.request | The agent asks you a question, just before your prompt function is called |
context.compacted | After a turn that compacted the context |
plan.submitted, execution_mode.changed | Plan mode |
task.changed, goal.updated | Tasks and goals |
agent.spawn.*, agent.wait.*, agent.interaction.*, agent.close.* | Sub-agents |
browser.* | The browser tools |
Events from a sub-agent carry AgentToolUseID, which identifies the tool call that started it.
Answering permission questions
When a tool needs approval and the mode is onRequest, the runtime calls your prompt function. Without one, the action is not approved.
client.SetPromptFn(func(ctx context.Context, req sdk.PromptRequest) (sdk.PromptResponse, error) {
if req.Type == "confirm" {
tool, _ := req.Metadata["tool_name"].(string)
ok := askUser("Allow " + tool + "?") // your own UI
return sdk.PromptResponse{Value: ok}, nil
}
return sdk.PromptResponse{Cancelled: true}, nil
})
For a permission question, return Value: true to allow it once, Value: "always" to allow that tool for the session, or Cancelled: true to refuse. Metadata contains tool_name, tool_input, tool_use_id and working_directory.
Lifecycle hooks
Three kinds of hooks run your code inside the runtime.
Loop hooks
RegisterHook attaches a handler to one event of the loop. The handler receives the session and turn identifiers and event data in Data.
id, err := client.RegisterHook(sdk.HookEventIterationStart,
func(ctx context.Context, p sdk.HookProgress) (*sdk.HookResult, error) {
if overBudget(p.Data) {
return &sdk.HookResult{Action: "stop", Message: "budget reached"}, nil
}
return nil, nil // continue
})
if err != nil {
log.Fatal(err)
}
client.HookRegistry().Remove(id)
Returning nil, nil means continue. RegisterHook returns an error for an event the runtime never emits, so a handler that could never run is refused instead of being accepted in silence.
The runtime emits 19 events:
| Event | When | A stop or deny |
|---|---|---|
session_start, session_end | A session is created, and closed | Observe only |
turn_start | A turn is requested, before your message is accepted | Refuses the turn, with an error |
query_start | The loop starts | Observe only |
iteration_start | Each iteration, before the model call | Ends the turn |
pre_compact | Before an automatic compaction | Skips that compaction, the turn goes on |
post_compact | After a compaction | Observe only |
tool_uses_start | The model asked for tools, before they run | Ends the turn |
pre_tool_use | Before each tool call | Ends the turn. A modify result rewrites the tool input (UpdatedInput) |
post_tool_use | After each tool call, with its success or error | Ends the turn (the tools already ran) |
tool_uses_complete | The tool results were added | Observe only |
iteration_stop, iteration_continue, iteration_complete | An iteration ends | Observe only |
query_complete, turn_end | A turn ends | Observe only |
subagent_start, subagent_stop, on_error | Around a sub-agent | Observe only |
Where a stop is not possible, the handler still runs, so use those events to observe and log. A retry result has no effect: the turn goes on as if the handler had returned continue. The SDK defines more event names than these 19, but RegisterHook refuses them because the runtime never emits them.
Shell hooks before a tool call
To check or rewrite a tool call before it runs, set PreToolHooks in the config. Each entry has a Matcher (a regular expression on the tool name), a Command and a Timeout. The command receives the tool input as JSON and answers with its exit code:
| Result | Effect |
|---|---|
| Exit 0 | The call goes ahead |
Exit 0 with {"decision":"allow"} | The call goes ahead and skips the permission question |
Exit 0 with {"updatedInput": ...} | The tool input is rewritten |
| Exit 2, message on stderr | The call is denied, with that reason |
| Exit 49 | The whole turn is halted |
Stop hooks
A stop hook runs when the model is about to stop. It can add messages and ask for one more cycle of the loop. Set them with StopHooks in ClientConfig.
Updated on 2026-10-13