docs: add curated public documentation (#1701)

* docs: add curated public documentation

* docs: correct public API and deployment guidance

* docs: fix workflow trigger anchor

* docs: harden deployment and websocket examples
This commit is contained in:
José Almeida
2026-07-15 12:06:06 +01:00
committed by GitHub
parent dd1b811930
commit 55960e7555
19 changed files with 2952 additions and 2 deletions
+467
View File
@@ -0,0 +1,467 @@
# Kandev WebSocket API
This page documents the WebSocket envelope, current domain model, and the request actions used by the main task and agent workflow. It is not a substitute for the Go request types: payloads evolve with the application.
## Scope and source of truth
Kandev uses one persistent WebSocket connection for browser request/response traffic and real-time notifications. This applies to WebSocket-backed operations only; Kandev also has dedicated HTTP APIs. For example, workflow import and export use HTTP routes because they transfer portable documents.
Use these sources when checking an action:
- [`apps/backend/pkg/websocket/actions.go`](../../apps/backend/pkg/websocket/actions.go) defines action names and error codes.
- Registered handlers such as [`task_handlers.go`](../../apps/backend/internal/task/handlers/task_handlers.go) and [`handlers.go`](../../apps/backend/internal/orchestrator/handlers/handlers.go) define which action constants accept client requests and their payload contracts.
- [`apps/backend/pkg/websocket/message.go`](../../apps/backend/pkg/websocket/message.go) defines the message envelope.
An action constant by itself does not guarantee a request handler. Some constants are server-to-client notifications only.
**Local endpoint:** `ws://localhost:38429/ws`
Use `wss://` when Kandev is served over HTTPS.
## Message envelope
### Request
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "request",
"action": "workflow.list",
"payload": {
"workspace_id": "workspace-uuid"
},
"timestamp": "2026-07-15T09:00:00Z"
}
```
### Response
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "response",
"action": "workflow.list",
"payload": {
"workflows": [],
"total": 0
},
"timestamp": "2026-07-15T09:00:00Z"
}
```
### Notification
```json
{
"type": "notification",
"action": "task.updated",
"payload": {
"id": "task-uuid",
"title": "Update authentication"
},
"timestamp": "2026-07-15T09:00:01Z"
}
```
### Error
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "error",
"action": "task.get",
"payload": {
"code": "NOT_FOUND",
"message": "Task not found"
},
"timestamp": "2026-07-15T09:00:01Z"
}
```
| Field | Type | Contract |
|-------|------|----------|
| `id` | string | Correlates a request with its response or error. Notifications omit it. |
| `type` | string | `request`, `response`, `notification`, or `error`. |
| `action` | string | Registered action name. |
| `payload` | object | Action-specific request or response data. |
| `timestamp` | string | ISO 8601 timestamp. Server responses, notifications, and errors always include it. Client requests may omit it; the backend does not use it for request correlation. |
| `metadata` | object | Optional string-to-string metadata used by selected internal flows. |
Error codes currently include `BAD_REQUEST`, `NOT_FOUND`, `INTERNAL_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `VALIDATION_ERROR`, `CONFLICT`, and `UNKNOWN_ACTION`.
## Current domain model
```mermaid
flowchart LR
Workspace --> Workflow
Workflow --> WorkflowStep["Workflow step"]
WorkflowStep --> Task
Task --> Session
Session --> Message
Session --> Agent
```
The common request flow is:
1. List or create a workspace.
2. List or create a workflow in that workspace.
3. List the workflow steps.
4. Create a task with `workspace_id`, `workflow_id`, and `workflow_step_id`.
5. Subscribe to the task and session for live updates.
6. Start work with `session.launch`.
7. Send user turns with `message.add`, or use `message.queue.*` while a turn is active.
8. Stop a session with `session.stop`, or update the task with `task.state`.
## Registered request actions
The tables below cover the core task workflow. Check `actions.go` and handler registration for integrations and less common operational actions.
### Workspaces and workflows
| Action | Purpose |
|--------|---------|
| `workspace.list` | List workspaces. |
| `workspace.create` | Create a workspace. |
| `workspace.get` | Get a workspace by ID. |
| `workspace.update` | Update a workspace. |
| `workspace.delete` | Delete a workspace. |
| `workflow.list` | List workflows, optionally scoped by `workspace_id`. |
| `workflow.create` | Create a workflow in a workspace. |
| `workflow.get` | Get a workflow by ID. |
| `workflow.update` | Update a workflow. |
| `workflow.delete` | Delete a workflow. |
| `workflow.reorder` | Reorder workflows in a workspace. |
| `workflow.template.list` | List workflow templates. |
| `workflow.template.get` | Get a workflow template. |
| `workflow.step.list` | List steps for a workflow. |
| `workflow.step.get` | Get a workflow step. |
| `workflow.step.create` | Create a workflow's steps from a template. |
| `workflow.history.list` | List workflow history for a session. |
Direct create, update, delete, and reorder operations for individual workflow steps are exposed through the HTTP workflow API and configuration-mode MCP tools. They are not registered as ordinary WebSocket requests.
### Tasks
| Action | Purpose |
|--------|---------|
| `task.list` | List tasks for a workflow. |
| `task.create` | Create a task. |
| `task.get` | Get a task. |
| `task.update` | Update task fields. |
| `task.repository.update` | Update a task repository's base branch. |
| `task.delete` | Delete a task. |
| `task.move` | Move a task to a workflow step. |
| `task.state` | Set the task state. |
| `task.archive` | Archive or unarchive a task. |
| `task.session.list` | List sessions for a task. |
| `task.session.status` | Get task session status. |
| `task.plan.create` | Create a task plan. |
| `task.plan.get` | Get a task plan. |
| `task.plan.update` | Update a task plan. |
| `task.plan.delete` | Delete a task plan. |
| `task.plan.revisions.list` | List plan revisions. |
| `task.plan.revision.get` | Get a plan revision. |
| `task.plan.revert` | Revert a plan revision. |
### Sessions, messages, and agents
| Action | Purpose |
|--------|---------|
| `session.launch` | Unified prepare, start, resume, workflow-step, or workspace-restore operation. |
| `session.ensure` | Return an existing task session or create one. |
| `session.recover` | Resume, fresh-start, or cancel a transient retry. |
| `session.reset_context` | Reset an agent session's context. |
| `session.stop` | Stop a session. |
| `session.delete` | Delete a session. |
| `session.set_primary` | Set the primary session for a task. |
| `session.set_plan_mode` | Enable or disable plan mode. |
| `message.add` | Persist a user message and forward it to the session when appropriate. |
| `message.list` | List session messages. |
| `message.search` | Search messages. |
| `message.queue.add` | Queue a message for a busy session. |
| `message.queue.get` | Read queued messages and status. |
| `message.queue.update` | Replace queued message content. |
| `message.queue.append` | Append content to a queued message. |
| `message.queue.drain` | Dispatch one queued entry when the session can accept it. |
| `message.queue.remove` | Remove one queued entry. |
| `message.queue.cancel` | Clear the session queue. |
| `agent.list` | List agent executions. |
| `agent.launch` | Low-level agent launch. Prefer `session.launch` for task sessions. |
| `agent.status` | Get agent status. |
| `agent.logs` | Get agent logs. |
| `agent.stop` | Stop an agent execution. |
| `agent.cancel` | Cancel the active agent turn. |
| `agent.types` | List available agent types. |
Agent and task-session lifecycle states are uppercase on the wire. For example,
agent states include `PENDING`, `STARTING`, `RUNNING`, `READY`, `COMPLETED`,
`FAILED`, and `STOPPED`.
### Orchestrator and subscriptions
Only these orchestrator request actions are registered:
| Action | Purpose |
|--------|---------|
| `orchestrator.status` | Get orchestrator status. |
| `orchestrator.queue` | Get queued tasks. |
| `orchestrator.stop` | Stop execution for a task. |
Lifecycle starts and user turns use `session.launch` and `message.add`; task completion uses `task.state` or workflow automation.
Subscription requests are handled by the WebSocket gateway:
| Action | Payload key |
|--------|-------------|
| `task.subscribe` / `task.unsubscribe` | `task_id` |
| `session.subscribe` / `session.unsubscribe` | `session_id` |
| `session.focus` / `session.unfocus` | `session_id` |
| `user.subscribe` / `user.unsubscribe` | user-scoped connection state |
| `run.subscribe` / `run.unsubscribe` | `run_id` |
| `system.metrics.subscribe` / `system.metrics.unsubscribe` | no resource ID |
## Core payload examples
### List workflow steps
```json
{
"id": "request-steps",
"type": "request",
"action": "workflow.step.list",
"payload": {
"workflow_id": "workflow-uuid"
},
"timestamp": "2026-07-15T09:01:00Z"
}
```
### Create a task
`workspace_id`, `workflow_id`, and `title` are required. `workflow_step_id` selects the initial step and should be supplied by normal clients.
```json
{
"id": "request-create-task",
"type": "request",
"action": "task.create",
"payload": {
"workspace_id": "workspace-uuid",
"workflow_id": "workflow-uuid",
"workflow_step_id": "step-uuid",
"title": "Implement feature X",
"description": "Detailed requirements",
"priority": "high",
"repositories": [
{
"repository_id": "repository-uuid",
"base_branch": "main"
}
]
},
"timestamp": "2026-07-15T09:02:00Z"
}
```
To create and immediately start a task, also send `start_agent: true` and a valid `agent_profile_id`. The response then includes `session_id` and `agent_execution_id` when launch succeeds.
### Move a task
```json
{
"id": "request-move-task",
"type": "request",
"action": "task.move",
"payload": {
"id": "task-uuid",
"workflow_id": "workflow-uuid",
"workflow_step_id": "target-step-uuid",
"position": 0
},
"timestamp": "2026-07-15T09:03:00Z"
}
```
### Launch a session
`session.launch` accepts these intents: `prepare`, `start`, `start_created`, `resume`, `workflow_step`, and `restore_workspace`. If `intent` is omitted, the backend infers it from the other fields.
```json
{
"id": "request-launch-session",
"type": "request",
"action": "session.launch",
"payload": {
"task_id": "task-uuid",
"intent": "start",
"agent_profile_id": "profile-uuid",
"prompt": "Implement the task and run focused tests"
},
"timestamp": "2026-07-15T09:04:00Z"
}
```
```json
{
"id": "request-launch-session",
"type": "response",
"action": "session.launch",
"payload": {
"success": true,
"task_id": "task-uuid",
"session_id": "session-uuid",
"agent_execution_id": "execution-uuid",
"state": "RUNNING"
},
"timestamp": "2026-07-15T09:04:01Z"
}
```
### Send a user turn
```json
{
"id": "request-add-message",
"type": "request",
"action": "message.add",
"payload": {
"task_id": "task-uuid",
"session_id": "session-uuid",
"content": "Also add regression coverage"
},
"timestamp": "2026-07-15T09:05:00Z"
}
```
### Mark a task complete
```json
{
"id": "request-complete-task",
"type": "request",
"action": "task.state",
"payload": {
"id": "task-uuid",
"state": "COMPLETED"
},
"timestamp": "2026-07-15T09:06:00Z"
}
```
## Notifications
Common server notifications include:
- Task lifecycle: `task.created`, `task.updated`, `task.deleted`, `task.state_changed`.
- Workflow lifecycle: `workflow.created`, `workflow.updated`, `workflow.deleted`, `workflow.step.created`, `workflow.step.updated`, `workflow.step.deleted`.
- Session messages and state: `session.message.added`, `session.message.updated`, `session.message.deleted`, `session.state_changed`, `session.waiting_for_input`, `session.turn.started`, `session.turn.completed`.
- Agent stream compatibility events: `acp.progress`, `acp.log`, `acp.result`, `acp.error`, `acp.status`, `acp.heartbeat`.
- Permission flow: `permission.requested`; clients answer with `permission.respond`. Its payload requires `session_id` and `pending_id`, plus `option_id` unless `cancelled` is `true`. The optional `rejected` flag distinguishes an explicit denial from dismissing the request.
- Repository and executor lifecycle events matching the `*.created`, `*.updated`, and `*.deleted` action constants.
Subscribe to the relevant task and session before depending on resource-scoped notifications.
## Browser request helper
```typescript
type PendingRequest = {
resolve: (message: unknown) => void;
reject: (error: Error) => void;
};
const pending = new Map<string, PendingRequest>();
const socket = new WebSocket('ws://localhost:38429/ws');
socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
const request = message.id ? pending.get(message.id) : undefined;
if (request) {
pending.delete(message.id);
request.resolve(message);
}
});
function rejectPending(reason: string) {
const error = new Error(reason);
for (const request of pending.values()) request.reject(error);
pending.clear();
}
socket.addEventListener('close', () => rejectPending('WebSocket closed'));
socket.addEventListener('error', () => rejectPending('WebSocket failed'));
async function waitForSocketOpen() {
if (socket.readyState === WebSocket.OPEN) return;
if (socket.readyState !== WebSocket.CONNECTING) {
throw new Error('WebSocket is not open');
}
await new Promise<void>((resolve, reject) => {
const cleanup = () => {
socket.removeEventListener('open', handleOpen);
socket.removeEventListener('error', handleError);
socket.removeEventListener('close', handleClose);
};
const handleOpen = () => {
cleanup();
resolve();
};
const handleError = () => {
cleanup();
reject(new Error('WebSocket failed to open'));
};
const handleClose = () => {
cleanup();
reject(new Error('WebSocket closed before opening'));
};
socket.addEventListener('open', handleOpen);
socket.addEventListener('error', handleError);
socket.addEventListener('close', handleClose);
});
}
async function request(action: string, payload: Record<string, unknown>) {
await waitForSocketOpen();
const id = crypto.randomUUID();
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
try {
socket.send(JSON.stringify({
id,
type: 'request',
action,
payload,
timestamp: new Date().toISOString(),
}));
} catch (error) {
pending.delete(id);
reject(error instanceof Error ? error : new Error('WebSocket send failed'));
}
});
}
await request('task.subscribe', { task_id: 'task-uuid' });
const launch = await request('session.launch', {
task_id: 'task-uuid',
intent: 'start',
agent_profile_id: 'profile-uuid',
prompt: 'Implement the task',
});
```
Production clients should also enforce request timeouts and route `type: "error"` responses into typed errors.
## HTTP operations that remain separate
Workflow document transfer is intentionally HTTP-based:
| Method | Route | Purpose |
|--------|-------|---------|
| `GET` | `/api/v1/workflows/:id/export` | Export one workflow. |
| `GET` | `/api/v1/workspaces/:id/workflows/export` | Export workflows from a workspace. |
| `POST` | `/api/v1/workspaces/:id/workflows/import` | Import a portable workflow document. |
Other focused HTTP routes also exist for snapshots, file transfer, health checks, and agentctl-local control APIs. Do not infer that a WebSocket action replaces every HTTP endpoint.