Skip to content

Upgrading to 0.3.0 ​

How to move a server from @cognipeer/agent-server 0.1.x or 0.2.x to 0.3.0: what to check first, what changes behaviour, and what is new. The full list of changes is in the release notes.

0.3.0 is mostly additive. The pause protocol (approvals and questions) does nothing for an agent that never pauses, and every new route, setting and field is optional. What can bite an existing server is the handful of behaviour changes below, most of them security hardening and storage parity.

Before you upgrade ​

  1. Back up the database. The SQLite and PostgreSQL providers upgrade their schema once, on connect (to version 5, under an advisory lock on PostgreSQL). The only data the upgrade deletes is the older results of a task that had several. MongoDB gets new collections and indexes.
  2. Check what lists conversations. A request that names no user is now 401 identity_required (Identity and listing).
  3. Check JWTs. Tokens must now be signed with the key you configure (Authentication).
  4. Check who relies on a disconnect. A client that closed the connection to stop a turn now has to call POST /conversations/:id/cancel (Streaming and runs).
  5. Check file retention. Files now expire after 30 days unless you set files.retentionDays: 0 (Files).
  6. Use agent-sdk 0.10.6 or later if you offer a Stop button: earlier SDKs let the model finish (and bill) after a cancel.

Behaviour changes at a glance ​

AreaBeforeNowDo this
ListingGET /conversations and GET /tasks answered everyone's records to a request that named nobody401 identity_required unless the request names a userResolve a user for every request (resolveContext, token auth, auth.resolveUserId), or set allowAnonymousList: true on a single-user server
Client-named identity?userId= and a userId in a create body were honoured with token authIgnored whenever the server established an identity; used only when the request carries noneStop sending them; use resolveContext
JWTcreateJWTAuthProvider read the claims without verifying the signatureVerifies the signature (HS/RS/ES), exp, nbf, iss, aud; throws without a secretConfigure the real key and algorithm; tokens signed with another key now fail
Auth without providerauth.enabled: true without provider ran unauthenticatedcreateAgentServer throwsAdd a provider, or set enabled: false and use resolveUserId / resolveContext
Generated tokenstok_ plus base64 of the user and the timetok_ plus 256 random bits; only a hash is kept, in memory; optional tokenTtlMsRe-issue the tokens generateToken made; the old shape is gone
DisconnectA streamed turn stopped when the client went awayThe turn runs to its end and is storedCall POST /conversations/:id/cancel to stop
Concurrent turnsA second message or answer started an overlapping turn (or was 409 action_not_pending)409 conversation_busy (error.details.activeRunId)Wait for the turn, or cancel it
Approval deadlinesexpiresAt was only passed throughAfter it the request answers 410 action_expired and hitl.onTimeout applies (default deny, after hitl.timeoutMinutes, default 1440)Set hitl.timeoutMinutes: 0 to keep requests open for ever, as before
HistoryGET /conversations/:id returned the first 100 messagesThe newest page of 100, oldest first within itNothing for conversations up to 100 messages; page with before / after for longer ones
Agent historyThe agent saw only the first 100 messages (after about 50 turns the newest message never reached the model)The agent gets the whole conversationNothing
Files expireFiles lived until their conversation was deletedexpiresAt = upload + files.retentionDays (30) and the sweep deletes themSet retentionDays: 0 to keep them. Files uploaded before 0.3.0 have no expiry
Files and ownershipA file could be attached to any conversation; GET /files/:id answered to anyoneOnly the uploader sees a file; a message attaches only files of its own conversation uploaded by the caller; a file without conversationId cannot be attachedUpload with conversationId
limits.maxFilesFiles per conversationFiles one message attaches; a conversation holds files.maxFiles (10)Set files.maxFiles for the old meaning
Inline filesSendMessageRequest.files got throwaway ids (404 when fetched)They are stored (owned, in the conversation, under the upload limits) and carry real idsNothing
FileRecord.mimeTypeKept as declaredStored without parameters (text/plain; charset=utf-8 becomes text/plain); application/octet-stream is guessed from the extensionNothing
FormsA multipart/* upload stored the whole envelope as the file415 unsupported_media_typeSend the bytes as the body, or use the JSON form
DownloadsThe file name and type went into headers as they wereSanitised, X-Content-Type-Options: nosniff, Cache-Control: private, no-store; ?disposition=inline for pictures, PDF and plain textNothing
Conversation metadataA client could set metadata.taskId and source: "task"These, and the new pinned, pinnedAt and titleGenerated, belong to the server: a client can neither set nor drop themUse PATCH { pinned } and PATCH { title }
SDK usageRaw ledger, or emptyThe usage of the run (a resumed turn no longer repeats the calls before the pause), with cachedInputTokens and costUsd when knownRe-read any code that added usage up
Handler stateWritten in the middle of a turnWritten with the rest of the turn, and cannot erase the server's own keysNothing
Assistant messagesmetadata.status and stopReason were not on every messageAlways present; an assistant message that stopped before saying anything (cancelled, no text) is left out of the history the agent is givenNothing
Next.js adapterRead the body before authenticatingAuthenticates first and refuses a body over limits.maxBodySize with 413Set limits.maxBodySize when agents are built per request
Fastify adapterA body over the limit answered Fastify's FST_ERR_CTP_BODY_TOO_LARGE413 { error: { code: "PAYLOAD_TOO_LARGE", message } }Update clients that matched Fastify's shape
GET /files/:idAmbiguous in the referenceAnswers the file record itself; only the upload wraps it as { file }Nothing

Identity and listing ​

The identity the server established always wins, in this order: context.user from resolveContext; the token user, then auth.resolveUserId; and only when the request carries no identity at all (an open demo server) the userId the client names. Listing needs one of them:

typescript
// a server with real users
createAgentServer({ basePath: '/api', storage, resolveContext: (req) => ({ user: { id: userIdFrom(req) }, data: {} }) });

// a local tool or a demo: everybody is the same person
createAgentServer({ basePath: '/api', storage, allowAnonymousList: true });

A context that carries only a tenant ({ data: { tenantId } }, no user) is not enough: resolve a user for every request, even a service user per tenant. Details: Request Context.

Authentication ​

  • createJWTAuthProvider({ secret }) verifies the signature with the key and algorithm you configure (HS256 by default; the PEM public key as secret for RS* and ES*). It never takes the algorithm from the token. There is no JWKS support: for keys that rotate, verify in front of the server or write an AuthProvider on a JWT library (Authentication).
  • The user of a token is taken from sub, then userId, then user_id (a non-empty string); set extractUserId for anything else.

Streaming and runs ​

  • A turn is a run that outlives its HTTP request: closing the tab, a dropped connection or a proxy timeout no longer stops it. See Runs, Cancel & Reattach.
  • Frames carry seq; stream.done carries stopReason and runId; a client that loses a message stream resumes with GET /conversations/:id/events/stream?after=<seq>.
  • A non-streamed request to an SDK agent now passes onEvent and cancellationToken to invoke, so tool calls reach the event log whether or not the request streams.
  • ProcessMessageParams.conversationId of a task is now the id of the task's own conversation (it was task_<id>).

Files ​

  • Uploads can be the file's own bytes: POST /files?name=...&conversationId=... with the file's Content-Type. The JSON form keeps working.
  • Attach by id: fileIds on the message. Both are checked against the agent's settings before the turn starts.
  • What the agent reads, images, retention and the per-agent files settings: Files.
  • The Node handler's default body limit follows the largest file any agent accepts (server.maxUploadBytes()); the Fastify plugin registers its own buffer parsers for the types Fastify has none for.

Storage ​

All four built-in providers now pass one contract suite against real databases, which changed behaviours that used to differ:

  • Rows created in the same millisecond keep insertion order; a new message bumps its conversation's updatedAt everywhere.
  • Records read back the same way: undefined and Dates in JSON fields are normalised as JSON.parse(JSON.stringify()) would, files: [] reads as no files, an absent userId / title reads as undefined, task comments is always an array.
  • Deleting a conversation deletes its files and their content in every provider (SQLite, PostgreSQL and MongoDB used to keep them); deleting a message keeps its files.
  • Saving a file is all-or-nothing. A file for a conversation or message that does not exist is a 404 (it was a 500 on SQL, accepted on memory and MongoDB), and so is a message for an unknown conversation.
  • A task has one result: createTaskResult replaces the existing one in every provider.
  • PostgreSQL: the migration adds tasks.comments, per-table seq columns and prefix-qualified index names (old unprefixed ones are dropped when they belong to this prefix's tables); an idle-client error no longer crashes the process; message text that is valid JSON no longer comes back as a number or object.
  • MongoDB: file content moves to GridFS (content written by older versions in <prefix>file_contents is still read), so files are no longer limited to 16 MiB; the client connects with ignoreUndefined and promoteBuffers; a conversation state over maxStateBytes (12 MiB by default) is refused instead of failing later; indexes cover the sort keys (older single-field indexes they replace are dropped).
  • Work that belongs to no request (recovery, pruning) calls the storage provider with the context { data: { system: true } }. A custom provider that scopes by ctx.user must let those calls through. Everything new in the interface is optional, so a 0.2.x provider keeps working; see Custom Storage.
  • The expiry sweep calls getConversations with actionDueBefore; a storage wrapper that logs or fails getConversations will see it.

Tasks ​

  • A task runs in a conversation of its own (task.conversationId); a task with input files has it from the start and its files are checked against the agent's files settings.
  • New status cancelled, waiting_for_input with pendingActions, POST /tasks/:taskId/cancel, GET /tasks?conversationId=, and 409 task_busy / action_pending on a comment. See Task System.
  • A task left running by a process that is gone is failed ("The server stopped while the task was running").

From 0.1.x ​

Everything in 0.2.0 is additive, so a 0.1.x server needs the changes above and nothing more. What 0.2.0 added, and fixed:

  • Request context: resolveContext(req), the context in dynamic agents, handlers (params.context), SDK agents, auth.resolveUserId and every storage call (Request Context).
  • Dynamic agents: config.agents as an array or a per-request function.
  • Node and Fastify adapters: createNodeHandler, @cognipeer/agent-server/node, @cognipeer/agent-server/fastify (Fastify & Node HTTP).
  • maskServerErrors: hide the text of every error that is not an explicit 4xx.
  • Fixes: a conversation or task without an owner is refused (403) when resolveContext is set; limit is capped at 200 and a negative offset becomes 0; an SDK agent that answers without streaming still ends the stream with a stream.text (isFinal: true); a rejected streaming invoke ends the stream with stream.error instead of hanging; errors thrown as plain { statusCode, code, message } objects keep their status; every exports entry has a default condition, so TypeScript compiled to CommonJS no longer fails with ERR_PACKAGE_PATH_NOT_EXPORTED (the package itself is ESM-only: require() needs Node 20.19+ or 22.12+).

If you copied code from older documentation ​

Some older examples used names that do not exist. The package exports createInMemoryProvider (not createMemoryProvider) and generateOpenAPISpec(config, agents) (there is no agentServer.getOpenAPISpec()), and a message is streamed with "stream": true (the field is stream, not streaming). An error answers { "error": { "code", "message", "details" } }, not { "error": "..." }.

What is new in 0.3.0 ​

FeatureGuide
Tool approvals, questions and client tools; requests that lapse; who may answer; edited arguments checkedApprovals & Questions
Detached runs, cancel, one turn at a time, the event log, reattach and the snapshot frame, recoveryRuns, Cancel & Reattach
Pricing, per-conversation budgets, continueUsage & Budgets
Files the agent reads: inline, in pages, images by reference; raw uploads, ownership, retentionFiles
Host-backed agentsHost-backed agents
Regenerate, edit, pin, rename, bulk delete, generated titles, starters, feedback, exportChat experience
Bearer auth for embedded chats, rateLimit and audit hooksEmbedding & limits
Storage contract suite, capabilities, GridFS, schema versioningStorage Providers
Paged history, orderBy=updatedAt, GET /conversations/:id/files, server.getRoutes(), the standalone exampleREST Endpoints, Examples

Verify the upgrade ​

  1. Start the server against a copy of the database and watch the log: the schema upgrade runs once on connect.
  2. GET /agents shows capabilities.attachments and metadata.experience for each agent.
  3. List conversations as a real user, and as a request with no identity (expect 401 identity_required).
  4. Send a message with stream: true, close the connection mid-answer, and check GET /conversations/:id reports runtime.status: "running" and then idle.
  5. If you use files: upload one with conversationId, attach it by fileIds, and read its expiresAt.

If something does not behave as described, Troubleshooting lists the common causes.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub