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
| Option | Required | Notes |
|---|---|---|
getBootstrapAssertion | yes | Returns the assertion string, sync or async. |
externalAgentRef | — | Public ref of the agent to bind to. |
apiBase / apiBaseUrl | — | Runtime base URL. Both spellings are accepted. |
storage | — | Session persistence adapter. Defaults to in-memory. See Sessions and storage. |
storageKey | — | Key the session record is stored under. |
transport / fetch | — | Override the HTTP layer, e.g. to add tracing headers. |
onError | — | Called 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,
}
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:
| Rule | Failure |
|---|---|
Host context must be enabled (maxTotalBytes > 0) | 403 host_context_not_allowed |
Every key must appear in acceptedFields | 400 host_context_field_not_allowed |
| Values must be primitives, and strings must not look like internal refs | 400 host_context_field_not_public |
Total JSON size must fit maxTotalBytes | 400 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);
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.