Files
kandev/docs/public/websocket-api.md
T
2026-07-16 08:32:40 +01:00

15 KiB

title, description
title description
WebSocket API Reference for Kandev WebSocket messages and API behavior.

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:

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

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "request",
  "action": "workflow.list",
  "payload": {
    "workspace_id": "workspace-uuid"
  },
  "timestamp": "2026-07-15T09:00:00Z"
}

Response

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "response",
  "action": "workflow.list",
  "payload": {
    "workflows": [],
    "total": 0
  },
  "timestamp": "2026-07-15T09:00:00Z"
}

Notification

{
  "type": "notification",
  "action": "task.updated",
  "payload": {
    "id": "task-uuid",
    "title": "Update authentication"
  },
  "timestamp": "2026-07-15T09:00:01Z"
}

Error

{
  "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

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

{
  "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.

{
  "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

{
  "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.

{
  "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"
}
{
  "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

{
  "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

{
  "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

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.