---
title: "Start one interactive agent-kernel turn"
method: POST
path: "/v1/agent/turn"
tags: ["Agent"]
---

# Start one interactive agent-kernel turn

`POST /v1/agent/turn`

## Request

- Base URL: `http://localhost:9998`
- URL: `http://localhost:9998/v1/agent/turn`
- Auth: HTTP bearer

Drives ToolPiper's native `AgentLoopKernel` for one turn on a session
(creating the session when `session_id` is absent). Provider-first:
the body carries NO model name — the route resolves to a *configuration*
through a three-layer ladder, the caller's optional
`endpoint_config_id`, then the authenticated token's endpoint-config
binding, then the local active model. Returns immediately; the
turn's events (token deltas, tool-call proposals, tool results, cost
frames, terminal turn-ended marker) ride `GET /v1/agent/stream`.

A session belongs to the client that opened it. Continuing, cancelling,
answering, streaming or reading one another client opened is refused
with `403` naming the owning client; the person at the Mac is always
admitted. A conversation saved before sessions recorded their owner is
open to the person and to ToolPiper's web app, and the first of them to
continue it becomes its owner.

## Request body

- object
  - `message` string, required: The user message for this turn
  - `session_id` string: The chat this turn runs (1-128 chars of [A-Za-z0-9_-]); omitted mints a new session. ToolPiper resolves what the turn may do without asking from that chat, never from the body: reads inside the pinned folders, the folders its project allowed and those of the code workbenches open in it, and the tool families its open workbenches trust. Reads auto-approve only in a chat a renderer journals: a session whose record holds no journal (a headless caller's) or no record at all reads nothing without asking.
  - `system` string: System prompt, applied only on a session's first turn
  - `endpoint_config_id` string: The caller's chosen provider, as an `EndpointConfig` id (`tp-<uuid>`; alphanumerics, `-` and `_`, at most 64 characters). This is a *configuration* reference, not a model name — provider-first is intact, and ToolPiper still resolves and injects the model server-side. Layer 1 of the route ladder: it wins over the calling token's endpoint-config binding, because a pick expressed in the request outranks a stored auth-scoped default. An id that no longer exists is a hard 503, never a silent fall-through to a different provider. Omit to resolve server-side from the token binding or the local active model — every headless / voice / worker caller does.
  - `tools_enabled` boolean: Per-turn tools switch. Absent (or JSON `null`) defaults to `true`, so every existing headless / voice / worker caller is byte-unchanged. When `false` the turn advertises NO tools at all — the surface set, the retrieval set and the `request_tools` escape hatch alike — and the kernel runs a plain single-completion turn. Present-but-not-boolean is a deterministic 400 (`tools_enabled must be a boolean`).
  - `background` boolean: Make this turn a background run. Losing every `GET /v1/agent/stream` subscriber never cancels it, and a parked approval waits out its 300 s idle timeout instead of resolving as a disconnect, so a client that reattaches can answer it. Absent (or JSON `null`) keeps the five-second disconnect grace. Anything but a JSON boolean is a 400 (`background must be a boolean`).
  - `history` object[]: Seed transcript for a fresh session — a bounded recent window, not the whole conversation. Server-authoritative caps, each violation its own deterministic 400 raised before provider resolution: at most 100 messages, at most 100000 UTF-8 bytes per entry `content`, and at most 1000000 UTF-8 bytes summed across all entries. `role` is restricted to `user` / `assistant`; `system` and `tool` roles are rejected. Absent for every headless / voice / worker / warm-session caller.
    - `role` 'user' | 'assistant', required
    - `content` string, required

## Response `200`

Turn accepted and running

- object
  - `ok` true
  - `session_id` string
  - `turn` integer: 1-based turn index within the session
  - `run_id` string: This turn's run. Name it as `run_id` on cancel and approve so a reply meant for a finished run can never land on a later one; read it back from `GET /v1/agent/run`.
  - `stream` string: Path of the session's SSE stream
  - `last_event_id` integer: The session frame ring's head id before this turn appends anything. A client that kept its replay cursor across a Stop fast-forwards to it, so a cancelled turn's unseen tail (including its terminal frame) never replays into the new turn's stream subscription.

## Other responses

- `409`: A turn is already running on this session
- `503`: No provider resolvable — either every ladder layer came up empty (no `endpoint_config_id`, no token binding, no local model loaded), or a supplied `endpoint_config_id` / token binding references a configuration that no longer exists. The reason names the dangling id.

---

[API](https://skmtc.dev/modelpiper/apis/toolpiper-api.md) · [All operations](https://skmtc.dev/modelpiper/apis/toolpiper-api/llms.txt) · [OpenAPI document](https://skmtc.dev/modelpiper/apis/toolpiper-api/revisions/6722bf56385c?raw)
