The shape of one turn

Each turn the CLI takes follows the same pattern. The default strategy is ReAct (Reason → Act → Observe), but plan-and-execute, parallel, and reflexion strategies are available as CLI flags.

  1. Reason: the model emits an assistant message. You see this as `message_start` → `message_update` (deltas) → `message_end`.
  2. Act: the model calls a tool. You see `tool_start` → optional `tool_update` chunks → `tool_end`. If permissions are interactive, a `permission_request` arrives first and the loop pauses until you respond.
  3. Observe: the tool result feeds back into the next prompt. The CLI starts the next turn or, if the model decides it is done, emits `agent_end`.

The whole turn is bracketed by `turn_start` and `turn_end`. A run is bracketed by `agent_start` and `agent_end`.

Run the loop

The high-level `Agent` API runs the loop to completion and returns the final result. Use it when you do not need to render intermediate events.

TypeScript

import { Agent } from '@autohandai/agent-sdk';

const agent = await Agent.create({
  cwd: '.',
  instructions: 'Analyze and fix bugs.',
  permissionMode: 'interactive',
});

const result = await agent.run('Fix the authentication bug in src/auth.ts');
console.log(result.text);
await agent.close();

Python

import asyncio
from autohand_sdk import AutohandSDK

async def main():
    async with AutohandSDK(cwd=".", permission_mode="interactive") as sdk:
        async for event in sdk.stream_prompt("Fix the authentication bug in auth.py"):
            if event["type"] == "message_end":
                print(event.get("content",""))

asyncio.run(main())

Go

package main

import (
    "context"
    "fmt"
    autohand "github.com/autohandai/agent-sdk-go"
)

func main() {
    ctx := context.Background()
    agent, _ := autohand.NewAgent(ctx, &autohand.Config{CWD: "."})
    defer agent.Close()

    result, _ := agent.Run(ctx, "Fix the authentication bug in auth.go", nil)
    fmt.Println(result.Text)
}

Java

import ai.autohand.sdk.sdk.Agent;
import ai.autohand.sdk.sdk.AgentOptions;
import ai.autohand.sdk.sdk.RunResult;
import ai.autohand.sdk.types.PermissionMode;

Agent agent = Agent.create(AgentOptions.builder()
    .cwd(".")
    .instructions("Analyze and fix bugs.")
    .permissionMode(PermissionMode.INTERACTIVE)
    .build());

RunResult result = agent.run("Fix the authentication bug in Auth.java");
System.out.println(result.text());
agent.close();

Swift

import AgentSDK

let agent = Agent(
    name: "Code Fixer",
    instructions: "Analyze and fix bugs.",
    tools: [.readFile, .writeFile, .bash],
    model: ModelID("gpt-4o"),
    provider: OpenAIProvider(apiKey: "sk-...")
)

let result = try await Runner.run(
    agent: agent,
    prompt: "Fix the authentication bug in Auth.swift"
)
print(result.finalOutput)

Observe the loop with streamed events

Use the low-level streaming API when you need to render every step of the loop, route tool output to a UI, or implement permission prompts.

TypeScript

import { AutohandSDK } from '@autohandai/agent-sdk';

const sdk = new AutohandSDK({ cwd: '.' });
await sdk.start();

for await (const event of sdk.streamPrompt({ message: 'List the TypeScript files and summarise each.' })) {
  switch (event.type) {
    case 'turn_start':
      console.log('--- new turn ---');
      break;
    case 'message_update':
      process.stdout.write(event.delta);
      break;
    case 'tool_start':
      console.log('\n[tool: ' + event.toolName + ']');
      break;
    case 'tool_end':
      console.log('[tool done: ' + event.toolName + ']');
      break;
    case 'permission_request':
      await sdk.permissionResponse({ requestId: event.requestId, allowed: true });
      break;
  }
}

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("List the Python files and summarise each."):
            t = event["type"]
            if t == "turn_start":
                print("--- new turn ---")
            elif t == "message_update":
                print(event.get("delta", ""), end="")
            elif t == "tool_start":
                print("\n[tool: " + str(event.get("tool_name")) + "]" )
            elif t == "tool_end":
                print("[tool done: " + str(event.get("tool_name")) + "]" )
            elif t == "permission_request":
                await sdk.respond_to_permission(
                    event["request_id"], decision="allow", allowed=True
                )

asyncio.run(main())

Go

events, _ := sdk.StreamPrompt(ctx, &autohand.PromptParams{
    Message: "List the Go files and summarise each.",
})

for event := range events {
    switch e := event.(type) {
    case autohand.TurnStartEvent:
        fmt.Println("--- new turn ---")
    case autohand.MessageUpdateEvent:
        fmt.Print(e.Delta)
    case autohand.ToolStartEvent:
        fmt.Printf("\n[tool: %s]\n", e.ToolName)
    case autohand.ToolEndEvent:
        fmt.Printf("[tool done: %s]\n", e.ToolName)
    case autohand.PermissionRequestEvent:
        _ = sdk.PermissionResponse(ctx, e.RequestID, true, autohand.ScopeOnce)
    }
}

Java

sdk.streamPrompt(
    new PromptParams("List the Java files and summarise each."),
    event -> {
        if (event instanceof Events.TurnStartEvent) {
            System.out.println("--- new turn ---");
        } else if (event instanceof Events.MessageUpdateEvent mue) {
            System.out.print(mue.delta());
        } else if (event instanceof Events.ToolStartEvent tse) {
            System.out.printf("%n[tool: %s]%n", tse.toolName());
        } else if (event instanceof Events.PermissionRequestEvent pre) {
            sdk.allowPermission(pre.requestId(), DecisionScope.ONCE);
        }
    }
);

Swift

let stream = Runner.runStream(
    agent: agent,
    prompt: "List the Swift files and summarise each."
)

for try await event in stream {
    switch event.type {
    case .turnStart:
        print("--- new turn ---")
    case .content:
        if let data = event.data { print(data, terminator: "") }
    case .toolStart:
        let toolName = event.toolName ?? ""
        print("\n[tool: \(toolName)]")
    default:
        break
    }
}

Loop strategies

The CLI exposes four strategies. Pick one with the `--loop-type` flag or the equivalent CLI config; the SDKs forward the choice without changing the public API.

  • react (default): the model alternates between reasoning and acting. Good for open-ended work.
  • plan-and-execute: the agent writes a plan first, then executes each step. Good for refactors and multi-file changes.
  • parallel: independent tool calls run concurrently. Good for read-heavy discovery passes.
  • reflexion: the agent self-critiques after each step and retries on error. Good for tasks with verifiable success criteria.

Plan mode is a related but distinct feature: it restricts the loop to read-only planning tools until you accept the plan. See `enablePlanMode()` and `setPlanMode()` in the TypeScript SDK.

Bound the loop

Two limits keep a runaway loop in check.

  • Iterations: in Python, `AutohandSDK(max_iterations=10)`. In TypeScript, configure on the CLI side via `~/.autohand/config.json` or per-run flags.
  • Per-request timeout: every SDK accepts a `timeout` (in ms) in the constructor. The default is generous (5 minutes) because long tool runs are common.

Use `sdk.abort()` to cancel an in-flight run from your application code, for example when the user clicks Stop in your UI.

TypeScript

await sdk.abort();

Python

await sdk.abort()

Go

_ = sdk.Abort(ctx)

Java

sdk.abort();

Best practices

  • Pick the smallest loop that fits the job. ReAct is the right default; reach for plan-and-execute only when planning meaningfully changes the result.
  • Stream events into your UI so users can see what the agent is doing and stop it early when needed.
  • Always handle `permission_request` in `interactive` mode. Forgetting to respond will hang the loop.
  • Keep instructions short and concrete. The loop length is largely a function of how clear the prompt is.
  • Pair a tight `max_iterations` with explicit success criteria when the job has a verifiable goal (tests pass, file compiles).