Skip to content
kazma.
Search Kazma Documentation & Site
ع

API & Extension Points

The HTTP/SSE surface of the Kazma Web UI, the SSE event contract, and the concrete places to extend the framework (tools, providers, adapters, skills, MCP).


All endpoints are mounted by KazmaAppBuilder in kazma-ui/kazma_ui/app.py:615-709. Routers:

RouterPrefix/areaSource
health_router/health/*health.py
chat_routerpage routes (/chat, …)chat.py
settings_router/settingssettings.py
skills_routerskillsskills routes
mcp_routerMCPmcp routes
agents_routeragentsagents routes
providers_router/api/providersproviders routes
sse_router/api/chat/*sse_chat/
telemetry_routertelemetrytelemetry routes
dashboard_router/api/dashboard/*dashboard.py
models_routermodelsmodels routes
workspace_routerworkspaceworkspace routes
swarm_router/api/swarm/*swarm_panel/
monitor_routermonitormonitor routes
metrics_routermetricsmetrics routes

Plus direct routes in routes_direct/ and a conditional Telegram webhook at /api/webhooks/telegram (app.py).


MethodPathPurpose
POST/api/chat/streamPrimary chat transport. Body \{message, session_id, model\}. Returns text/event-stream. (sse_chat/__init__.py)
GET/api/chat/sessionsList sessions. (line 547)
POST/api/chat/sessionsBind an empty session shell to the authenticated caller before opening its telemetry socket. Requires CSRF protection.
DELETE/api/chat/sessions/\{session_id\}Delete session. (line 555)
GET/api/chat/sessions/\{session_id\}/messagesSession history. (line 561)

Graph transport: POST /api/chat/stream. /ws/chat/{session_id} is the telemetry / cursor bus: it runs no turn and takes no turn control (send, approve, stop, steer and abort are HTTP/SSE; the socket refuses them, naming the route).

MethodPathPurpose
GET/api/provider/activeActive provider/model.
POST/api/provider/switchSwitch active provider/model.
GET/api/providersList providers, with masked keys, discovered models and declared capabilities.
POST/api/providersAdd or update a provider. A masked **** key means “keep the stored one”.
DELETE/api/providers/{name}Remove a provider.
POST/api/providers/{name}/toggleEnable or disable.
POST/api/providers/{name}/testHealth check — see below.
POST/api/providers/{name}/discoverFetch the provider’s model list.

/api/providers/* is the only provider surface. A parallel /api/settings/providers/* existed with the same six operations and was deleted: two routes for one concept is how they drift, and this pair drifted far enough that a fix shipped into the one nothing called.

What …/test returns. Reachable and working are different claims, so the response carries both:

fieldmeaning
reachablethe model list answered. Not a claim that the provider works.
chat_oka real completion came back on POST /chat/completions.
chat_ms, chat_modelthat completion’s latency, and the model the provider says it served.
successboth of the above.

The check resolves its key through ModelRegistry.resolve_provider_credentials and its model through ModelRegistry.probe_model_for — the same paths a real message uses. It resolves nothing by hand, and it does not write.

Probe failures report the HTTP status and an actionable hint. Remote response bodies and credential-bearing request URLs are excluded from diagnostics.

MethodPathPurpose
GET/api/pending-approvalsPending HITL approvals. (hitl_approval.py:146)
POST/api/approve/\{thread_id\}Approve/deny a paused tool. Body `{action: “approve”
MethodPathPurpose
GET/api/dashboard/statusDashboard overview. (dashboard.py:177)
GET/api/sessionsSessions list — admin only (it lists every user’s threads). (line 221)
POST/api/sessions/clear-allClear sessions — admin only. (line 330)
MethodPathPurpose
GET/api/swarm/statusSwarm status.
GET/POST/DELETE/api/swarm/workers[/\{name\}]Worker CRUD.
POST/api/swarm/dispatchDispatch a task (all patterns via type).
GET/api/swarm/tasks[/\{id\}]Task list / detail.
POST/api/swarm/tasks/\{id\}/approveApprove a pipeline checkpoint: 202 once the resumed run is recorded; progress on the task stream. Admin.
POST/api/swarm/tasks/\{id\}/rejectReject a pipeline checkpoint. Admin.
GET/api/swarm/workers/\{name\}/metricsWorker metrics.
GET/api/swarm/circuit-breakersBreaker states.
MethodPathPurpose
GET/health/liveLiveness. (health.py:94)
GET/health/readyReadiness. (line 104)
GET/health/detailsDetailed health. (line 148)
GET/api/gateway/statusGateway/adapter status.

2.7 Memory compatibility and operator interfaces

Section titled “2.7 Memory compatibility and operator interfaces”
MethodPathCurrent contract
GET/api/memory/graphHTTP 410; use /api/memory/v2/graph.
GET/api/memory/graph/statsHTTP 410; use /api/memory/v2/health.
GET/api/memory/graph/exportHTTP 410; use the V2 graph surface.
GET/api/memory/graph/searchCompatibility search backed by V2 beliefs.
POST/api/memory/graph/clearRequires confirmation and tenant authorization; invalidates current beliefs while preserving history and episodes.

Operator diagnostics and public integration endpoints can intentionally have no browser caller. Their maintained inventory and caller reasons are in tests/test_api_route_callers.py; absence from the frontend is not a deletion criterion. Restore remains available through python -m kazma_core.backup.restore; see Disaster recovery.


POST /api/chat/stream answers with Server-Sent Events: an event: line naming the frame and one data: line holding a JSON object. Source: kazma-ui/kazma_ui/sse_chat/ (the route and the turn streamer _streaming.py) and the turn journal kazma-ui/kazma_ui/delivery.py; the browser reads it with dispatch in static/js/streaming.js.

The turn’s frames are journaled first. Each one is written to the thread’s journal before it is sent, so it also carries an id: line, and its data carries seq (the same number) and turn_id. A client that loses the stream sends the request again with last_event_id (or last_seq) in the body instead of a message: it gets a resumed frame, then every frame it missed, then the live turn. A frame replayed as history carries replay: true; a pending approval in it is shown only after the gate registry confirms it is still open. error, capacity, user_message and thread_rewound frames, and any frame whose data says capacity: true, are never replayed. During a silence the stream sends : keepalive comment lines.

event:Whendata fields
resumedThe first frame of every stream that follows a turn: a new prompt, a reconnect, and the wait after an approval. Not journaled.from, to (the journal’s newest seq), count (frames replayed), gap, running, session_id, thread_id
tokenA piece of the answer, appended in order: a streamed chunk, a paragraph break between two model calls, the final text read from the checkpoint when nothing streamed, or a notice (the turn paused without an approval card, ended with no text, or failed before replying).content
tool_callA tool starts.tool_name, tool_call_id (the same on its tool_result), inputs (JSON text, at most 2,000 characters)
tool_resultThe tool finished.tool_name, tool_call_id, result (at most 5,000 characters)
memory_explainThe supervisor was given memories for this question; the turn stores the same record.query, empty, detail, beliefs, episodes, weekly_summaries, knowledge (each item: id, kind, content, score, sources), summary (the counts)
turn_heartbeatAbout every 10 seconds with no other frame (every 8 while a turn resumes after an approval).phase (llm, supervisor, tool or resuming), current (the tool), detail, step, elapsed_s
status_updateThe graph reached its end and the answer is being written (status: "synthesizing", active_node: "Respond"). Or the client’s cursor is not in the journal (status: "resync", seq): it is older than the journal keeps, or ahead of it after a server restart or an hour idle, when the journal starts again at 1. The stream ends there. The client reloads the chat from the store and takes seq as its new cursor (a resumed frame with gap means the same).status, then active_node or seq
statusA second resume reached a turn that is already running; this stream follows that turn.content, status (thinking)
context_compactedThis turn trimmed the model’s context or collapsed old tool output.detail, with counts such as stubbed_segments, dropped_user, dropped_assistant
approval_requiredThe graph paused for an approval. No done follows: the stream stays open, and the turn continues in it after POST /api/approve/{thread_id}.thread_id, interrupt_id, kind, tool, args, tools, items, message, yolo_allowed, always_ask (an always-ask tool such as an X write: approve once or deny only, no session grant), approval_deadline, and the gate registry’s view and gate_views
hitlAn approval was decided, in this tab, another tab or a chat app.state (approved, denied or timeout), interrupt_id, tool, thread_id, turn_id, actor, view, gate_views
approval_timeoutNobody answered in time: the tool is not run, the model is told the approval expired (not that it was refused), and the turn ends with its answer.thread_id, interrupt_id, tool, turn_id, message
capacity/long, /plan or /yolo, answered without the model; a done with capacity: true follows.action, reply, and long_active and yolo_active, or plan_active
errorThe request or the turn failed. Before a turn starts (a body that is not JSON, a message over 512,000 characters, an empty message, no API key for a cloud provider, the session budget used up) it is the only frame.content (internals removed)
doneThe turn ended. content is the answer and replaces what the token frames built.content, tokens, cost, duration_ms, interrupted, empty, model, turn_id, session_tokens, session_cost, gate_views
turn_completeRight after done, with the same data.as done
snapshotAfter done, when Time Travel saved this step.snapshot_id, iteration, model
user_messageAnother tab started a turn on this thread. Only on streams already attached to the thread, never on the sender’s own and never replayed; the chat page takes it from its WebSocket, which carries the same journal.content, turn_id, client_msg_id, session_id, ts
thread_rewoundThe thread was restored to a Time Travel step (on any surface). Every open tab reloads the chat from the store; never replayed.thread_id, session_id, snapshot_id, turn_index, message_count

Replies without the model. /reset, /compact and the bare /swarm usage answer with a token and a done carrying content, tokens, cost and duration_ms; they are not journaled. The /research usage, /replay and /fork send the same two frames with capacity: true, journaled so the thread’s other tabs see them. An attached stream ends after done or turn_complete (unless interrupted), or after error.

HITL approval expiry: if the user clicks Approve/Deny on a card that has already timed out or been resumed, POST /api/approve/{thread_id} returns HTTP 409 with {"status": "expired", "error": "No pending approval for this thread (already resumed or expired)."}. The frontend (hitl_approval.js) detects this and transitions the card to “Expired or already resumed” then removes it. A decision that names a gate (interrupt_id) already decided, or a gate of another thread, is also a 409: reason: "not_pending", with hitl_state (inflight, settled or foreign), registry_state and the gate’s interrupt_id.

// chat.js uses KS.sse('/api/chat/stream', {...}); the raw shape is:
const resp = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Hello', session_id: sess, model: 'gpt-4o-mini' }),
});
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// SSE events are separated by blank lines
let idx;
while ((idx = buffer.indexOf('\n\n')) !== -1) {
const block = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
const eventType = (block.match(/^event: (.+)$/m) || [])[1];
const data = JSON.parse(((block.match(/^data: (.+)$/m) || [])[1]) || '{}');
handleEvent(eventType, data);
}
}
function handleEvent(type, data) {
switch (type) {
case 'token': appendToken(data.content); break;
case 'tool_call': showToolCall(data.tool_name, data.inputs); break;
case 'tool_result': showToolResult(data.tool_name, data.result); break;
case 'approval_required': promptApproval(data.thread_id, data.interrupt_id, data.tool); break;
case 'done': finishTurn(data.content, data.tokens, data.cost); break;
case 'error': showError(data.content); break;
}
}
import httpx
resp = httpx.post(
"http://127.0.0.1:8000/api/approve/<thread_id>",
headers={"X-Kazma-Secret": KAZMA_SECRET}, # required if KAZMA_SECRET is set
json={"action": "approve", "reason": "looks safe"},
)
print(resp.status_code, resp.json())

Register a function with the ToolRegistry:

from kazma_core.agent.tool_registry import register_tool
@register_tool(
name="weather_lookup",
description="Look up current weather for a city.",
danger=False, # True → triggers HITL
)
async def weather_lookup(city: str) -> str:
...
return f"Weather in {city}: sunny, 25C"

Register during startup (or via a skill entry point). The supervisor exposes it to the LLM automatically. Schemas are generated from type hints with additionalProperties: false. Optional parameters stay optional unless KAZMA_STRICT_TOOLS=1. For a one-off JSON reply (not a tool call), pass response_format= to LLMProvider.chat — do not put it on every supervisor turn.

4.1b Tool hooks (PreToolUse / PostToolUse)

Section titled “4.1b Tool hooks (PreToolUse / PostToolUse)”
from kazma_core.agent.tool_hooks import register_pre_tool_hook, ToolHookDecision
def block_shell(event):
cmd = str((event.tool_input or {}).get("command") or "")
if "rm -rf" in cmd:
return ToolHookDecision(decision="deny", reason="blocked pattern")
return None
register_pre_tool_hook(block_shell, matcher="shell_exec")

Or YAML (agent.hooks.pre_tool / post_tool): each entry is {matcher, command, timeout_seconds?}. The command receives JSON on stdin (hook_event_name, tool_name, tool_input, and for post tool_response) and may print JSON (decision, tool_input / updatedInput, reason, extra) or exit 2 to deny. Hooks cannot auto-approve HITL. Kill-switch: KAZMA_TOOL_HOOKS=0.

Providers are ConfigStore entries under providers.list. The 10 built-in presets are in kazma_core/providers.py:13-84. To add a custom OpenAI-compatible endpoint:

from kazma_core.config_store import get_config_store
from kazma_core.model_registry import get_model_registry
store = get_config_store()
reg = get_model_registry()
# Option A: use the 'custom' preset shape
reg.upsert_provider(
name="my-endpoint",
display_name="My Inference Server",
base_url="https://infer.example.com/v1",
api_key="sk-...",
enabled=True,
)
# Option B: switch active provider/model
reg.set_active_provider("my-endpoint")
reg.set_active_model("my-model-id")

Any OpenAI-compatible endpoint works (vLLM, Together, Groq, Fireworks, …). For non-OpenAI auth schemes, note that LLMProvider.chat() always sends Authorization: Bearer — route through an OpenAI-compatible proxy if the upstream needs a different header.

Subclass BaseAdapter (kazma-gateway/kazma_gateway/gateway.py:239), implement receive/send, produce IncomingMessage, and register it. For swarm HITL on the new platform, also subclass BusAdapter (kazma_core/swarm/bus.py:66) and wire it in app.py’s bus-singleton block.

See Skills, MCP & Tools → Adding a custom skill. Sign it with kazma hub sign.

See Skills, MCP & Tools → Configuring an MCP server. Tools are discovered at runtime and classified by classify_mcp_tool.

Terminal window
kazma swarm worker add researcher --model deepseek-chat --provider deepseek --type in_process --role researcher

Or via the API:

import httpx
httpx.post("http://127.0.0.1:8000/api/swarm/workers", json={
"name": "researcher",
"model": "deepseek-chat",
"provider": "deepseek",
"worker_type": "in_process",
"roles": ["researcher"],
})

The V2 Cognitive Engine is the chat default (per-turn recall, tools, auto-store, compaction) and is also used by self-improvement / phonebook. (The V1 UnifiedMemoryAdapter was removed in the V1→V2 cutover.) Custom code:

from kazma_core.memory.recall import recall
from kazma_core.paths import primary_memory_db
import sqlite3
conn = sqlite3.connect(primary_memory_db(), check_same_thread=False)
conn.row_factory = sqlite3.Row
result = recall("what does the user prefer?", conn=conn, limit=5)
# result.beliefs -> list[RecallHit] of currently-valid beliefs
# result.episodes -> list[RecallHit], ranked by evidence (meaning above the question's
# background + content words covered); empty when nothing clears the threshold

Writing a belief (functional predicates supersede; set predicates append):

from kazma_core.memory.belief_mutation import mutate_belief
from kazma_core.paths import primary_memory_db, ops_memory_db
primary = sqlite3.connect(primary_memory_db(), check_same_thread=False)
ops = sqlite3.connect(ops_memory_db(), check_same_thread=False)
mutate_belief(
primary, "user", "prefers", "dark mode",
ops_conn=ops, predicate_type="set",
extraction_method="custom", source_session="my-integration",
)

See Memory & RAG.


  • /api/telemetry/* (telemetry_router) — runtime telemetry.
  • /api/dashboard/status — overview for the dashboard.
  • Swarm metrics at /api/swarm/workers/\{name\}/metrics.

Prometheus /metrics does not exist. OTel packages are declared but Kazma’s tracing is an in-house span emitter. See Architecture → Observability.


  • Graph transport is SSE (POST /api/chat/stream). /ws/chat/{session_id} is the telemetry / cursor bus: it runs no turn and takes no turn control (send, approve, stop, steer and abort are HTTP/SSE; the socket refuses them, naming the route).
  • The SSE approval_required event is the canonical way for frontends to surface HITL pauses; pair it with POST /api/approve/\{thread_id\}.
  • /api/approve ownership enforcement (403 on cross-user) means approval tokens are per-user — an admin can’t approve another user’s task without matching identity fields.
  • V2 is the single memory stack — per-turn recall, tools, auto-store, and compaction all use recall() from memory/recall.py. The V1 4-layer adapter (get_adapter()) was removed in the V1→V2 cutover.