Agents API
List, retrieve, and invoke AI agents via the Console SDK. Agents are invoked through the OpenAI Responses API format.
What's new in 3.1.0
client.agents.sessions (start, list, read, update and delete sessions), responses.create({ session_id }), session files, approvals and questions, the session event log, and client.webhooks. See the Agent Sessions guide and the pages linked in Sessions, approvals, events, files and webhooks.
Quick Start
import { ConsoleClient } from '@cognipeer/console-sdk';
const client = new ConsoleClient({
apiKey: process.env.COGNIPEER_API_KEY!,
});
// Invoke an agent
const response = await client.agents.responses.create({
model: 'support-bot',
input: 'How do I reset my password?',
});
console.log(response.output[0]?.content[0]?.text);List Agents
Fetch all agents available to the current API token.
const agents = await client.agents.list();
console.log(agents);
// Filter by status
const activeAgents = await client.agents.list({ status: 'active' });Parameters
| Name | Type | Required | Description |
|---|---|---|---|
status | string | No | Filter: active, inactive, draft |
Return Type
interface AgentListItem {
key: string;
name: string;
description?: string;
config: {
modelKey: string;
temperature?: number;
topP?: number;
maxTokens?: number;
};
status: 'active' | 'inactive' | 'draft';
createdAt: string;
}Get Agent
Retrieve details for a specific agent.
const agent = await client.agents.get('support-bot');
console.log(agent.name, agent.config.modelKey);Parameters
| Name | Type | Required | Description |
|---|---|---|---|
agentKey | string | Yes | Unique agent key |
Files: agent.attachments
The single-agent lookup (not list()) also says whether the agent takes files, so a client can decide whether to show an attach button before it uploads anything:
const agent = await client.agents.get('support-bot');
if (agent.attachments?.enabled) {
console.log(agent.attachments.max_files, agent.attachments.max_file_size_mb, agent.attachments.allowed_mime_types);
if (!agent.attachments.accepts_images) console.log('Images would be replaced by a note: the model is text only');
}interface AgentAttachmentsInfo {
enabled: boolean; // files are on AND the agent's bucket can take a file now
max_files: number; // files one session holds (0 while off)
max_file_size_mb: number; // largest file (0 while off)
allowed_mime_types: string[]; // `image/*` wildcards allowed; empty = any
accepts_images: boolean; // the agent's model reads images
}Where the files live (bucket, folder, retention) is the agent's Configure → Files setting in the Console, not something a caller chooses. Files are off by default for an agent and need a bucket; with files off (or on with no bucket) an upload is refused with 403 attachments_disabled. attachments is absent on a Console that predates session files. See Session files.
Invoke Agent (Responses API)
Use client.agents.responses.create() to invoke an agent. The request format follows the OpenAI Responses API.
const response = await client.agents.responses.create({
model: 'support-bot',
input: 'What are your business hours?',
});
console.log(response.id); // "resp_64a1b2c3d4e5f6"
console.log(response.output); // Assistant response
console.log(response.usage); // Token usagePOST /api/client/v1/responses
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Agent key |
input | string | ResponseInputItem[] | Yes | User message, or message items (see Input parts) |
previous_response_id | string | No | Continue a conversation (resp_... or run_...) |
session_id | string | No | Continue a session (for example one from sessions.create()); same session as previous_response_id |
conversation | string | { id } | No | OpenAI-style alias of session_id |
version | number | No | Specific published version |
instructions | string | No | System prompt override |
temperature | number | No | Sampling temperature |
top_p | number | No | Top-p sampling |
max_output_tokens | number | No | Maximum output tokens |
runtime_context | RuntimeContext | No | Headers / metadata forwarded to the agent's downstream tool, MCP and connected-agent calls |
budget | { max_cost_usd, on_reach? } | No | Spend cap for the conversation this request creates; ignored on a continued session. See Session budget |
background | boolean | No | Queue the turn and return an AgentRun — see Background Runs |
callback_url / callback_secret | string | No | Background only — see Background Parameters |
Multi-Turn Conversations
Pass previous_response_id to continue a conversation:
const first = await client.agents.responses.create({
model: 'support-bot',
input: 'I have a billing question',
});
const followUp = await client.agents.responses.create({
model: 'support-bot',
input: 'Can you show me the pricing page?',
previous_response_id: first.id,
});session_id (alias conversation, a string or { id }) names the same conversation as previous_response_id (resp_<id>) in the session's own words. Give one, or several that agree: session_id or conversation naming a different session than previous_response_id is a 400 session_conflict (session_id and conversation disagreeing with each other is a plain 400 invalid_request), and a session of another agent or project is a 404 session_not_found. It is what you use to continue a session you started with sessions.create(), because that one has no response yet:
const session = await client.agents.sessions.create({ agent: 'support-bot' });
const reply = await client.agents.responses.create({
model: 'support-bot',
session_id: session.id,
input: 'I have a billing question',
});Input parts: files and images
input can be an array of message items whose content mixes text, files and images:
interface ResponseInputItem {
role: 'user' | 'system' | 'assistant';
content: string | Array<
| { type: 'input_text'; text: string }
| { type: 'input_file'; file_id?: string; file_data?: string; filename?: string }
| { type: 'input_image'; file_id?: string; image_url?: string | { url: string } }
>;
}file_idnames a file already uploaded to the conversation withsessions.files.upload(). The request must name that conversation (session_id,conversationorprevious_response_id).file_datais inline base64 or adata:URL, used when there is nofile_id; the Console stores it as a session file (filenamenames it).input_image.image_urlis adata:URL or anhttp(s)URL the Console fetches (public hosts only).- Sending files to an agent that takes none is a
403 attachments_disabled.
Response Type
interface AgentResponse {
id: string; // "resp_<conversationId>"
object: 'response';
model: string; // the agent key
output: ResponseOutputMessage[];
status: 'completed' | 'failed' | 'incomplete' | 'requires_action';
incomplete_details?: { reason: string } | null; // e.g. { reason: 'max_cost' }
stop_reason?: string; // 'completed' | 'limit' | 'cancelled' | 'requires_action' | ...
stop_detail?: string; // the runtime's own wording, e.g. which limit
required_action?: { // present when status is 'requires_action'
type: 'agent_actions';
actions: PendingAction[];
};
usage: { input_tokens: number; output_tokens: number; total_tokens: number };
created_at: number; // unix seconds
previous_response_id: string | null;
version: number | null; // published version used
}
interface ResponseOutputMessage {
id: string;
type: 'message';
role: 'assistant';
content: Array<{ type: 'output_text'; text: string }>;
}status is no longer limited to completed / failed (3.1.0). An exhaustive switch over it needs the incomplete and requires_action cases.
incomplete— a run limit or a cancellation ended the turn before a final answer (seestop_reason).requires_action— the turn paused on a tool approval or a question.required_action.actionslists what to answer; answer withsessions.actions.resolve(). The conversation id is the responseidwithout itsresp_prefix.
Background Runs
Long agent turns (many tool calls, slow downstream services) can outlive proxies and HTTP timeouts. Pass background: true to queue the turn and get an AgentRun back immediately (202), then poll it or receive a callback.
const run = await client.agents.responses.create(
{
model: 'research-agent',
input: 'Compare these three vendors',
background: true,
callback_url: 'https://example.com/hooks/agent-run', // optional
callback_secret: process.env.AGENT_CALLBACK_SECRET, // optional, min 16 chars
},
{ idempotencyKey: 'vendor-compare-42' }, // optional, sent as Idempotency-Key
);
run.id; // "run_66f2..."
run.status; // "queued"
const done = await client.agents.runs.wait(run.id, {
pollIntervalMs: 2000, // default 1000
timeoutMs: 15 * 60_000, // default: wait until terminal
});
if (done.status === 'succeeded') {
console.log(done.result?.output[0]?.content[0]?.text);
console.log(done.result?.id); // "resp_..."
} else {
console.log(done.status, done.error); // failed | canceled, { type, message }
}Background Parameters
| Name | Where | Description |
|---|---|---|
background | body | true queues the turn and returns an AgentRun |
callback_url | body | Webhook the Console POSTs the terminal event to |
callback_secret | body | Min 16 chars; signs callbacks with HMAC-SHA256 |
idempotencyKey | 2nd argument | Idempotency-Key header. Background only — sending it on a synchronous call is a 400 |
Retrying an identical background request with the same idempotencyKey returns the original run instead of starting a second one. Reusing the key with a different request is a 409 idempotency_key_conflict.
Run Methods
await client.agents.runs.get('run_66f2...'); // current status
await client.agents.runs.cancel('run_66f2...'); // request cancellation
await client.agents.runs.wait('run_66f2...', { signal: controller.signal });wait()resolves with the run forsucceeded,failedandcanceled— and forrequires_action, where the run is paused on a tool approval or a question and polling would never end (run.result.required_action.actionslists what to answer withclient.agents.sessions.actions.resolve()) — checkstatus. It throwsAgentRunWaitTimeoutError(withlastRun) whentimeoutMselapses and the signal's reason whensignalaborts. Neither stops the run; callcancel()for that.cancel()is cooperative: the run it returns may still sayrunningfor about a second before it lands oncanceled. Canceling a finished run is a409witherrorCode === 'agent_run_already_terminal'.
runs.wait() changed in 3.1.0
It now also returns when a run is requires_action. Code that assumed "returned means finished" must check run.status. isAgentRunTerminal() is unchanged (requires_action is not terminal); the new isAgentRunSettled() is true for terminal statuses and requires_action.
Continuing the Conversation
Either the run id (run_...) or its result id (resp_...) works as previous_response_id:
const next = await client.agents.responses.create({
model: 'research-agent',
input: 'Now summarize it in three bullets',
previous_response_id: done.id,
});AgentRun Type
// `requires_action` is not terminal: the run continues once the actions are resolved.
type AgentRunStatus = 'queued' | 'running' | 'requires_action' | 'succeeded' | 'failed' | 'canceled';
interface AgentRun {
id: string; // "run_..."
object: 'agent.run';
status: AgentRunStatus;
stop_reason?: string | null; // 'end_turn' | 'limit' | 'requires_action' | 'cancelled' | 'error' | ...
agent?: string;
conversation_id?: string;
result?: AgentResponse | null; // when succeeded, or the requires_action response
error?: { type: string; message: string | null } | null; // when failed/canceled
created_at: number;
started_at?: number | null;
completed_at?: number | null;
cancel_requested_at?: number | null;
max_duration_ms?: number | null;
callback?: { url: string; status: string | null; attempts: number; signed: boolean } | null;
}The 202 from create({ background: true }) is already the full run (stop_reason, agent, conversation_id, callback... with result: null and error: null until it finishes); runs.get() returns the same shape.
Error codes
Errors are CognipeerAPIErrors; branch on errorCode (the body's error.code) with isAgentRunErrorCode:
import { AgentRunErrorCodes, isAgentRunErrorCode } from '@cognipeer/console-sdk';
try {
await client.agents.responses.create({ model: 'research-agent', input: '...', previous_response_id: prev });
} catch (error) {
if (isAgentRunErrorCode(error, AgentRunErrorCodes.Conflict)) {
// 409: a run is already queued/running on this conversation
} else if (isAgentRunErrorCode(error, AgentRunErrorCodes.SyncTimeout)) {
// 504: synchronous turn hit the server ceiling — retry with background: true
} else if (isAgentRunErrorCode(error, AgentRunErrorCodes.ConcurrencyLimit)) {
// 429: too many background runs in flight for this tenant
}
}errorCode | Status | Meaning |
|---|---|---|
agent_run_conflict | 409 | A run is already active on this conversation (sync or background) |
timeout | 504 | Synchronous turn exceeded the server ceiling. Tool side effects may have happened, so the SDK does not retry it |
agent_run_concurrency_limit | 429 | Tenant's concurrent background-run limit reached |
idempotency_key_conflict | 409 | Idempotency-Key reused with a different request |
idempotency_key_sync_not_supported | 400 | Idempotency-Key sent without background: true |
agent_run_already_terminal | 409 | cancel() on a finished run |
Session, approval, file and budget codes (session_not_found, action_not_pending, file_too_large, budget_reached, …) are listed on Agent error codes.
Verifying Callbacks (Node.js)
When the run finishes, the Console POSTs JSON to callback_url with:
X-Cognipeer-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(callback_secret, "<t>.<raw body>")>X-Cognipeer-Event: agent_run.succeeded | agent_run.failed | agent_run.canceled | agent_run.requires_actionX-Cognipeer-Event-Id— stable per run + event; deliveries are retried, so dedupe on it
verifyWebhook (alias: verifyAgentRunCallback) lives in the Node-only @cognipeer/console-sdk/webhooks entry point (it uses node:crypto, which the browser-safe main entry avoids). Webhook subscriptions (client.webhooks) are signed with the same scheme. Always verify the raw request body:
import express from 'express';
import { verifyWebhook } from '@cognipeer/console-sdk/webhooks';
import type { AgentRunCallbackEvent } from '@cognipeer/console-sdk';
app.post('/hooks/agent-run', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifyWebhook({
rawBody: req.body, // Buffer
signatureHeader: req.get('X-Cognipeer-Signature'),
secret: process.env.AGENT_CALLBACK_SECRET!,
toleranceSeconds: 300, // default
});
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8')) as AgentRunCallbackEvent;
// event.event === 'agent_run.succeeded' → event.data.result
res.sendStatus(204);
});The callback body is an AgentRunCallbackEvent: { id, event, createdAt, runId, tenantId?, projectId?, conversationId?, data }, where data is { result } on success and on requires_action (the result carries required_action), { errorReason, message? } on failure, and {} on cancel. For the full signature scheme, retries and troubleshooting see Webhooks.
Sessions, approvals, events, files and webhooks
A turn can pause for a tool approval or a question (status: 'requires_action', or a run in requires_action). Everything around a conversation's runtime lives under client.agents.sessions, and event subscriptions under client.webhooks:
| You want to... | Call | Page |
|---|---|---|
| Start a session without sending a message, list, read, update the budget of, or delete sessions | sessions.create / list / get / update / delete | Agent sessions |
| Give the agent a file before or between messages, read it back | sessions.files.upload / list / content / contentStream / delete | Session files |
| Answer a tool approval or a question | sessions.actions.list / resolve | Approvals & questions |
| Page or follow the append-only event log | sessions.events.list / stream | Session events |
| Get called when a session idles, needs an answer or hits its budget | webhooks.* and verifyWebhook | Webhooks |
| Branch on an error | errorCode, AgentActionErrorCodes | Agent error codes |
Answering an approval is the Console's agent.approve permission (the person who started the session always may); a question follows session ownership. Managing webhooks is the Console's webhooks permission: webhooks.read lists endpoints and deliveries (with each URL's userinfo and query string removed, https://host/path?…), webhooks.admin changes anything and sees the full URL. A call without the permission answers 403 (forbidden, with a decisionId).
Legacy Chat Method
Deprecated
client.agents.chat() is deprecated. Use client.agents.responses.create() instead.
// Deprecated — avoid in new code
const result = await client.agents.chat('support-bot', {
message: 'Hello',
});An agent that pauses for an approval or a question answers with an empty content; the result then also carries status: 'requires_action' and pendingActions (resolve them with client.agents.sessions.actions.resolve()). A completed turn has neither.
See Also
- Agent Sessions guide
- Agents API (Gateway)
- Tools API — Tools that can be bound to agents
- Client Reference

