Lumin exposes three interfaces: HTTP API, WebSocket, and IPC (stdin/stdout JSON).
Start the server with lumin serve --port 3001 or programmatically via startServer().
Health check endpoint.
Response:
{
"status": "ok",
"version": "0.3.1",
"uptime": 12345
}List all registered tools.
Response:
{
"tools": [
{
"name": "bash",
"description": "Execute a bash command in the container.",
"parameters": { ... }
}
],
"count": 42
}Send a message and receive the complete response (synchronous).
Request:
{
"content": "Write a LaTeX survey on attention mechanisms",
"sessionId": "optional-session-id",
"config": {
"model": "gpt-4o",
"agentId": "researcher",
"maxIterations": 20,
"tools": ["latex", "arxiv"]
}
}Routing behavior (dual-loop mode, Phase A):
Before a new task is created, the server checks whether the session already has
an active task (via getActiveForSession(sessionId)):
- If no active task exists → a new task is created and dispatched in the
background. The response contains
taskIdfor the newly-created task. - If an active task exists → the message is enqueued to the task's process-
global message queue (delivered at the next inner-loop iteration boundary),
and the response returns immediately with
queued: trueand the existing task'staskId. No new task is created.
A task.message.enqueued WebSocket event is emitted for each enqueued message.
Response:
{
"status": "success",
"response": "I'll help you write...",
"thinking": "Let me plan the survey structure...",
"directives": [
{ "type": "SWITCH_COMPONENT", "payload": { "component": "latex-editor" } }
],
"toolsUsed": ["latex_project", "arxiv_search"],
"usage": { "promptTokens": 1500, "completionTokens": 800, "totalTokens": 2300 },
"sessionId": "session-1234567890",
"iterations": 3,
"taskId": "task-abc123",
"queued": false,
"loopMode": "dual"
}| Field | Type | Description |
|---|---|---|
taskId |
string? |
Present in dual-loop mode. ID of the newly-created or existing active task. |
queued |
boolean? |
true if the message was enqueued to an already-running task rather than starting a new one. |
loopMode |
"single" | "dual" |
Current server loop mode. |
Error Response:
{
"status": "error",
"error": "LLM request failed: timeout",
"sessionId": "session-1234567890"
}List all tasks known to the dual-loop agent (active, completed, interrupted, failed, cancelled). Available only when the server runs in dual-loop mode; the array will be empty in single-loop mode.
Response:
{
"tasks": [
{
"id": "task-abc123",
"sessionId": "session-1234567890",
"instruction": "Write a survey on attention",
"status": "executing",
"checkpoints": [],
"progress": { "iterations": 3, "toolsUsed": ["bash"], "lastActivity": 1712700000000 },
"createdAt": 1712699000000,
"updatedAt": 1712700000000
}
],
"count": 1
}Return a single task by ID.
Response: same shape as an element of GET /v1/tasks:
{
"id": "task-abc123",
"sessionId": "session-1234567890",
"instruction": "Write a survey on attention",
"status": "completed",
"checkpoints": [...],
"progress": { "iterations": 5, "toolsUsed": ["bash","arxiv_search"], "lastActivity": 1712700500000 },
"plan": { "steps": ["...", "..."] },
"result": "Survey complete. See /workspace/survey.pdf.",
"error": null,
"createdAt": 1712699000000,
"updatedAt": 1712700500000
}404 Response if the task ID is unknown:
{ "error": "Task task-abc123 not found" }Cancel the active task identified by :id with a structured AbortReason
(Phase C7). The cancellation propagates into the inner loop via a per-task
AbortController, aborts any in-flight LLM fetch + tool execution, and injects
a synthetic [Aborted: <reason>] tool_result into the transcript for any
unresolved tool calls.
Request body (optional):
{ "reason": "user_explicit_cancel" }reason must be one of:
| Value | Meaning |
|---|---|
user_interrupted |
User pressed Ctrl-C / closed WS connection |
user_explicit_cancel |
User pressed "Cancel" in UI (default when omitted) |
timeout |
Task exceeded its deadline |
sibling_error |
A sibling sub-agent failed and cancellation cascaded |
server_shutdown |
Server is shutting down (SIGTERM / SIGINT) |
Response 200:
{ "status": "cancelled", "taskId": "task-abc123", "reason": "user_explicit_cancel" }Response 400 — invalid reason string.
Response 404 — task not found.
Response 409 — task is not in an active status (executing / planning / paused); nothing to cancel.
v1 limitation: see
handleCancelTaskJSDoc insrc/server.tsfor the current per-task cancellation scope and semantics.
Resume an interrupted task (Phase B5). On server startup loadPersistedTasks()
scans {workspaceDir}/.lumin/sessions/*/tasks/*.meta.json; non-terminal tasks
from a prior run are re-registered with status interrupted. This endpoint
replays the persisted transcript (*.jsonl) and re-dispatches the inner loop.
Request body: none.
Response 200:
{ "status": "resumed", "taskId": "task-abc123", "sessionId": "session-1234567890" }Response 404 — task not found. Response 405 — current loop mode does not support resume (single-loop). Response 409 — task is not in a resumable state (e.g. already completed).
Connect to ws://<host>:<port>/v1/stream for real-time streaming.
Send a message to the agent.
{
"type": "chat.send",
"content": "Write a LaTeX paper on transformers",
"sessionId": "optional-session-id",
"config": {
"model": "gpt-4o",
"agentId": "researcher"
}
}Keep-alive ping.
{ "type": "ping" }Sent immediately after WebSocket connection.
{
"type": "connected",
"sessionId": "session-1234567890",
"version": "0.3.1"
}Agent loop has started processing.
{
"type": "lifecycle.start",
"sessionId": "session-1234567890"
}Streaming text token from the LLM.
{
"type": "text.delta",
"delta": "I'll help you "
}Tool execution has begun. toolId uniquely identifies this invocation (format: toolName:index).
{
"type": "tool.start",
"tool": "latex_project",
"toolId": "latex_project:0",
"args": { "action": "compile", "file": "main.tex" }
}Tool execution completed.
{
"type": "tool.end",
"tool": "latex_project",
"toolId": "latex_project:0",
"result": "Compilation successful. PDF: output/main.pdf"
}UI directive from a plugin tool.
{
"type": "directive",
"directive": {
"type": "SWITCH_COMPONENT",
"payload": { "component": "latex-editor" }
}
}Agent loop completed. Contains the full response.
{
"type": "chat.final",
"content": "I've compiled your LaTeX paper...",
"thinking": "...",
"directives": [...],
"toolsUsed": ["latex_project"],
"sessionId": "session-1234567890",
"iterations": 2,
"usage": { "promptTokens": 1500, "completionTokens": 800, "totalTokens": 2300 }
}A dual-loop task was created. Emitted immediately after the HTTP /v1/chat
response (before the inner loop starts).
{
"type": "task.created",
"data": { "taskId": "task-abc123", "sessionId": "session-1234567890", "instruction": "Write a survey..." }
}Emitted when the task enters the planning phase.
{
"type": "task.planning",
"data": { "taskId": "task-abc123", "goal": "Write a survey..." }
}Emitted after planning, with the ordered step list.
{
"type": "task.planned",
"data": { "taskId": "task-abc123", "steps": ["Search arxiv", "Outline", "Draft", "Compile"] }
}Emitted once per inner-loop iteration.
{
"type": "task.progress",
"data": {
"taskId": "task-abc123",
"iteration": 3,
"toolsUsed": ["bash", "arxiv_search"],
"lastActivity": 1712700000000
}
}Emitted when POST /v1/chat enqueues a message into an already-running task
rather than spawning a new one.
{
"type": "task.message.enqueued",
"data": {
"taskId": "task-abc123",
"messageId": "msg-xyz",
"content": "also include figure-level results (truncated to 500 chars)"
}
}Emitted when a task terminates (completion, failure, or cancellation) while queued messages remain undrained. One event is emitted per orphaned message.
{
"type": "task.message.orphaned",
"data": {
"taskId": "task-abc123",
"messageId": "msg-xyz",
"content": "also include figure-level results",
"reason": "task_completed"
}
}reason is one of task_completed | task_aborted.
Emitted when the inner loop finishes successfully.
{
"type": "task.completed",
"data": {
"taskId": "task-abc123",
"sessionId": "session-1234567890",
"result": "Survey complete. See /workspace/survey.pdf.",
"toolsUsed": ["bash", "arxiv_search"]
}
}An error occurred during processing.
{
"type": "error",
"error": "LLM request failed: timeout"
}Response to a ping.
{ "type": "pong" }A typical agent interaction produces events in this order:
Client: chat.send
Server: lifecycle.start
Server: text.delta (0..N tokens) ← LLM streaming
Server: tool.start ← Tool begins
Server: directive (0..N) ← UI directives from tool
Server: tool.end ← Tool completes
Server: text.delta (0..N tokens) ← LLM continues
Server: chat.final ← Done
Multiple tool.start → tool.end cycles may occur in a single turn. Pair them by toolId.
Used in embedded mode (docker exec lumin agent --message "...") and CLI piping.
JSON messages on stdin, one per line:
{"type":"message","content":"hello","sessionId":"sess-1","config":{"model":"us-kimi-k2.5"}}Responses are wrapped with markers for reliable parsing:
---LUMIN_OUTPUT_START---
{"status":"success","response":"Hello! How can I help?","sessionId":"sess-1","iterations":1}
---LUMIN_OUTPUT_END---
| Field | Type | Description |
|---|---|---|
status |
"success" | "error" |
Result status |
response |
string |
Agent's text response |
thinking |
string? |
Reasoning content (if thinking model used) |
directives |
Directive[] |
UI directives emitted during execution |
toolsUsed |
string[] |
Tool names invoked during the turn |
usage |
object? |
Token usage stats |
sessionId |
string |
Session identifier |
iterations |
number |
Agent loop iterations |
error |
string? |
Error message (when status: "error") |
import { runAgent, EventBus } from '@prismer/agent-core';
// Simple usage
await runAgent({
type: 'message',
content: 'Hello',
sessionId: 'my-session',
config: { model: 'us-kimi-k2.5' },
});
// Server mode with custom EventBus
const bus = new EventBus();
bus.subscribe('*', (event) => console.log(event));
await runAgent(
{ type: 'message', content: 'Hello' },
{
bus,
onResult: (result, sessionId) => {
console.log('Agent response:', result.text);
console.log('Tools used:', result.toolsUsed);
},
},
);// Core
export { runAgent, RunAgentOptions } from './index.js';
export { PrismerAgent, AgentResult, AgentOptions } from './agent.js';
export { loadConfig, resetConfig, LuminConfigSchema, LuminConfig } from './config.js';
export { createLogger, Logger, LogLevel } from './log.js';
// Infrastructure
export { OpenAICompatibleProvider, FallbackProvider, Provider } from './provider.js';
export { ToolRegistry, Tool, ToolContext } from './tools.js';
export { EventBus, StdoutSSEWriter } from './sse.js';
export { SessionStore, Session } from './session.js';
export { PromptBuilder, PromptSection } from './prompt.js';
export { SkillLoader, LoadedSkill, SkillMeta } from './skills.js';
export { MemoryStore } from './memory.js';
export { HookRegistry, Hook, HookType, HookContext } from './hooks.js';
export { ChannelManager } from './channels/manager.js';
// Tools
export { loadWorkspaceToolsFromPlugin, createTool, createClawHubTool } from './tools/index.js';
// Protocol
export { InputMessage, OutputMessage, writeOutput, parseOutput } from './ipc.js';