Skip to main content

Getting started

Install

npm install @agentshelf/external-agents-sdk

The package ships ESM (import), CJS (require), and TypeScript declarations. It depends on @agentshelf/agent-runtime-sdk-core, which provides the shared transport, streaming, and error primitives.

The bootstrap assertion

Before the SDK can open a session it needs a bootstrap assertion: a short string, produced and signed by your backend, that vouches for the current end user. The SDK does not create or sign it — it only asks for one, via the getBootstrapAssertion callback you supply.

browser ──▶ your backend ──▶ signed assertion
│ │
└──── SDK ──▶ AgentShelf ◀─────┘

Your backend endpoint is the trust boundary. Authenticate the user there, then mint the assertion. Never ship the credential that signs it to a client.

// Your server, e.g. GET /api/agentshelf/bootstrap-assertion
app.get('/api/agentshelf/bootstrap-assertion', requireLogin, async (req, res) => {
res.type('text/plain').send(await mintAgentShelfAssertion(req.user));
});

Create a client

import { createExternalAgentClient } from '@agentshelf/external-agents-sdk';

const client = createExternalAgentClient({
externalAgentRef: 'ext_agent_public_12345678',
apiBase: 'https://api.agentshelf.ai',
getBootstrapAssertion: async () => {
const response = await fetch('/api/agentshelf/bootstrap-assertion');
if (!response.ok) {
throw new Error('Could not obtain a bootstrap assertion');
}
return response.text();
},
});

Options

OptionRequiredNotes
getBootstrapAssertionyesReturns the assertion string, sync or async.
externalAgentRefPublic ref of the agent to bind to.
apiBase / apiBaseUrlRuntime base URL. Both spellings are accepted.
storageSession persistence adapter. Defaults to in-memory. See Sessions and storage.
storageKeyKey the session record is stored under.
transport / fetchOverride the HTTP layer, e.g. to add tracing headers.
onErrorCalled with a RuntimeSdkError for every failed call.

Full type: ExternalAgentClientOptions.

Open a session

ensure() obtains an assertion, creates a public session, and stores the session token. If a valid stored session already exists it reuses it and never calls getBootstrapAssertion.

const result = await client.ensure({
hostContext: {
page: 'Checkout',
},
});

if (result.kind === 'created') {
console.log(result.policy.defaultStreamProfile);
}
console.log(result.session.sessionRef);

ensure() returns a discriminated union — kind: 'stored' carries only the session, kind: 'created' also carries the freshly negotiated policy. Narrow on kind before reaching for policy. Pass forceRefresh: true to discard a stored session and mint a new one.

Host context

hostContext tells the agent where it is being used. For external agents it is a flat map of primitives — string, number, or boolean:

hostContext: {
page: 'Checkout',
cartTotal: 129.5,
isReturningCustomer: true,
}
Primitives only — nested values are rejected

The TypeScript signature is RuntimeHostContext | JsonObject, and the RuntimeHostContext arm describes a richer per-field shape ({ type, value, sourceTrustLevel, persistencePolicy, modelVisibilityPolicy }) used by other AgentShelf runtimes. The public external-agent API does not accept it.

The server keeps only string | number | boolean field values and rejects the request if anything was dropped, so a nested { type, value } object fails with 400 host_context_field_not_public even though it type-checks. Send flat primitives.

Four rules the server enforces, all against policy.hostContextPolicy:

RuleFailure
Host context must be enabled (maxTotalBytes > 0)403 host_context_not_allowed
Every key must appear in acceptedFields400 host_context_field_not_allowed
Values must be primitives, and strings must not look like internal refs400 host_context_field_not_public
Total JSON size must fit maxTotalBytes400 host_context_too_large

Key names must match ^[a-zA-Z0-9_.:-]{1,80}$. String values are also screened for internal-looking content — raw UUIDs, gs:// or s3:// URIs, and substrings like bucket or /workspaces/ — and a value caught by that screen trips the same host_context_field_not_public error.

Read the accepted field list before sending anything:

const { hostContextPolicy } = await client.getEffectivePolicy();
console.log(hostContextPolicy.acceptedFields, hostContextPolicy.maxTotalBytes);

See Policy and capabilities.

Send a first message

const conversation = await client.createConversation({ title: 'Checkout support' });

const stream = await client.streamMessage({
conversationRef: conversation.conversationRef,
content: 'Why was my card declined?',
});

for await (const event of stream) {
if (event.type === 'message:delta') {
process.stdout.write(event.delta);
}
}

See Conversations and streaming for the full event set and for collectAssistantMessage, which reduces a stream to a single message when you do not need token-by-token rendering.

Handling errors

Every failure surfaces as a RuntimeSdkError with a stable code, an optional HTTP status, and a retryable flag:

import { RuntimeSdkError } from '@agentshelf/agent-runtime-sdk-core';

try {
await client.ensure();
} catch (error) {
if (error instanceof RuntimeSdkError && error.code === 'session_expired') {
await client.clearStoredSession();
}
}

Codes include unauthorized, forbidden, not_found, rate_limited, timeout, aborted, conflict, session_expired, capability_unsupported, policy_denied, stream_interrupted, network_error, and server_error. How much detail the message carries depends on the agent's publicErrorDetail policy.