AgentNava · Reference

Types and errors

The whole surface on one page.

Every call

Five entry points. Everything else is a method on what you already hold.

// GETTING IN
AgentNava.getWorkspace(options?)                      -> Workspace     // no request
ws.get()                                     -> Workspace
ws.agents.create(fields)                     -> Agent
ws.agents.list()                             -> Agent[]
ws.projects.create(fields)                   -> Project
ws.projects.list()                           -> Project[]

ws.agent(agentId)                            -> Agent           // no request
agent.conversation(conversationId)                    -> Conversation     // no request
ws.project(projectId)                        -> Project         // no request
ws.account(accountId)                        -> Account         // no request; any account, the agent creator's or an end user's
ws.knowledgeBase(key)                        -> KnowledgeBase   // no request
ws.endUser(endUserId)                        -> EndUser         // no request

// APPS: every app the platform can connect, as a constant. A plain string works too.
App.GMAIL                                    -> 'gmail'         // App.GOOGLE_CALENDAR -> 'google-calendar'

// ACCOUNTS: one saved sign-in to one app. The agent creator's live in the workspace.
ws.accounts.connect({ app, nickname? })      -> { account, url } // send them to url
ws.accounts.list({ app? })                   -> Account[]       // each with usedBy: the agents that use it
ws.apps.tools(app)                           -> AppTool[]       // an app's own tools: { id, name, summary, deletes? }
account.get()                                -> AccountData     // a missing account throws account_not_found
account.waitUntilConnected({ timeoutMs? })   -> Account         // connected, and not reconnecting
account.reconnect()                          -> { url }         // every agent that names it follows
account.rename(nickname)                     -> Account
account.check()                              -> AccountData
account.disconnect()                         -> { agents }      // always allowed; names the agents that lose it

// AN END USER'S OWN ACCOUNTS: each person signs in once, remembered for them
endUser.accounts.connect({ app })            -> { account, url }
endUser.accounts.list()                      -> Account[]
endUser.accounts.disconnectAll()             -> { disconnected }

// WHAT THIS WORKSPACE HAS LEFT TO SPEND
ws.credits()                                 -> { credits, standardTurnsRemaining, premiumTurnsRemaining }
ws.plan()                                    -> SeatPlan | null

// AN AGENT
agent.start({ endUserId, version?, parameters?, attach?, secrets?, secretsExpireIn? }) -> Conversation
agent.update(fields)                         -> Agent         // publishes a version
agent.get({ version? })                      -> Agent
agent.versions()                             -> Agent[]
agent.delete()                               -> void
agent.conversations({ limit?, endUser?, before?, of? }) -> Conversation[]   // of: 'customers' | 'team'

agent.setWorkflow(workflow)                  -> Agent         // publishes a version
agent.removeWorkflow(name)                   -> Agent         // publishes a version
agent.setTriggers(triggers)                  -> Agent         // publishes a version
agent.rotateWebhookUrl()                     -> string        // new webhook URL; the old one stops working
agent.webhookDeliveries({ limit? })          -> WebhookDelivery[] // newest first: accepted, refused, repeat, test
agent.sendTestWebhook()                      -> { conversationId } // runs the agent on a sample event; a real turn
agent.websiteChat.get()                      -> WebsiteChat   // who can chat, the share link, the embed id
agent.websiteChat.access.get()               -> 'restricted' | 'anyone'  // new agents: 'restricted'
agent.websiteChat.access.set(access)         -> 'restricted' | 'anyone'  // 'anyone': anyone with the link
agent.websiteChat.allowedWebsites.get()      -> string[]      // websites a Restricted chat may be shown on
agent.websiteChat.allowedWebsites.set(list)  -> string[]      // replace it; a domain covers its subdomains

agent.knowledge.put(path, bytes)             -> FileEntry
agent.knowledge.get(path)                    -> Uint8Array
agent.knowledge.text(path)                   -> string
agent.knowledge.list(dir?)                   -> FileEntry[]
agent.knowledge.remove(path)                 -> void

// CONNECTIONS: a name, an app, and whose account. Plain objects of the same shapes work too.
Connection.creatorAccount(app, account, { name?, tools? }) -> Connection  // { name?, app, account: '<id>', tools? }
Connection.endUserAccount(app, { name?, tools? })    -> Connection    // { name?, app, endUser: true, tools? }
agent.connections.add(connection)            -> Agent         // publishes a version
agent.connections.change(name, { account } | { endUser: true }) -> Agent   // publishes a version
agent.connections.remove(name)               -> Agent         // publishes a version; the account stays
agent.connections.list()                     -> ConnectionStatus[]

agent.attach({ secrets })                    -> Agent         // secret values; accounts are never attached

// SECRETS: a value is written, never read back
ws.secrets.create({ name, value, agentId?, endUserId?, description?, allowedHosts? }) -> Secret   // value: 8 characters or more
ws.secrets.list(agentId?)                    -> Secret[]      // what exists, never the values
ws.secret(secretId)                          -> Secret        // no request
secret.update(value)                         -> Secret        // attached conversations use it on their next call
secret.delete()                              -> void          // attached conversations stop using it at once
secret.setAllowedHosts(hosts)                -> Secret        // replaces the list of hosts it may be sent to; [] unpins
secret.allowedHosts                          -> string[]      // as last read
endUser.secrets.create({ name, value, agentId?, description?, allowedHosts? }) -> Secret   // this person's own
endUser.secrets.list()                       -> Secret[]

// A CONVERSATION, FROM AN AGENT OR A PROJECT
conversation.ask(text, { attachments?, secrets?, secretsExpireIn? }) -> Turn   // secrets: fresh values, in force before the agent reads it
conversation.messages()                           -> Message[]
conversation.end()                                -> void
conversation.attach(secret | { secrets }, { expiresIn? }) -> Conversation   // secrets only; a value lasts 1 hour by default
conversation.accountsNeeded()                     -> AccountNeeded[] // end user's accounts still missing, with links
conversation.files(dir?)                          -> FileEntry[]
conversation.readFile(path)                       -> string
conversation.readFileBytes(path)                  -> Uint8Array
conversation.writeFile(path, bytes)               -> FileEntry
conversation.removeFile(path)                     -> void
conversation.fileUrl(path, { download?, ttl? })   -> string

// A PROJECT
project.start({ parameters? })               -> Conversation
project.update(fields)                       -> Project
project.get()                                -> Project
project.addAgent(agentId)                    -> Project
project.removeAgent(agentId)                 -> Project
project.archive()                            -> Project
project.delete()                             -> void
project.conversations()                           -> Conversation[]
project.files(dir?)                          -> FileEntry[]
project.readFile(path)                       -> string
project.fileUrl(path, { download?, ttl? })   -> string

// HELPERS
loadWorkflows(dir)                           -> Workflow[]

ws.agent(id), ws.project(id) and ws.endUser(id) make no network call. You get something you can call methods on, and the first request happens when you call one. A returned object carries methods, so store the id and use ws.agent(id) to come back in.

Disconnecting an account

account.disconnect() signs the account out and removes it here. It is always allowed, and it hands back the agents that used it. From their next turn those connections say the account was disconnected, and none of them switches to another account in its place.

const [gmail] = await ws.accounts.list({ app: App.GMAIL });
const { agents } = await gmail.disconnect();
// [{ id: 'inbox-helper', agentName: 'Inbox helper', connectionName: 'work-gmail',
//    published: true, inDraft: false }]

Removing a connection from an agent is the other operation, and it never touches the account: agent.connections.remove(name).

disconnect first, if you want different permissions

Providers remember that you already approved the application. Sign in again on the same account with account.reconnect() and many of them skip the consent screen entirely and reissue exactly what you had before, including anything the person declined the first time.

This is not hypothetical. Google shows some permissions as separate checkboxes, so a person can approve an app and leave half of it unticked. The account then reports connected and looks healthy, and the gap only appears later as a 403 from the part that was never granted. Disconnecting and connecting again is what brings the consent screen back; then change the connections that should use the new account.

What is left to spend

A turn is charged against a prepaid balance, so this is the call that answers “can I still run something?”.

const { credits, standardTurnsRemaining, premiumTurnsRemaining } = await ws.credits();
const plan = await ws.plan();   // null when the workspace is on no seat plan

The two turn counts are what the balance BUYS, which is the useful number: each grade has its own price per turn, so the same balance is worth a different number of turns at each. On plan(), seats: null means negotiated rather than unlimited, and usdCentsPerMonth: null means there is no published price, which is a different fact from a price of zero.

Types

type Workspace = { id: string; name: string; credits: number; createdAt: string };

type Agent = {
  id:           string;
  name:         string;
  instructions: string;
  greeting?:    string;
  grade:        'lite' | 'standard' | 'premium' | 'best';
  filesystem:   'drive' | 'none';
  tools:        string[];
  widgets:      boolean;
  workflows:    Workflow[];
  connections:  AgentConnection[];
  secrets:      SecretRequirement[];
  parameters:   Parameter[];
  triggers:     Trigger[];
  mcpServers:   McpServer[];
  httpTools:    HttpTool[];
  version:      number;
  createdAt:    string;
  updatedAt:    string;
};

type Workflow = { name: string; when: string; content: string };

type Trigger =
  | { kind: 'chat' }
  | { kind: 'schedule'; cron: string; timezone?: string; message: string }
  | { kind: 'interval'; seconds: number; maxRuns?: number; message: string }
  | { kind: 'webhook';  message?: string; url?: string };

// One connection in the setup: a name, an app, and whose account. Exactly one of the two.
// tools: which of the app's own tools it may use; absent is the app's default, [] is none.
type AgentConnection =
  | { name: string; app: AppId; account: string; tools?: string[] }   // the agent creator's account, by id
  | { name: string; app: AppId; endUser: true; tools?: string[] };    // each end user's own

// What you send: Connection.creatorAccount() / Connection.endUserAccount() build it.
type Connection =
  | { name?: string; app: AppId; account: Account | string; tools?: string[] }
  | { name?: string; app: AppId; endUser: true; tools?: string[] };
type AppId      = (typeof App)[keyof typeof App] | (string & {});   // App.GMAIL is 'gmail'; any app id works
type SecretRequirement = { name: string; scope: Scope; required?: boolean };   // what an agent needs; a stored Secret holds the value
type Scope      = 'fixed' | 'conversation';   // secrets only; 'conversation' comes only from the conversation

// One saved sign-in to one app.
type AccountData = {
  id: string;
  app: AppId;
  label: string | null;      // who it is signed in as, once someone has
  nickname: string | null;
  status: 'pending' | 'connected' | 'needs_sign_in' | 'error';
  reconnecting: boolean;     // a reconnect waits for the person; the account still works
  owner: 'workspace' | 'end-user';   // every account call works on either
  endUserId?: string;
  connectedAt: string | null;
  lastCheckedAt: string | null;
  usedBy: UsedBy[];          // the agents that name it
};

type UsedBy        = { id: string; agentId: string; agentName: string; connectionName: string; inDraft: boolean; published: boolean };   // id is Agent.id; agentId is the internal agt_ handle
type AccountNeeded = { name: string; app: AppId; url: string };

type Parameter = {
  name: string;
  type: 'string' | 'number' | 'date' | 'choice';
  required?: boolean;
  default?: string;
  choices?: string[];
};

type McpServer = {
  name: string;
  url: string;
  transport?: 'http' | 'sse';
  headers?: Record<string, string>;
  timeoutMs?: number;
};

type HttpTool = {
  name: string;
  description: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
  url: string;
  headers?: Record<string, string>;
  body?: string;
  params?: Record<string, HttpToolParam>;
  secrets?: string[];
  timeoutMs?: number;
};

type ConnectionStatus = {   // from agent.connections.list()
  name: string;
  app: AppId;
  account?: AccountData;     // the agent creator's account, read fresh
  endUser?: true;            // each end user's own
  status: string;            // whether it works now
  inDraft: boolean;
  removedInDraft: boolean;
};

type AgentConversation = {
  id: string; agentId: string; version: number;
  startedAt: string; title?: string;
  ready: boolean; requires: Requirement[];
};

type ProjectConversation = {
  id: string; projectId: string;
  startedAt: string; title?: string;
  ready: boolean; requires: Requirement[];
};

type Project = {
  id: string;
  name: string;
  goal: string | null;
  managerInstructions: string | null;
  grade: 'lite' | 'standard' | 'premium' | 'best';
  status: 'active' | 'archived';
  agents: ProjectAgent[];
  createdAt: string;
  updatedAt: string;
};

type ProjectAgent = { agentId: string; name: string; addedAt: string };   // an AGENT, not a person
type Requirement = { kind: 'secret' | 'connection'; name: string; required: boolean };

type Turn = Promise<string> & AsyncIterable<TurnEvent>;

type TurnEvent =
  | { type: 'start';    turnId: string }
  | { type: 'progress'; steps: Step[] }
  | { type: 'tool';     name: string; state: 'start' | 'end'; label?: string; ok?: boolean }
  | { type: 'text';     text: string }
  | { type: 'file';     path: string }
  | { type: 'widget';   widget: Widget }
  | { type: 'credential_needed'; name: string; reason: 'missing' | 'expired' }   // at the start of a turn
  | { type: 'done';     reply: string; widgets?: Widget[] }
  | { type: 'error';    message: string; retryable: boolean };

type Step = {
  id: string;
  label: string;
  kind: 'read' | 'write' | 'edit' | 'search' | 'shell'
      | 'web' | 'data' | 'task' | 'plan' | 'other';
  status: 'active' | 'done' | 'error';
};

type Message    = { role: 'user' | 'assistant'; content: string; at: string; widgets?: Widget[] };
type Widget     = { surfaceId: string; at: number; components: WidgetPart[]; closed: boolean };
type WidgetPart = { id: string; component: string; [prop: string]: unknown };  // A2UI v0.9, AgentNava's set of parts
type Attachment = { kind: 'image' | 'file'; name?: string; mimeType: string; data: string };

type FileEntry = {
  path: string;
  kind: 'file' | 'directory';
  mimeType: string;
  size: number;
  modifiedAt: string;
};

type AgentNavaError = {
  code: string;
  message: string;
  status: number;
  retryable: boolean;
  retryAfter?: number;
};

There are two kinds of conversation and they are two types. You know which one you hold, because you know which call returned it.

Errors

Every failure throws AgentNavaError.

import { AgentNavaError } from '@cerebro-labs/agentnava-sdk';

try {
  await conversation.ask('...');
} catch (err) {
  if (err instanceof AgentNavaError) {
    err.code;       // 'agent_not_found'
    err.status;     // 404
    err.retryable;  // false
    err.retryAfter; // seconds, on 429
  }
}
status
400Something in the request was wrong
401Missing or invalid key
404It does not exist, or it is not in your workspace
409Conflicts with current state, such as reusing an agent id
429Rate limited. retryAfter says how long
5xxOurs. retryable is true

One subclass carries extra fields: SecretHostNotAllowed (code secret_host_not_allowed, 400), thrown when a save would send a key to a host it is not pinned to. It adds secret, host and allowedHosts. See Where a key may be sent.

404 covers both "no such thing" and "not yours", so a caller probing ids cannot tell another customer's agent from one that never existed. There is no 403.

Branch on code, not on message

Codes are stable. Messages are written for a person reading a log and will be reworded. retryable is the only thing worth automating on: true for 5xx and 429, false for everything else, because a 400 will fail identically forever.

When you iterate a turn instead of awaiting it, a failure arrives as an error event and the loop ends.

Client options

AgentNava.getWorkspace();
AgentNava.getWorkspace({ apiKey, baseURL, timeout });
AgentNava.getWorkspace({ fetch: myFetch });
apiKeyDefaults to AGENTNAVA_API_KEY
baseURLDefaults to AGENTNAVA_BASE_URL, then https://api.agentnava.com
timeoutPer request, in milliseconds. Not applied to a streaming turn
fetchYour own fetch, for custom transport or authentication

Construction is synchronous and makes no request. The SDK does not retry, cache or queue on your behalf: a call maps to a request, and what comes back is what the API returned.

Pointing at a different deployment

Two ways, and the explicit option wins over the environment variable.

// a dedicated endpoint
const ws = AgentNava.getWorkspace({ apiKey, baseURL: 'https://acme.agentnava.example' });
# staging, which is where a test key works
AGENTNAVA_BASE_URL=https://api-staging.agentnava.com

A trailing slash is fine, and a base with a path prefix works too, for an API gateway sitting in front of us.

a test key against the production url says “invalid api key”

Which reads like a bad key rather than the wrong host, and that is the wrong thing to go and check. Staging is https://api-staging.agentnava.com. If you were given a test key, set it.