@cogitator-ai/server-shared
v0.17.0
Published
Shared streaming protocol and utilities for Cogitator server adapters
Maintainers
Readme
@cogitator-ai/server-shared
Shared streaming protocol, request validation, response serialization, and utilities for Cogitator server adapters.
Overview
@cogitator-ai/server-shared is the common foundation under every Cogitator server adapter (express, fastify, hono, koa, next, tetsu) and protocol bridge (openai-compat, a2a). It establishes a unified Server-Sent Events (SSE) streaming protocol, robust request body and media-type validation, standardized client response shaping, heartbeat management, ID generation, OpenAPI document generation, and adapter conformance test suites.
Who is this for?
This package is intended for adapter authors and developers building custom servers or integrations directly on top of the Cogitator AI runtime. Most applications interact with Cogitator through an official adapter (such as @cogitator-ai/hono, @cogitator-ai/express, @cogitator-ai/fastify) and receive this package transitively without needing to install it directly.
Installation
pnpm add @cogitator-ai/server-shared
# or
npm install @cogitator-ai/server-sharedQuick Start: SSE Encoding
Adapters and custom HTTP servers use encodeSSE to format event objects into SSE wire frames (data: <json>\n\n) and encodeDone to signal stream completion (data: [DONE]\n\n):
import {
createStartEvent,
createTextStartEvent,
createTextDeltaEvent,
createTextEndEvent,
createFinishEvent,
encodeSSE,
encodeDone,
generateId,
} from '@cogitator-ai/server-shared';
function streamResponse(res: NodeJS.WritableStream) {
const messageId = generateId('msg');
const textId = generateId('txt');
// 1. Announce stream start and thread
res.write(encodeSSE(createStartEvent(messageId, 'thread_abc123')));
// 2. Stream text parts
res.write(encodeSSE(createTextStartEvent(textId)));
res.write(encodeSSE(createTextDeltaEvent(textId, 'Hello from ')));
res.write(encodeSSE(createTextDeltaEvent(textId, 'Cogitator!')));
res.write(encodeSSE(createTextEndEvent(textId)));
// 3. Emit finish event with usage and outcome
res.write(
encodeSSE(
createFinishEvent(
messageId,
{ inputTokens: 12, outputTokens: 8, totalTokens: 20 },
{ threadId: 'thread_abc123', status: 'completed' }
)
)
);
// 4. Terminate the SSE stream
res.write(encodeDone());
}Exports
All exports from packages/server-shared/src/index.ts are grouped below by purpose:
1. SSE Streaming Protocol & Event Factory Functions
Standardized Server-Sent Events protocol for agent runs, workflow runs, swarm runs, and interactive tool approvals.
Event Factory Functions
createStartEvent(messageId, threadId?): Emits the initialstartevent announcing the message ID and the active or newly assigned memorythreadId.createTextStartEvent(id): Starts a streamed text segment.createTextDeltaEvent(id, delta): Emits a text chunk (delta) for the active text part.createTextEndEvent(id): Closes the active text segment.createReasoningStartEvent(id): Starts a streamed reasoning summary segment (whenreasoning.summaryis enabled on the agent).createReasoningDeltaEvent(id, delta): Emits a reasoning chunk (delta).createReasoningEndEvent(id): Closes the reasoning summary segment.createToolCallStartEvent(id, toolName): Signals a tool call with its provider ID and name. In a streamed run it goes out as soon as the model picked the tool.createToolCallDeltaEvent(id, argsTextDelta): Emits raw JSON argument deltas for a tool call. In a streamed run each piece the model writes is a delta, else the arguments come in one delta right before the call runs.createToolCallEndEvent(id, dropped?): Closes a tool call as soon as it is complete, so the events of two calls do not interleave unless the provider mixes their pieces. Withdroppedit closes a call the model never finished, which never runs.createToolDroppedEvent(id, toolCallId, reason): Thetool-resultof a call that will never run,{ dropped: true, reason }withreason'incomplete'(the model never finished it) or'discarded'(the run did not keep it). A client removes the call, one that does not knowdroppedsees a call with an empty result.createToolUpdateEvent(toolCallId, { content?, details?, presentation? }): Emits what a running tool reported withcontext.update(), between itstool-call-endand itstool-result.createToolResultEvent(id, toolCallId, result, { details?, presentation? }): Emits the result of an executed tool call linked bytoolCallId:resultis what the model read,detailsandpresentationcome from atoolOutput()result or the tool'spresentation.createDataEvent({ id, name, data, transient?, toolCallId? }): A data part a tool sent withcontext.data(), structured data a client renders as a widget of its own. A laterdatawith the sameidreplaces the part in place, atransientone is never kept with the message.createStateSnapshotEvent(snapshot),createStateDeltaEvent(delta): The run's state (RunOptions.state, changed by tools withcontext.state.set()), whole or as a JSON Patch (RFC 6902) on the state the client holds.createResetStepEvent(partIds, toolCallIds): A step the model streamed starts over (a fallback model took over mid-answer, an empty answer is asked again): the client takes back these text and reasoning parts and tool calls.createApprovalRequiredEvent(threadId, approvals): Emits anapproval-requiredevent when tool calls require human-in-the-loop review, pausing the run.createInterruptRequiredEvent(threadId, interrupts): Emits aninterrupt-requiredevent when the run paused on questions its tools asked (context.interrupt()), answered by id in theresponsesof the resume.createErrorEvent(message, code?): Builds anerrorstream event ({ type: 'error', message, code }).createFinishEvent(messageId, usage?, details?): Emits the terminalfinishevent containing sanitized token counts and run completion details (AgentRunOutcome).createWorkflowEvent(event, data): Emits workflow execution events (node_started,node_completed,node_error,node_progress,workflow_completed,workflow_failed).createSwarmEvent(event, data): Emits multi-agent swarm coordination events (agent_start,agent_complete,agent_error,message,swarm_completed).
Protocol Types
StreamEvent: Discriminated union of all streaming events emitted over the SSE wire.StartEvent: Structure of thestartevent.TextStartEvent: Structure of thetext-startevent.TextDeltaEvent: Structure of thetext-deltaevent.TextEndEvent: Structure of thetext-endevent.ReasoningStartEvent: Structure of thereasoning-startevent.ReasoningDeltaEvent: Structure of thereasoning-deltaevent.ReasoningEndEvent: Structure of thereasoning-endevent.ToolCallStartEvent: Structure of thetool-call-startevent.ToolCallDeltaEvent: Structure of thetool-call-deltaevent.ToolCallEndEvent: Structure of thetool-call-endevent, with optionaldropped.ToolUpdateEvent: Structure of thetool-updateevent: what a running tool reported, or withargumentsthe arguments a streamed call runs with when they differ from the streamed ones.ToolResultEvent: Structure of thetool-resultevent, with optionaldetailsandpresentation(ToolPresentation:label,icon?,detail?), ordroppedandreasonfor a call that never ran.DataEvent,StateSnapshotEvent,StateDeltaEvent(StateEventfor either),ResetStepEvent: Structures of thedata,state-snapshot,state-deltaandreset-stepevents.ApprovalRequiredEvent: Structure of theapproval-requiredevent.PendingApproval: Client-facing tool approval request (toolCallId,toolName,arguments,description, optionalsideEffects, and once the run has permissions set upreason(why the call asks),rule(the text of theaskrule that made it ask, never the file it came from),suggestedRule(the rule an "always allow" answer saves) and the run's permissionmode).InterruptRequiredEvent: Structure of theinterrupt-requiredevent.PendingInterrupt: A question a paused run waits on (id,kind,reason,message, optionalresponseSchema,expiresAt,toolCallId,toolName,details).kindisapproval,input(a tool asked),plan(a plan to approve before the run leaves plan mode, the plan indetails.plan) orbudget(a budget the run reached, itsscope,metric,usedandlimitindetails). The answer goes byidinresponsesand must matchresponseSchema.ErrorEvent: Structure of theerrorevent.FinishEvent: Structure of thefinishevent extending partialAgentRunOutcome.FinishDetails: Optional outcome details and thread passed when building aFinishEvent.WorkflowEvent: Structure of workflow progress events.SwarmEvent: Structure of swarm progress events.Usage: Token usage counts (inputTokens,outputTokens,totalTokens, and optionalreasoningTokens,cachedInputTokens,cacheWriteTokens).
Protocol Schemas
Every event has a Zod schema of the same shape as its type, so a client can check what a server sends at its boundary:
import { StreamEventSchema } from '@cogitator-ai/server-shared';
for (const data of sseDataLines) {
if (data === '[DONE]') break;
const event = StreamEventSchema.parse(JSON.parse(data));
if (event.type === 'text-delta') process.stdout.write(event.delta);
}StreamEventSchema: Discriminated union of every event schema ontype. An unknowntypeor a field of the wrong type fails to parse, a field a newer server adds is dropped.StartEventSchema,TextStartEventSchema,TextDeltaEventSchema,TextEndEventSchema,ReasoningStartEventSchema,ReasoningDeltaEventSchema,ReasoningEndEventSchema,ToolCallStartEventSchema,ToolCallDeltaEventSchema,ToolCallEndEventSchema,ToolUpdateEventSchema,ToolResultEventSchema,DataEventSchema,StateSnapshotEventSchema,StateDeltaEventSchema,ResetStepEventSchema,ApprovalRequiredEventSchema,ErrorEventSchema,FinishEventSchema,WorkflowEventSchema,SwarmEventSchema: The schema of each event.JsonPatchOperationSchema: One operation of a JSON Patch, asstate-deltacarries them.RunUsageSchema,PendingApprovalSchema,ToolPresentationSchema: The schemas of theusageoffinish, of a pending approval and of thepresentationof a tool call.streamEventComponents(): The same schemas as OpenAPI 3.0 components:StreamEvent(aoneOfwith a discriminator ontype), aStream<Name>Eventper event,RunUsage,PendingApprovalandToolPresentation.OpenAPISchemaObject: A component schema as plain JSON.
2. SSE Helpers & Heartbeat Management
Wire formatting and keep-alive ping mechanisms to maintain streaming connections through intermediate proxies and strict runtime timeouts.
encodeSSE(data, id?): Serializes any JavaScript object or event into SSE format (data: <json>\n\n), with anid: <id>line first when given (the events of a resumable stream).encodeDone(): Returns the standard terminal event frame (data: [DONE]\n\n).DEFAULT_SSE_HEARTBEAT_MS: Default heartbeat interval of 5,000 ms (5 seconds). Keeps streams active under Bun's 10-secondidleTimeoutand reverse-proxy disconnects (e.g., nginx 60s).encodeHeartbeat(): Returns a comment frame (: keep-alive\n\n) that SSE clients silently ignore.resolveSseHeartbeatMs(value): Validates and resolves the heartbeat interval option (0turns heartbeats off; throwsRangeErrorif negative, non-integer, or non-finite).startHeartbeat(beat, intervalMs): Sets up a recurring interval callingbeat()until aborted or untilbeat()returnsfalse(indicating stream closure). Returns a cleanup teardown function.
3. Agent Stream Session
Stateful session manager coordinating stream event ordering, part transitions, and runtime event hooks.
AgentStreamSession: High-level session coordinator for streaming agent runs. Guarantees that parts never interleave (closes an open text or reasoning part before emitting tool calls or ending, and ends each streamed tool call as soon as it is complete, so tool calls interleave only when the provider mixes their pieces, and then no piece is lost), drops the calls it announced that never came to run (atool-call-endwithdroppedwhen the call was never finished, then a droppedtool-result, also atcompleteandfail, while calls that run or wait for approval are kept, and a dropped call that runs after all under another tool name is announced again), generates IDs, emitsstart, buffers non-streamed results, handles tool approvals, and emitsfinishorerror. Exposescallbacks(onToken,onReasoning,onToolCall,onToolResult,onToolUpdate,onToolInputStart,onToolInputDelta,onToolInputAvailable,onToolInputAbandoned) ready to spread intocogitator.run({ stream: true, ...session.callbacks }), plusonDataandonState, which stream the data parts and state changes of tools (data,state-snapshotthenstate-delta), andonStepReset, which sendsreset-stepwith the parts and tool calls of a step that starts over and lets a model failing mid-answer move to the next of the agent'sfallbackModels.onStepResetcomes only withresetSteps: true, for a client that takes back what areset-stepnames (off by default),statenames the state the client already holds so the first change goes out as a delta.createThreadId(): Generates a cryptographically random thread identifier (thread_${crypto.randomUUID()}).AgentStreamCallbacks: Required callback interface matching Cogitator's streaming callbacks.AgentStreamSessionOptions: Options interface accepting an optional client-suppliedthreadId.
3a. Resumable Streams
The machinery behind the resumableStreams option every adapter takes (express, fastify, hono, koa, tetsu, next). With it on, every event of /agents/:name/stream and /agents/:name/resume/stream carries an SSE id, <streamId>:<seq> with seq from 1 up, so the id alone names the stream (streamId is stream_<uuid>, which nobody can guess). A client that disconnects no longer stops the run: it goes on, its events kept in a store. The same route with Last-Event-ID: <id> (or ?lastEventId=<id>) reconnects instead of starting a run and reads no body: the answer replays the events after that id, then follows the run to finish or error (and data: [DONE] after finish). Several clients may follow one stream. DELETE /agents/:name/stream/:streamId cancels the run (204), on whatever process runs it.
- Owner. A stream is bound to the caller who started it: by default every field of what the adapter's
authreturned (user id, roles, permissions, metadata such as a tenant), so callers without a user id are still told apart.owner(auth)returns a key of your own, for example the user id alone when the identity carries values that change between requests, such as the expiry of a token. Only a caller with the same key may reconnect or cancel, anyone else gets404 STREAM_NOT_FOUNDbefore any SSE header, and so does an unknown or expired stream or an event id the stream never sent. An id that is not<streamId>:<seq>gets400 INVALID_INPUT. - authorizeThread. After the owner check, a reconnect and a cancel ask the adapter's
authorizeThreadabout the stream's thread, so a caller it turns away now gets403 THREAD_FORBIDDEN. - Header first.
?lastEventId=is for clients that cannot set headers. It puts the id into URLs and access logs, preferLast-Event-ID. - Gaps. When events after the id are no longer kept (more than
maxEventssince, or a store that lost some, found by their sequence numbers), the reconnect gets oneerroreventSTREAM_RESUME_GAP, without an id, telling the client to fetch the thread (GET /threads/:id) to catch up. - Slow clients. At most
maxEventswait for a client that reads slowly. One that falls further behind reads the store again, and getsSTREAM_RESUME_GAPonly for events the store no longer keeps. - Stopped runs. A run nobody follows for
detachedTimeoutMs(5 minutes) is stopped, so a client that never comes back does not run up cost, and every run is stopped whenuntilaborts (a server that shuts down: abort it before the HTTP server closes, which waits for the streams still open. Fastify stops the plugin's runs by itself inpreClose). Such a stream ends with anerroreventSTREAM_ABORTED, a cancelled one withSTREAM_CANCELLED, which a late reconnect replays. - Room. A stream whose run goes on is never evicted. When the store keeps
maxStreamsstreams that all go on, a new resumable stream is refused with503 STREAM_LIMIT_REACHEDbefore anything is sent. - Store failures. The clients of the process that runs a stream get every event from it directly, the last one too, also when the store fails. A store that cannot keep an event stops the run (
STREAM_ABORTED).
import { CogitatorServer } from '@cogitator-ai/express';
new CogitatorServer({
app,
cogitator,
agents,
config: {
auth: verifyToken,
resumableStreams: {
maxEvents: 2000,
retentionMs: 10 * 60_000,
detachedTimeoutMs: 2 * 60_000,
owner: (auth) => auth?.userId,
until: shutdownSignal,
},
},
});ResumableStreams<Auth>: The hub an adapter builds from its option (ResumableStreams.from(option),undefinedwhen off).startAgentRun({ auth, threadId, signal?, execute, describeError })starts a streamed run or resume on a new stream and answers{ ok: true, stream }(frames(signal): the events with their ids, until the stream ends orsignalaborts) or{ ok: false, error }(503 STREAM_LIMIT_REACHED).reconnect(lastEventId, caller)andcancel(streamId, caller)take the caller ({ auth, authorizeThread }) and answer{ ok: true, ... }or the refusal.close()stops every run of the process, asuntildoes.open(owner)is the lower level: a stream of your own events (emit,end,signal,stopReason).ResumableStreamOptions<Auth>:{ store?, maxEvents?, retentionMs?, maxStreams?, detachedTimeoutMs?, owner?, until? }.retentionMsandmaxStreamsset up the default in-memory store, next to astoreof your own they throw.maxEventsalso bounds what waits for one client.StreamCaller<Auth>,defaultStreamOwner(auth): Who asks to reconnect or cancel, and the default owner key.StreamReplayStore: Where the events are kept:create(falsewhen it has no room without evicting a stream that goes on),append,read(streamId, afterSeq)(the events after it, whether some were evicted (gap) and whether the stream finished),finish,get(theowner,lastSeqand how manysubscribersfollow it),subscribe(the events appended from now on, then the end),cancelandonCancel(a cancel asked on any process reaches the process that runs the stream). Several processes behind a load balancer need a store they share, with pub/sub (RedisPUBLISH/SUBSCRIBEor similar), so a reconnect or a cancel that lands on another process reaches the run, andsubscriberscounts the clients of every process, which the run reads before it stops as detached.InMemoryStreamReplayStore: The default store, for one process:maxEventsof each stream (1000, a ring that evicts the oldest), a finished stream forretentionMs(5 minutes),maxStreamsstreams (1000, a new one evicts the stream that finished first, and is refused while all of them go on). No timers: expired streams are dropped as the store is used.nowinjects the clock.STREAM_NOT_FOUND,STREAM_RESUME_GAP,STREAM_ABORTED,STREAM_CANCELLED,STREAM_LIMIT_REACHED: The error codes of resumable streams.formatStreamEventId(streamId, seq),parseStreamEventId(id),isStreamId(id),lastEventIdOf(header, query): The event id format, and the id a request reconnects with (the header first, an empty value names none).invalidLastEventId(),invalidStreamId(),streamNotFound(),streamLimitReached(): The refusals of a reconnect, a cancel and a new stream.
3b. JSON Patch
Exported from the main entry and from @cogitator-ai/server-shared/json-patch, a subpath that imports nothing else, so a browser bundle can apply state-delta events:
createJsonPatch(from, to): The RFC 6902 patch that turnsfromintoto, key by key and item by item.applyJsonPatch(document, patch):documentwith the patch applied, every operation of RFC 6902, the input left as it is. ThrowsJsonPatchErrorwhen an operation does not apply.jsonEqual(a, b),parseJsonPointer(pointer),JsonPatchOperation.
3c. AG-UI
@cogitator-ai/server-shared/agui serves agents to AG-UI 1.0 clients (CopilotKit, @ag-ui/client), built on @ag-ui/core. Every adapter's POST /agents/:name/agui route uses it.
toAGUI(events, { threadId, runId }): The AG-UI events of a list of stream events, a pure function.AGUITranslatordoes the same one event at a time (push(event)), for a stream that is pushed. Text, reasoning, tool calls and results map one to one,tool-updateanddatabecome activities (ACTIVITY_SNAPSHOT, thenACTIVITY_DELTApatches by id), state eventsSTATE_SNAPSHOTandSTATE_DELTA, delegated runs subagents (SUBAGENT_*), workflow nodes steps, andfinishRUN_FINISHEDwith outcomesuccess,interrupt(the approvals and questions of a paused run asInterrupts) orcancelled. Everything is closed beforeRUN_FINISHED, nothing goes out after it.streamAGUIRun({ request, host, send, signal, describeError }): Runs or resumes an agent for an AG-UI request and streams it:hostbindsrun,resume,pausedRunandcancelPauseof a Cogitator to the agent, thread and caller.parseAGUIRunInput(body)validates aRunAgentInputand reads what it asks,resumeResponses(entries, interrupts)turns AG-UI resume entries into theresponsesof a resume.approvalInterrupt(),questionInterrupt(): A pending approval or question as an AG-UIInterrupt.AGUIConformanceRuntime,runAGUIConformance(runtime, send): The conformance check every adapter's AG-UI route passes in its tests.
4. ID Generation
generateId(prefix = 'id'): Generates collision-resistant, URL-safe identifiers combining prefix, base-36 timestamp, random characters, and monotonic counter (e.g.,msg_...,txt_...,rsn_...,res_...).
5. Request Validation & Schemas
Uniform validation logic across all adapters to reject malformed payloads with HTTP 400 (INVALID_INPUT) before reaching the model or runtime.
Functions & Constants
parseRunRequest(body, options?): ValidatesPOST /agents/:name/runand/streamrequest bodies. Rejects blank strings (""," "), verifiescontextagainstacceptContextpolicy, and validatesthreadIdandmultitaskStrategy(one ofMULTITASK_STRATEGIES).runRequestRunOptions(body): The run options a parsed run request adds, itsmultitaskStrategywhen it names one.parseSwarmRunRequest(body, options?): ExtendsparseRunRequestwith positive finitetimeoutvalidation up toMAX_RUN_TIMEOUT_MS.parseResumeRequest(body): ValidatesPOST /agents/:name/resumepayloads (threadId,decisionsmap,defaultDecision, and theresponsesto the run's interrupts). A decision is{ approved: boolean, reason?: string, updatedArguments?: object, remember?: boolean | { rule?: string, layer?: 'session' | 'local' | 'project' | 'user' } }: an approval may change the arguments of the call (the run checks them against the tool's schema and the rules again) and save an "always allow" rule (remember: truesaves thesuggestedRule), a refusal keeps itsreason.parseAddMessageRequest(body, options?): ValidatesPOST /threads/:id/messagespayloads (role, non-blankcontent, optionalmetadata).parseWorkflowRunRequest(body, options?): ValidatesPOST /workflows/:name/runpayloads (inputobject, integermaxConcurrency, integermaxIterations, booleancheckpoint).checkpoint: trueis accepted only with{ checkpoints: true }, for a server that keeps a checkpoint store, and refused otherwise.parseWorkflowResumeRequest(body): ValidatesPOST /workflows/:name/resumepayloads (non-blankcheckpointId, the samemaxConcurrencyandmaxIterationsoptions).isNonBlankString(value): Type guard ensuring value is a string with at least one non-whitespace character.isJsonObject(value): Type guard ensuring value is a non-null, non-array plain object.NON_BLANK_PATTERN: Regex pattern string ("\\S") for schema-driven adapters (e.g., Fastify, Tetsu).RUN_INPUT_SCHEMA: JSON Schema object describing theinputfield.DEFAULT_THREAD_MESSAGE_ROLES: Default accepted roles for thread messages (['user', 'assistant']).MAX_RUN_TIMEOUT_MS: Upper limit for run timeouts (2,147,483,647 ms, approx. 24.8 days).
Types
ParseResult<T>: Result union ({ ok: true; value: T } | { ok: false; message: string }).RunRequestBody: Validated agent run request body.RunRequestOptions: Options specifying allowed context keys viaacceptContext.ContextPolicy: Allowed context policy:false(refuse all, default),true(accept all), or a whitelist of allowed string keys.SwarmRunRequestBody: Validated swarm run request body including optionaltimeout.ResumeRequestBody: Validated tool approval resumption body.AddMessageRequestBody: Validated thread message insertion body.AddMessageRequestOptions: Options configuring permitted thread message roles.ThreadMessageRole: Supported message roles ('user' | 'assistant' | 'system').WorkflowRunRequestBody: Validated workflow run request body.WorkflowRunRequestOptions: Whether the server keeps a checkpoint store (checkpoints), so a run may ask for checkpoints.WorkflowResumeRequestBody: Validated workflow resume request body.WorkflowRunLimitsBody: ThemaxConcurrencyandmaxIterationsoptions of a workflow run or resume.
6. Request Body Parsing & Media Type Validation
Safeguards preventing unauthorized cross-origin execution and parsing body payloads safely.
Functions & Constants
refuseNonJsonBody(method, headers): Inspects incoming request headers before reading the body. Refuses non-JSON POST/PUT/PATCH/DELETE requests with HTTP 415 (UNSUPPORTED_MEDIA_TYPE) to prevent cross-origin preflight bypasses from HTML forms ortext/plain.isJsonMediaType(contentType): Checks if a Content-Type header matchesapplication/jsonorapplication/*+json.parseJsonBody(contentType, text): Validates media type and parses raw JSON body text, returning typed results or refusals.readJsonRequestBody(request, limit?): Reads a Fetch APIRequestbody stream up to the byte limit, returning parsed JSON or a structuredBodyRefusal.DEFAULT_JSON_BODY_LIMIT: Default maximum request body limit (1 MiB: 1,048,576 bytes).UNSUPPORTED_MEDIA_TYPE: Refusal object (status: 415,code: 'UNSUPPORTED_MEDIA_TYPE').INVALID_JSON: Refusal object (status: 400,code: 'INVALID_INPUT').PAYLOAD_TOO_LARGE: Refusal object (status: 413,code: 'PAYLOAD_TOO_LARGE').
Types
BodyRefusal: Structured refusal representation (status,code,message).JsonBodyResult: Result union ({ ok: true; value: unknown } | { ok: false; refusal: BodyRefusal }).
7. Response Serialization & Token Usage
Sanitizes runtime results into safe client-facing JSON responses without leaking internal server state (such as system instructions, raw message history, or execution checkpoints).
Functions
toAgentRunResponse(result): Converts a CogitatorRunResultinto the canonical client response shape (AgentRunResponse), preservingoutput,threadId,usage,toolCalls,traceId, and outcome flags.toAgentRunOutcome(result): Extracts status outcomes (completedvspaused),pendingApprovals, structured outputs, and run termination flags (truncated,blocked,iterationLimitReached).toAgentToolCall(call): Sanitizes tool calls into{ id, name, arguments }, stripping provider internal state.toPendingApprovals(requests): Sanitizes pending tool approval requests for human review.toPendingInterrupts(interrupts): The questions of a paused run as clients see them, none while it waits for approvals only.toRunUsage(usage): Filters runtime usage down to client-facing token counts (RunUsage), omitting server-side cost and duration.toWorkflowCompletedData(result): The data of theworkflow_completedstream event,{ workflowId, duration, checkpointId? }(WorkflowCompletedData).toWorkflowFailedData(result): The data of theworkflow_failedevent a workflow stream sends right before itserrorevent,{ checkpointId? }(WorkflowFailedData).withResumedCheckpoint(result, checkpointId): The result of a workflow resume, naming the checkpoint it started from when the resumed run saved none of its own, so a resume that fails at once can be resumed again.
Types
AgentRunResponse: Canonical response shape forPOST /agents/:name/runand/resume.AgentRunOutcome: Flags detailing how a run terminated.AgentToolCall: Sanitized client-facing tool call.RunUsage: Standardized token usage structure.
8. Swarm Lifecycle Utilities
Resource safety helpers for multi-agent swarm orchestration.
withSwarm(swarm, task): Executestask(swarm)and ensuresswarm.close()is called in afinallyblock. Prevents connection leaks in distributed swarms (which maintain Redis connections and state TTLs).swarmAgentNames(config): Extracts deduplicated list of all agent names participating in a swarm across supervisor, worker, moderator, router, and pipeline stage slots.ClosableSwarm: Interface for swarm instances exposingclose(): Promise<void>.SwarmAgentSlots: Structural definition of agent roles and slots in a swarm configuration.
9. OpenAPI & Swagger UI Generation
Dynamic documentation generators reflecting registered agents, tools, workflows, and swarms.
generateOpenAPISpec(context, config): Builds an OpenAPI 3.0 specification from registered agents, tools (with JSON schemas), workflows, and swarms. Every SSE route answerstext/event-streamwith theStreamEventschema.documentStreamEvents(document): Adds the stream protocol to an OpenAPI document a framework generated from its own routes, such as@fastify/swagger: the StreamEvent components, andStreamEventas thetext/event-streambody of every POST route ending in/stream.generateOpenAPISpecalready documents its streams this way.StreamDocumentable: WhatdocumentStreamEventsneeds of a document.generateSwaggerHTML(spec): Generates a standalone HTML page embedding Swagger UI from CDN with the OpenAPIspecobject embedded inline for/docs.OpenAPISpec: TypeScript interface for the generated OpenAPI 3.0 document.SwaggerConfig: Customization options for Swagger UI and OpenAPI generation (title, version, base path, auth,limits, which the spec shows as the defaults and caps of the capped request fields, andresumableStreams, which documentsLast-Event-ID,lastEventIdandSTREAM_NOT_FOUNDon the stream routes).OpenAPIContext: Context containing the registered agents, workflows, and swarms.
10. Conformance Testing Suite
Shared conformance test cases and mock runtime ensuring all adapters behave identically.
checkConformance(testCase, response, runtime, check?): Compares a response the adapter already returned against the test case and runtime state, returning a list of mismatches (an empty array means the adapter conforms).ConformanceRuntime: Mock runtime tracking runs, resumes, streaming callbacks, and memory operations.new ConformanceRuntime(testCase)follows the case'sruntimeoptions: runs that fail with aCogitatorError(fail) or a plain error (crash), a thread holdingthreadEntriesmessages, and streamed runs thatholdafter their parts untilrelease()or until their signal aborts.CONFORMANCE_CASES: Collection of standardized test cases covering input validation, context policy, thread isolation, error conditions and the streams of runs and resumes. Every event of a stream a case reads must matchStreamEventSchema, and nothing may follow itsfinishorerror.CONFORMANCE_ROUTES: Standard test route mapping (/agents/:name/run,/threads/:id/messages, etc.).CONFORMANCE_NAMES: Standard test entity names (assistant,team,flow,thread-1).CONFORMANCE_SWARM: Standard swarm fixture with router and pipeline stages.CONFORMANCE_GENERATED_THREAD: Constant identifying generated thread assertions.conformanceMethod(case): Resolves HTTP method for a conformance test case.conformanceContentType(case): Resolves Content-Type header for a test case.conformanceRunResult(): Generates mock completedRunResult.conformancePausedResult(): Generates mock pausedRunResultwith pending tool approvals.parseSseEvents(text): Parses raw SSE wire text into structured event objects for test assertions.parseSseFrames(text)keeps theidof each event.runConformanceCase(case, runtime, send): Sends a case and its follow-ups (reconnects, cancels, a second stream) throughsend, in order, dropping the first answer afterdisconnectAfterevents when the case says so and releasing a held run once the first follow-up opens. Put what it returns intocheckConformance.sendConformanceRequest(request, fetcher, codeOf?): Thesendof an adapter whose test canfetch(a listening server,app.request, a route handler): it reads the status, headers, error code and body, or the first events of a stream it then drops.ConformanceRequest,ConformanceSend,ConformanceFollowUp: One request of a case, how an adapter sends it, and the follow-ups of a case (reconnectwith the id of an event of the first answer,cancel,start, with the caller's headers and what each answer must be).conformanceCaller(header),conformanceAuthorizeThread(server),conformanceResumableStreams(server),ConformanceCaller: What an adapter'sauth,authorizeThreadandresumableStreamsare for a case: callers told apart by thex-conformance-userandx-conformance-roleheaders (callers), roles turned away from every thread (forbiddenRoles), streams bound to the user id alone (streamOwner: 'user').ConformanceCase: Interface defining a test case.ConformanceCheckOptions: Custom status code and error code mapping options forcheckConformance(e.g. for adapters following framework-specific error conventions).ConformanceResponse: Standardized response representation for adapter fetchers, with the answer'sheaders(names in lower case) for the cases that checkRetry-After.ConformanceRoute: Union of supported conformance route names.ConformanceServerOptions: Adapter server options required by specific conformance test cases:acceptContext,threadMessageRoles,limits,anonymous,forbiddenThreads(threads the adapter'sauthorizeThreadrefuses),deps(what itsdepshook returns),workflowCheckpoints(the adapter's workflow checkpoint store, an empty one in the cases),resumableStreams(the adapter's option of that name),callers,forbiddenRolesandstreamOwner(seeconformanceCaller).
11. Server Contract
What every adapter keeps the same, so an application behaves alike on any of them.
RunLimits,checkRunLimits(limits): The operator's caps on a run (swarmTimeout,workflowMaxIterations,workflowMaxConcurrency), checked to be positive integers.swarmTimeoutWithin(requested, limits),workflowOptionsWithin(options, limits): What a run gets, the smaller of what the request asked and the cap, the cap when it asked for nothing.ownedCheckpoints(store, owner): ACheckpointStoreof one user in a store servers share:savewrites the checkpoint withowner(noownerkey forundefined),loadanswersnullfor a checkpoint of anyone else,listkeeps only the owner's, anddeleteremoves only an owned one. A caller without a user id owns only checkpoints saved without one. Adapters run workflows withnew WorkflowExecutor(cogitator, ownedCheckpoints(store, userId)).findOwnedCheckpoint(store, owner, workflowName, checkpointId),checkpointNotFound(checkpointId): The checkpoint a workflow resume names, when it is one of that workflow owned by the caller, and the404 CHECKPOINT_NOT_FOUNDthe adapter answers otherwise, also when it keeps no store.AuthorizeThread,authorizeThreadAccess(authorize, auth, threadId),threadForbidden(threadId): The hook an adapter asks about every thread a request names, and the403 THREAD_FORBIDDENit answers when the hook says no.AnonymousPolicy,admitCaller(userId, policy),UNAUTHORIZED:'deny'turns away a caller without a user id with401 UNAUTHORIZED.describeHttpError(error): The status, code, message andretryAfter(whole seconds) an error is answered with: aCogitatorErrorits own status or the one of its code, aSwarmTimeoutError504 RUN_TIMEOUT, anything elseundefined, which the adapter logs and answers withINTERNAL_ERRORwithout its text.cogitatorErrorStatusandretryAfterSecondsare the parts of it.errorEnvelope(error),errorHeaders(error): The{ error: { message, code, checkpointId? } }body and theRetry-Afterheader of an error answer.workflowRunFailure(described, checkpointId): How a failed workflow run is answered: as the error it failed with, plus thecheckpointIdof the last checkpoint it saved, whicherrorEnvelopecarries next tocodeandmessage. A run that saved none is answered as any other error.parseThreadPageQuery(query),readThreadPage(memory, threadId, page, thread?),MAX_THREAD_PAGE_LIMIT:GET /threads/:id?limit&cursor, the newestlimitmessages of what is left and anextCursorfor the page before them. The cursor tells apart entries that share a time, so no message is skipped or repeated across pages.
12. Triggers and Inbox (experimental)
The routes every adapter serves when it is given the runtime's trigger manager (triggers: cog.experimental_triggers), and none without it:
POST /triggers/:id/fireandPOST /triggers/:id/webhook:handleTriggerRoute(host, route, id, request)hands the body to the trigger byte for byte, any content type, and answers what the trigger answers (202with aTriggerFireResult,200for a duplicate,401,404,409,429withRetry-After). Adapters mount them before their auth and JSON body guard, since a trigger checks its caller itself by token or signature, and refuse bodies overMAX_TRIGGER_BODY_BYTES(10 MiB) withTRIGGER_BODY_TOO_LARGE.triggerRequestOf()builds the request from a Node one.GET /inboxandPATCH /inbox/:id:listInboxRoute(host, query, userId, anonymous?)(status,kind,routineId,limitup toMAX_INBOX_PAGE,beforeandbeforeId, answers{ items, nextBefore?, nextBeforeId? }) andpatchInboxRoute(host, id, body, userId, anonymous?)({ status }, never the answered state) behind the adapter's auth: a caller sees and changes only the items of their user id.anonymousis the adapter's policy as configured: a caller without a user id gets401(INBOX_SIGN_IN_REQUIRED,admitInboxCaller) unless it is'allow', then they see the items without one.TriggersHostis what the routes need of the manager,TRIGGER_ROUTES,INBOX_STATUSESandINBOX_KINDSthe routes and values.generateOpenAPISpec(ctx, { triggers: true })documents them.- Conformance:
TRIGGER_CONFORMANCE_CASES,ConformanceTriggersandcheckTriggerConformanceCasecheck that raw bodies arrive untouched, trigger routes ignore the adapter's auth and the inbox keeps to the caller's items.
Packages Built on @cogitator-ai/server-shared
The following adapters and integrations build on @cogitator-ai/server-shared:
| Package | Description | Documentation |
| -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| @cogitator-ai/express | Express.js server adapter with REST endpoints, SSE streaming, WebSocket, and Swagger UI | README |
| @cogitator-ai/fastify | Fastify server adapter leveraging schema compilation and hooks | README |
| @cogitator-ai/hono | Multi-runtime Hono adapter (Node.js, Bun, Deno, Cloudflare Workers, AWS Lambda) | README |
| @cogitator-ai/koa | Koa server adapter with middleware pipeline and SSE streaming | README |
| @cogitator-ai/next | Next.js App Router streaming handlers and React client hooks | README |
| @cogitator-ai/tetsu | Tetsu framework adapter for Bun with automatic OpenAPI generation | README |
| @cogitator-ai/openai-compat | OpenAI-compatible server (/v1/chat/completions, /v1/responses, /v1/assistants) | README |
| @cogitator-ai/a2a | Agent2Agent (A2A) protocol v0.3 server and client implementation | README |
Documentation
- Full Cogitator documentation: cogitator.app
- Server adapters overview: cogitator.app/docs/server-adapters
License
MIT
