Deep Agent
Event-driven streaming from Deep Agent, exposing typed content blocks.
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.
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:
| Mode | Endpoint |
|---|---|
| Threads | POST /v1/threads/{thread_id}/run/events/stream |
| Chat Completions | POST /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.
| Type | Key field(s) | Description |
|---|---|---|
text | text | A fragment of the final answer text. |
reasoning | reasoning | Internal thinking produced before the answer. |
tool_call | name, args, id | A tool invocation the caller must execute and answer with a separate tool message. |
server_tool_call | name, args, id | Deep Agent only. A tool invocation the agent executes itself, inline, within the same turn. |
server_tool_result | tool_call_id, status, output | Deep 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.
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.
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_id | user_tenant_id | The agent can read |
|---|---|---|
| set | set | the files and the spaces |
| set | not set | the files, no spaces |
| not set | set | the API rejects the request with 422 |
| not set | not set | no 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:
| Field | Type | Description |
|---|---|---|
questions | array of Question | One to five questions, in the order the user sees them. |
Question:
| Field | Type | Description |
|---|---|---|
question | string | The question the user answers. |
options | array of Option | Empty for a free text question. Two to five options for a choice, in the order the user sees them. |
multi_select | boolean | Choice only. true: the user can pick more than one option. |
Option:
| Field | Type | Description |
|---|---|---|
label | string | The option as the user sees it. |
description | string or null | What the option means. Optional. |
recommended | boolean | true 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)"
}