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`.