Agent bootstrap
Stand up agents with createAgent and joinConversation — no agents table, no mirror relations, no forum auto-creation.
Agent bootstrap follows a strict convention: forums are created separately, agents are actor + egress, and each forum join creates a monologue stint with ingestion and processor instances.
1. Create a chat room (forum)
Agents do not create forums. Use the Chat SDK or HTTP client:
import { createChatClient } from "@yoke/sdk";
const chat = createChatClient(ctx.conversations);
const room = await chat.createRoom({ title: "support" });
const human = await chat.createActor({ name: "alice" });
const humanP = await chat.addMember(room.id, human.id);
await chat.send(room.id, "Hello!", { authorParticipantId: humanP.id });Headless HTTP
For apps without a running actor context, use HTTP clients against the control plane:
import { createChatHttpClient, createAgentBootstrapHttpClient } from "@yoke/sdk";
const chat = createChatHttpClient({ baseUrl: controlPlaneUrl, tenantId });
const agents = createAgentBootstrapHttpClient({
baseUrl: `${controlPlaneUrl}/v1/tenants/${tenantId}/ingress`,
});
const room = await chat.createRoom({ title: "support" });
const agent = await agents.createAgent({ name: "support-bot" });
await agents.joinConversation(agent, room.id, { model: "gpt-4o" });2. Create an agent
import { createAgent } from "@yoke/sdk";
const agent = await createAgent(ctx, { name: "support-bot" });
// → { actorId, egressInstanceId }This creates:
- One
actorsrow withrole: "agent"(the persona) - One egress actor instance with config
{ agentActorId }— routes replies from any monologue stint back to forums
createAgent does not create a monologue. Monologues are created per forum join.
3. Join a chat room
import { joinConversation } from "@yoke/sdk";
const join = await joinConversation(ctx, agent, room.id, {
model: "gpt-4o", // optional, default "fixture-echo"
});Returns a JoinHandle:
type JoinHandle = {
forumId: string;
forumParticipantId: string;
monologueId: string;
monologueParticipantId: string;
systemActorId: string;
systemParticipantId: string;
ingestionInstanceId: string;
processorInstanceId?: string;
followUpInstanceId?: string;
guardianSubscriptionInstanceId?: string;
};What joinConversation does
sequenceDiagram
participant App
participant Store as Conversation store
participant RT as Runtime
App->>Store: createMonologueForStint (system + agent participants)
App->>Store: addParticipant(forum, agent, metadata.monologueId)
App->>RT: create ingestion instance
App->>RT: create processor instance
App->>RT: create followUp instance (optional)
App->>RT: create guardianSubscription instance (optional)
App->>RT: append agent.forum.joined
Note over RT: Egress updates ctx.state.joins- Creates a monologue for this stint (
system+ agent participants) - Adds the agent as a forum participant with
metadata.monologueId - Spins up ingestion and processor instances for this monologue
- Optionally creates follow-up and guardian subscription instances
- Emits
agent.forum.joinedso egress learnsforumId → { monologueId, forumParticipantId }
Ingested forum messages are copied into the monologue by external shadow actors — one per forum author. No mirror relations. Ingested copies carry body.meta.sourceConversationId for egress routing.
Join options
await joinConversation(ctx, agent, forumId, {
metadata: { team: "support" }, // extra forum participant metadata
model: "claude-sonnet-4", // processor model id
createProcessor: true, // default true
followUp: false, // disable follow-up timer
followUp: { delayMs: 60_000, text: "Still need help?" },
guardianAngel: false, // disable guardian subscription
guardianAngel: { model: "guardian-steer" },
});4. Register actor definitions
Register SDK-provided actors in your yoke.ts package:
import {
defineAgentStack,
defineEgressActor,
defineFollowUpActor,
defineGuardianAngel,
defineIngestionActor,
defineProcessorActor,
agentEvents,
conversationEvents,
defineEvents,
defineFor,
defineRuntime,
egressConfigSchema,
egressStateSchema,
followUpConfigSchema,
followUpStateSchema,
ingestionConfigSchema,
ingestionStateSchema,
processorConfigSchema,
processorStateSchema,
} from "@yoke/sdk";
const events = defineEvents({ ...conversationEvents, ...agentEvents });
const agentStack = defineAgentStack({
compaction: async (messages) => {
if (messages.length <= 20) return messages;
return messages.slice(-10);
},
});
const guardianAngel = defineGuardianAngel({
filter: agentSaidV1Filter,
model: "guardian-steer",
});
export default defineRuntime()
.events(events)
.actors(({ events }) => {
const { actor } = defineFor(events);
return {
ingestion: actor(defineIngestionActor({
configSchema: ingestionConfigSchema,
stateSchema: ingestionStateSchema,
})),
egress: actor(defineEgressActor({
configSchema: egressConfigSchema,
stateSchema: egressStateSchema,
})),
processor: actor(defineProcessorActor({
configSchema: processorConfigSchema,
stateSchema: processorStateSchema,
compaction: agentStack.compaction,
})),
followUp: actor(defineFollowUpActor({
configSchema: followUpConfigSchema,
stateSchema: followUpStateSchema,
})),
guardianSubscription: actor(guardianAngel.subscription),
guardianAngel: actor(guardianAngel.runner),
};
});Optional coordinator registry
Store join handles in a coordinator actor's durable SQLite state:
type AgentRegistryState = {
agents: Record<string, AgentHandle & { joins: Record<string, JoinHandle> }>;
};Postgres participants.metadata.monologueId is canonical — the registry is optional convenience for portal handlers.
Checklist
- Chat room created separately (
createChatClient/ HTTP) - Agent created with
createAgent(actor + egress only) - Each forum joined via
joinConversation(monologue + ingestion + processor per stint) -
monologueIdon forum participant metadata (automatic) - No mirror relations anywhere
- Processor posts
type: "reply"when ready for forum (egress copies it out)
Expose agent bootstrap via portal routes (POST /agents, POST /agents/:agentId/join) — see Portals.