DSAgent provides a REST API and WebSocket for building custom UIs and integrations. The streaming chat endpoint uses Server-Sent Events (SSE).
All API routes are under /api (no version prefix). Health endpoints are at the root.
http://localhost:8000
| Path | Description |
|---|---|
GET /health |
Health check (no auth) |
GET /health/ready |
Readiness check (no auth) |
/api/sessions |
Session CRUD |
/api/sessions/{id}/chat |
Chat (sync and stream) |
/api/sessions/{id}/kernel |
Kernel state, execute, reset |
/api/sessions/{id}/files |
List/upload/download/delete files |
/api/sessions/{id}/artifacts |
List/download/delete artifacts |
/api/sessions/{id}/hitl/* |
Human-in-the-loop |
WS /ws/chat/{session_id} |
WebSocket chat |
If DSAGENT_API_KEY is set, all API endpoints (except /health, /health/ready) require the key.
Header (recommended):
X-API-Key: your-api-keyQuery (WebSocket only): ?api_key=your-api-key (see security note in server docs)
POST /api/sessionsRequest Body:
{
"name": "My Analysis Session",
"model": "gpt-4o",
"hitl_mode": "none"
}Response:
{
"id": "20260112_215333_b5537c",
"name": "My Analysis Session",
"status": "active",
"created_at": "2026-01-12T21:53:33.204Z",
"updated_at": "2026-01-12T21:53:33.204Z",
"message_count": 0,
"kernel_variables": 0,
"workspace_path": "/workspace/sessions/20260112_215333_b5537c"
}GET /api/sessions?status=active&limit=50Query: status (optional), limit (default 50, max 100).
GET /api/sessions/{session_id}DELETE /api/sessions/{session_id}POST /api/sessions/{session_id}/archiveGET /api/sessions/{session_id}/exportReturns session data with Content-Disposition: attachment; filename="...json".
GET /api/sessions/{session_id}/notebookReturns the session's Jupyter notebook (.ipynb) if available.
Update session configuration including model and HITL mode at runtime.
PUT /api/sessions/{session_id}
Content-Type: application/json
{
"name": "New Session Name",
"model": "claude-sonnet-4-20250514",
"hitl_mode": "plan_only"
}All fields are optional. Only provided fields will be updated.
Request Body:
| Field | Type | Description |
|---|---|---|
name |
string | New name for the session |
status |
string | Session status: active, paused, completed |
model |
string | LLM model (e.g., gpt-4o, claude-sonnet-4-20250514) |
hitl_mode |
string | HITL mode (see below) |
HITL Modes:
| Mode | Description |
|---|---|
none |
Fully autonomous (default) |
plan_only |
Pause after generating plan for approval |
on_error |
Pause only when code execution fails |
plan_and_answer |
Pause for plan + before final answer |
full |
Pause before every code execution |
Response: Updated session object
Example - Change model mid-conversation:
curl -X PUT "http://localhost:8000/api/sessions/{session_id}" \
-H "Content-Type: application/json" \
-d '{"model": "claude-sonnet-4-20250514"}'Example - Enable HITL:
curl -X PUT "http://localhost:8000/api/sessions/{session_id}" \
-H "Content-Type: application/json" \
-d '{"hitl_mode": "plan_only"}'HITL allows human approval before the agent executes plans or code. When enabled, the agent pauses and waits for approval via these endpoints.
Check if the agent is awaiting human approval.
GET /api/sessions/{session_id}/hitl/statusResponse:
{
"enabled": true,
"mode": "plan_only",
"awaiting_feedback": true,
"awaiting_type": "plan",
"pending_plan": {
"raw_text": "1. [ ] Load data\n2. [ ] Analyze\n3. [ ] Visualize",
"steps": [
{"number": 1, "description": "Load data", "completed": false},
{"number": 2, "description": "Analyze", "completed": false},
{"number": 3, "description": "Visualize", "completed": false}
]
},
"pending_code": null,
"pending_error": null,
"pending_answer": null
}Approve the pending plan/code and continue execution.
POST /api/sessions/{session_id}/hitl/approveResponse:
{
"success": true,
"message": "Approved"
}Reject and abort the current task.
POST /api/sessions/{session_id}/hitl/rejectResponse:
{
"success": true,
"message": "Rejected - task aborted"
}Send detailed HITL response with optional modifications.
POST /api/sessions/{session_id}/hitl/respond
Content-Type: application/json
{
"action": "modify",
"message": "Please also add error handling",
"modified_plan": "1. [ ] Load data with error handling\n2. [ ] Analyze\n3. [ ] Visualize"
}Actions:
| Action | Description |
|---|---|
approve |
Approve and continue |
reject |
Reject and abort |
modify |
Provide modified plan or code |
retry |
Retry the failed operation |
skip |
Skip current step |
feedback |
Send textual feedback |
1. Create session with HITL enabled:
curl -X POST "http://localhost:8000/api/sessions" \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_KEY" \
-d '{"name": "HITL Session", "hitl_mode": "plan_only"}'2. Send a message (this will block waiting for approval):
# In terminal 1 — waits for approval
curl -X POST "http://localhost:8000/api/sessions/{session_id}/chat" \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_KEY" \
-d '{"message": "Analyze sales data and create visualizations"}'3. Check status (in another terminal):
curl "http://localhost:8000/api/sessions/{session_id}/hitl/status" \
-H "X-API-Key: YOUR_KEY"
# Returns: awaiting_feedback: true, awaiting_type: "plan"4. Approve the plan:
curl -X POST "http://localhost:8000/api/sessions/{session_id}/hitl/approve" \
-H "X-API-Key: YOUR_KEY"5. The chat request in terminal 1 now continues execution.
When using the streaming endpoint, a hitl_request event is emitted when approval is needed:
event: hitl_request
data: {
"request_type": "plan",
"plan": {
"steps": [...],
"raw_text": "..."
},
"code": null,
"error": null
}
The stream pauses until you call /hitl/approve or /hitl/reject.
For simple integrations that don't need real-time updates:
POST /api/sessions/{session_id}/chat
Content-Type: application/json
{
"message": "Analyze the iris dataset"
}Response:
{
"content": "I'll analyze the iris dataset...",
"code": "import pandas as pd\ndf = pd.read_csv('data/iris.csv')",
"execution_result": {
"stdout": " sepal.length sepal.width ...",
"stderr": "",
"error": null,
"images": [],
"success": true
},
"plan": {
"steps": [...],
"completed_steps": 1,
"total_steps": 3,
"is_complete": false
},
"has_answer": false,
"answer": null,
"is_complete": false
}POST /api/sessions/{session_id}/chat/stream
Content-Type: application/json
{
"message": "Analyze the iris dataset and create visualizations"
}Response: Server-Sent Events stream
The streaming endpoint emits granular events that allow UIs to show real-time progress.
┌─────────────────────────────────────────────────────────────┐
│ ROUND N │
├─────────────────────────────────────────────────────────────┤
│ │
│ thinking ──► llm_response ──► plan ──► code_executing │
│ │ │
│ ▼ │
│ code_result │
│ │ │
│ ▼ │
│ round_complete │
│ │
└─────────────────────────────────────────────────────────────┘
│
(repeat for each round)
│
▼
┌──────────────┐
│ done │
└──────────────┘
Emitted when the LLM starts processing. Use this to show a loading indicator.
event: thinking
data: {"message": "Processing..."}
UI Action: Show spinner or "Thinking..." indicator
Emitted when the LLM response is received (before code execution).
event: llm_response
data: {
"content": "I'll analyze the iris dataset. Let me start by loading the data...\n\n<plan>\n1. [ ] Load data\n2. [ ] Create visualizations\n</plan>\n\n<code>\nimport pandas as pd\ndf = pd.read_csv('data/iris.csv')\n</code>"
}
UI Action: Display the agent's response text (you may want to parse and hide <plan>, <code> tags)
Emitted when a plan is extracted from the response.
event: plan
data: {
"steps": [
{"number": 1, "description": "Load and explore data", "completed": false},
{"number": 2, "description": "Create visualizations", "completed": false},
{"number": 3, "description": "Save to artifacts/", "completed": false}
],
"raw_text": "1. [ ] Load and explore data\n2. [ ] Create visualizations\n3. [ ] Save to artifacts/",
"total_steps": 3,
"completed_steps": 0,
"is_complete": false
}
UI Action: Update plan panel with steps and progress bar
Emitted just before code is executed in the kernel.
event: code_executing
data: {
"code": "import pandas as pd\ndf = pd.read_csv('data/iris.csv')\ndf.head()"
}
UI Action: Show code block with "Executing..." badge
Emitted after code execution completes.
event: code_result
data: {
"stdout": " sepal.length sepal.width petal.length petal.width variety\n0 5.1 3.5 1.4 0.2 Setosa\n...",
"stderr": "",
"error": null,
"images": [
{"format": "png", "data": "iVBORw0KGgoAAAANSUhEUgAAA..."}
],
"success": true
}
UI Action:
- Show output in console panel
- Render images from base64
- Update code block badge to "Success" or "Error"
Emitted at the end of each autonomous execution round. Contains the full response for that round.
event: round_complete
data: {
"round": 1,
"content": "Full LLM response text...",
"code": "import pandas as pd\n...",
"execution_result": {
"stdout": "...",
"stderr": "",
"error": null,
"images": [],
"success": true
},
"plan": {
"steps": [...],
"completed_steps": 1,
"total_steps": 3,
"is_complete": false
},
"has_answer": false,
"answer": null,
"thinking": null,
"is_complete": false
}
UI Action: This is the "complete" event for the round - useful for logging or if you prefer to wait for complete data rather than granular events.
Emitted when the entire task is complete.
event: done
data: {}
UI Action:
- Hide loading indicators
- Enable input for new message
- Optionally show "Task Complete" notification
Emitted if an error occurs during processing.
event: error
data: {
"error": "Session not found"
}
UI Action: Show error notification/toast
async function streamChat(sessionId: string, message: string) {
const response = await fetch(`/api/sessions/${sessionId}/chat/stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key',
},
body: JSON.stringify({ message }),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('event: ')) {
const eventType = line.slice(7);
continue;
}
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
handleEvent(eventType, data);
}
}
}
}
function handleEvent(type: string, data: any) {
switch (type) {
case 'thinking':
showSpinner();
break;
case 'llm_response':
appendMessage(data.content);
break;
case 'plan':
updatePlanPanel(data);
break;
case 'code_executing':
showCodeBlock(data.code, 'executing');
break;
case 'code_result':
updateCodeResult(data);
renderImages(data.images);
break;
case 'done':
hideSpinner();
enableInput();
break;
case 'error':
showError(data.error);
break;
}
}// Note: EventSource only supports GET, so you'd need a different approach
// or use a library like eventsource-parser for POST requestsimport { useState, useCallback } from 'react';
interface StreamState {
isLoading: boolean;
plan: Plan | null;
code: string | null;
codeStatus: 'idle' | 'executing' | 'success' | 'error';
output: string;
images: Image[];
error: string | null;
}
function useChatStream(sessionId: string) {
const [state, setState] = useState<StreamState>({
isLoading: false,
plan: null,
code: null,
codeStatus: 'idle',
output: '',
images: [],
error: null,
});
const sendMessage = useCallback(async (message: string) => {
setState(s => ({ ...s, isLoading: true, error: null }));
try {
const response = await fetch(`/api/sessions/${sessionId}/chat/stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});
// ... parse SSE events and update state
} catch (err) {
setState(s => ({ ...s, error: err.message, isLoading: false }));
}
}, [sessionId]);
return { ...state, sendMessage };
}curl -X POST "http://localhost:8000/api/sessions/SESSION_ID/chat/stream" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"message": "Analyze iris.csv"}' \
--no-bufferGET /api/sessions/{session_id}/messages?limit=50&offset=0&role=assistantQuery Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 50 | Max messages to return (1-200) |
offset |
int | 0 | Skip N messages |
role |
string | null | Filter by role: user, assistant, execution, system |
Response:
{
"messages": [
{
"id": "msg_001",
"role": "user",
"content": "Analyze the iris dataset",
"timestamp": "2026-01-12T21:54:44.673Z",
"metadata": {}
},
{
"id": "msg_002",
"role": "assistant",
"content": "I'll analyze the iris dataset...",
"timestamp": "2026-01-12T21:54:47.900Z",
"metadata": {
"has_code": true,
"has_plan": true
}
}
],
"total": 10,
"has_more": false
}Returns conversation history as structured turns, matching the round_complete SSE event format.
Use this endpoint to load historical messages so the UI can render them identically to live streaming.
!!! info "Data Source"
This endpoint reads from {workspace}/logs/events.jsonl which contains the complete
event history including all LLM responses, code executions, and plan updates.
This ensures the UI receives the full conversation history, even after internal
summarization events.
GET /api/sessions/{session_id}/turns?limit=50&offset=0Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 50 | Max turns to return (1-200) |
offset |
int | 0 | Skip N turns |
Response:
{
"turns": [
{
"round": 1,
"timestamp": "2026-01-12T21:54:47.900Z",
"user_message": "Analyze the iris dataset and create visualizations",
"content": "I'll analyze the iris dataset...\n\n<plan>\n1. [ ] Load data\n</plan>\n\n<code>\nimport pandas as pd\n</code>",
"code": "import pandas as pd\ndf = pd.read_csv('data/iris.csv')",
"execution_result": {
"stdout": " sepal.length sepal.width...",
"stderr": "",
"error": null,
"images": [],
"success": true
},
"plan": {
"steps": [
{"number": 1, "description": "Load data", "completed": false}
],
"total_steps": 3,
"completed_steps": 0,
"is_complete": false
},
"has_answer": false,
"answer": null,
"thinking": null,
"is_complete": false
},
{
"round": 2,
"timestamp": "2026-01-12T21:54:52.100Z",
"user_message": null,
"content": "Now I'll create visualizations...",
"code": "import matplotlib.pyplot as plt\n...",
"execution_result": {...},
"plan": {...},
"has_answer": false,
"answer": null,
"thinking": null,
"is_complete": false
}
],
"total": 6,
"has_more": false
}Note: user_message is null for autonomous continuation rounds (when the agent continues without user input).
UI Usage Pattern:
// On session load, fetch historical turns
const history = await fetch(`/api/sessions/${sessionId}/turns`);
const { turns } = await history.json();
// Render each turn using the same component as round_complete events
turns.forEach(turn => renderRoundComplete(turn));
// For live streaming, connect to SSE
const eventSource = connectToStream(sessionId);
eventSource.on('round_complete', renderRoundComplete);Files are organized by category: data, artifacts, or notebooks. Session ID must match pattern [a-zA-Z0-9_-]+.
GET /api/sessions/{session_id}/files?category=dataQuery: category — data, artifacts, or notebooks (default: data).
POST /api/sessions/{session_id}/files
Content-Type: multipart/form-data
files: (one or more files)
category: data (optional; default: data)Upload size per file is limited by DSAGENT_MAX_UPLOAD_MB (default 50 MB; 0 = no limit).
GET /api/sessions/{session_id}/files/{filename}?category=dataDELETE /api/sessions/{session_id}/files/{filename}?category=dataGET /api/sessions/{session_id}/artifacts?type=imageQuery: type — optional filter: image, document, data, model, code, notebook.
GET /api/sessions/{session_id}/artifacts/{filename}Returns the file content (image, CSV, etc.).
DELETE /api/sessions/{session_id}/artifacts/{filename}interface ChatResponseModel {
content: string; // Full LLM response text
code: string | null; // Extracted code (if any)
execution_result: ExecutionResult | null;
plan: PlanResponse | null;
has_answer: boolean; // True if contains <answer> tag
answer: string | null; // Extracted answer text
thinking: string | null; // Model's thinking (if available)
is_complete: boolean; // True when task is done
}interface ExecutionResult {
stdout: string; // Standard output
stderr: string; // Standard error
error: string | null; // Error message if failed
images: Image[]; // Generated images (base64)
success: boolean; // True if code ran successfully
}
interface Image {
format: string; // "png", "jpeg", etc.
data: string; // Base64 encoded image data
}interface PlanResponse {
steps: PlanStep[];
raw_text: string; // Original plan text
total_steps: number;
completed_steps: number;
is_complete: boolean; // True when all steps done
}
interface PlanStep {
number: number;
description: string;
completed: boolean;
}| SSE Event | Suggested UI Component |
|---|---|
thinking |
Loading spinner / "Thinking..." text |
llm_response |
Chat message bubble |
plan |
Collapsible plan panel with checkboxes |
code_executing |
Code block with syntax highlighting + "Running" badge |
code_result |
Output console + image gallery |
round_complete |
(Optional) Round divider |
done |
Enable input, hide spinner |
error |
Toast notification / error banner |
# Start the API server (host/port via flags; config from env)
dsagent serve --port 8000
# With API key (from env)
export DSAGENT_API_KEY=my-secret-key
dsagent serve
# Production: require API key
export DSAGENT_REQUIRE_API_KEY=true
export DSAGENT_API_KEY=my-secret-key
dsagent serve --port 8000OpenAPI docs: http://localhost:8000/docs. Health: http://localhost:8000/health.