Cogitator
Integrations

App-server (JSON-RPC)

A typed JSON-RPC 2.0 app-server on stdio: threads, turns, steering, approvals and reviews of a Cogitator runtime, with a generated client and JSON Schema.

Overview

@cogitator-ai/app-server serves a Cogitator runtime to another program over JSON-RPC 2.0, one message per line (NDJSON) on stdio. It is how an editor, a desktop app or a script drives agents: it starts threads, runs turns, streams their events, steers and interrupts them, asks for tool approvals and reviews code changes. The protocol is written as Zod schemas, and a typed client with no dependencies is generated from them.

  • SessionHost - threads in the runtime's memory, one turn per thread at a time
  • Turns - turn/start, turn/steer, turn/interrupt, every event as turn/event, subagent events tagged by their parent tool call
  • Approvals - asked live with approval/request, or the turn ends paused and is resumed with decisions
  • Reviews - review/start for uncommitted changes, a branch, a commit or custom instructions, the findings structured
  • Traces - the spans of every turn, sent as they end to clients that ask and kept for _cogitator/trace/get
  • Extensions - memory search, rewind, traces, workflows and swarms under _cogitator/*
  • Generated client - AppServerClient and a type for every message, plus the JSON Schema
pnpm add @cogitator-ai/app-server @cogitator-ai/core

Running it

From the CLI, with the agent of cogitator run:

cogitator app-server -m ollama/qwen3:8b

Or from code, with your own agents:

import { Agent, Cogitator } from '@cogitator-ai/core';
import { SessionHost, createAppServer, serveStdio } from '@cogitator-ai/app-server';

const cogitator = new Cogitator({
  memory: { adapter: 'sqlite', sqlite: { path: './sessions.db' } },
});

const host = new SessionHost({
  cogitator,
  agents: {
    assistant: new Agent({ name: 'assistant', model: 'ollama/qwen3:8b', instructions: 'Help.' }),
  },
});

await serveStdio(createAppServer(host));
await host.close();

The host needs memory, since threads live there. SessionHost takes defaultAgent, review (agent, cwd, context), workflows and swarms runners, serverInfo, and exposeErrors to show clients the message of errors that are not the runtime's own. createAppServer(host).connect(send) gives a connection for any other transport that moves whole messages.

The client

import { APP_SERVER_PROTOCOL_VERSION, AppServerClient } from '@cogitator-ai/app-server/client';

const client = new AppServerClient(transport); // { send(line), onLine(callback), close() }

await client.initialize({
  protocolVersion: APP_SERVER_PROTOCOL_VERSION,
  clientInfo: { name: 'my-app', version: '1.0.0' },
  capabilities: { approvals: 'request', optOutNotifications: ['thread/log'] },
});

client.on('turn/event', ({ event, parentToolCallId }) => {
  if (event.type === 'text-delta' && !parentToolCallId) process.stdout.write(event.delta);
});
client.on('turn/completed', ({ turn }) => console.log(turn.status, turn.usage));
client.onRequest('approval/request', ({ toolCall }) => ({
  outcome: { outcome: 'selected', optionId: 'allow_once' },
}));

const { thread } = await client.threadStart();
const { turn } = await client.turnStart({
  threadId: thread.id,
  input: [{ type: 'text', text: 'Plan the release' }],
});
await client.turnSteer({
  threadId: thread.id,
  expectedTurnId: turn.id,
  input: [{ type: 'text', text: 'Skip the changelog' }],
});

For a project of your own, write the client and the schema out:

cogitator app-server generate-ts --out src/generated
cogitator app-server generate-json-schema --out schema

Methods

MethodWhat it does
initializeCapabilities both ways, the server's agents. Everything else waits for it
thread/startA thread with an agent (agent, threadId, metadata, permissionMode)
thread/resumeA kept thread with the messages of its active branch and what it waits on
thread/forkA new thread holding the branch up to an entry
thread/listThreads a page at a time, by archive state and agent
thread/archiveArchives a thread, thread/unarchive takes it back
thread/setPermissionModeThe permission mode of new turns, and of the turn going on from its next call
turn/startA turn with input, or resume for a paused thread, outputSchema, model
turn/steerA message to the turn going on: steer, followUp or inject
turn/interruptStops the turn, returns the messages it did not take
review/startA review turn in a review thread of its own
_cogitator/memory/searchSearches the turns of a thread and the agent's memory items
_cogitator/thread/rewindRewinds a thread to an entry, its files too with files
_cogitator/trace/getThe spans kept of a turn (turnId), -32002 for a turn no longer kept
_cogitator/workflow/*list and run of the host's workflows, only when it has a runner
_cogitator/swarm/*list and run of the host's swarms, only when it has a runner

The answer to turn/start always goes out before turn/started, then come turn/event (each a StreamEvent, with parentToolCallId and agent for delegated runs) (reset-step among them: when a model fails mid-answer and a fallback model takes the step over, the client takes back the parts and tool calls it names) and turn/completed with the turn: completed, paused (with pending), interrupted or failed (with error). Other notifications: thread/started, thread/archived, thread/unarchived and thread/log, the memory adapter's changes to a thread.

Traces

Every turn runs with onSpan, and the host keeps the spans of each turn: the last 2000 of each of the last 200 turns. A client that initializes with capabilities: { traces: true } gets every span as it ends:

client.on('_cogitator/trace/span', ({ turnId, span }) => {
  console.log(turnId, span.name, `${span.duration}ms`, span.status);
});
const { spans } = await client.traceGet({ turnId: turn.id });

Others can still read a turn's spans with _cogitator/trace/get. optOutNotifications leaves the notification out too.

Approvals

When the client says capabilities.approvals: 'request', a tool call that needs approval is asked with approval/request while the turn waits, in the shape of ACP session/request_permission: the toolCall and the options allow_once, allow_always (approves and saves the suggested allow rule), reject_once and reject_always (refuses and saves the same rule as a deny rule). { outcome: { outcome: 'cancelled' } } leaves the turn paused. Otherwise the turn ends paused, and turn/start with resume: { decisions, responses, defaultDecision } goes on. New input to a paused thread is refused with -32022.

Reviews

const { turn, reviewThreadId } = await client.reviewStart({
  threadId: thread.id,
  target: { type: 'baseBranch', branch: 'main' },
});
client.on('turn/completed', ({ turn: done }) => {
  if (done.id === turn.id) console.log(done.review?.findings);
});

Targets are uncommittedChanges, baseBranch { branch }, commit { sha, title? } (read with git in the host's review.cwd) and custom { instructions }. The review runs the thread's agent with review instructions and a response format, and the completed turn carries review: findings with title, body, priority (0 must be fixed, 3 a nit), confidence and location, then overallCorrectness and overallExplanation.

Errors

CodeMeaning
-32602Invalid params, data.issues says where
-32002Not found, data.kind names a thread, turn, agent or entry
-32010Not initialized
-32011Already initialized
-32020The thread has a turn going on
-32021The turn named is not the one going on
-32022The thread is paused
-32023The thread is archived
-32030The runtime refused or failed, data.code is its code

Plus the standard -32700, -32600, -32601 and -32603.

ACP mapping

The core methods line up with the Agent Client Protocol, and the ACP integration serves the agents of a host to editors over it:

ACPApp-server
session/newthread/start
session/loadthread/resume
session/promptturn/start + turn/completed
session/cancelturn/interrupt
session/updateturn/event
session/request_permissionapproval/request

External agents

SessionHost takes providers next to agents: agents that run outside the runtime, such as ACP agents (Claude Code, Codex, Gemini CLI) through AcpAgentProvider. Clients see them in initialize with external: true and start threads with them like any agent. The host keeps the thread and its messages, the provider runs each turn and its events reach turn/event. Their threads cannot be forked, rewound or reviewed, and their turns take no steering. From the CLI: cogitator app-server --acp-agent claude. See the ACP integration.

For one-shot and scripted runs without a client of your own, see headless mode.

On this page