@agent-os-lab/agent-sdk
v0.1.24
Published
TypeScript SDK for the Hermes Memory Lab Agent Service `/api/v1` API.
Readme
Agent Service SDK
TypeScript SDK for the Hermes Memory Lab Agent Service /api/v1 API.
The SDK is intentionally HTTP-only. It does not import UI code, Next.js route handlers, server adapters, or AgentCore.
Use the server entrypoint for trusted backend code and the browser entrypoint for frontend conversation flows.
For the full integration guide, see USAGE.md.
Install
After publishing, install the SDK package:
npm install @agent-os-lab/agent-sdkUse the published entrypoints:
import { AgentServiceServerClient } from "@agent-os-lab/agent-sdk/server";
import { AgentServiceBrowserClient } from "@agent-os-lab/agent-sdk/browser";
import type { AgentRunEvent } from "@agent-os-lab/agent-sdk/types";Local source imports inside this repository use:
import { AgentServiceServerClient } from "./src/server";
import { AgentServiceBrowserClient } from "./src/browser";Publish
Publishing is controlled from this SDK package directory. The script bumps the SDK package version, builds the package, runs npm pack --dry-run, and then publishes to npm:
bun run sdk:publishThe default release is patch. Use --minor or --major when needed:
bun run sdk:publish -- --minorUse --dry-run to validate the next version and package contents without publishing. The script restores the previous version after a dry run.
If npm requires two-factor authentication, pass the one-time password with --otp:
bun run sdk:publish -- --otp 123456Runtime
- Node.js 20+ or a modern browser runtime with
fetch,Headers,Response, andReadableStream. - Runtime dependency:
@ag-ui/clientfor AG-UI chat adapters. - API keys must never be logged or sent to browsers.
Server Authentication
const client = new AgentServiceServerClient({
baseUrl: "https://agent-service.example.com",
apiKey: process.env.AGENT_SERVICE_API_KEY!,
requestId: () => crypto.randomUUID(),
});
const { tenant } = await client.getCurrentTenant();In production, tenant identity comes from the persisted API key row. tenantId is optional in the SDK and should only be supplied for development tenant switching or trusted internal tools:
const localClient = new AgentServiceServerClient({
baseUrl: "http://localhost:3000",
apiKey: "dev-service-key",
tenantId: "tenant-demo",
});Browser Authentication
Browser code must not receive AGENT_SERVICE_API_KEY. Use a same-origin business-project BFF/proxy or a short-lived scoped token minted by the business backend:
const client = new AgentServiceBrowserClient({
baseUrl: "/api/agent-service",
});const client = new AgentServiceBrowserClient({
baseUrl: "https://agent-service.example.com",
accessToken: async () => getScopedAgentToken(),
});accessToken is a business-application user token for the BFF/proxy, not an Agent Service API key. Do not pass tenant API keys, platform admin tokens, or any long-lived service credential to the browser client.
Caller-provided authorization and x-hermes-tenant-id headers are stripped by the browser client. Only accessToken may set a browser bearer token, and only when the business backend expects that browser token.
AG-UI Chat
Use createAgUiChat when building a production chat surface with AG-UI compatible clients such as assistant-ui. It wraps the public AG-UI chat contract for session list/create/read/delete, active-run recovery, and AG-UI agent creation. threadId is the AgentOS session ID.
const chat = browserClient.createAgUiChat({
agentId: "support-agent",
profileId: "business-user-123",
getAttachmentIds: () => pendingAttachmentIds,
});
const { session } = await chat.createSession();
const history = await chat.getSession(session.sessionId);
const active = await chat.getActiveRun(session.sessionId);
const agent = chat.createAgent({
threadId: session.sessionId,
});Use active.run?.runId as resumeRunId in the AG-UI runtime custom run config when reconnecting to an existing run.
Use the lower-level createAgUiAgent when you already own session/history management and only need the AG-UI transport.
const agent = browserClient.createAgUiAgent({
agentId: "support-agent",
threadId: session.sessionId,
profileId: "business-user-123",
getAttachmentIds: () => pendingAttachmentIds,
});
await agent.runAgent({
toolContext: { businessUserId: "user-123" },
});Trusted backend or BFF code can create the same AG-UI agent from the server client. The server client signs requests with the service API key and tenant scope configured on the client.
const agent = serverClient.createAgUiAgent({
agentId: "support-agent",
threadId: session.sessionId,
profileId: "business-user-123",
});To recover a running stream after refresh without the facade, call getAgUiActiveRun(agentId, sessionId, profileId) and pass the returned run ID through the AG-UI runtime's custom run config as resumeRunId.
Create Profile, Agent, And Stream
const client = new AgentServiceServerClient({
baseUrl: "https://agent-service.example.com",
apiKey: process.env.AGENT_SERVICE_API_KEY!,
});
await client.createProfile({
profileId: "business-user-123",
displayName: "Business User 123",
metadata: { source: "billing-app" },
});
await client.createAgent({
agentId: "support-agent",
displayName: "Support Agent",
billingSubjectId: "company-a/user-123",
systemPrompt: "You are a support assistant.",
model: "openai/gpt-5.2",
memoryProvider: "none",
memoryScopeMode: "both",
compressionEnabled: false,
memoryReviewEnabled: false,
});
const { bot } = await client.createBot({
displayName: "WeChat Support Bot",
agentId: "support-agent",
});
const { channelAccount } = await client.createBotChannelAccount(bot.botId, {
channelType: "wechat",
displayName: "Primary WeChat",
});
await client.refreshBotChannelQrCode(bot.botId, channelAccount.channelAccountId);
const { session } = await client.createSession("support-agent", {
profileId: "business-user-123",
});
for await (const event of client.streamMessage("support-agent", session.sessionId, {
profileId: "business-user-123",
message: "Help me understand my invoice.",
})) {
if (event.type === "message.delta") {
process.stdout.write(event.delta);
}
if (event.type === "tool.started") {
console.log("tool started", event.name);
}
}Billing Attribution And Export
Set billingSubjectId on Agents or Wikis when a tenant application needs cost attribution for one of its own business users, teams, or accounts. Per-request APIs that accept billingSubjectId can override the resource default for that request.
await client.createAgent({
agentId: "support-agent",
displayName: "Support Agent",
billingSubjectId: "company-a/user-123",
systemPrompt: "You are a support assistant.",
model: "openai/gpt-5.2",
memoryProvider: "none",
memoryScopeMode: "both",
compressionEnabled: false,
memoryReviewEnabled: false,
});
for await (const event of client.streamMessage("support-agent", session.sessionId, {
profileId: "business-user-123",
message: "Help me understand my invoice.",
billingSubjectId: "company-a/user-456",
})) {
// ...
}Pull billing events from a trusted backend with listBillingEvents. The cursor is afterSequence; poll until hasMore is false, then store nextSequence for the next polling cycle.
let afterSequence = loadLastBillingSequence();
while (true) {
const page = await client.listBillingEvents({
afterSequence,
limit: 1000,
});
await saveBillingEvents(page.events);
afterSequence = page.nextSequence;
if (!page.hasMore) {
await saveLastBillingSequence(afterSequence);
break;
}
}Use billingSubjectId to fetch one subject's ledger:
const page = await client.listBillingEvents({
billingSubjectId: "company-a/user-123",
limit: 1000,
});Create A Bot-Owned Agent And Wiki
For tenant product setup flows, use createBotBundle to create a Bot with its own Agent, optional Wiki, and optional channel account in one call. The SDK creates the Wiki first, passes the generated wikiId into Agent creation, creates the Bot bound to that Agent, then creates the channel account bound to the Bot.
const { bot, agent, wiki, channelAccount } = await client.createBotBundle({
wiki: {
displayName: "Support Knowledge",
description: "Knowledge used by the support Bot.",
},
agent: {
displayName: "Support Agent",
systemPrompt: "You are a support assistant.",
model: "openai/gpt-5.2",
memoryProvider: "none",
memoryScopeMode: "both",
compressionEnabled: false,
memoryReviewEnabled: false,
},
bot: {
displayName: "WeChat Support Bot",
context: { channel: "wechat" },
},
channelAccount: {
channelType: "wechat",
displayName: "Primary WeChat",
},
});Delete the same owned bundle with:
await client.deleteBotBundle(bot.botId);deleteBotBundle deletes Bot, Agent, and the Agent-bound Wiki. Use it only when those resources are owned by that Bot; for shared Agents or Wikis, call the lower-level delete APIs explicitly.
Scheduled Agent Tasks
Use schedules from a trusted backend to run an Agent on a one-time, interval, or cron schedule. Scheduled runs use a fresh execution session by default.
const { schedule } = await client.createSchedule("support-agent", {
profileId: "business-user-123",
name: "Daily support summary",
prompt: "Summarize yesterday's urgent support issues.",
schedule: "0 9 * * *",
timezone: "Asia/Shanghai",
sessionId: "session-a",
});
await client.pauseSchedule("support-agent", schedule.id);
await client.resumeSchedule("support-agent", schedule.id);
await client.runScheduleNow("support-agent", schedule.id);
const { fires } = await client.listScheduleFires("support-agent", schedule.id);
const { deliveries } = await client.listScheduleDeliveries("support-agent", schedule.id);Skills
Skills are human-authored SKILL.md procedures plus optional linked files under references/, templates/, scripts/, or assets/. The SDK manages Skills and Agent bindings; runtime execution reads Skills through AgentOS.
const { draft, validation } = await client.createSkillDraft({
intent: "Create a TypeScript backend code review skill.",
category: "engineering",
tags: ["review"],
});
if (!validation.ok) {
throw new Error(validation.errors.join("\n"));
}
const { skill } = await client.createSkill({
displayName: draft.displayName,
markdown: draft.markdown,
files: draft.files,
});Draft generation does not save anything. You can also create a Skill directly from a reviewed SKILL.md:
const { skill } = await client.createSkill({
displayName: "Code Review",
markdown: `---
name: code-review
description: Review code changes.
version: 1.0.0
metadata:
agentos:
category: engineering
tags: [review]
---
# Code Review
`,
files: [{
path: "references/checklist.md",
contentType: "text/markdown",
contentText: "# Checklist",
}],
});
await client.setSkillAgentBindings(skill.skill.skillId, {
agentIds: ["support-agent"],
});
const { skills: agentSkills } = await client.listAgentSkills("support-agent");
const { wiki } = await client.getAgentWiki("support-agent");Agent Groups And A2A
Agent Groups are server-only control-plane APIs. Put Agents in the same group, then mark target Agents callable.
await client.createAgent({
agentId: "crm-analyst",
displayName: "CRM Analyst",
systemPrompt: "Analyze CRM records.",
model: "openai/gpt-5.2",
memoryProvider: "none",
memoryScopeMode: "both",
compressionEnabled: false,
memoryReviewEnabled: false,
a2aCallable: true,
});
await client.createAgentGroup({
groupId: "sales",
displayName: "Sales",
});
await client.replaceAgentGroupAgents("sales", {
agentIds: ["support-agent", "crm-analyst"],
});
await client.appendAgentGroupAgents("sales", {
agentIds: ["quote-agent"],
});
await client.removeAgentGroupAgents("sales", {
agentIds: ["quote-agent"],
});
const { card } = await client.getA2aAgentCard("crm-analyst");
const result = await client.sendA2aMessage("crm-analyst", {
callerAgentId: "support-agent",
profileId: "business-user-123",
message: "Summarize this account.",
});An Agent can call another Agent only when both share at least one Agent Group and the target Agent has a2aCallable: true.
WeChat Bot Binding
Bot management is a server-only API. Create the Agent first, then bind a WeChat channel to a Bot:
import {
AgentServiceServerClient,
SERVICE_BOT_LOGIN_STATUS,
} from "@agent-os-lab/agent-sdk/server";
const client = new AgentServiceServerClient({
baseUrl: "https://agent-service.example.com",
apiKey: process.env.AGENT_SERVICE_API_KEY!,
});
const { bot, channelAccount } = await client.createWechatBotBinding({
displayName: "WeChat Support Bot",
agentId: "support-agent",
channelDisplayName: "Primary WeChat",
});
await client.refreshBotChannelQrCode(bot.botId, channelAccount.channelAccountId);
const { qrCodeText } = await client.waitForBotChannelQrCode(bot.botId, channelAccount.channelAccountId, {
timeoutMs: 30000,
intervalMs: 1000,
});
console.log("Open this WeChat login QR URL:", qrCodeText);
const { channelAccounts } = await client.listBotChannelAccounts(bot.botId);
const binding = channelAccounts.find((account) => account.channelAccountId === channelAccount.channelAccountId);
if (binding?.runtimeState?.loginStatus === SERVICE_BOT_LOGIN_STATUS.loggedIn) {
console.log("WeChat binding is logged in.");
}createWechatBotBinding does not accept a botId; Agent Service generates it. The QR code is returned as a URL string in qrCodeText, so render it as a link or open it in a new page.
Tenant HTTP Tools
Tool provider registration, tool definition, Agent binding, and invocation reads are server-only APIs:
const { provider } = await client.createToolProvider({
name: "crm",
baseUrl: "https://crm.example.com/agentos",
auth: { type: "bearer", token: process.env.CRM_AGENTOS_TOKEN! },
});
await client.upsertTool(provider.providerId, "profile_completion.update", {
description: "Update profile completion percentage.",
inputSchema: {
type: "object",
required: ["percent"],
properties: {
percent: { type: "number", minimum: 0, maximum: 100 },
},
additionalProperties: false,
},
executor: { type: "http", path: "/tools/profile-completion/update", timeoutMs: 5000 },
contextPolicy: { includeMessageContext: true, requireMessageContext: true },
resultPolicy: "hidden",
});
await client.setAgentTools("support-agent", {
tools: ["crm.profile_completion.update"],
});Send opaque business context per message when a tool needs to identify the business-side user:
await client.sendMessage("support-agent", session.sessionId, {
profileId: "business-user-123",
message: "Update this user's profile completion.",
toolContext: { businessUserId: "user-123" },
});Agent Service does not interpret the tool context object. It stores it with the user message and forwards it to tools according to each tool's contextPolicy.
Production tool setup also requires AGENTOS_USER_DATA_ENCRYPTION_KEY in Agent Service so provider credentials can be encrypted at rest. The full tools guide covers schema support, HTTP request/response envelopes, error codes, token rotation, and business-side handler examples in USAGE.md.
Frontend chat surfaces should use the browser client against a BFF/proxy:
const browserClient = new AgentServiceBrowserClient({
baseUrl: "/api/agent-service",
});
const { session } = await browserClient.createSession("support-agent", {
profileId: "business-user-123",
});
for await (const event of browserClient.streamMessage("support-agent", session.sessionId, {
profileId: "business-user-123",
message: "Help me understand my invoice.",
})) {
if (event.type === "message.delta") {
appendAssistantDelta(event.delta);
}
}Attachments
Attachments are uploaded directly from the browser to the Agent Service configured object store. When the upload is confirmed, Agent Service reads the object, calls file2md, and stores the converted Markdown during the confirm request. Send only converted attachment IDs with a message or queued run.
const created = await browserClient.createAttachmentUpload("support-agent", session.sessionId, {
profileId: "business-user-123",
filename: file.name,
contentType: file.type || "application/octet-stream",
sizeBytes: file.size,
});
await fetch(created.upload.url, {
method: created.upload.method,
headers: created.upload.headers,
body: file,
});
const { attachment } = (
await browserClient.confirmAttachmentUpload("support-agent", session.sessionId, created.attachment.id, {
profileId: "business-user-123",
})
);
if (attachment.status !== "converted") {
throw new Error(attachment.conversionError ?? "Attachment conversion failed.");
}
await browserClient.sendMessage("support-agent", session.sessionId, {
profileId: "business-user-123",
message: "Analyze this file.",
attachmentIds: [attachment.id],
});Available methods:
createAttachmentUploadconfirmAttachmentUploadlistSessionAttachmentsgetSessionAttachmentretrySessionAttachment
Converted Markdown is complete. Agent Service rejects oversized attachment prompts instead of truncating them.
Wiki File Uploads
Use uploadConsoleFile when you need the converted Markdown. Use uploadAndCreateWikiSource when you want to upload a file, convert it, and attach it to a Wiki in one SDK call.
import { uploadAndCreateWikiSource, uploadConsoleFile } from "@agent-os-lab/agent-sdk";
const converted = await uploadConsoleFile({
client,
file,
});
console.log(converted.markdown);
const { source } = await uploadAndCreateWikiSource({
client,
wikiId: "wiki-a",
file,
});Lower-level file methods are also available:
createConsoleFileUploadconfirmConsoleFileUploadgetConsoleFileretryConsoleFile
Async Runs
Async runs require a runtime worker process in the Agent Service environment.
const created = await client.createRun("support-agent", {
profileId: "business-user-123",
sessionId: session.sessionId,
message: "Summarize my open invoices.",
toolContext: { businessUserId: "user-123" },
});
let run = created.run;
while (run.status === "queued" || run.status === "running") {
await new Promise((resolve) => setTimeout(resolve, 1000));
run = (await client.getRun("support-agent", run.runId)).run;
}
const replay = await client.listRunEvents("support-agent", run.runId);Webhooks
const { webhook, secret } = await client.createWebhook({
url: "https://billing.example.com/hermes-webhooks",
eventTypes: ["run.completed", "run.failed", "run.cancelled"],
});The raw webhook secret is returned once. Store it securely and verify x-hermes-webhook-signature in the receiver. Do not log the secret.
Error Handling
import { AgentServiceError } from "@agent-os-lab/agent-sdk/server";
try {
await client.getAgent("missing-agent");
} catch (error) {
if (error instanceof AgentServiceError) {
console.error({
status: error.status,
code: error.code,
requestId: error.requestId,
details: error.details,
});
}
throw error;
}Request Options
Every SDK method accepts optional request options as the final parameter:
const controller = new AbortController();
await client.sendMessage("support-agent", "session-id", {
profileId: "business-user-123",
message: "Hello",
}, {
requestId: "business-request-123",
signal: controller.signal,
headers: {
"x-business-workflow-id": "workflow-123",
},
});The browser client accepts the same requestId, signal, and safe custom headers, but strips caller-provided authorization and x-hermes-tenant-id.
Delete Semantics
Delete operations archive or deactivate control-plane resources. deleteAgent and deleteProfile remove resources from normal SDK reads and writes, but historical sessions, runs, messages, memory audit data, and cost ledger records remain available to the platform for audit and retention. API key deletion revokes the key instead of removing its audit record.
Coverage
The server SDK covers tenant-scoped agent registry, profile registry, sessions, streaming, async runs, memory, session search, lineage, billing event export, and webhooks.
The browser SDK covers the current-user conversation surface: sessions, sync messages, streaming messages, async runs, run polling, cancellation, and run event replay.
