Docs menu
DocsAPI reference

The Agent Engine

com.chatoss.engine — the platform coding-agent engine with its five modes, streaming progress, and interactive hooks.

ChatOSS ships a built-in service app that hosts the ONE battle-tested coding-agent engine: the multi-round tool loop with its FIVE modes (agent, auto, plan, ask, orchestrate), the tsc verify gate, compaction, the read cache, and the edit-retry circuit breaker. (auto is Agent's full toolset run autonomously — shell commands are approved automatically and it never pauses to ask; plan and ask are read-only; orchestrate delegates to sub-agents.) Code, Create, Term Coder, and any coding app plug into it through the SAME app-to-app API instead of each reimplementing ~5-8k lines of agent-loop code. The service is always available (no install) and answers headless.

Calling the engine

Declare the request in app.json (like any app-to-app call):

// app.json
"apiRequests": [
  { "appId": "com.chatoss.engine", "methods": ["runTurn", "status", "cancel", "answer", "buildSystemPrompt", "shouldCompact", "compact"], "why": "Run the platform coding-agent engine." }
]

Run one full agent turn. runTurn is LONG-RUNNING — pass a long timeoutMs (the default 30s call timeout would cut it off):

const result = await window.chatoss.apps.call('com.chatoss.engine', 'runTurn', {
  conversationId: 'my-conv-1',
  messages: [{ role: 'user', content: 'Add a dark mode toggle.' }],
  tools: myToolDefinitions,          // OpenAI-style function defs
  mode: 'agent',                     // one of FIVE: agent | auto | plan | ask | orchestrate
  model: modelId,                    // optional (OS default)
  projectRoot: '/path/to/project',   // optional
  projectRoots: ['/path/to/project'],
  verify: true,                      // tsc --noEmit after file changes
  executor: 'default',               // 'default' = the service executes tools;
                                     // 'caller' = YOUR app executes them (below)
}, { timeoutMs: 30 * 60_000 });
// → { messages, finalMessage, aborted, rounds }

Streaming progress

The app-to-app API is data-in/data-out (no callbacks), so the service publishes live progress events to the shared data key engine.progress while a run is in flight. Subscribe with data.onChanged and filter by conversationId:

window.chatoss.data.onChanged('engine.progress', (ev) => {
  if (!ev || ev.conversationId !== 'my-conv-1') return;
  if (ev.kind === 'token') output.textContent += ev.text;        // streamed tokens
  if (ev.kind === 'thinking') thinkingEl.textContent += ev.text;
  if (ev.kind === 'toolCall') showToolBlock(ev.name, ev.args);
  if (ev.kind === 'toolResult') fillToolResult(ev.name, ev.result);
  if (ev.kind === 'plan') renderPlan(ev.steps);
  if (ev.kind === 'done') setStatus(ev.aborted ? 'Stopped.' : 'Done.');
});

Event kinds: token, thinking, toolCall, toolResult, status, plan, queueTask, assistantMessage, ask, modeRequest, toolRequest, done.

🔴 Every event carries conversationId — including the interactive ones (ask / modeRequest / toolRequest). Always filter on it, on every kind, so an app running two conversations at once can never answer the wrong one's question. The high-frequency kinds (token, thinking, toolCall, toolResult) ride data.publish — ephemeral, this-window-only, no SQLite row per token — so subscribe before you start the run and don't expect to replay them later. See shared data.

Interactive hooks

When the engine needs the user (ask_question, request_agent_mode) or the caller (tool execution in caller mode, spawn_subagent delegation), it publishes an event with an id + callId + conversationId and waits; your app answers through the answer API:

// ev.kind === 'ask' → show the question, then:
await window.chatoss.apps.call('com.chatoss.engine', 'answer', { callId: ev.callId, id: ev.id, value: 'Yes' });
// ev.kind === 'toolRequest' (executor: 'caller') → run it with YOUR executor:
const result = await myToolExecutor(ev.name, ev.args, ev.turnCtx);
await window.chatoss.apps.call('com.chatoss.engine', 'answer', { callId: ev.callId, id: ev.id, value: result });

Executor modes

  • executor: 'default' — the SERVICE executes the tools on its own bridge. It is a built-in, so it carries the built-in trust level: terminal runs without per-command prompts, and its file access is seeded with YOUR app's picked roots (the folders your user already consented to).
  • executor: 'caller' — the engine keeps its hook design: the service publishes each tool call to the progress channel and YOUR app executes it with its own executor (e.g. to supervise its own live terminal pane) and answers via answer.

The raw primitives (chat.runTurn, terminal, files) remain available as the escape hatch for custom orchestrators.

status / cancel

  • status({ conversationId }){ running, callId, startedAt, round } | null
  • cancel({ conversationId }) aborts the in-flight run (it resolves with aborted: true).
  • buildSystemPrompt({ mode, projectRoot, … }), shouldCompact({ messages, compaction }), and compact({ messages, compaction, model }) expose the engine's prompt/compaction logic so callers stay in lockstep with the engine.

Pass projectRoot (and hostOs)

The engine builds a project-context block from them. The system prompt the service builds includes the project block (Root, host OS, the folders it may touch) the same way the built-in Code agent's does. Without projectRoot the model is working blind: it does not know where it is, so it writes absolute paths, asks the user which directory to use, and picks the wrong shell quoting. A sub-agent the engine spawns inherits the same context, so get it right on the parent call.