> ## Documentation Index
> Fetch the complete documentation index at: https://www.truefoundry.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Use an agent

> Invoke a saved agent with the AI Gateway SDK — create sessions, drive turns, stream and merge events, and handle approvals, threads, and reconnects.

This guide covers everything you need to **run** a saved agent: open a session, send turns, consume the event stream, and handle pauses, threads, and disconnects.

<Note>
  Prerequisite: an agent saved in the Registry. See [Create an agent](/docs/agent-platform/agent-harness/sdk/create-agent) or the [Agent Playground](/docs/agent-platform/agent-harness/getting-started). For the conceptual overview and a full walkthrough, see [SDK overview](/docs/agent-platform/agent-harness/sdk/overview).
</Note>

## Install and connect

Running agents uses the **gateway SDK**, not the `truefoundry` client.

<CodeGroup>
  ```bash Install theme={"dark"}
  pip install -U truefoundry-gateway-sdk
  ```

  ```bash Environment theme={"dark"}
  export TFY_GATEWAY_URL="https://<your-gateway-host>/<tenant>"
  export TFY_API_KEY="<your-api-key>"
  ```
</CodeGroup>

For TypeScript, install `truefoundry-gateway-sdk` from npm. Both languages import `AgentSessionClient` — Python from `truefoundry_gateway_sdk.agents`, TypeScript from `truefoundry-gateway-sdk/agents`. Set `TFY_GATEWAY_URL` and `TFY_API_KEY` as above. Every example below comes in both languages.

## Create a session

A **session** is the conversation context for a saved agent — one customer working through one issue. It persists across many turns; persist `session.id` to resume later.

```mermaid theme={"dark"}
stateDiagram-v2
  [*] --> active: createSession()
  active --> active: prepare_turn().execute() (many turns)
  active --> cancelled: sessions.cancel()
  cancelled --> [*]
```

### Open a session

A [Session](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#session) is the conversation context for a saved agent. Create one by name with [`create`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#create).

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os
  from truefoundry_gateway_sdk.agents import AgentSessionClient

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )

  # A Session is the conversation context for a saved agent.
  session = client.create_session(agent_name="support-bot")
  ```

  ```typescript TypeScript lines theme={"dark"}
  import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });

  // A Session is the conversation context for a saved agent.
  const session = await client.createSession({ agentName: "support-bot" });
  ```
</CodeGroup>

### List sessions for an agent

List the existing sessions for a saved agent with [`list`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#list). It iterates newest-first and auto-paginates. Use it to resume an earlier conversation — each [Session](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#session) exposes its `id` (persist or reuse it to continue).

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os
  from truefoundry_gateway_sdk.agents import AgentSessionClient

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )

  for session in client.list_sessions(agent_name="support-bot"):
      print("session id:", session.id)
      print("created at:", session.created_at)
  ```

  ```typescript TypeScript lines theme={"dark"}
  import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });

  for await (const session of await client.listSessions({ agentName: "support-bot" })) {
    console.log("session id:", session.id);
    console.log("created at:", session.createdAt);
  }
  ```
</CodeGroup>

## Create a turn

A **turn** is one request/response cycle. Send input, the agent runs until it finishes or pauses, then the stream closes. Create another turn on the same session to continue — turns chain automatically.

### Create and stream a turn

Create a turn with [`session.prepare_turn()`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#python-reference), then call [`turn.execute(stream=True)`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#python-reference) to start it and stream its [events](/docs/agent-platform/agent-harness/sdk/turn-events-reference) as they arrive. The first event is always [`turn.created`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#turncreatedevent) (carrying the `turn_id`); the stream closes with [`turn.done`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#turndoneevent).

When the stream runs to completion, read the terminal state from [`turn.state`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#python-reference). If you break out of the loop early, call `turn.refresh()` first.

To continue the conversation, create another Turn on the same session. Turns within a session are chained automatically, so each Turn sees the history of the ones before it.

<Note>
  Creating a new Turn in a session automatically cancels any other Turn that is still running in that session.
</Note>

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os
  from truefoundry_gateway_sdk.agents import AgentSessionClient
  from truefoundry_gateway_sdk.types import UserMessage

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )
  session = client.create_session(agent_name="support-bot")

  # Each turn is one request/response cycle.
  turn = session.prepare_turn(
      input=[UserMessage(content="I would like to file a support ticket.")],
  )
  for data in turn.execute(stream=True):
      print(data.event)

  # When the stream runs to completion, state is already up to date.
  turn_state = turn.state
  print("final status:", turn_state.status)

  # Create another turn on the same session. Turns are chained automatically,
  # so this turn sees the history of the first one.
  turn = session.prepare_turn(
      input=[UserMessage(content="Use my last order, ORD-2031.")],
  )
  for data in turn.execute(stream=True):
      print(data.event)
  ```

  ```typescript TypeScript lines theme={"dark"}
  import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });
  const session = await client.createSession({ agentName: "support-bot" });

  // Each Turn is one request/response cycle.
  let turn = session.prepareTurn({
    input: [{ type: "user.message", content: "I would like to file a support ticket." }],
  });
  for await (const data of turn.execute({ stream: true })) {
    console.log(data.event);
  }

  // When the stream runs to completion, state is already up to date. If you break out of
  // the loop early, call await turn.refresh() to fetch the latest state from the server.
  const turnState = turn.state;
  console.log("final status:", turnState.status);

  // Create another Turn on the same session. Turns are chained automatically,
  // so this turn sees the history of the first one.
  turn = session.prepareTurn({
    input: [{ type: "user.message", content: "Use my last order, ORD-2031." }],
  });
  for await (const data of turn.execute({ stream: true })) {
    console.log(data.event);
  }
  ```
</CodeGroup>

### List turns

List the turns in a session with [`listTurns()`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#typescript-reference) / [`list_turns()`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#python-reference). Each turn exposes its `input`, and `state` holds the current lifecycle state.

<Note>
  `list_turns()` always returns turns in descending order (newest-first).
</Note>

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os
  from truefoundry_gateway_sdk.agents import (
      AgentSessionClient,
      TurnStateRunning,
      TurnStateDone,
      TurnStateCancelled,
      TurnStateError,
  )

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )

  session = client.get_session(session_id="sess-abc123")

  # list_turns() always returns turns in descending order (newest-first).
  for turn in session.list_turns():
      turn_state = turn.state
      print("turn id:", turn.id)
      # input is the list that started the turn: UserMessage(s), or
      # UserToolApprovalEvent / UserToolResponseEvent items.
      print("input:", turn.input)

      if isinstance(turn_state, TurnStateRunning):
          print("status: running (no output yet)")
      elif isinstance(turn_state, TurnStateDone):
          print("status: done")
          # output is the final assistant message (model.message), or None.
          if turn_state.output is not None:
              print("output:", turn_state.output.content)
          # required_actions lists any pause events the turn surfaced
          # (tool.approval_required, tool.response_required, mcp.auth_required).
          for item in turn_state.required_actions:
              print("required action:", item)
      elif isinstance(turn_state, TurnStateCancelled):
          print("status: cancelled", turn_state.reason)
      elif isinstance(turn_state, TurnStateError):
          print("status: error", turn_state.message)
  ```

  ```typescript TypeScript lines theme={"dark"}
  import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });

  const session = await client.getSession({ sessionId: "sess-abc123" });

  // listTurns() always returns Turns in descending order (newest-first).
  for await (const turn of await session.listTurns()) {
    const turnState = turn.state;
    console.log("turn id:", turn.id);
    // input is the list that started the turn: user.message items, or
    // user.tool_approval / user.tool_response items.
    console.log("input:", turn.input);

    if (turnState.status === "running") {
      console.log("status: running (no output yet)");
    } else if (turnState.status === "done") {
      console.log("status: done");
      // output is the final assistant message (model.message), or null.
      if (turnState.output != null) {
        console.log("output:", turnState.output.content);
      }
      // requiredActions lists any pause events the Turn surfaced
      // (tool.approval_required, tool.response_required, mcp.auth_required).
      for (const item of turnState.requiredActions) {
        console.log("required action:", item);
      }
    } else if (turnState.status === "cancelled") {
      console.log("status: cancelled", turnState.reason);
    } else if (turnState.status === "error") {
      console.log("status: error", turnState.message);
    }
  }
  ```
</CodeGroup>

### Non-streaming turn

If you don't need live events, call [`turn.execute(stream=False)`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#python-reference) and read the returned terminal state (or read [`turn.state`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#python-reference) after consuming a full stream).

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os
  from truefoundry_gateway_sdk.agents import (
      AgentSessionClient,
      TurnStateDone,
      TurnStateCancelled,
      TurnStateError,
  )
  from truefoundry_gateway_sdk.types import UserMessage

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )
  session = client.create_session(agent_name="support-bot")

  turn = session.prepare_turn(
      input=[UserMessage(content="Summarize my last order.")],
  )
  turn_state = turn.execute(stream=False)

  if isinstance(turn_state, TurnStateDone):
      # output and required_actions are mutually exclusive: if output is present,
      # required_actions is empty, and vice versa.
      # output is the final assistant message, or None if the turn paused.
      if turn_state.output is not None:
          print(turn_state.output.content)
      # required_actions holds any pause events (tool.approval_required,
      # tool.response_required, mcp.auth_required).
      else:
          print("required actions:", turn_state.required_actions)
  elif isinstance(turn_state, TurnStateCancelled):
      print("cancelled:", turn_state.reason)
  elif isinstance(turn_state, TurnStateError):
      print("error:", turn_state.message)

  print("completed at:", turn_state.completed_at)
  ```

  ```typescript TypeScript lines theme={"dark"}
  import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });
  const session = await client.createSession({ agentName: "support-bot" });

  const turn = session.prepareTurn({
    input: [{ type: "user.message", content: "Summarize my last order." }],
  });

  const turnState = await turn.execute({ stream: false });
  if (turnState.status === "done") {
    // output and requiredActions are mutually exclusive: if output is present,
    // requiredActions is empty, and vice versa.
    // output is the final assistant message, or null if the Turn paused.
    if (turnState.output != null) {
      console.log(turnState.output.content);
    // requiredActions holds any pause events (tool.approval_required,
    // tool.response_required, mcp.auth_required).
    } else {
      console.log("required actions:", turnState.requiredActions);
    }
  } else if (turnState.status === "cancelled") {
    console.log("cancelled:", turnState.reason);
  } else if (turnState.status === "error") {
    console.log("error:", turnState.message);
  }

  console.log("completed at:", turnState.completedAt);
  ```
</CodeGroup>

### Attach images or files to a turn

A [`UserMessage`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#usermessage)'s `content` can be a plain string or a list of content parts. Use content parts to send text alongside one or more file uploads (images, PDFs, and other documents). Each file is passed as a [data URI](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basic_of_HTTP/Data_URLs) of the form `data:<mime>;base64,<payload>`.

<CodeGroup>
  ```python Python lines theme={"dark"}
  import base64
  import os
  from truefoundry_gateway_sdk.agents import AgentSessionClient
  from truefoundry_gateway_sdk.types import (
      UserMessage,
      TextContent,
      FileContent,
  )

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )
  session = client.create_session(agent_name="support-bot")

  def to_data_uri(path: str, mime: str) -> str:
      with open(path, "rb") as f:
          return f"data:{mime};base64,{base64.b64encode(f.read()).decode()}"

  # Attach both an image and a document in the same message.
  image_uri = to_data_uri("invoice.png", "image/png")
  doc_uri = to_data_uri("notes.txt", "text/plain")

  turn = session.prepare_turn(
      input=[
          UserMessage(
              content=[
                  TextContent(text="Does the invoice total match the notes?"),
                  FileContent(name="invoice.png", data=image_uri),
                  FileContent(name="notes.txt", data=doc_uri),
              ]
          )
      ],
  )
  for data in turn.execute(stream=True):
      print(data.event)
  ```

  ```typescript TypeScript lines theme={"dark"}
  import { readFileSync } from "node:fs";
  import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });
  const session = await client.createSession({ agentName: "support-bot" });

  function toDataUri(path: string, mime: string): string {
    return `data:${mime};base64,${readFileSync(path).toString("base64")}`;
  }

  // Attach both an image and a document in the same message.
  const imageUri = toDataUri("invoice.png", "image/png");
  const docUri = toDataUri("notes.txt", "text/plain");

  const turn = session.prepareTurn({
    input: [
      {
        type: "user.message",
        content: [
          { type: "text", text: "Does the invoice total match the notes?" },
          { type: "file", name: "invoice.png", data: imageUri },
          { type: "file", name: "notes.txt", data: docUri },
        ],
      },
    ],
  });
  for await (const data of turn.execute({ stream: true })) {
    console.log(data.event);
  }
  ```
</CodeGroup>

<Note>
  Whether an attached file is understood depends on the agent's model. Send images only to a vision-capable model; document handling (for example, PDFs) likewise depends on model and agent configuration.
</Note>

<Note>
  Non-image files such as PDFs require the [sandbox](/docs/agent-platform/agent-harness/sandbox) to be enabled on the agent - the harness uses it to process the document.
</Note>

### Cancel a turn

To stop the currently running turn, call [`cancel`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#cancel) on the session. Cancelling aborts any in-flight model request, waits for running MCP tool calls to finish, and force-stops any sandbox the turn provisioned. Cancellation is idempotent. You can continue by creating a new turn, which chains on the cancelled turn's history.

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os
  from truefoundry_gateway_sdk.agents import AgentSessionClient
  from truefoundry_gateway_sdk.types import UserMessage

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )
  session = client.create_session(agent_name="support-bot")

  # Start a turn, then cancel the session partway through the stream.
  turn = session.prepare_turn(
      input=[UserMessage(content="Run a long research task.")],
  )
  i = 0
  for data in turn.execute(stream=True):
      print(data.event)
      if i == 5:
          # Don't break here. After cancel(), the backend closes the SSE stream
          # gracefully: it emits a terminal turn.done event and then ends the
          # stream, which closes this iterator on its own.
          session.cancel()
      i += 1

  # A cancelled turn is terminal, so its event log is stored and can be replayed.
  for event in turn.list_events(order="asc"):
      print(event)

  # Continue the conversation: a new turn chains on the cancelled turn's history.
  next_turn = session.prepare_turn(
      input=[UserMessage(content="Actually, just give me a quick summary instead.")],
  )
  for data in next_turn.execute(stream=True):
      print(data.event)
  ```

  ```typescript TypeScript lines theme={"dark"}
  import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });
  const session = await client.createSession({ agentName: "support-bot" });

  // Start a turn, then cancel the session partway through the stream.
  const turn = session.prepareTurn({
    input: [{ type: "user.message", content: "Run a long research task." }],
  });
  let i = 0;
  for await (const data of turn.execute({ stream: true })) {
    console.log(data.event);
    if (i === 5) {
      // Don't break here. After cancel(), the backend closes the SSE stream
      // gracefully: it emits a terminal turn.done event and then ends the
      // stream, which closes this iterator on its own. Keep consuming until
      // the loop finishes so cancellation completes cleanly.
      await session.cancel();
    }
    i += 1;
  }

  // A cancelled turn is terminal, so its event log is stored and can be replayed.
  for await (const event of await turn.listEvents({ order: "asc" })) {
    console.log(event);
  }

  // Continue the conversation: a new turn chains on the cancelled turn's history.
  const nextTurn = session.prepareTurn({
    input: [{ type: "user.message", content: "Actually, just give me a quick summary instead." }],
  });
  for await (const data of nextTurn.execute({ stream: true })) {
    console.log(data.event);
  }
  ```
</CodeGroup>

### Handle MCP outbound auth

When the agent needs to call a tool on an MCP server that requires a separate outbound authentication, it emits an [`mcp.auth_required`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#mcpauthrequiredevent) event and the Turn ends. The event lists each server that needs authentication along with an `auth_url`. Depending on how the server is configured, this may be an OAuth flow or an API-key entry - see [MCP authentication scenarios](/docs/ai-gateway/mcp/mcp-gateway-auth-security#end-to-end-authentication-scenarios) for details. Send the user to that URL to complete authentication, then resume by creating a new Turn.

<Note>
  When the previous Turn ended with `mcp.auth_required`, passing a `UserMessage` in the resuming Turn is not allowed.
</Note>

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os
  from truefoundry_gateway_sdk.agents import AgentSessionClient, McpAuthRequiredEvent
  from truefoundry_gateway_sdk.types import UserMessage

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )
  session = client.create_session(agent_name="support-bot")

  # First turn: the agent needs an MCP server the user hasn't authorized yet.
  # A turn emits at most one mcp.auth_required event.
  pending_auth = None
  turn = session.prepare_turn(
      input=[UserMessage(content="Summarize my latest GitHub issues.")],
  )
  for data in turn.execute(stream=True):
      event = data.event
      print(event)
      if isinstance(event, McpAuthRequiredEvent):
          pending_auth = event

  # Direct the user to each server's auth URL and wait for them to finish.
  for server in pending_auth.mcp_servers:
      print(f"Authorize {server.name}: {server.auth_url}")
  input("Press Enter once you have authorized the server(s)...")

  # Second turn: resume — the agent continues the interrupted work.
  turn = session.prepare_turn()
  for data in turn.execute(stream=True):
      print(data.event)
  ```

  ```typescript TypeScript lines theme={"dark"}
  import { createInterface } from "node:readline/promises";
  import { stdin, stdout } from "node:process";
  import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });
  const session = await client.createSession({ agentName: "support-bot" });
  const rl = createInterface({ input: stdin, output: stdout });

  // First turn: the agent needs an MCP server the user hasn't authorized yet.
  // A Turn emits at most one mcp.auth_required event.
  let pendingAuth;
  let turn = session.prepareTurn({
    input: [{ type: "user.message", content: "Summarize my latest GitHub issues." }],
  });
  for await (const data of turn.execute({ stream: true })) {
    const message = data.event;
    console.log(message);
    if (message.type === "mcp.auth_required") {
      pendingAuth = message;
    }
  }

  // Direct the user to each server's auth URL and wait for them to finish.
  for (const server of pendingAuth.mcpServers) {
    console.log(`Authorize ${server.name}: ${server.authUrl}`);
  }
  await rl.question("Press Enter once you have authorized the server(s)...");

  // Second turn: resume - the agent continues the interrupted work.
  turn = session.prepareTurn();
  for await (const data of turn.execute({ stream: true })) {
    console.log(data.event);
  }
  rl.close();
  ```
</CodeGroup>

### Handle tool approvals

When a tool call is configured to require [human approval](/docs/agent-platform/agent-harness/human-in-the-loop), the agent emits a [`tool.approval_required`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#toolapprovalrequiredevent) event and the turn ends. Each event carries a `thread_id` and the `tool_calls` awaiting a decision — a single turn can emit more than one (for example, when parallel threads each call a gated tool), so collect all of them. Resume by creating a new turn with one [`UserToolApprovalEvent`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#usertoolapprovalevent) per pending tool call — allow it, or deny it with an optional reason.

Each pending [`ToolCallRef`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#toolapprovalrequiredevent) carries the `source_event_id` of the [`model.message`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#modelmessageevent) that emitted the tool call. Keep the same `id`-keyed [event index](#handling-event-delta-while-streaming) you build while streaming, then look up `events[source_event_id]` to read the tool's name and arguments — no separate bookkeeping required.

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os
  from truefoundry_gateway_sdk.agents import (
      AgentSessionClient,
      ToolApprovalRequiredEvent,
      TurnEvent,
      UserToolApprovalEvent,
      is_event_delta,
      merge_event_delta,
  )
  from truefoundry_gateway_sdk.types import ApprovalAllow, ApprovalDeny, UserMessage

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )
  session = client.create_session(agent_name="support-bot")

  # First turn: the agent calls one or more tools that need approval.
  events: dict[str, TurnEvent] = {}  # event id -> assembled event
  pending_approvals = []             # a turn can emit multiple tool.approval_required events

  turn = session.prepare_turn(
      input=[UserMessage(content="Restart the billing service.")],
  )
  for data in turn.execute(stream=True):
      message = data.event
      if is_event_delta(message):
          base = events.get(message.id)
          if base is not None:
              merge_event_delta(base, message)
      else:
          events[message.id] = message
      if isinstance(message, ToolApprovalRequiredEvent):
          pending_approvals.append(message)

  # Decide on each pending tool call, showing its name and arguments.
  approvals = []
  for pending in pending_approvals:
      for ref in pending.tool_calls:
          # Look up the model.message that emitted this tool call, then find the call.
          model_message = events[ref.source_event_id]
          tool_call = next(tc for tc in (model_message.tool_calls or []) if tc.id == ref.id)
          print(f"\napproval needed: {tool_call.tool_info.name}")
          print(f"  arguments: {tool_call.function.arguments}")
          answer = input("allow? [y/n] ").strip().lower()
          approvals.append(
              UserToolApprovalEvent(
                  thread_id=pending.thread_id,
                  tool_call_id=ref.id,
                  approval=ApprovalAllow() if answer == "y" else ApprovalDeny(reason="denied by user"),
              )
          )

  # Second turn: resume with the approval decisions.
  turn = session.prepare_turn(input=approvals)
  for data in turn.execute(stream=True):
      print(data.event)
  ```

  ```typescript TypeScript lines theme={"dark"}
  import { createInterface } from "node:readline/promises";
  import { stdin, stdout } from "node:process";
  import {
    AgentSessionClient,
    isEventDelta,
    mergeEventDelta,
    type TurnEvent,
  } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });
  const session = await client.createSession({ agentName: "support-bot" });
  const rl = createInterface({ input: stdin, output: stdout });

  // First turn: the agent calls one or more tools that need approval.
  const events = new Map<string, TurnEvent>(); // event id -> assembled event
  const pendingApprovals = [];                  // a Turn can emit multiple tool.approval_required events

  let turn = session.prepareTurn({
    input: [{ type: "user.message", content: "Restart the billing service." }],
  });
  for await (const data of turn.execute({ stream: true })) {
    const message = data.event;
    if (isEventDelta(message)) {
      const base = events.get(message.id);
      if (base) mergeEventDelta(base, message);
    } else {
      events.set(message.id, message);
    }
    if (message.type === "tool.approval_required") {
      pendingApprovals.push(message);
    }
  }

  // Decide on each pending tool call, showing its name and arguments.
  const approvals = [];
  for (const pending of pendingApprovals) {
    for (const ref of pending.toolCalls) {
      // Look up the model.message that emitted this tool call, then find the call.
      const modelMessage = events.get(ref.sourceEventId)!;
      const toolCall = modelMessage.toolCalls!.find((tc) => tc.id === ref.id)!;
      console.log(`\napproval needed: ${toolCall.toolInfo.name}`);
      console.log(`  arguments: ${toolCall.function.arguments}`);
      const answer = (await rl.question("allow? [y/n] ")).trim().toLowerCase();
      approvals.push({
        type: "user.tool_approval",
        threadId: pending.threadId,
        toolCallId: ref.id,
        approval: answer === "y" ? { status: "allow" } : { status: "deny", reason: "denied by user" },
      });
    }
  }

  // Second turn: resume with the approval decisions.
  turn = session.prepareTurn({ input: approvals });
  for await (const data of turn.execute({ stream: true })) {
    console.log(data.event);
  }
  rl.close();
  ```
</CodeGroup>

### Answer agent questions

When the agent needs input it cannot safely assume, it can ask the user a structured question via the built-in client-side [`ask_user_question`](/docs/agent-platform/agent-harness/ask-user-questions) tool. Since the tool runs on your side, the agent emits a [`tool.response_required`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#toolresponserequiredevent) event and the turn ends. The pending tool call's `arguments` carry the `question` and its `options`. Collect the user's answer and resume by creating a new turn with one [`UserToolResponseEvent`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#usertoolresponseevent) per pending tool call.

Each pending [`ToolCallRef`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#toolresponserequiredevent) carries the `source_event_id` of the [`model.message`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#modelmessageevent) that emitted the tool call. Keep the same `id`-keyed [event index](#handling-event-delta-while-streaming) you build while streaming, then look up `events[source_event_id]` to read the tool's name and arguments — no separate bookkeeping required.

<CodeGroup>
  ```python Python lines theme={"dark"}
  import json
  import os
  from truefoundry_gateway_sdk.agents import (
      AgentSessionClient,
      ToolResponseRequiredEvent,
      TurnEvent,
      UserToolResponseEvent,
      is_event_delta,
      merge_event_delta,
  )
  from truefoundry_gateway_sdk.types import TrueFoundrySystemToolInfo, UserMessage

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )
  session = client.create_session(agent_name="support-bot")

  # First turn: the agent asks the user a question.
  events: dict[str, TurnEvent] = {}  # event id -> assembled event
  pending_questions = []             # a turn can emit multiple tool.response_required events

  turn = session.prepare_turn(
      input=[UserMessage(content="Create a new environment called testing-1.")],
  )
  for data in turn.execute(stream=True):
      message = data.event
      if is_event_delta(message):
          base = events.get(message.id)
          if base is not None:
              merge_event_delta(base, message)
      else:
          events[message.id] = message
      if isinstance(message, ToolResponseRequiredEvent):
          pending_questions.append(message)

  # Answer each pending question, reading the prompt and options from arguments.
  responses = []
  for pending in pending_questions:
      for ref in pending.tool_calls:
          # Look up the model.message that emitted this tool call, then find the call.
          model_message = events[ref.source_event_id]
          tool_call = next(tc for tc in (model_message.tool_calls or []) if tc.id == ref.id)
          # tool.response_required covers any client-side tool; handle ask_user_question here.
          if not (
              isinstance(tool_call.tool_info, TrueFoundrySystemToolInfo)
              and tool_call.tool_info.name == "ask_user_question"
          ):
              continue
          args = json.loads(tool_call.function.arguments or "{}")
          print(f"\n{args.get('question', '')}")
          for idx, option in enumerate(args.get("options") or [], 1):
              print(f"  {idx}. {option}")
          answer = input("your answer: ").strip()
          responses.append(
              UserToolResponseEvent(
                  thread_id=pending.thread_id,
                  tool_call_id=ref.id,
                  # content is a free-form string — the chosen option, or any text.
                  content=answer,
              )
          )

  # Second turn: resume with the user's answers.
  turn = session.prepare_turn(input=responses)
  for data in turn.execute(stream=True):
      print(data.event)
  ```

  ```typescript TypeScript lines theme={"dark"}
  import { createInterface } from "node:readline/promises";
  import { stdin, stdout } from "node:process";
  import {
    AgentSessionClient,
    isEventDelta,
    mergeEventDelta,
    type TurnEvent,
  } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });
  const session = await client.createSession({ agentName: "support-bot" });
  const rl = createInterface({ input: stdin, output: stdout });

  // First turn: the agent asks the user a question.
  const events = new Map<string, TurnEvent>(); // event id -> assembled event
  const pendingQuestions = [];                  // a Turn can emit multiple tool.response_required events

  let turn = session.prepareTurn({
    input: [{ type: "user.message", content: "Create a new environment called testing-1." }],
  });
  for await (const data of turn.execute({ stream: true })) {
    const message = data.event;
    if (isEventDelta(message)) {
      const base = events.get(message.id);
      if (base) mergeEventDelta(base, message);
    } else {
      events.set(message.id, message);
    }
    if (message.type === "tool.response_required") {
      pendingQuestions.push(message);
    }
  }

  // Answer each pending question, reading the prompt and options from arguments.
  const responses = [];
  for (const pending of pendingQuestions) {
    for (const ref of pending.toolCalls) {
      // Look up the model.message that emitted this tool call, then find the call.
      const modelMessage = events.get(ref.sourceEventId)!;
      const toolCall = modelMessage.toolCalls!.find((tc) => tc.id === ref.id)!;
      // tool.response_required covers any client-side tool; handle ask_user_question here.
      if (!(toolCall.toolInfo?.type === "truefoundry-system" && toolCall.toolInfo.name === "ask_user_question")) {
        continue;
      }
      const args = JSON.parse(toolCall.function.arguments || "{}");
      console.log(`\n${args.question ?? ""}`);
      (args.options ?? []).forEach((option, idx) => {
        console.log(`  ${idx + 1}. ${option}`);
      });
      const answer = (await rl.question("your answer: ")).trim();
      responses.push({
        type: "user.tool_response",
        threadId: pending.threadId,
        toolCallId: ref.id,
        // content is a free-form string - the chosen option, or any text.
        content: answer,
      });
    }
  }

  // Second turn: resume with the user's answers.
  turn = session.prepareTurn({ input: responses });
  for await (const data of turn.execute({ stream: true })) {
    console.log(data.event);
  }
  rl.close();
  ```
</CodeGroup>

## Subscribe to events

Every turn emits a stream of **events** over SSE. The stream opens with [`turn.created`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#turncreatedevent) and closes with [`turn.done`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#turndoneevent). See the [Turn events reference](/docs/agent-platform/agent-harness/sdk/turn-events-reference) for every event type.

### Handling Event Delta while streaming

Most events in a Turn are complete on their own - a single payload you can use directly. Some updates are streamed instead: the base event arrives first, followed by a series of Event Deltas - incremental fragments that you merge into the base. All deltas for one update share the base event's `id`, while `sequence_number` increases across the base and all its deltas.

```json theme={"dark"}
// Base assembled event arrives first
{ "type": "model.message", "id": "0f3a9c2b-7d41-4e8a-b2c6-1a5f9e3d2b48", "sequence_number": 6, "content": "" }

// Deltas follow: same id, increasing sequence_number, each merged into the base
{ "type": "model.message.delta", "id": "0f3a9c2b-7d41-4e8a-b2c6-1a5f9e3d2b48", "sequence_number": 7, "content": "Hel" }
{ "type": "model.message.delta", "id": "0f3a9c2b-7d41-4e8a-b2c6-1a5f9e3d2b48", "sequence_number": 8, "content": "lo" }
{ "type": "model.message.delta", "id": "0f3a9c2b-7d41-4e8a-b2c6-1a5f9e3d2b48", "sequence_number": 9, "content": "!", "finish_reason": "stop" }

// After merging every delta, the base holds the full message
{ "type": "model.message", "id": "0f3a9c2b-7d41-4e8a-b2c6-1a5f9e3d2b48", "content": "Hello!", "finish_reason": "stop" }
```

Because every delta carries the base event's `id`, keep an `id`-keyed index of assembled events: store each non-delta event under its `id`, and merge each delta into the base with the same `id`. The `id` is unique per message, so deltas from concurrently streaming threads (the main agent and any sub-agents) always merge into the right base.

<Note>
  Both languages ship `is_event_delta` and `merge_event_delta` from `truefoundry_gateway_sdk.agents` (Python) and `truefoundry-gateway-sdk/agents` (TypeScript).
</Note>

<Note>
  Event Deltas appear only while streaming. When you [list turn events](#listing-events) via the events API, the deltas are already merged into a single assembled event.
</Note>

The most common example is the assistant's model output: a base [`ModelMessageEvent`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#modelmessageevent) followed by [`ModelMessageDeltaEvent`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#modelmessagedeltaevent) deltas that carry incremental text and tool-call chunks. Merge the deltas into the base as they arrive — read the base's growing `content` for a live typing effect.

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os
  from truefoundry_gateway_sdk.agents import (
      AgentSessionClient,
      TurnEvent,
      is_event_delta,
      merge_event_delta,
  )
  from truefoundry_gateway_sdk.types import UserMessage

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )
  session = client.create_session(agent_name="support-bot")

  events: dict[str, TurnEvent] = {}  # event id -> assembled event

  turn = session.prepare_turn(
      input=[UserMessage(content="Summarize my last order.")],
  )
  for data in turn.execute(stream=True):
      message = data.event
      if is_event_delta(message):
          base = events.get(message.id)
          if base is not None:
              merge_event_delta(base, message)
      else:
          events[message.id] = message
  ```

  ```typescript TypeScript lines theme={"dark"}
  import {
    AgentSessionClient,
    isEventDelta,
    mergeEventDelta,
    type TurnEvent,
  } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });
  const session = await client.createSession({ agentName: "support-bot" });

  const events = new Map<string, TurnEvent>(); // event id -> assembled event

  const turn = session.prepareTurn({
    input: [{ type: "user.message", content: "Summarize my last order." }],
  });
  for await (const data of turn.execute({ stream: true })) {
    const message = data.event;
    if (isEventDelta(message)) {
      const base = events.get(message.id);
      if (base) mergeEventDelta(base, message);
    } else {
      events.set(message.id, message);
    }
  }
  ```
</CodeGroup>

### Resume streaming

When you lose the original `execute(stream=True)` stream (for example, after a page reload), call [`client.get_session()`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#python-reference) and [`session.get_turn()`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#python-reference), then check `turn.state`. If it is still running, reconnect with [`turn.stream(after_sequence_number=...)`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#python-reference) and keep merging; if it has already finished, rebuild the index from [`turn.list_events()`](#listing-events) instead.

When resuming a running turn, pass `after_sequence_number` to continue after a known point; the stream closes when the turn reaches a terminal state. Either way, merge into the same `id`-keyed index of assembled events, so base events and their deltas keep merging seamlessly across the reconnect.

In Python, track `last_sequence_number` from each event's `sequence_number` field while streaming (use `data.sequence_number` when present). The stream wrapper may leave `TurnStreamData.sequence_number` as `None`.

<CodeGroup>
  ```python Python lines theme={"dark"}
  from truefoundry_gateway_sdk.agents import (
      AgentSessionClient,
      TurnStateRunning,
      is_event_delta,
      merge_event_delta,
  )

  # `client`, `session_id`, `turn_id`, `events`, and `last_sequence_number` already exist from the original stream:
  #   session_id: str               - the id of the session
  #   turn_id: str                  - the id of the turn you were streaming
  #   events: dict[str, TurnEvent]  - the id-keyed index of assembled events
  #   last_sequence_number: int     - the sequence_number of the last event you processed
  # Persist all four (for example, in your session store) so you can resume after a restart.

  session = client.get_session(session_id)
  turn = session.get_turn(turn_id)

  turn_state = turn.state
  if isinstance(turn_state, TurnStateRunning):
      # Still running: reconnect to the live stream. after_sequence_number skips events
      # you've already processed.
      for data in turn.stream(after_sequence_number=last_sequence_number):
          event = data.event
          if getattr(event, "sequence_number", None) is not None:
              last_sequence_number = event.sequence_number
          elif data.sequence_number is not None:
              last_sequence_number = data.sequence_number
          message = event
          if is_event_delta(message):
              base = events.get(message.id)
              if base is not None:
                  merge_event_delta(base, message)
          else:
              events[message.id] = message
  else:
      # Already finished: rebuild the index from the stored event log, which is
      # authoritative and overwrites any partial state from the lost stream.
      # list_events() returns already-merged events, so there are no deltas to merge here.
      for event in turn.list_events():
          events[event.id] = event
  ```

  ```typescript TypeScript lines theme={"dark"}
  import { AgentSessionClient, isEventDelta, mergeEventDelta, type TurnEvent } from "truefoundry-gateway-sdk/agents";

  // `client`, `sessionId`, `turnId`, `events`, and `lastSequenceNumber` already exist from the original stream:
  //   sessionId: string              - the id of the Session
  //   turnId: string                 - the id of the Turn you were streaming
  //   events: Map<string, TurnEvent> - the id-keyed index of assembled events
  //   lastSequenceNumber: number     - the sequenceNumber of the last event you processed
  // Persist all four (for example, in your session store) so you can resume after a restart.

  const session = await client.getSession({ sessionId });
  const turn = await session.getTurn({ turnId });

  const turnState = turn.state;
  if (turnState.status === "running") {
    // Still running: reconnect to the live stream. afterSequenceNumber skips events
    // you've already processed.
    for await (const data of turn.stream({ afterSequenceNumber: lastSequenceNumber })) {
      lastSequenceNumber = data.sequenceNumber;
      const message = data.event;
      if (isEventDelta(message)) {
        const base = events.get(message.id);
        if (base) mergeEventDelta(base, message);
      } else {
        events.set(message.id, message);
      }
    }
  } else {
    // Already finished: rebuild the index from the stored event log, which is
    // authoritative and overwrites any partial state from the lost stream.
    // listEvents() returns already-merged events, so there are no deltas to merge here.
    for await (const event of await turn.listEvents()) {
      events.set(event.id, event);
    }
  }
  ```
</CodeGroup>

### Handle threads

A single Turn stream interleaves events from the root agent and any [sub-agents](/docs/agent-platform/agent-harness/context-engineering/subagents) that run in parallel. Every event carries a `thread_id`:

* `"main"` — the root agent.
* A unique ID — a [sub-agent](/docs/agent-platform/agent-harness/context-engineering/subagents) thread. [`thread.created`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#threadcreatedevent) and [`thread.done`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#threaddoneevent) mark sub-agent thread lifecycle; they are not emitted for the main thread.
* `null` — a turn-level event, not tied to any thread: [`turn.created`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#turncreatedevent) and [`turn.done`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#turndoneevent) (first and last on the stream), [`sandbox.created`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#sandboxcreatedevent), and [`mcp.auth_required`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#mcpauthrequiredevent).

See [Turn Events](/docs/agent-platform/agent-harness/sdk/turn-events-reference) for the full reference.

The sequence below shows a turn where the root agent spawns a **research** sub-agent. Events interleave across two threads (`main` and the sub-agent), and the run pauses for an approval before a second turn resumes it. Solid arrows are turn inputs you send; dashed arrows are SSE events streamed back to your client.

```mermaid theme={"dark"}
sequenceDiagram
  actor Client
  box Session
    participant Main as main thread
    participant Research as research thread
  end

  Note over Client,Research: Turn 1 — user message
  Client->>Main: user.message
  Note over Client,Main: SSE stream opens
  Main-->>Client: model.message
  Main-->>Client: model.message.delta
  Main-->>Client: model.message.delta
  Main->>Research: spawn sub-agent
  Main-->>Client: thread.created
  Research-->>Client: model.message
  Research-->>Client: model.message.delta
  Research-->>Client: model.message.delta
  Research-->>Client: tool.approval_required
  Note over Client,Research: Turn 1 pauses — awaiting approval

  Note over Client,Research: Turn 2 — approval resumes the run
  Client->>Research: user.tool_approval
  Research-->>Client: tool.response
  Research-->>Client: model.message
  Research-->>Client: model.message.delta
  Research->>Main: sub-agent completes
  Main-->>Client: thread.done
  Main-->>Client: model.message
  Main-->>Client: model.message.delta
  Main-->>Client: model.message.delta
```

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os
  from collections import defaultdict
  from truefoundry_gateway_sdk.agents import (
      AgentSessionClient,
      ModelMessageDeltaEvent,
      ModelMessageEvent,
      ThreadCreatedEvent,
      ThreadDoneEvent,
      TurnEvent,
      is_event_delta,
      merge_event_delta,
  )
  from truefoundry_gateway_sdk.types import TrueFoundrySystemToolInfo, UserMessage

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )
  session = client.create_session(agent_name="support-bot")

  threads: dict[str, dict[str, TurnEvent]] = defaultdict(dict)

  turn = session.prepare_turn(
      input=[UserMessage(content="Summarize my last order.")],
  )
  for data in turn.execute(stream=True):
      message = data.event
      if message.thread_id is None:
          print("turn level event:", message)
          continue

      if isinstance(message, (ModelMessageEvent, ModelMessageDeltaEvent)):
          for tool_call in message.tool_calls or []:
              if (
                  isinstance(tool_call.tool_info, TrueFoundrySystemToolInfo)
                  and tool_call.tool_info.name == "create_sub_agent"
              ):
                  print("sub agent is getting initialized")
      if isinstance(message, ThreadCreatedEvent):
          print(f"{message.thread_id} created: {message.title}")

      if is_event_delta(message):
          thread_events = threads.get(message.thread_id)
          if thread_events is not None:
              base = thread_events.get(message.id)
              if base is not None:
                  merge_event_delta(base, message)
      else:
          threads[message.thread_id][message.id] = message

      if isinstance(message, ThreadDoneEvent):
          events_for_thread = threads.pop(message.thread_id)
          print(f"{message.thread_id} done: {message.title} ({len(events_for_thread)} events)")
  ```

  ```typescript TypeScript lines theme={"dark"}
  import {
    AgentSessionClient,
    isEventDelta,
    mergeEventDelta,
    type TurnEvent,
  } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });
  const session = await client.createSession({ agentName: "support-bot" });

  const threads = new Map<string, Map<string, TurnEvent>>();

  const turn = session.prepareTurn({
    input: [{ type: "user.message", content: "Summarize my last order." }],
  });
  for await (const data of turn.execute({ stream: true })) {
    const message = data.event;
    if (message.threadId == null) {
      console.log("turn level event:", message);
      continue;
    }

    if (message.type === "model.message" || message.type === "model.message.delta") {
      for (const toolCall of message.toolCalls ?? []) {
        if (
          toolCall.toolInfo?.type === "truefoundry-system" &&
          toolCall.toolInfo.name === "create_sub_agent"
        ) {
          console.log("sub agent is getting initialized");
        }
      }
    }
    if (message.type === "thread.created") {
      console.log(`${message.threadId} created: ${message.title}`);
    }

    if (isEventDelta(message)) {
      const base = threads.get(message.threadId)?.get(message.id);
      if (base) mergeEventDelta(base, message);
    } else {
      let bucket = threads.get(message.threadId);
      if (!bucket) {
        bucket = new Map();
        threads.set(message.threadId, bucket);
      }
      bucket.set(message.id, message);
    }

    if (message.type === "thread.done") {
      const eventsForThread = threads.get(message.threadId) ?? new Map();
      threads.delete(message.threadId);
      console.log(`${message.threadId} done: ${message.title} (${eventsForThread.size} events)`);
    }
  }
  ```
</CodeGroup>

### Listing Events

Fetch the full event log of a finished turn with [`turn.list_events()`](/docs/agent-platform/agent-harness/sdk/runtime-api-reference#python-reference). It yields the turn's [events](/docs/agent-platform/agent-harness/sdk/turn-events-reference) in order and auto-paginates. Pass `order="asc"` (default, oldest-first) or `order="desc"` to control the direction.

Unlike `turn.execute(stream=True)` and `turn.stream()`, `turn.list_events()` returns already-merged events: each model message arrives as a single assembled [`ModelMessageEvent`](/docs/agent-platform/agent-harness/sdk/turn-events-reference#modelmessageevent), never as a base plus [deltas](/docs/agent-platform/agent-harness/sdk/turn-events-reference#modelmessagedeltaevent). There is nothing to merge — you can use each event directly.

For example, a model message that `stream()` delivers as a base event followed by deltas:

```json theme={"dark"}
// On stream(): a base event plus its deltas, all sharing one id
{ "type": "model.message",       "id": "0f3a...", "content": "" }
{ "type": "model.message.delta", "id": "0f3a...", "content": "Hel" }
{ "type": "model.message.delta", "id": "0f3a...", "content": "lo!", "finish_reason": "stop" }
```

is returned by `turn.list_events()` as one fully-merged event:

```json theme={"dark"}
// On list_events(): the same message, already assembled
{ "type": "model.message", "id": "0f3a...", "content": "Hello!", "finish_reason": "stop" }
```

<Note>
  `turn.list_events()` is only available for turns that have completed. A running turn has no stored event log yet — use `turn.execute(stream=True)` or `turn.stream()` for live delivery instead.
</Note>

<CodeGroup>
  ```python Python lines theme={"dark"}
  import os
  from truefoundry_gateway_sdk.agents import AgentSessionClient, TurnStateRunning

  client = AgentSessionClient(
      base_url=os.environ["TFY_GATEWAY_URL"],
      api_key=os.environ["TFY_API_KEY"],
  )

  session = client.get_session(session_id="sess-abc123")

  # Find the latest completed turn (newest-first).
  for turn in session.list_turns():
      # list_events() is only available once the turn has completed.
      if isinstance(turn.state, TurnStateRunning):
          continue
      # order="asc" (default) yields oldest-first; use "desc" for newest-first.
      for event in turn.list_events(order="asc"):
          print(event)
      break
  ```

  ```typescript TypeScript lines theme={"dark"}
  import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";

  const client = new AgentSessionClient({
    apiKey: process.env.TFY_API_KEY!,
    baseUrl: process.env.TFY_GATEWAY_URL!,
  });

  const session = await client.getSession({ sessionId: "sess-abc123" });

  // Find the latest completed turn (newest-first).
  for await (const turn of await session.listTurns()) {
    const turnState = turn.state;
    // listEvents() is only available once the turn has completed.
    if (turnState.status === "running") {
      continue;
    }
    // order "asc" (default) yields oldest-first; use "desc" for newest-first.
    for await (const event of await turn.listEvents({ order: "asc" })) {
      console.log(event);
    }
    break;
  }
  ```
</CodeGroup>

## Complete example

The [Complete example](/docs/agent-platform/agent-harness/sdk/complete-example) is a runnable terminal chat client that implements every pattern in this guide — streaming, delta merging, approvals, questions, MCP auth, sub-agent threads, and multi-turn chaining.
