Signed API
The reference for the agents server HTTP API, where every request is a signed DAG-CBOR action envelope, including versioning, authorization, and every action.

The Seed Agents HTTP API is a signed action API encoded as DAG-CBOR. Every request is signed with a Seed account key, or with a key that account has authorized. Canonical protocol types live in agents/protocol/src/index.ts and are re-exported from agents/src/api.ts. Dispatch lives in agents/src/api-service.ts. HTTP routing lives in agents/src/main.ts.

Endpoint

POST /api/message Content-Type: application/cbor Accept: application/cbor

Equivalent prefixed endpoint:

POST /agents/api/message

Responses are DAG-CBOR encoded AgentResponse values. Every response carries the server's protocol version in the X-Agents-Protocol header (also in /api/version as protocol and minClientProtocol).

Protocol versioning

Clients and servers are deployed independently, so the wire surface is versioned by a single integer, AGENTS_PROTOCOL_VERSION in agents/protocol/src/version.ts. A client declares the version it speaks in envelope.protocol. The server answers in that version's shape (shims in agents/src/protocol-compat.ts). Below MIN_CLIENT_PROTOCOL it refuses with HTTP 426 and {_: 'Error', code: 'protocol_too_old'}. bun run protocol:check in CI fails any change to the surface that would break a released client without a version bump. The rules, the table of what counts as breaking, and the per-version changelog are in agents/protocol/PROTOCOL.md.

Signed envelope

type SignedActionEnvelope = { type: 'AgentsAction' signer: blobs.Principal sig: blobs.Signature account: blobs.Principal protocol?: number // AGENTS_PROTOCOL_VERSION of the client; absent = 1 action: AgentAction } type AgentAction = UnsignedAgentAction & { ts: number // Unix epoch milliseconds }

signer and account are principals, and sig is an Ed25519 signature, the same scheme used to sign blobs. Server validation:

    envelope shape and type;

    principal/signature byte shapes;

    signed action timestamp is within five minutes of server local time;

    Ed25519 signature through verify() from @seed-hypermedia/client (imported through the @shm/shared/blobs re-export);

    signer is account or locally authorized for account;

    action is valid for the transport.

Implementation:

    agents/src/auth.ts: shape/signature/authorization.

    agents/src/api-service.ts: action dispatch and ownership checks.

    frontend/packages/ui/src/agents/client.ts: signAgentAction() and sendAgentAction(), shared by both apps. The desktop's platform (frontend/apps/desktop/src/agents-platform.ts) supplies a signer backed by the Seed daemon.

Signing caveat: omit undefined

DAG-CBOR helpers encode undefined as null in some paths. If the desktop signs an action that contains an explicit undefined, the server decodes it as null and signature verification fails.

The desktop calls omitUndefined() before signing in signAgentAction(), adds ts: Date.now() to the signed action, and never constructs Subscribe with afterSeq: undefined.

Keep this rule: never sign action objects containing explicit undefined fields.

Actions

Current AgentAction union (UnsignedAgentAction in agents/protocol/src/index.ts, dispatched by the switch in Service.message()):

    ListAgents

    ListAgentInvites

    ListAgentCollaborators

    InviteAgentCollaborator

    RemoveAgentCollaborator

    SetAgentPublicRead

    SetAgentPublicChat

    AcceptAgentInvite

    DeclineAgentInvite

    CreateAgent

    ListModelProviders

    ListProviderModels

    ListSigningIdentities

    CreateSigningIdentity

    UpdateSigningIdentity

    DeleteSigningIdentity

    SetModelProvider

    DeleteModelProvider

    StartProviderOAuth

    SubmitProviderOAuthCode

    GetProviderOAuthStatus

    CancelProviderOAuth

    SetSecret

    GetAgent

    UpdateAgent

    DeleteAgent

    ListAgentTriggers

    GetAgentTrigger

    CreateAgentTrigger

    UpdateAgentTrigger

    DeleteAgentTrigger

    ListAgentMemory

    ListAgentTools

    ReadAgentMemoryFile

    WriteAgentMemoryFile

    DeleteAgentMemoryFile

    DownloadAgentMemoryFile

    UploadAgentMemoryFileToIpfs

    CreateSession

    ListSessions

    UpdateSession

    DeleteSession

    GetSession

    MessageSession

    InvokeSessionTool

    UploadSessionAttachment

    ReadSessionAttachment

    BeginFileUpload

    AppendFileUploadChunk

    CommitFileUpload

    AbortFileUpload

    StopSession

    RetrySession

    GetRun

    ListRuns

    CancelRun

    SignalRun

    GetRunJournal

    Subscribe

Subscribe is signed with the same envelope type, but the server accepts it only over WebSocket.

Responses

Success responses are action-specific. Errors use:

type ErrorResponse = { _: 'Error' message: string }

HTTP status is set on expected API errors. Unexpected errors are logged and returned as 500 with a generic message.

Action reference

ListAgents

Request:

{ _: 'ListAgents' }

Response:

{_: 'ListAgentsResponse'; agents: AgentInfo[]}

Lists agents owned by the verified account plus agents on which it is an accepted reader or writer, ordered by update time descending. Each AgentInfo.accessRole is owner, reader, or writer. An agent read through public access reports reader, or chatter when public chat is on. Pending invitations are not returned here, on purpose.

Agent invitations and collaborators


    ListAgentInvites {} returns pending AgentInviteInfo rows for the signed account. An invite shows only the agent id and name, owner account, role, and timestamps. Agent contents stay hidden until the invite is accepted.

    ListAgentCollaborators {agentId} returns the owner and accepted members plus the agent's publicRead and publicChat flags. The owner also sees pending invitations.

    InviteAgentCollaborator {agentId, accountId, role} creates an invitation (reader or writer) or updates an existing member's role. Owner-only.

    RemoveAgentCollaborator {agentId, accountId} revokes an accepted member or cancels a pending invitation. Owner-only.

    AcceptAgentInvite {agentId} accepts the signed account's pending invitation and returns the now-accessible agent.

    DeclineAgentInvite {agentId} deletes the signed account's pending invitation.

    SetAgentPublicRead {agentId, publicRead} turns public read access on or off. Owner-only. While on, every signed account that knows the agent id is treated as a reader (the same view an invited reader gets, including live subscriptions). The agent is still never returned from ListAgents or account-wide ListSessions for accounts that are not owner or collaborator. AgentInfo.publicRead reports the flag. Turning it off also clears publicChat.

    SetAgentPublicChat {agentId, publicChat} turns public chat on or off. Owner-only, and enabling requires publicRead to already be on (400 otherwise). While on, every signed account that reads the agent publicly is a chatter: it may CreateSession, MessageSession, UploadSessionAttachment (and the chunked-upload actions targeting a session), StopSession, and RetrySession on any of the agent's sessions. It still cannot do anything writer-level: UpdateAgent, memory, tool, or trigger writes, UpdateSession, DeleteSession, InvokeSessionTool, CancelRun, SignalRun. AgentInfo.publicChat reports the flag. Public chat is narrower than inviting a writer on purpose. Anyone can talk to the agent, and nobody but writers can change it.

Readers can inspect all agent-scoped state. Chatters (public chat only, and not a role you can invite) can also create, message, and stop sessions. Writers can also create, update, and delete agent-scoped resources, rename and delete sessions, control runs, and run session tools. Managing collaborators and deleting the agent stay owner-only. An agent collaboration never grants account-scoped provider or secret changes. Agent settings may list the owner's redacted providers and signing identities through optional agentId fields. These agent roles are separate from Hypermedia roles on documents.

CreateAgent

Request:

{ _: 'CreateAgent' definition: AgentDefinition clientRequestId?: string }

Creates a new agent. Checks that the referenced provider exists for the account. Creates a per-agent state directory.

When the definition's primary signingKey resolves to an hm-account-key secret, the server also creates a default enabled activity trigger, "Mentions, replies, and comments for <name>", for that signing identity's account uid: a user-mention, a comment-reply, and a document-author-comment condition (prompt: "Respond to the mention, reply, or comment on your document, performing any action requested."). Mentioning the agent's account, replying to one of its comments, or commenting on a document it authored then starts one session in which it responds. This is best-effort and never blocks agent creation. Agents without a signing key get no default trigger.

Idempotent when clientRequestId is supplied.

ListModelProviders

Request:

{ _: 'ListModelProviders' }

Response:

{_: 'ListModelProvidersResponse'; providers: RedactedModelProvider[]}

Returns provider metadata only. Config and secret refs are redacted. See model providers.

ListProviderModels

Request:

{ _: 'ListProviderModels' provider: string }

Response:

{ _: 'ListProviderModelsResponse' models: Array<{id: string; name: string}> }

Looks up one configured provider for the verified account, decrypts its referenced API key in memory, and calls the provider's model-list endpoint. Plain secrets and provider config are not returned.

ListSigningIdentities

Request:

{ _: 'ListSigningIdentities' agentId?: string }

Response:

{_: 'ListSigningIdentitiesResponse'; identities: SigningIdentity[]}

Lists account-scoped secrets whose metadata has kind: 'hm-account-key'. Plain secret material is never returned, and only keys uploaded by the signed account are visible. With agentId, the request resolves against the owning account of a shared agent. The owner sees every identity. Collaborators (reader or writer) see only the identities granted to that agent, and the owner's other keys stay private to the owner. Changing the granted set itself (definition.signingKeys via UpdateAgent) is owner-only. A writer's UpdateAgent must carry the grant set unchanged, or it is rejected with 403.

CreateSigningIdentity

Request:

{ _: 'CreateSigningIdentity' label?: string clientRequestId?: string }

Response:

{ _: 'CreateSigningIdentityResponse' identity: SigningIdentity }

Generates a new server-side Ed25519 HM account key, publishes a profile blob with the supplied label to the configured HM server, encrypts the raw seed as an account-scoped secret tagged kind: 'hm-account-key', and returns redacted identity metadata. clientRequestId makes repeated creates idempotent.

UpdateSigningIdentity

Request:

{ _: 'UpdateSigningIdentity' name: string label: string }

Republishes the server-side account's profile blob with the new display name and updates redacted metadata.

DeleteSigningIdentity

Request:

{ _: 'DeleteSigningIdentity' name: string }

Deletes the encrypted server-side account key secret for the signed account. Published profile blobs are append-only and stay in HM storage.

SetModelProvider

Request:

{ _: 'SetModelProvider' name: string provider: ModelProviderConfig }

Upserts provider config by account and name. ModelProviderConfig.authMode selects how requests authenticate: api-key (default, uses the secretRefs.apiKey secret) or subscription (uses OAuth credentials in the secretRefs.oauth secret).

DeleteModelProvider

Request:

{ _: 'DeleteModelProvider' name: string }

Deletes the named provider record for the account, plus every secret it referenced that no remaining provider still references. Subscription providers of the same type share one OAuth secret, so a shared credential survives the deletion of one of its providers. 404 when the account has no provider by that name. Response: {_: 'DeleteModelProviderResponse'; name}.

Provider OAuth actions ("Sign in with ChatGPT")

Subscription-authenticated providers use a four-action login flow in place of a pasted API key. The flow is offered only when the server runs with SEED_AGENTS_SUBSCRIPTION_AUTH enabled (shown as subscriptionAuth on /api/health). Otherwise StartProviderOAuth returns 403. Implementation lives in agents/src/provider-oauth.ts.

    StartProviderOAuth {providerType} โ†’ {_: 'StartProviderOAuthResponse'; loginId; authUrl; expiresAt}. Only openai is supported. Starting a new login cancels the account's previous pending one.

    GetProviderOAuthStatus {loginId} โ†’ {_: 'ProviderOAuthStatusResponse'; loginId; status: 'pending' | 'completed' | 'failed'; secretName?; error?}. On completed, secretName is the stored credentials secret to reference as secretRefs.oauth.

    SubmitProviderOAuthCode {loginId, code} โ†’ {_: 'SubmitProviderOAuthCodeResponse'}. For deployments where the provider's localhost redirect cannot reach the server, the client pastes the code (or the full redirect URL).

    CancelProviderOAuth {loginId} โ†’ {_: 'CancelProviderOAuthResponse'; loginId}.

RedactedModelProvider.authStatus reports subscription health afterwards: ok, or needs-login when credentials are missing or a token refresh failed.

MCP server actions

Account-scoped, like model providers. Full behavior is in MCP servers.

{_: 'ListMcpServers'} โ†’ {_: 'ListMcpServersResponse'; servers: RedactedMcpServer[]} {_: 'SetMcpServer'; name: string; config: McpServerConfig} โ†’ {_: 'SetMcpServerResponse'; server: RedactedMcpServer} {_: 'RefreshMcpServer'; name: string} โ†’ {_: 'SetMcpServerResponse'; server: RedactedMcpServer} {_: 'DeleteMcpServer'; name: string} โ†’ {_: 'DeleteMcpServerResponse'; name: string}
type McpServerConfig = { url: string // http(s) only transport?: 'http' | 'sse' // absent: Streamable HTTP, then SSE headers?: Record<string, string> // non-secret headers secretRefs?: Record<string, string> // header name โ†’ account secret name } type RedactedMcpServer = { id: string name: string // slug, ^[a-z0-9][a-z0-9_-]{0,31}$ url: string transport: 'http' | 'sse' headerNames: string[] secretHeaderNames: string[] hasSecrets: boolean tools: {name: string; toolName: string; description?: string; inputSchema?: JsonSchema}[] status: {state: 'ok' | 'error' | 'unknown'; error?: string; checkedAt?: number} createdAt: number updatedAt: number }

SetMcpServer validates the name (slug), URL (http/https), transport, and header maps (valid header names, at most 16 each, values โ‰ค 4 KiB), saves, then connects to discover the server's tools. The response reports status and tools either way, and a failed discovery still saves. RefreshMcpServer discovers again. DeleteMcpServer deletes the record and the mcp-<name>-โ€ฆ secrets it owns, removes the name from every agent's definition.mcpServers, and drops their projected mcp tool documents. Writes emit account-change with reason mcp-servers-changed, plus agent-tools-changed per re-projected agent.

AgentDefinition.mcpServers?: string[] (โ‰ค 16 names) is the per-agent grant, accepted by CreateAgent and UpdateAgent.

SetSecret

Request:

{ _: 'SetSecret' name: string value: Uint8Array metadata?: Record<string, unknown> }

Encrypts and upserts a secret. The response is redacted and never includes the secret value.

GetAgent

Request:

{ _: 'GetAgent' agentId: string }

Response:

{_: 'GetAgentResponse'; agent: AgentInfo; sessions: SessionInfo[]}

Requires owner, reader, or writer access to the agent.

UpdateAgent

Request:

{ _: 'UpdateAgent' agentId: string definition: AgentDefinition }

Updates the definition for the owner or an accepted writer after validating the owning account's provider and signing identities.

DeleteAgent

Request:

{ _: 'DeleteAgent' agentId: string }

Response:

{ _: 'DeleteAgentResponse' agentId: string }

Deletes the agent after validating ownership, including its triggers, sessions, session events, trigger firings, drafts, and per-agent state directory. Live runs of the agent are canceled first, cascading through their trees. Run history survives, detached: runs.agent_id, runs.session_id, and runs.trigger_firing_id are nulled inside the delete transaction. They are enforced foreign keys, and without the detach any agent that had ever executed a run could not be deleted. Sub-sessions of other agents hanging off this agent's sessions promote to top level.

Agent trigger actions

The trigger API supports signed CRUD for agent-scoped triggers. The ActivityFeed monitor processes HM activity triggers, and the schedule monitor processes schedule triggers.

Trigger source shape:

type AgentActivitySource = | {type: 'document-comment'; resource: string; author?: string} | {type: 'user-mention'; mentionedAccounts: string[]; resourcePrefix?: string} | {type: 'comment-reply'; repliedToAccounts: string[]; resourcePrefix?: string} | {type: 'document-author-comment'; documentAuthors: string[]; resourcePrefix?: string} | {type: 'site-update'; resourcePrefix: string; eventTypes?: string[]} type AgentTriggerSource = | AgentActivitySource | {type: 'activity'; conditions: {id: string; source: AgentActivitySource}[]} | {type: 'webhook'} | {type: 'schedule'; schedule: AgentScheduleTrigger} | { type: 'run-completed' agentId?: string status?: 'succeeded' | 'failed' | 'canceled' titleMatch?: string } type AgentScheduleTrigger = | {kind: 'interval'; every: number; unit: 'minutes' | 'hours'} | {kind: 'weekly'; daysOfWeek: number[]; timeOfDay: string; timezone: string} | {kind: 'once'; runAt: number; timezone?: string} type TriggerContinuation = {kind: 'newThread'} | {kind: 'wake'; signal: string; runId?: string; payload?: unknown} type AgentTriggerInput = { name: string enabled?: boolean source: AgentTriggerSource prompt: string | AgentPromptBlock[] continuation?: TriggerContinuation }

Trigger prompts accept the same rich Seed block format as agent system prompts. Legacy string input is parsed as markdown. Trigger prompt blocks are converted to resolved markdown before starting the triggered session.

A user-mention source watches a list of accounts. A legacy singular mentionedAccount on input is still normalized into mentionedAccounts, and an empty list is rejected. A comment-reply source matches a comment replying directly to a comment by one of repliedToAccounts, never an account's reply to itself. A document-author-comment source matches a comment on a document authored by one of documentAuthors (the feed's targetAuthorUids), never an author's own comment.

Document fields (resource, resourcePrefix) must name an account or document (hm://<account uid>[/path]); a bare hm:// or a wildcard is rejected rather than treated as "everything". site-update eventTypes must be one of doc-update, comment, citation, capability, contact (or the legacy aliases document-update, change, ref).

Agents writing ~/triggers/<name> can pass dryRun: true to validate the trigger and replay about 100 recent activity events through it without saving. An agent's edit cannot change the kind of source that starts a trigger unless it passes replaceSource: true, and interval schedules under 5 minutes need frequentSchedule: true; both are meant only for explicit user requests.

An activity source matches any of its 1โ€“32 conditions. Condition IDs must be unique and remain stable when edited; the server assigns an ID if omitted on input. Nested groups and schedule/webhook/run-completed conditions are rejected. One comment matching both a document filter and a mention filter admits only one firing, including sibling feed events arriving in separate polls. Distinct triggers remain independent.

CombineAgentTriggers {triggerId, otherTriggerId, expectedUpdatedAt, otherExpectedUpdatedAt, useOtherAction?} combines two activity triggers of the same agent and returns UpdateAgentTriggerResponse. The first survives, retaining its name and enabled state. Its action stays unless useOtherAction is true. The other becomes disabled with mergedInto and cannot be edited or enabled again. Both original histories remain available; previously handled events remain deduplicated. UpdateAgentTrigger also accepts optional expectedUpdatedAt to reject stale edits with HTTP 409.

Compound-source operations require protocol 3. Older clients receive a typed update-required response rather than an incomplete rule. Existing single-source operations and session attribution remain compatible.

continuation says what a firing does. See trigger continuations. Omitted (or newThread), it starts a fresh thread from the trigger's prompt, which is what every trigger did before continuations existed. wake delivers a signal to a run parked on ctx.waitForEvent instead, through the same delivery path as SignalRun. Without runId, the server searches the account's parked runs for one the signal satisfies.

run-completed lets automations chain: it fires when a run of this account reaches a terminal status. Chains are loop-guarded. A firing whose ancestry already contains the same trigger within 8 hops is skipped (TRIGGER_CHAIN_MAX_HOPS in api-service.ts).

The agent_triggers table carries a cooldown_ms column, but no protocol field writes it and no monitor reads it. It is vestigial. Do not document a cooldown feature until one exists.

Actions:

    ListAgentTriggers {agentId} returns {_: 'ListAgentTriggersResponse'; triggers: AgentTriggerInfo[]}.

    GetAgentTrigger {triggerId} returns {_: 'GetAgentTriggerResponse'; trigger: AgentTriggerInfo; sessions: SessionInfo[]}.

    CreateAgentTrigger {agentId, trigger, clientRequestId?} returns {_: 'CreateAgentTriggerResponse'; trigger}.

    UpdateAgentTrigger {triggerId, patch} returns {_: 'UpdateAgentTriggerResponse'; trigger}.

    DeleteAgentTrigger {triggerId} returns {_: 'DeleteAgentTriggerResponse'; triggerId}.

All trigger actions verify account ownership through the owning agent and trigger rows. CreateAgentTrigger supports the same clientRequestId idempotency pattern as other create actions.

Agent memory actions

Each agent owns a private memory filesystem at <stateDir>/memory. It is the ~/memory/ half of its Space. The agent reaches it through the read and write verbs, and its owner sees it on the desktop Memory tab. All actions validate agent ownership for the signed account. Every path is a sandboxed relative path: no absolute paths, no .., symlinks refused. Files can be UTF-8 text or binary.

type AgentMemoryEntry = {path: string; type: 'file' | 'dir'; size: number; updatedAt: number; mimeType?: string} type AgentMemoryFile = { path: string size: number updatedAt: number mimeType?: string encoding: 'utf8' | 'binary' content?: string // present when encoding is 'utf8' data?: Uint8Array // present when encoding is 'binary' }

Actions:

    ListAgentMemory {agentId} returns {_: 'ListAgentMemoryResponse'; agentId; entries: AgentMemoryEntry[]; totalBytes} with every file and directory sorted by path.

    ReadAgentMemoryFile {agentId, path} returns {_: 'ReadAgentMemoryFileResponse'; agentId; file: AgentMemoryFile}. Small clean-UTF-8 files come back as text. Everything else comes back as raw bytes for preview and download in the Memory tab.

    WriteAgentMemoryFile {agentId, path, content} returns {_: 'WriteAgentMemoryFileResponse'; agentId; entry} after writing the full file content, creating parent directories as needed. content may be a string (UTF-8 text) or Uint8Array bytes (e.g. a local file uploaded from the Memory tab). Writes, downloads, and deletes emit an account-change event with reason agent-memory-changed, which is also fanned out to agents/<agentId> WebSocket subscribers so open Memory tabs refresh.

    DeleteAgentMemoryFile {agentId, path} removes a file, or a directory recursively, and returns {_: 'DeleteAgentMemoryFileResponse'; agentId; path; deleted}. deleted is false when nothing existed.

    DownloadAgentMemoryFile {agentId, url, path?} server-side fetches a public http(s) URL into memory (streamed, with a 60-second idle timeout) and returns {_: 'DownloadAgentMemoryFileResponse'; agentId; entry; finalUrl; contentType?}. Omitting path stores the file under downloads/, named from the URL. Extension-less paths gain an extension from the response content type.

    UploadAgentMemoryFileToIpfs {agentId, path} chunks the file as UnixFS and publishes its blocks through the typed HM API's PublishBlobs action, then returns {_: 'UploadAgentMemoryFileToIpfsResponse'; agentId; path; cid; url; size; mimeType?}, where url is the ipfs://<cid> URL usable from Hypermedia content. Publishing makes the file publicly retrievable.

Path limits (agents/src/agent-memory.ts): 512 bytes per normalized relative path, 16 levels of nesting. Memory itself has no per-file, per-agent, or entry-count size cap. The server accepts uploads of any size (main.ts raises Bun's request-body limit for this). The 256 KiB MAX_WRITE_CONTENT_BYTES bound in api-service.ts applies only to hypermedia content the write verb publishes. It does not apply to memory files.

ListAgentTools

Request:

{ _: 'ListAgentTools' agentId: string }

Response:

{_: 'ListAgentToolsResponse'; agentId: string; tools: AgentToolInfo[]}

Lists every tool document in the agent's ~/tools, builtin bindings and authored lambdas alike, from the tool_documents table. It rebuilds the builtin rows first if the registry contract has changed. This is the owner's view of the same documents the agent sees when it reads ~/tools/. See tools.

type AgentToolInfo = { name: string kind: 'builtin' | 'lambda' | 'mcp' server?: string // mcp: the account MCP server it is projected from remoteName?: string // mcp: the tool's name on that server summary: string // one line, for listings and the Space index description: string // full model-facing instructions input: Record<string, unknown> // JSON Schema output?: Record<string, unknown> // JSON Schema, when declared source?: string // lambda source, exactly as authored runtime?: 'typescript' | 'python' cid: string // DAG-CBOR CIDv1 of the document; changes on every edit enabled: boolean granted: boolean // builtins: whether the agent's grant set offers it. Lambdas: always true createdAt: number updatedAt: number }

CreateSession

Request:

{ _: 'CreateSession' agentId: string title?: string clientRequestId?: string }

Creates an idle session for an account-owned agent.

Idempotent when clientRequestId is supplied.

ListSessions

Request:

{ _: 'ListSessions' agentId?: string limit?: number cursor?: {updatedBefore: number; idBefore: string} parentSessionId?: string includeChildren?: boolean }

Lists the signed account's sessions newest-first across every agent on the server, or a single agent's sessions when agentId is set. Response:

{ _: 'ListSessionsResponse' sessions: SessionInfo[] agents: AgentInfo[] nextCursor?: {updatedBefore: number; idBefore: string} }

agents contains only the agents referenced by sessions, so a client rendering a cross-agent session list can label each row without a follow-up GetAgent per session. This exists because the desktop assistant sidebar shows one merged list spanning every agent on every configured server; without it the client would have to walk ListAgents and then GetAgent per agent just to enumerate sessions.

limit defaults to 50 and is clamped to 200.

Pagination is keyset on the composite (updatedAt, id). Sessions often share an updatedAt millisecond, because one trigger firing over a batch of activity events creates several at once. A timestamp-only cursor would silently drop every tied row past a page boundary. Pass nextCursor back verbatim as cursor. When it is absent, the list is exhausted.

Child sessions (spawned by delegate, a script's ctx.delegate, or an agent starting a session) are included by default. An absent includeChildren returns every session, because older deployed clients cannot send the field, and hiding agent-started sessions from them would be a silent regression. Lineage-aware clients (the current desktop) pass includeChildren: false to get top-level rows only. Parents carry childSessionCount. These clients fetch children per parent with parentSessionId, which ignores includeChildren.

UpdateSession

Request:

{ _: 'UpdateSession' sessionId: string title: string }

Updates editable session metadata for an account-owned session. The server trims and bounds the title, marks the title as user-authored, updates updatedAt, emits session-change, and fans out an account change with reason session-updated. A title saved this way is marked title_source = 'user', which the server's automatic session titling refuses to overwrite.

Response:

{ _: 'UpdateSessionResponse' session: SessionInfo }

DeleteSession

Request:

{ _: 'DeleteSession' sessionId: string }

Deletes an account-owned session and its durable events. Every live run rooted at the session is canceled first, including descendants (spawned sub-sessions and workflows). A parked parent is never stranded waiting because its session disappeared, and no executor streams into deleted rows. Run history survives, detached (runs.session_id nulled). Child sessions promote to top level (parent_session_id nulled). A creating trigger firing is kept but detached. The server emits an account change with reason session-deleted.

Response:

{ _: 'DeleteSessionResponse' sessionId: string agentId: string }

GetSession

Request:

{ _: 'GetSession' sessionId: string afterSeq?: number }

Returns session metadata, durable events with seq > afterSeq if provided, and systemPromptMarkdown, the current markdown system prompt that would be used to continue the session.

MessageSession

Request:

{ _: 'MessageSession' sessionId: string content: Array< | {type: 'text'; text: string; blocks?: AgentMessageBlock[]} | {type: 'context'; lines: string[]} | {type: 'attachment'; id: string} > clientMessageId?: string }

context parts carry ambient client state. The desktop sidebar sends the current window (open document, view, focused block) so "this document" resolves for the model. All context lines in a request collapse onto its first user message as contextLines. They reach the model appended to that message inside a <window_context> block, and never appear in the transcript content. At least one text part is required.

attachment parts reference files already staged with UploadSessionAttachment (or a committed chunked upload). They are session-private attachments. They live with the session, the agent reaches them through read attachment:<id>, and they are deleted with the session.

Flow:

    Verify that the signed account has write access to the session's agent.

    Append the durable user message immediately, with content and rawMarkdown, optional rich blocks, and meta.accountId plus the exact cryptographic meta.signerId from the verified envelope.

    Enqueue a durable run for that message.

    Claim it inline when no other turn owns the session. Otherwise leave it queued behind the current turn.

    Run the model loop with one model turn at a time per session.

    Emit live partials over WebSocket.

    Append tool events and the final assistant or error event.

    Start the next queued collaborator turn, if any.

So several writers may submit to one session at once. Their messages are saved and broadcast in append order, and nobody gets a 409 while the agent is streaming. Model turns stay serialized. A queued turn gets an in-memory handoff that names the exact message events that arrived during the preceding response. Later assistant events from that preceding turn are then not mistaken for answers to the newly queued messages.

Internally each turn is a durable run row in the dispatch queue (agents/src/runs.ts). MessageSessionResponse.assistantEventId is an empty string when the request returned before a final assistant event existed. That happens for concurrent and background enqueues (including triggers and agent-started sessions), and for turns that parked on children spawned with delegate. The rest of the turn streams over WebSocket.

Idempotent through clientMessageId. It avoids one long SQLite transaction around network calls, on purpose.

InvokeSessionTool

Request:

{ _: 'InvokeSessionTool' sessionId: string verb: 'read' | 'write' | 'call' input: unknown }

Response:

{ _: 'InvokeSessionToolResponse' sessionId: string resultEventId: string // durable event id of the appended tool_result output?: unknown error?: string }

Runs one verb as the user against the session's shared Log. The log is a shared workspace. The same read, write, and call implementations the agent uses execute here. Both the call and its result append as durable events stamped actor: 'user', so the agent reads them on its next turn as ground truth. There is no side channel. The desktop composer's wrench palette sends this action.

Only those three verbs are accepted. delegate and plan cannot be invoked by the user, on purpose. Delegation is a conversational ask, and any path that reaches session-spawning from a user verb rejects with "Delegation is a conversational ask; message the agent instead".

Execution failures are log entries too, because the user's failed attempt is context. They come back in error, with no HTTP error. Only pre-execution problems reject the request outright: an unknown verb (400), an unowned session (404), and a session with a live run (409, "The agent is working in this thread right now").

Session attachments and chunked uploads


    UploadSessionAttachment {sessionId, name, mimeType?, content} โ†’ {_: 'UploadSessionAttachmentResponse'; attachment: SessionAttachmentInfo}. The attachment id is the SHA-256 hex of the bytes, so re-uploading the same file returns the same id. Caps: 100 MiB per attachment and 200 attachments per session (agents/src/session-attachments.ts), stored under <stateDir>/session-attachments/<sessionId>/.

    ReadSessionAttachment {sessionId, attachmentId} โ†’ {_: 'ReadSessionAttachmentResponse'; attachment; data} for rendering an attachment back in the thread.

Large files upload in bounded chunks, so each signed action stays small and clients can show progress:

    BeginFileUpload {target, size} โ†’ {_: 'BeginFileUploadResponse'; uploadId; maxChunkBytes}. target is {kind: 'memory', agentId, path} or {kind: 'session-attachment', sessionId, name, mimeType?}, validated up front so a long upload cannot fail at the very end. maxChunkBytes is 8 MiB.

    AppendFileUploadChunk {uploadId, offset, content} โ†’ {_: 'AppendFileUploadChunkResponse'; uploadId; received}. Chunks must arrive in order: offset must equal the bytes already staged. Oversized chunks return 413.

    CommitFileUpload {uploadId} โ†’ {_: 'CommitFileUploadResponse'; entry?; attachment?}: entry for a memory target, attachment for a session attachment. The staged byte count must equal the declared size.

    AbortFileUpload {uploadId} โ†’ {_: 'AbortFileUploadResponse'; uploadId}. Staged uploads also expire after an hour.

StopSession

Request:

{ _: 'StopSession' sessionId: string }

Response:

{ _: 'StopSessionResponse' sessionId: string stopped: boolean }

Stops the in-flight Pi agent turn for the signed account and session when one is active, and cancels every live run rooted at the session including descendants (delegated model children and script children). stopped is false when the session is already idle.

RetrySession

Request:

{ _: 'RetrySession' sessionId: string }

Response:

{ _: 'RetrySessionResponse' sessionId: string assistantEventId: string }

Re-runs a session whose latest run failed, without appending a new user message. The turn re-enters from the durable transcript, and error events are not replayed to the provider. Rejected when a run is live or the latest run did not fail. assistantEventId is an empty string when the retried turn parked (the rest streams over WebSocket), exactly like MessageSession.

GetRun

{_: 'GetRun', runId} โ†’ {_: 'GetRunResponse', run: RunInfo}. 404 when the run does not belong to the account.

ListRuns

{ _: 'ListRuns' rootRunId?: string // the whole tree of one root, oldest first (tree rendering) sessionId?: string // root runs referencing a session, newest first agentId?: string // runs of one agent, newest first status?: RunStatus limit?: number // default 50, clamped to 200 }

Exactly one selector is required. Response: {_: 'ListRunsResponse', runs: RunInfo[]}.

CancelRun

{_: 'CancelRun', runId} โ†’ {_: 'CancelRunResponse', runId, canceled}. Cancels the run and every non-terminal descendant: queued runs never start, waiting runs never wake, executing runs are aborted (Pi abort for agent runs, VM interrupt for script runs). canceled is false when everything was already terminal.

SignalRun

{ _: 'SignalRun' runId: string signal: string // a wait with no criteria accepts any name payload?: unknown // must be JSON-serializable }

Response: {_: 'SignalRunResponse', runId, delivered}.

Delivers a named signal to a run parked on ctx.waitForEvent, and wakes it with the payload. A person or another system uses this to answer a workflow that waits for something the activity feed cannot express: an approval, a webhook, a human decision. The run card's Answer button sends the run's RunWaitInfo.answerWith signal. Signalling a run that is not listening for this signal is not an error. delivered is false. See wake source. A trigger with a wake continuation rides this same delivery path.

GetRunJournal

{_: 'GetRunJournal', runId, afterSeq?} โ†’ {_: 'GetRunJournalResponse', runId, entries}. It returns a script (workflow) run's durable journal entries ({runId, seq, entry, createdAt}). It is empty for agent runs, and replayable with afterSeq like session events.

Subscribe

Request:

{ _: 'Subscribe' key: `account/${string}` | `agents/${string}` | `sessions/${string}` | `runs/${string}` afterSeq?: number }

Used over /agents/ws. See WebSocket subscriptions.

Idempotency

Idempotency rows store:

    account ID;

    action name;

    client request or message ID;

    request CBOR bytes;

    response CBOR bytes;

    creation timestamp.

Same ID and same request bytes replay the response. Same ID with different request bytes returns 409.

Agent definition

type AgentDefinition = { name: string systemPrompt: string | AgentPromptBlock[] modelProvider: string model: string reasoningLevel?: ReasoningLevel tools?: string[] signingKey?: string signingKeys?: string[] metadata?: Record<string, unknown> } type AgentPromptBlock = { block: Record<string, unknown> & {id: string; type: string} children?: AgentPromptBlock[] }

systemPrompt is normalized to Seed block nodes on create and update. Legacy string input is parsed as markdown first. Before a model run, the server converts the stored blocks back to markdown and appends the shared runtime instructions and the agent's <space> index.

reasoningLevel applies to reasoning-capable models and must be one of the levels modelReasoningSupport reports for the model (agents/protocol/src/reasoning.ts). Absent means off, or the provider default where reasoning cannot be disabled.

tools is a grant list. It does not define the tool surface. The verbs (read, write, call, delegate, plan, status, and continue_session) are never granted or revoked. See grants and the glossary. When a verb is missing, the reason is the run's shape: delegate is omitted for a runless invocation and for a leaf at its delegation budget's depth, and continue_session is omitted for delegated children. tools narrows two things:

    the callable set dispatched through call (today search, query, attributes, web_search, execute; navigate is registered for the assistant runtime only and never offered by the service). An omitted tools array grants every service-runtime callable. An explicit array keeps only the names it lists. Unknown and legacy names are ignored, and execute_code normalizes to execute (normalizeSeedToolName). execute is dropped silently on hosts that cannot run sandboxes, so the model never sees a tool that can only fail.

    the publish grant: the pseudo-tool name publish authorizes signed public writing (hm:// documents and comments, IPFS uploads). Legacy write-group names (write, memory_publish_document, ipfs_write, attachment_to_ipfs) still count, so a pre-verbs agent keeps the posture its owner configured. An omitted tools array publishes. Memory writes are never gated.

signingKeys stores the secret names of the selected uploaded HM account keys used for signing and publishing. signingKey stays as a legacy single-key field. When an agent runs, selected keys are appended to the system prompt with both profile names and public key IDs so the model can map user-facing names to signing IDs. Pi's own builtin tools are disabled by the Seed runner (noTools: 'builtin').

Protocol sync

The apps and the server consume the same private package, @seed-hypermedia/agents-protocol. Nobody maintains manual protocol mirrors. Change protocol action, response, session-event, or WebSocket-event types in agents/protocol/src/index.ts. agents/src/api.ts re-exports those types for service-local imports, and frontend/packages/ui/src/agents/client.ts aliases them for the shared UI.

When changing the protocol package, update service dispatch, desktop behavior, and docs in the same change.

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime