Stream responses in real-time
`streamPrompt` returns an async iterable of structured events. Use it to render assistant text as it generates, surface tool runs, and respond to permission requests without blocking the event loop.
Basic streaming
Loop over the stream and pull `delta` from `message_update` events. The full text is also available on `message_end.content` when the message finishes.
TypeScript
import { AutohandSDK } from '@autohandai/agent-sdk';
const sdk = new AutohandSDK({ cwd: '.' });
await sdk.start();
for await (const event of sdk.streamPrompt({
message: 'Summarise the project structure.',
})) {
if (event.type === 'message_update') {
process.stdout.write(event.delta);
}
}
await sdk.stop();
Python
import asyncio
from autohand_sdk import AutohandSDK
async def main():
async with AutohandSDK(cwd=".") as sdk:
async for event in sdk.stream_prompt("Summarise the project structure."):
if event["type"] == "message_update":
print(event.get("delta", ""), end="")
asyncio.run(main())
Go
events, _ := sdk.StreamPrompt(ctx, &autohand.PromptParams{
Message: "Summarise the project structure.",
})
for event := range events {
if e, ok := event.(autohand.MessageUpdateEvent); ok {
fmt.Print(e.Delta)
}
}
Java
sdk.streamPrompt(
new PromptParams("Summarise the project structure."),
event -> {
if (event instanceof Events.MessageUpdateEvent mue) {
System.out.print(mue.delta());
}
}
);
Swift
let stream = Runner.runStream(
agent: agent,
prompt: "Summarise the project structure."
)
for try await event in stream {
if event.type == .content, let data = event.data {
print(data, terminator: "")
}
}Event types you will see
The CLI emits the same set of events on every SDK. Match on `event.type` (TS/Python) or the sealed event subtype (Go/Java) and ignore the rest.
- `agent_start` / `agent_end`: brackets the whole run.
- `turn_start` / `turn_end`: brackets a single agent turn.
- `message_start` / `message_update` / `message_end`: assistant text. `delta` carries each new chunk; `content` carries the full final message.
- `tool_start` / `tool_update` / `tool_end`: tool execution. Use `toolName` (or `tool_name`) for routing.
- `permission_request`: pause; respond with `permissionResponse` / `respond_to_permission`.
- `file_modified`: the agent wrote to disk. Useful for invalidating caches or refreshing editors.
- `error`: transport, runtime, or tool failure. Always handle this.
Render a full chat loop
A typical chat surface routes message text to the transcript, tool runs to a side panel, and permission requests to a modal. Here is the same shape across SDKs.
TypeScript
for await (const event of sdk.streamPrompt({ message: userInput })) {
switch (event.type) {
case 'message_update':
ui.appendDelta(event.delta);
break;
case 'tool_start':
ui.toolPanel.start(event.toolName);
break;
case 'tool_update':
ui.toolPanel.append(event.output);
break;
case 'tool_end':
ui.toolPanel.finish(event.toolName);
break;
case 'permission_request': {
const allow = await ui.askPermission(event.tool, event.description);
await sdk.permissionResponse({
requestId: event.requestId,
allowed: allow,
});
break;
}
case 'error':
ui.showError(event.error);
break;
}
}
Python
async for event in sdk.stream_prompt(user_input):
t = event["type"]
if t == "message_update":
ui.append_delta(event.get("delta", ""))
elif t == "tool_start":
ui.tool_panel.start(event.get("tool_name"))
elif t == "tool_update":
ui.tool_panel.append(event.get("output", ""))
elif t == "tool_end":
ui.tool_panel.finish(event.get("tool_name"))
elif t == "permission_request":
allow = await ui.ask_permission(event.get("tool"), event.get("description"))
await sdk.respond_to_permission(
event["request_id"],
decision="allow" if allow else "deny",
allowed=allow,
)
elif t == "error":
ui.show_error(event.get("error"))
Go
for event := range events {
switch e := event.(type) {
case autohand.MessageUpdateEvent:
ui.AppendDelta(e.Delta)
case autohand.ToolStartEvent:
ui.ToolPanel.Start(e.ToolName)
case autohand.ToolEndEvent:
ui.ToolPanel.Finish(e.ToolName)
case autohand.PermissionRequestEvent:
allow := ui.AskPermission(e.Tool, e.Description)
scope := autohand.ScopeOnce
_ = sdk.PermissionResponse(ctx, e.RequestID, allow, scope)
case autohand.ErrorEvent:
ui.ShowError(e.Error)
}
}
Java
sdk.streamPrompt(new PromptParams(userInput), event -> {
if (event instanceof Events.MessageUpdateEvent mue) {
ui.appendDelta(mue.delta());
} else if (event instanceof Events.ToolStartEvent tse) {
ui.toolPanel().start(tse.toolName());
} else if (event instanceof Events.ToolEndEvent tee) {
ui.toolPanel().finish(tee.toolName());
} else if (event instanceof Events.PermissionRequestEvent pre) {
boolean allow = ui.askPermission(pre.tool(), pre.description());
if (allow) {
sdk.allowPermission(pre.requestId(), DecisionScope.ONCE);
} else {
sdk.denyPermission(pre.requestId(), DecisionScope.ONCE);
}
} else if (event instanceof Events.ErrorEvent err) {
ui.showError(err.error());
}
});
Swift
for try await event in stream {
switch event.type {
case .content:
if let data = event.data { ui.appendDelta(data) }
case .toolStart:
ui.toolPanel.start(event.toolName ?? "")
case .toolEnd:
ui.toolPanel.finish(event.toolName ?? "")
case .error:
ui.showError(event.errorMessage ?? "unknown")
default:
break
}
}Cancel an in-flight stream
Call `abort()` to end the run. The stream loop closes and any pending permission request is cancelled. Use this when the user cancels in your UI or when a parent task is unwound.
TypeScript
await sdk.abort();
Python
await sdk.abort()
Go
_ = sdk.Abort(ctx)
Java
sdk.abort();When to stream vs await
- Stream when the user is watching: chat UIs, terminal sessions, long-running refactors.
- Await with `agent.run()` when you only need the final result: scripts, batch jobs, JSON pipelines.
- Combine the two: stream for UI rendering, then call `run.wait()` (TS) / `run.waitForResult()` (Java) for the structured `RunResult`.