Follow a turn
Every message you send to a chat session starts a turn, or waits Up next behind the one running. Send the message, read the delivery field, then follow the turn's job until it ends. When it ends, read the agent's reply from the session's messages.
Send a message
curl -s -X POST \
"https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/jobs/chat" \
-H "Authorization: Bearer $KOSUKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Add a loading state to the search results."}'
With the CLI, kosuke sessions send <projectId> <sessionId> --content "..." does the same.
| Field | Type | What it does |
|---|---|---|
content | string | The message. Required. It may be empty when you send diffCommentsContext. |
model | string | A model id for this turn, which the session then keeps. See Choose the model. |
previewContext | object | { "frontendName", "path" }, the preview page the message is about. |
diffCommentsContext | array | Up to 50 diff comments to send as review feedback. See Send review comments to the agent. |
retryFailedJobId | uuid | The id of the session's last job, when it failed with errorCode context_window_exceeded. The new turn starts with the conversation handed over as text. |
runningJobId | uuid | The job you saw running when you sent. If a person stopped it meanwhile, the send answers 409 JOB_STOPPED instead of starting a turn. |
To attach files, send multipart/form-data with the fields above, attachmentCount, and the files as attachment_0 to attachment_9. In multipart, previewContext and diffCommentsContext are JSON strings. The CLI takes files with --attach. Responses and errors lists the attachment limits.
A message that starts with a skill's name runs the skill. Claude, GLM and Kimi sessions take /name, and Codex sessions take $name. /clear and /compact are Runtime Commands, run with POST /api/projects/{projectId}/chat-sessions/{sessionId}/runtime-commands. Sent as a message, they reach the agent as plain text.
Read the delivery
A send answers 201 with a delivery field, and a steer answers 200:
{ "data": { "delivery": "job", "chatJobId": "c47a1d3b-…", "jobNumber": 3 } }
delivery | What happened | Id you get | What to do |
|---|---|---|---|
job | The message started a turn. | chatJobId | Follow the job. |
queued | A turn was running, so the message waits Up next. It starts its own turn when the running one ends. | queuedMessageId | Wait for the running turn, or steer the message into it. |
steer | You steered a queued message into your running turn. | steerId | Keep following the running turn's job. No new job starts. |
When a queued message starts its turn, GET /api/projects/{projectId}/chat-sessions/{sessionId} names the new job in latestChatJob.
Follow the job
Poll the job every few seconds:
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/jobs/chat/$CHAT_JOB_ID" \
-H "Authorization: Bearer $KOSUKE_API_KEY"
{
"data": {
"job": {
"id": "c47a1d3b-…",
"status": "running",
"jobNumber": 3,
"error": null,
"errorCode": null,
"createdAt": "2026-09-30T09:12:04.118Z",
"startedAt": "2026-09-30T09:12:09.502Z",
"completedAt": null
}
}
}
status | Meaning | What to do next |
|---|---|---|
pending | The turn is waiting to start, for example while the sandbox starts. | Keep polling. |
running | The agent is working. | Keep polling. You can steer or stop it. |
awaiting_response | The turn ended with a Clarification for you. Up next pauses. | Answer it. |
completed | The turn finished. | Read the reply. |
failed | The turn failed. error and errorCode say why. Up next pauses. | Fix the cause and send again. For context_window_exceeded, resend with retryFailedJobId. |
cancelled | A person stopped the turn. Up next pauses. | Start nothing new until a person asks you to. |
Polling faster does not make a turn finish sooner. To follow a turn live instead, stream it with the CLI:
kosuke sessions watch "$PROJECT_ID" "$SESSION_ID" "$CHAT_JOB_ID" \
| jq -r 'select(.type == "message") | .data.text'
The MCP server's watch tool polls a job until it settles and returns the final status, the last assistant message and the preview URLs. Call it again when it returns done: false. The CLI command's exit code says how the job ended:
| Exit code | Meaning |
|---|---|
| 0 | The job ended completed or awaiting_response. |
| 1 | The job failed or was stopped. |
| 3 | The token was refused. |
| 4 | The project, session or job was not found. |
| 124 | --timeout passed first. |
Read the reply
GET /api/projects/{projectId}/chat-sessions/{sessionId}/messages returns the whole conversation, oldest first. Your messages carry content, and the agent's carry blocks. The reply is in the text blocks of the last message with role assistant:
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/messages" \
-H "Authorization: Bearer $KOSUKE_API_KEY" \
| jq -r '[.data.messages[] | select(.role == "assistant")] | last | .blocks[] | select(.type == "text") | .content'
Other blocks record the agent's plan, each tool it ran, each artifact it wrote, any clarification it asked, and the Backlog sessions it filed (filed_sessions).
Answer a Clarification
A turn that ends awaiting_response holds a clarification block. Its clarificationData.questions lists each question with a topic, the question text and two to four options. The recommended option comes first, with isRecommended: true.
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/messages" \
-H "Authorization: Bearer $KOSUKE_API_KEY" \
| jq '[.data.messages[] | select(.role == "assistant")] | last | .blocks[]
| select(.type == "clarification") | .clarificationData.questions[]
| {topic, question, options: [.options[].title]}'
Send the answers as an ordinary message. It starts the next turn and resumes Up next. The web app words its answers like this, and the agent reads any clear wording:
My answers:
- **Display order:** Newest first
- **Empty state:** Show a message
Steer a running turn
A message sent during a turn waits Up next. To fold it into the running turn instead, steer it with the queuedMessageId the send returned:
curl -s -X POST \
"https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/queue/$QUEUED_MESSAGE_ID/steer" \
-H "Authorization: Bearer $KOSUKE_API_KEY"
{ "data": { "delivery": "steer", "steerId": "e1f09a7c-…" } }
Only the message's author can steer it, and only into a turn of their own. Claude, Codex, GLM and Kimi sessions all take steers. When no turn can take the message, the steer answers 409 STEER_UNAVAILABLE and the message stays Up next. A steer the turn has not taken when it ends goes back to the top of Up next.
The CLI manages Up next with kosuke sessions queue list, update, move, send, steer, delete and resume. Messages, Up next and steering explains Up next in the web app.
Stop a turn
curl -s -X POST \
"https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/jobs/chat/$CHAT_JOB_ID/cancel" \
-H "Authorization: Bearer $KOSUKE_API_KEY"
The job ends cancelled and Up next pauses. Steers the turn had not taken go back to the top of Up next. Stopping a job that ended completed, failed or cancelled answers 400. To carry on, send a message, which goes at once, or resume Up next with POST /api/projects/{projectId}/chat-sessions/{sessionId}/queue/resume.
Choose the model
Send model with a message to switch the session's model from that turn on. Model ids name a provider, a model and a reasoning effort, such as anthropic:opus__high. kosuke models list prints every id.
- The override applies only to a message that starts a turn. A message that waits Up next ignores it.
- Kosuke ignores an id missing from the catalog, and a model the session's Assignee cannot run. The turn then runs on the session's current model.
- Switching to another provider starts the agent fresh, with the conversation handed over as text.
- To confirm the switch, read
data.session.modelfromGET /api/projects/{projectId}/chat-sessions/{sessionId}.
Creating or updating a session, saving a cron and setting the deploy model check the id strictly. An unknown id there answers 400.
When a send is refused
| Code | Meaning | What to do |
|---|---|---|
JOB_IN_PROGRESS | A turn started while your message was on its way. | Send it again. It waits Up next. |
QUEUE_FULL | Up next already holds 20 messages. | Wait for one to go, or delete one. |
JOB_STOPPED | A person stopped the turn named by runningJobId. | Do not resend on your own. |
SESSION_UPDATING | Another change to the session is in progress. | Send the same request again. |
SESSION_STATUS_CHANGED | The session's status, branch, model or Assignee changed, or someone ran /clear. | Read the session again, then decide whether to send. |
SESSION_CHANGED | The session changed while your message was being queued. | Read the session again, then send. |
SANDBOX_LIMIT_REACHED | Waking the sandbox would pass the workspace's concurrent sandbox limit. | Free a slot, then send again. |
SANDBOX_STARTING | The session's sandbox is still starting. | Send the same message again shortly. |
DEPLOY_ACTIVE | The sandbox is gone or paused and cannot be created while the project deploys. | Send again once the deploy ends. |
All of these answer 409. A send can also answer:
- 400 when the session is in the Backlog, Completed or Archived. Start a Backlog session first.
- 400 when a Cron, another session or a Slack relay sends into an Under Review session. A member's message resumes an Under Review session and is sent; resume it first for any other sender.
- 402
CREDIT_LIMIT_REACHEDwhen the workspace is blocked on billing.