Skip to main content
You build TerminalUse agents with the Python runtime API. The public runtime surface is AgentServer, and it exposes three lifecycle hooks:
  • @server.on_create
  • @server.on_event
  • @server.on_cancel
There is no public on_complete hook.

Start With tu init

tu init
That creates an agent project with:
my-agent/
├── src/agent.py
├── config.yaml
├── Dockerfile
└── pyproject.toml

Minimal Agent

from typing import Any

from terminaluse.lib import AgentServer, TaskContext
from terminaluse.types import Event, TextPart

server = AgentServer()


@server.on_create
async def handle_create(ctx: TaskContext, params: dict[str, Any]):
    await ctx.state.create({"turns": 0})
    await ctx.messages.send("Task created.")


@server.on_event
async def handle_event(ctx: TaskContext, event: Event):
    if not isinstance(event.content, TextPart):
        await ctx.messages.send("Only text events are supported.")
        return

    state = await ctx.state.get() or {"turns": 0}
    turns = int(state.get("turns", 0)) + 1
    await ctx.state.update({"turns": turns})
    await ctx.messages.send(f"turn {turns}: {event.content.text}")


@server.on_cancel
async def handle_cancel(ctx: TaskContext):
    await ctx.messages.send("Task cancelled.")

The Handler Model

on_create

Runs once when the task is created. Use it for:
  • initializing state
  • validating params
  • sending the first message

on_event

Runs every time your app sends an event to the task. This is where most agent logic lives.

on_cancel

Runs when the task is cancelled. Use it for cleanup or last-message behavior if needed. Important runtime detail: on_cancel does not run with the normal automatic filesystem sync wrapper. Do not rely on /workspace writes made in on_cancel being persisted automatically.

TaskContext

Every handler receives a TaskContext.
AttributePurpose
ctx.taskCurrent task metadata
ctx.agentCurrent agent metadata
ctx.messagesSend or list task messages
ctx.stateRead and update per-task state
ctx.eventsSend or list events
TaskContext is the default path. The lower-level adk.* modules are still useful in helper code when you need to pass IDs explicitly.

Messages

The most common thing you do inside a handler is emit messages:
await ctx.messages.send("plain text works")
For structured output, send a typed part:
from terminaluse.types import DataPart

await ctx.messages.send(DataPart(data={"status": "complete"}))

State

State is persisted per task and agent.
await ctx.state.create({"status": "started"})

state = await ctx.state.get() or {}

await ctx.state.update({"status": "done"})
Use it for:
  • conversation continuity
  • IDs or handles you need on future turns
  • lightweight workflow progress

Filesystem Access

If the task has an attached filesystem, your agent sees it at /workspace. TerminalUse syncs that filesystem around on_create and on_event. If the caller only has read access to the filesystem, /workspace is mounted read-only for that task. on_cancel is the exception: the runtime skips the automatic sync wrapper for cancel handlers. See Filesystem Sync and Access Control.

TypeScript vs Python

The TypeScript SDK is for calling deployed agents and managing TerminalUse resources from your application. The public agent runtime shown on this page is Python.