---
title: "Chat Main Agent"
method: POST
path: "/api/v1/chat/stream"
tags: ["chat"]
---

# Chat Main Agent

`POST /api/v1/chat/stream`

Receives and processes a streaming chat request via the AgentService.

This endpoint streams the agent's response in real-time using Server-Sent Events.

Args:
    request: The chat request data containing message and project_id (pre-validated).
    agent_service: The AgentService instance (dependency).

Returns:
    StreamingResponse with Server-Sent Events containing streaming chat data.

Raises:
    HTTPException: 400 for validation errors.
    HTTPException: 404 if project not found or user doesn't own it.
    HTTPException: 500 for unexpected errors.

## Request body

- ChatRequest
  - `message` string, required
  - `message_id` string, nullable — Client-minted identity for this message (UUID string). The same value rides the optimistic bubble, the stream echo, and the persisted history metadata, so every surface dedupes by one key. Server mints one when absent or malformed.
  - `project_id` string, uuid, required
  - `current_focused_layer_id` string, uuid, nullable
  - `action_type` 'new_enrichment' | 'new_layer' | 'analyze_row' | 'invoke_skill' — Enum for different action types in the system.
  - `action_subtype` string, nullable — Optional preset identifier that disambiguates actions sharing an action_type. For detail-panel presets, matches the client-side noteSource (e.g. 'environment', 'zoning', 'summary').
  - `hidden_context` object, nullable
  - `current_view_id` string, nullable
  - `iterate_over_rows` boolean
  - `selected_model` string, nullable — Model-picker selection for this chat (a curated picker key, see GET /chat/models). Persisted on the chat; honored only when the workspace's model-picker feature toggle is on.
  - `selected_thinking` 'auto' | 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' — Provider-native thinking choices exposed by the main-agent picker.
  - `feature_context` FeatureContext — Reference to a specific feature the user is viewing. Sent when the detail view is open so the agent knows which feature the user is talking about. Reused by @ mentions. `feature_id` is a string because sandbox-backed layers (place-type) use UUID feature ids, while CloudSQL `features.id` is bigint serialized as a numeric string, so the only shape check is non-empty and bounded. The bound rejects rather than truncates, unlike `label`: an over-long label still names the right parcel, while a truncated id names a different one. Both real shapes are an order of magnitude under it.
    - `feature_id` string, required
    - `layer_id` string, uuid, required
    - `source_table` string, nullable — The layer source the client minted `label` from, as named by the layer's `source_table`. The share release policy reads it to decide whether the title can only have come from columns it already releases; None is treated as unknown and withheld.
    - `title_source` 'parcelnumb' | 'feature_id' | 'name' | 'address' | 'source_type' — Which attribute branch of the client's title resolver produced a label. The release boundary needs the title's own provenance, not the layer's. A value the client can name is the only thing that distinguishes a parcel number from a name column that happens to read like one, because the rendered string cannot.
    - `label` string, nullable — The title the compose chip was showing for this feature at send. Carried on the reference itself so the two cannot disagree, and persisted as the sent turn's receipt so the rendered history can still name the parcel after it leaves context. None when the client had no row to title it with — and therefore showed no chip. Longer than 200 characters is truncated, never rejected.
  - `mentioned_feature_contexts` FeatureContext[], nullable
    - `feature_id` string, required
    - `layer_id` string, uuid, required
    - `source_table` string, nullable — The layer source the client minted `label` from, as named by the layer's `source_table`. The share release policy reads it to decide whether the title can only have come from columns it already releases; None is treated as unknown and withheld.
    - `title_source` 'parcelnumb' | 'feature_id' | 'name' | 'address' | 'source_type' — Which attribute branch of the client's title resolver produced a label. The release boundary needs the title's own provenance, not the layer's. A value the client can name is the only thing that distinguishes a parcel number from a name column that happens to read like one, because the rendered string cannot.
    - `label` string, nullable — The title the compose chip was showing for this feature at send. Carried on the reference itself so the two cannot disagree, and persisted as the sent turn's receipt so the rendered history can still name the parcel after it leaves context. None when the client had no row to title it with — and therefore showed no chip. Longer than 200 characters is truncated, never rejected.
  - `prior_answer` InteractiveQuestionAnswer
    - `optionId` string, required
    - `label` string, required
    - `questionId` string, required
  - `tool_approval` ToolApprovalAnswer — The user's approve/decline for a pending tool call. ``message`` is wire-forward-compat: no UI affordance sends it yet (the card is approve/decline only), but the denial seam already threads it into the synthesized ToolDenied as context for a future decline-reason input. Capped because it persists into chat history and rides every subsequent model call.
    - `toolCallId` string, required
    - `approved` boolean, required
    - `message` string, nullable
  - `continue_turn` boolean — Continue a terminally-failed turn from its persisted partial without a new prompt. The FE sends an empty `message` and the endpoint runs a promptless turn on the persisted history (the same no-user-bubble shape as a tool approval, minus the approval payload) so the agent resumes where it left off.
  - `attachment_id` string, uuid, nullable — A document previously uploaded to this project's chat via POST /chat/{project_id}/attachment. The agent receives it as a native document alongside `message`; the turn's persisted history keeps only a reference, so a later turn can read the same document without re-uploading it.

## Response `200`

Successful Response

- unknown

## Other responses

- `422` — Validation Error

## Changes

- **2026-09-12** `604ea84d0ff7` — 2 warning, 6 info
  - the `feature_context/anyOf[subschema #1: FeatureContext]/feature_id` request property's maxLength was set to `200`
  - the `mentioned_feature_contexts/anyOf[subschema #1]/items/feature_id` request property's maxLength was set to `200`
  - added the new optional request property `feature_context/anyOf[subschema #1: FeatureContext]/label`
  - added the new optional request property `feature_context/anyOf[subschema #1: FeatureContext]/source_table`
  - …4 more
- **2026-09-02** `d0e2bf840dfc` — 1 info
  - added the new optional request property `message_id`
- **2026-08-18** `fff2cd2dfbac` — 1 info
  - added the new optional request property `selected_thinking`
- **2026-08-13** `18539ae8bf0a` — 1 info
  - added the new optional request property `attachment_id`

[Change history](https://skmtc.dev/maia-analytics/apis/maia-api/changes/api/v1/chat/stream/post.md)

---

[API](https://skmtc.dev/maia-analytics/apis/maia-api.md) · [All operations](https://skmtc.dev/maia-analytics/apis/maia-api/llms.txt) · [OpenAPI document](https://skmtc.dev/maia-analytics/apis/maia-api/revisions/95495ef535bc?raw)
