@agent-os-lab/agent-sdk
v0.2.1
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.
Local Demo
Run the SDK demo page from this package directory:
bun run demoOpen http://127.0.0.1:4177. The demo is a localhost-only Bun server that uses AgentServiceServerClient behind a local BFF-style endpoint to test tenant reads, Agents, tool providers, tool drafts, tool connectivity, tools, and tool invocations. The page stores the Agent Service URL, service API key, tenant ID, and subtenant ID in browser localStorage for local convenience; it does not persist provider bearer tokens, import the browser client with a service key, or call Agent Service directly from browser code.
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",
});Office Game Config From A Tenant BFF
Use the server client to read and save the Office configuration for an authorized subtenant. Agent SDK owns the AgentOS HTTP operation; Game SDK owns the concrete configuration type.
import { AgentServiceServerClient } from "@agent-os-lab/agent-sdk/server";
import type { AgentGameOfficeConfig } from "@agent-os-lab/agent-game-sdk/office";
const officeClient = new AgentServiceServerClient({
baseUrl: process.env.AGENTOS_BASE_URL!,
apiKey: process.env.AGENTOS_API_KEY!,
subtenantId: authorizedOrgId,
requestId: () => crypto.randomUUID(),
});
const result = await officeClient.getOfficeGameConfig<AgentGameOfficeConfig>();
if (result.source !== "none") {
await officeClient.saveOfficeGameConfig<AgentGameOfficeConfig>(result.config);
}The application must authenticate the browser user and authorize authorizedOrgId before using it as the trusted subtenant scope. Browser code calls the application's same-origin BFF; it never receives the AgentOS API key. See USAGE.md for the response contract and endpoint details.
When neither a scoped configuration nor a stored platform default exists, AgentOS returns HTTP 200 with source: "none" and config: null. Handle that branch before reading config.
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.
When the chat belongs to a tenant customer, team, or account, create the SDK client with the same subtenantId used when the session was created. AG-UI session reads are scoped by tenant plus subtenant; omitting subtenantId reads the tenant root scope and will not find subtenant sessions.
const chat = browserClient.createAgUiChat({
agentId: "support-agent",
profileId: "business-user-123",
getAttachmentIds: () => pendingAttachmentIds,
getCallableAgentIds: () => currentUserCallableAgentIds,
});
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.
For first-screen bootstrap, prefer summary calls before loading transcript detail:
const latest = await chat.getLatestSessionSummary();
const history = latest ? await chat.getSession(latest.sessionId) : null;chat.listSessions({ limit: 1 }) and chat.getLatestSessionSummary() return compact summaries without messages. chat.getSession(sessionId) returns full detail with user-visible transcript messages; internal context compaction summaries are kept for model continuity but are not returned as normal chat messages. Session summaries and full session responses include compression metadata when available: compactionCount is the materialized number of completed compression continuations before that session, contextWindowUsage is the latest measured context-window usage, and contextWindowUsage.compressionWindowProgress contains the computed compression trigger progress (usedTokens, denominatorTokens, usedPercent, and remaining values). lastCompactionTrace is present on full session detail when a recent compression trace exists.
Persisted history messages include messageReference:
const { session } = await chat.getSession(sessionId);
const selectedMessageIds = session.messages
.map((message) => message.messageReference?.persistedMessageId)
.filter((messageId): messageId is string => Boolean(messageId));Use getSessionMessagesByIds when you already have persisted message ids and only need those messages. This avoids loading the full session transcript and filtering in application code.
const selected = await chat.getSessionMessagesByIds(sessionId, selectedMessageIds);During streaming, AgentOS may emit a CUSTOM AG-UI event named agentos.message.persisted. Its value maps the current visible runtime message id to the durable persisted message id:
{
type: "message.persisted",
runtimeMessageId: "client-message-id",
persistedMessageId: "agentos-message-id"
}Use this event as the persistence acknowledgment before treating a streamed message as safe to reference or reload by id.
AgentOS emits this acknowledgment for persisted visible user and assistant messages. For assistant replies, runtimeMessageId matches the AG-UI message id currently visible to the client; persistedMessageId is the AgentOS id to store for durable references.
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({
modelContext: { userName: "唐子君" },
toolContext: { businessUserId: "user-123" },
});Use modelContext for hidden model-visible user context. Use toolContext only for opaque data that AgentOS tools need.
Browser AG-UI agents automatically forward the browser's IANA timezone through
forwardedProps.agentos.toolRuntimeContext.timezone, using
Intl.DateTimeFormat().resolvedOptions().timeZone. Pass
toolRuntimeContext.timezone explicitly to override it for one run.
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!,
subtenantId: "company-a",
});
await client.createProfile({
profileId: "business-user-123",
displayName: "Business User 123",
metadata: { source: "billing-app" },
});
await client.createAgent({
agentId: "support-agent",
displayName: "Support Agent",
systemPrompt: "You are a support assistant.",
model: "openai/gpt-5.2",
builtinMemoryEnabled: false,
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);
}
if (event.type === "tool.progress" && event.progress.kind === "call_agent.child_message_delta") {
process.stdout.write(event.progress.delta);
}
}Agent routing tools such as route_agents and call_agent are enabled by the trusted AgentOS runtime host, not by passing functions through SDK request bodies. The SDK sends ordinary HTTP/AG-UI requests; the service runtime injects its configured CallableAgentResolver when the run reaches AgentCore.
For per-user routing, pass the current user's allowed Agent ids with callableAgentIds or configure getCallableAgentIds on createAgUiChat; AgentOS validates tenant scope and a2aCallable before exposing route_agents / call_agent.
builtinMemoryEnabled defaults to true when omitted. Set it to false to fully disable AgentOS built-in memory for the Agent: memory prompts, memory tools, provider reads/writes, review, and flush are skipped. Existing stored memory is not deleted. Session history, session_search, wiki, runtime tools, schedules, A2A, and compression still work. When disabled, AgentOS normalizes memoryProvider to "none" and memoryReviewEnabled to false.
Subtenant Usage And Export
Set subtenantId on AgentServiceServerClient when a tenant application needs to isolate resources and billing for one of its own customers, teams, or accounts. Requests made by that client create and read resources inside the subtenant scope, and run/LLM usage is attributed to that same subtenant.
const customerClient = new AgentServiceServerClient({
baseUrl: "https://agent-service.example.com",
apiKey: process.env.AGENT_SERVICE_API_KEY!,
subtenantId: "company-a",
});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 getUsageSummary for tenant or subtenant aggregate run usage without pulling billing events or listing Agents first. Add granularity: "day" when you also need sparse UTC daily buckets for the selected window:
const { usage, series } = await customerClient.getUsageSummary({
from: "2026-05-01T00:00:00.000Z",
to: "2026-05-31T23:59:59.999Z",
granularity: "day",
});
console.log(usage.runs, usage.completedRuns, usage.estimatedCost);
console.log(series?.[0]?.date, series?.[0]?.runs);Use getUsage({ agentId }) for a single Agent. Use getUsageByAgents when a page needs per-Agent totals for multiple Agents:
const { usageByAgentId } = await customerClient.getUsageByAgents({
agentIds: ["support-agent", "sales-agent"],
from: "2026-05-01T00:00:00.000Z",
to: "2026-05-31T23:59:59.999Z",
});For Agent card pages that also need resource badges, use getAgentResourceCountsByAgents instead of listing each Agent's tools, skills, schedules, and Wiki pages:
const { countsByAgentId, missingAgentIds } = await customerClient.getAgentResourceCountsByAgents({
agentIds: ["support-agent", "sales-agent"],
});
console.log(countsByAgentId["support-agent"]?.skillCount);
console.log(countsByAgentId["support-agent"]?.defaultToolPermissionCount);
console.log(countsByAgentId["support-agent"]?.activeScheduleCount);
console.log(countsByAgentId["support-agent"]?.knowledgePageCount);
console.log(missingAgentIds);Use getLlmUsage when you need model cost totals and recent generation records:
const { usage } = await customerClient.getLlmUsage({
from: "2026-05-01T00:00:00.000Z",
to: "2026-05-31T23:59:59.999Z",
status: "billed",
limit: 100,
});
console.log(usage.totalBilledCost, usage.currency);
console.log(usage.recentGenerations);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",
builtinMemoryEnabled: false,
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",
builtinMemoryEnabled: false,
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",
displayName: "Customer CRM",
baseUrl: "https://crm.example.com/agentos",
headers: { "x-provider-region": "us" },
auth: { type: "bearer", token: process.env.CRM_AGENTOS_TOKEN! },
});
// Omit auth for providers that do not need bearer authentication.
await client.upsertTool(provider.providerId, "profile_completion.update", {
displayName: "Update profile completion",
description: "Update profile completion percentage.",
inputSchema: {
type: "object",
required: ["percent"],
properties: {
percent: { type: "number", minimum: 0, maximum: 100 },
},
additionalProperties: false,
},
executor: {
type: "http",
method: "PATCH",
path: "/tools/profile-completion/update",
timeoutMs: 5000,
headers: { "x-crm-feature": "profile-write" },
},
contextPolicy: { includeMessageContext: true, requireMessageContext: true },
resultPolicy: "hidden",
});
await client.setAgentTools("support-agent", {
tools: ["crm.profile_completion.update"],
});name and toolName are stable identifiers used for bindings and model tool names. displayName is optional UI text returned by read APIs; omit it or set it to null to display the stable identifier instead.
Use listToolProviderTools for tool-picker or bootstrap screens that need providers and their tools in one lightweight request:
const { providers } = await client.listToolProviderTools({
status: "enabled",
});This returns provider and tool summaries only. It does not include provider credentials, provider headers, tool input schemas, or executor configuration.
Generate draft tool definitions from an API document when you want a review step before registration:
const { drafts } = await client.createToolDrafts(provider.providerId, {
apiDocument: openApiOrMarkdownText,
endpointHints: ["PATCH /tools/profile-completion/update"],
maxDrafts: 5,
});
for (const draft of drafts) {
await client.upsertTool(provider.providerId, draft.toolName, {
displayName: draft.displayName,
description: draft.description,
inputSchema: draft.inputSchema,
executor: draft.executor,
contextPolicy: draft.contextPolicy,
resultPolicy: draft.resultPolicy,
status: draft.status,
});
}Draft generation is not persisted. Review the returned drafts before saving them; upsertTool validates the schema again. apiDocument is limited to 200,000 characters, endpointHints to 50 items of 2,000 characters each, and maxDrafts to 1-20.
Test a registered tool's connectivity from trusted backend code:
const { test } = await client.testTool(provider.providerId, "profile_completion.update", {
input: { percent: 80 },
context: { businessUserId: "user-123" },
});
console.log(test.ok, test.status, test.errorCode);This sends a real POST to the provider endpoint with x-agentos-test-call: true; provider handlers should avoid mutating production data for test calls.
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 shape, raw HTTP responses, error codes, token rotation, and business-side handler examples in USAGE.md.
MCP Servers
MCP management is server-only. Register remote Streamable HTTP MCP servers from trusted backend code, then bind them to Agents:
const { server } = await client.createMcpServer({
name: "crm",
displayName: "CRM MCP",
transport: {
type: "streamable-http",
url: "https://crm.example.com/mcp",
},
credential: {
type: "agentos-runtime-jwt-jwks",
audience: "crm-mcp",
},
});
await client.setAgentMcpServers("support-agent", {
servers: [{
mcpServerId: server.mcpServerId,
enabled: true,
toolNameFilter: { include: ["customer_search"], exclude: ["admin_delete"] },
}],
});At runtime, AgentOS calls MCP tools/list with a short-lived Bearer token. The token includes AgentOS runtime claims such as tenant, Agent, profile, MCP server, operation, and nested tool_context when SDK toolContext was supplied. The MCP server filters the returned tools according to its own permission model, so AgentOS only injects the allowed tools for that turn.
Tenant MCP servers verify AgentOS runtime JWTs through public metadata:
const discovery = await client.getMcpRuntimeJwtDiscovery();
const jwks = await client.getMcpRuntimeJwtJwks();Use agentos-runtime-jwt-jwks for tenant-facing MCP servers. The full guide covers discovery/JWKS claims, binding filters, and invocation diagnostics 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",
});
const latest = await browserClient.getLatestSessionSummary("support-agent", "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.");
}
const withMarkdown = await browserClient.getSessionAttachment(
"support-agent",
session.sessionId,
attachment.id,
"business-user-123",
{ includeMarkdown: true, includeDownloadUrl: true },
);
console.log(withMarkdown.attachment.markdown);
console.log(withMarkdown.attachment.downloadUrl);
await browserClient.sendMessage("support-agent", session.sessionId, {
profileId: "business-user-123",
message: "Analyze this file.",
attachmentIds: [attachment.id],
});Available methods:
createAttachmentUploadconfirmAttachmentUploadlistSessionAttachmentsgetSessionAttachment({ includeMarkdown: true }returns the converted Markdown,{ includeDownloadUrl: true }returns a short-lived original-file download URL)retrySessionAttachment
Converted Markdown is complete. By default attachment read APIs return only metadata. Agent Service rejects oversized attachment prompts instead of truncating them.
Wiki Overview And Lists
Use getWikiOverview for Agent detail pages or Wiki cards that need the Wiki title, small source/page/job panels, and status counters in one bounded request:
const overview = await client.getWikiOverview("wiki-a", {
sourceLimit: 10,
pageLimit: 10,
jobLimit: 5,
});
console.log(overview.counts?.pageCount);
console.log(overview.sources);Overview and list responses return summaries. Source and page summaries omit markdown; job summaries omit trace. Read a single source, page, or job detail only when the UI needs the detail payload.
SDK Wiki list methods use cursor pagination by default:
const wikiList = await client.listWikis({ page: 1, pageSize: 50 });
console.log(wikiList.totalCount);
let page = await client.listWikiSources("wiki-a", { limit: 20, status: "ready" });
while (page.nextCursor) {
page = await client.listWikiSources("wiki-a", {
cursor: page.nextCursor,
limit: 20,
status: "ready",
});
}listWikiSources and listWikiPages exclude archived rows unless a status filter is provided. listWikiJobs returns all job statuses unless filtered.
Use rebuildWiki to queue a full regeneration from the Wiki's current non-archived sources:
const { job } = await client.rebuildWiki("wiki-a");
console.log(job.jobType); // "rebuild"Wiki File Uploads
Use uploadConsoleFile when you need the converted Markdown. Use uploadAndCreateWikiSource when you want to upload a file, wait for conversion, and attach the converted file to a Wiki as a file-backed source in one SDK call. The Wiki source keeps the originating fileId; the SDK no longer sends the converted Markdown back in the create-source request.
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,
});
console.log(source.fileId);
const existingFileSource = await client.createWikiSourceFromFile("wiki-a", {
fileId: converted.id,
});
console.log(existingFileSource.source.id);
const removal = await client.deleteWikiSource("wiki-a", source.id);
console.log(removal.source.status); // "archived"
console.log(removal.job.jobType); // "source_removed"deleteWikiSource archives a ready or failed Wiki source and creates a source_removed Wiki job so generated pages can be updated without that source. Sources that are still uploaded or converting return a conflict response.
Lower-level file methods are also available:
createConsoleFileUploadconfirmConsoleFileUploadgetConsoleFileretryConsoleFilecreateWikiSourceFromFile
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" },
toolRuntimeContext: { timezone: "Asia/Shanghai" },
});
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);Use toolRuntimeContext.timezone for user-semantic date phrases such as
today, tomorrow, and yesterday in async runs. Browser AG-UI runs set this
automatically; backend-created runs should pass the user's IANA timezone when it
is known.
Memory
Memory APIs are server-only. Use readMemory for one profile's full memory state, and listMemories for lightweight paginated inspection across profiles for one Agent:
const { memories, nextCursor } = await client.listMemories("support-agent", {
limit: 50,
scope: "shared-profile",
});
for (const entry of memories) {
console.log(entry.profileId, entry.scope, entry.target, entry.content);
}Use clearAgentMemory to clear active private memory for an Agent. Pass profileId, scope, or target to narrow the operation. Clearing shared-profile memory requires profileId because that memory is shared across Agents for the same profile.
await client.clearAgentMemory("support-agent", {
profileId: "business-user-123",
scope: "agent-private",
});listMemories returns memory entry summaries only. It does not fetch session transcripts or session detail.
When using query, also pass profileId; cross-profile content search is intentionally not part of this lightweight list endpoint.
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. deleteWikiSource archives the source and queues a source_removed Wiki job. 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, tenant HTTP tools, 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.
0.2.0 Breaking Change
OfficeGameConfigResponse now includes { config: null, updatedAt: null, source: "none" }. Check source before reading config; AgentOS no longer substitutes a code-built Office default when persistence is empty.
