@emergentic/client
v0.1.2
Published
TypeScript client for Emergentic simulations and agent chat.
Readme
@emergentic/client
TypeScript client for creating, attaching to, and chatting with Emergentic simulations from non-Python apps.
Use this package from a trusted server context. Do not expose an Emergentic service API key in browser JavaScript.
API documentation: https://emergentic.ai/api-docs
Install
pnpm add @emergentic/clientFor local handoff before publishing:
pnpm add file:../path/to/packages/emergentic-clientNext.js Route Handler Example
import { EmergenticClient } from "@emergentic/client";
const emergentic = new EmergenticClient({
baseUrl: process.env.EMERGENTIC_API_URL!,
token: process.env.EMERGENTIC_API_KEY!,
});
export async function POST(req: Request) {
const { message } = await req.json();
const session = await emergentic.createAndAttachChat({
worldId: Number(process.env.EMERGENTIC_WORLD_ID!),
name: "Landing chat",
agentIds: [Number(process.env.EMERGENTIC_AGENT_ID!)],
start: true,
});
const stopHeartbeat = session.startHeartbeat();
try {
await session.sendMessage(message);
const logs = await session.getLogs({ limit: 20 });
return Response.json({
simulationId: session.simulation.id,
sessionId: session.sessionId,
logs,
});
} finally {
stopHeartbeat();
await session.stop();
}
}session.getLogs() uses the public REST API and returns log metadata only:
ids, action/agent/location ids, timestamps, and is_player. Use realtime
callbacks such as session.onConversationMessage() for live message payloads.
REST log and agent list calls default to 20 records per page and cap limit at
100; use afterId for cursor pagination.
Reuse an Existing Simulation
const session = await emergentic.attachSimulation({
simulationId: 123,
agentIds: [45],
start: true,
});
const stopHeartbeat = session.startHeartbeat();
try {
await session.sendMessage("Hi, can you introduce yourself?");
await session.addAgents([46]);
await session.sendMessage({
content: "Can you weigh in too?",
targetAgentIds: [46],
});
} finally {
stopHeartbeat();
}In chat mode, the simulation's selected_agent_ids are the default roster.
Use session.setAgents(), session.addAgents(), or session.removeAgents() to
change that roster. Use targetAgentIds on one message to target a subset
without changing the roster.
REST Resources
const world = await emergentic.createWorld({
name: "Harbor District",
width: 1600,
height: 1200,
});
const location = await emergentic.createLocation(world.id, {
name: "Studio Lobby",
description: "A public lobby where agents meet guests.",
});
const mira = await emergentic.createAgent(world.id, {
name: "Mira",
backstory: "A producer coordinating the studio opening.",
current_location_id: location.id,
});
const sol = await emergentic.createAgent(world.id, {
name: "Sol",
backstory: "A designer reviewing the guest experience.",
current_location_id: location.id,
});
const plot = await emergentic.createPlot(world.id, {
name: "Opening Schedule",
narrative_mode: "schedule",
schedule_state: {
entries: [
{
title: "Lobby walkthrough",
start_time: "2026-05-16T18:00:00Z",
end_time: "2026-05-16T18:30:00Z",
location_id: location.id,
description: "Prepare the lobby walkthrough.",
},
],
},
});
const simulation = await emergentic.createSimulationRecord(world.id, {
name: "Harbor District walkthrough",
plot_id: plot.id,
mode: "chat",
agent_ids: [mira.id, sol.id],
});
const questionJob = await emergentic.questionAgentPair({
question: "Where do these agents disagree?",
agent_a_id: mira.id,
agent_b_id: sol.id,
simulation_id: simulation.id,
});
const recommendations = await emergentic.getAgentRecommendations(world.id, {
simulationId: simulation.id,
agentName: "Mira",
limit: 5,
});
const analysis = await emergentic.getAgentAnalysisSummary(
simulation.id,
mira.id,
{
topLimit: 5,
timelineLimit: 12,
recommendationLimit: 3,
},
);
if (recommendations.status === "missing") {
await emergentic.refreshAgentRecommendations(world.id, {
simulation_id: simulation.id,
top_k: 5,
});
}
async function waitForQuestionJob(jobId: number) {
for (;;) {
const job = await emergentic.getQuestionJob(jobId);
if (job.status === "succeeded") return job.result;
if (job.status === "failed") {
throw new Error(job.error_message ?? "Question failed.");
}
await new Promise((resolve) => setTimeout(resolve, 2500));
}
}
const answer = await waitForQuestionJob(questionJob.job_id);End A Simulation
const simulation = await emergentic.startSimulation(session.simulation.id);
console.log(simulation.status);
const stopHeartbeat = session.startHeartbeat();
try {
await session.sendMessage("Wrap up the scene.");
} finally {
stopHeartbeat();
await session.stop();
}Realtime Updates
const unsubscribe = session.onConversationMessage((message) => {
console.log(message.entry);
});
await session.sendMessage("What should I explore first?");Call unsubscribe() when the request or UI lifecycle ends.
Rate Limits
Public REST endpoints allow 120 requests per minute per API key or client IP.
Question submission endpoints additionally allow 30 requests per minute because
they enqueue LLM-backed jobs. Polling question jobs counts against the general
REST limit, not the question submission limit. Rate-limited requests return 429 with a
Retry-After header.
API Docs Smoke Test
EMERGENTIC_API_URL=http://localhost:8000 \
EMERGENTIC_API_KEY=... \
npm --prefix packages/emergentic-client run smoke:api-docsUse --base-url https://emergentic.ai and --api-key ... to target prod.
The default smoke mode is mostly read-only and skips LLM-backed question jobs.
Use --include-questions to queue and poll question jobs. Use
--mode full --include-realtime when you want create/update fixture coverage and
Socket.IO lifecycle coverage before a load test. Full mode can leave created
world, agent, location, action, and simulation records.
