🚀 Simplify your work by integrating Haufe Copilot via API
Guides

Deep Agent

Event-driven streaming from Deep Agent, exposing typed content blocks.

warning

Deep Agent is currently under development phase.

Deep Agent streams responses as typed content blocks instead of plain text. Each chunk exposes what the agent is doing internally - reasoning, tool calls executed inline, or the final answer - in real time.

info

The events/stream endpoints are dedicated to assistants that stream content_blocks e.g. Deep Agent - streams content_blocks instead of plain text. In addition, what is specific to Deep Agent is the server_tool_call / server_tool_result block pair: Deep Agent executes tools itself within the same turn and reports the trace inline, whereas other assistants that call tools emit a tool_call block and expect the caller to execute it and return the result as a separate tool message.

Endpoints

Deep Agent is available on both the Threads and Chat Completions APIs:

ModeEndpoint
ThreadsPOST /v1/threads/{thread_id}/run/events/stream
Chat CompletionsPOST /v1/chat/completions/events/stream

Both endpoints return the same NDJSON response format (one JSON object per line) with identical content block types. The only difference is how you provide the conversation context — thread ID vs. inline messages.

The request body is identical to the corresponding regular stream endpoint (/run/stream or /chat/completions/stream).

Content Block Types

Each chunk's delta.content_blocks is a list of content blocks. A single chunk typically contains one block.

TypeKey field(s)Description
texttextA fragment of the final answer text.
reasoningreasoningInternal thinking produced before the answer.
tool_callname, args, idA tool invocation the caller must execute and answer with a separate tool message.
server_tool_callname, args, idDeep Agent only. A tool invocation the agent executes itself, inline, within the same turn.
server_tool_resulttool_call_id, status, outputDeep Agent only. The result of a preceding server_tool_call. status is success or error.

Blocks arrive in the order they are produced by the agent.

Response Shape

Each NDJSON line looks like this:

// server_tool_call
{
  "message_id": "969b216f-605f-44f4-9df9-cc43e56326fd",
  "thread_id": "...",
  "created_at": "...",
  "delta": {
    "role": "assistant",
    "content_blocks": [
      {
        "type": "server_tool_call",
        "name": "human_resource_search_tool",
        "args": { "user_query": "wiederholtes Zuspätkommen Abmahnung verhaltensbedingte Kündigung" },
        "id": "call_pjGaby08YGRYEOVdqQtcIYRj"
      }
    ],
    "sources": []
  }
}

// server_tool_result
{
  "message_id": "969b216f-605f-44f4-9df9-cc43e56326fd",
  "thread_id": "...",
  "created_at": "...",
  "delta": {
    "role": "assistant",
    "content_blocks": [
      {
        "type": "server_tool_result",
        "tool_call_id": "call_pjGaby08YGRYEOVdqQtcIYRj",
        "status": "success",
        "output": "<documents><document id=..."
      }
    ],
    "sources": []
  }
}

// reasoning
{
  "message_id": "969b216f-605f-44f4-9df9-cc43e56326fd",
  "thread_id": "...",
  "created_at": "...",
  "delta": {
    "role": "assistant",
    "content_blocks": [
      {
        "type": "reasoning",
        "reasoning": "Searching for specifics about repeated lateness and dismissal..."
      }
    ],
    "sources": []
  }
}

{ "..." }

// text (final answer)
{
  "message_id": "969b216f-605f-44f4-9df9-cc43e56326fd",
  "thread_id": "...",
  "created_at": "...",
  "delta": {
    "role": "assistant",
    "content_blocks": [
      {
        "type": "text",
        "text": "Wenn Sie nicht Arbeitgeber oder kündigungsberechtigte Person sind..."
      }
    ],
    "sources": [
      {
        "content": "§ 622 Abs. 3 BGB: Während einer vereinbarten Probezeit...",
        "meta_data": { "document_title": "Bürgerliches Gesetzbuch (BGB)", "section": "§ 622 Abs. 3" }
      }
    ]
  }
}

Sources are attached to the chunk that finalizes the answer - earlier chunks typically have an empty sources array.

Files and Spaces of the User

Deep Agent can read the files and the spaces of the end user. A file belongs to a user. A space belongs to a user and a tenant. The thread tells the agent which user and which tenant to use.

info

The agent can only read files and spaces. It lists them, reads a file, and searches in files. It cannot create, change, or delete a file or a space.

Set the user and the tenant of a thread

With an api-key, set user_id and user_tenant_id when you create the thread. Use the same values as in the user-id and x-tenant-id headers of /v1/files and /v1/spaces.

Create a thread with user_id and user_tenant_id
curl --request POST \
  --url https://api.haufe.ai/agents/v1/threads \
  --header 'content-type: application/json' \
  --header 'api-key: <API_KEY>' \
  --data '{
    "assistant_id": "<ASSISTANT_ID>",
    "user_id": "<USER_ID>",
    "user_tenant_id": "<USER_TENANT_ID>"
  }'

POST /v1/chat/completions accepts the same two fields next to messages.

user_iduser_tenant_idThe agent can read
setsetthe files and the spaces
setnot setthe files, no spaces
not setsetthe API rejects the request with 422
not setnot setno files and no spaces

The thread response shows the stored user_tenant_id and the scope of the thread: HAUFE with an access token, HAI with an api-key only.

With an OAuth user token, the token gives the user and the tenant. Do not send user_id or user_tenant_id. If you send one of them, the API rejects the request with 422. This also applies to a value that is equal to the token value.

A thread that you create with a token needs a token of the same user for every run. A run without a token gets 403 with the error code thread_requires_access_token. A run with the token of a different user gets 404 with the error code thread_not_found.

You cannot change the user or the tenant of a thread. To use a different user or tenant, create a new thread.

Interrupting tools

Some tools stop the loop and need an interaction from the user. The run ends at the call, and your client answers it:

UserMessage -> AssistantMessage (tool_call with name = the tool) -> ToolMessage (the answer) -> next run.

The tool_call block comes in the last chunk of the stream and in the assistant message of the thread. The answer is a tool message with the tool_call_id of the block, see Message validation. A run without the answer is rejected. The engine owner turns an interrupting tool on in the agent configuration. You do not pass a tool definition in the request.

ask_user

The agent asks one to five questions in one form. A question is a free text question, without options, or a choice with two to five options. A choice can mark one option as recommended. Your client always adds a "something else" entry with a free text field to a choice. The user can add a note to every answer.

args of the tool_call block:

FieldTypeDescription
questionsarray of QuestionOne to five questions, in the order the user sees them.

Question:

FieldTypeDescription
questionstringThe question the user answers.
optionsarray of OptionEmpty for a free text question. Two to five options for a choice, in the order the user sees them.
multi_selectbooleanChoice only. true: the user can pick more than one option.

Option:

FieldTypeDescription
labelstringThe option as the user sees it.
descriptionstring or nullWhat the option means. Optional.
recommendedbooleantrue for the option the agent recommends. At most one.
// tool_call (the run stops here)
{
  "type": "tool_call",
  "name": "ask_user",
  "id": "call_7Qm2x1kZ9PbT0cVwE4rL8sYd",
  "args": {
    "questions": [
      {
        "question": "Bei welcher Berufsgenossenschaft ist der Betrieb versichert?",
        "options": [],
        "multi_select": false
      },
      {
        "question": "Was ist Ihnen lieber?",
        "options": [
          { "label": "Erst die Gefährdungsbeurteilung aktualisieren", "description": "Die Pflicht mit der kürzesten Frist.", "recommended": true },
          { "label": "Erst die Unterweisungen nachholen", "description": null, "recommended": false }
        ],
        "multi_select": false
      }
    ]
  }
}

The answer goes back as one tool message. The content holds one line per question, in the order of the questions: the number, a colon, then the free text or the labels of the picked options joined with a comma, then the note of the user in parentheses when there is one. "Something else" goes back as the free text the user typed.

// POST /v1/threads/{thread_id}/messages
{
  "role": "tool",
  "tool_call_id": "call_7Qm2x1kZ9PbT0cVwE4rL8sYd",
  "content": "1: BG Holz und Metall\n2: Erst die Gefährdungsbeurteilung aktualisieren (note: die Werkstatt zuerst)"
}

Next Steps

On this page