Agent SDK
Embed the Downcity Agent runtime in a Node application
Agent SDK
Use @downcity/agent to create an Agent, manage Sessions, and execute model calls in the same process. See the Agent SDK docs for the complete API.
import { Agent, Workspace } from "@downcity/agent";
import { Shell } from "@downcity/shell";
import { MacOsSeatbeltSandbox } from "@downcity/sandbox-macos";
const workspace = new Workspace({
path: process.cwd(),
shell: new Shell({ sandbox: new MacOsSeatbeltSandbox() }),
env: { API_KEY: process.env.API_KEY ?? "" },
});
const agent = new Agent({
id: "repo-helper",
workspace,
model,
tools: { search: search_tool },
plugins: [plugin],
});
const session = await agent.sessions.create();
const turn = await session.prompt({ query: "Inspect this project" });
const result = await turn.finished;
await agent.dispose();Workspace is the unified resource container for AgentStore, WorkspaceTools, Env, and an optional Shell. Session history and metadata live in the same project's .downcity directory and remain readable and editable through Workspace file tools; Agent construction does not require separate Store configuration.
Each Workspace instance binds to exactly one Agent and is released by agent.dispose(). Multiple Agents may operate on the same physical directory by using separate Workspace instances.
Construction starts plugin lifecycle and background work. Use await agent.ready() when an explicit readiness barrier is needed, and await agent.dispose() to release resources. HTTP and RPC are supplied by AgentHTTP and AgentRPC from @downcity/server.
Streaming messages
subscribe() returns the unified live Session Mutation stream:
const unsubscribe = session.subscribe((mutation) => {
if (mutation.variant === "delta" && mutation.type === "text") {
process.stdout.write(mutation.delta);
}
});Tool lifecycle updates use type: "tool"; user participation uses type: "interaction". A pending Interaction carries the request while its Tool is waiting-user; submit responses through the Session command API:
Part order within an Assistant step always follows the model stream. If Tool execution becomes ready before its Tool Part reaches the stream, execution waits at that boundary instead of overtaking preceding text. Waits are isolated by tool_call_id, so multiple Tools can still execute concurrently. Clients should render Mutations and history snapshots in their recorded order without reordering them.
const unsubscribe = session.subscribe((mutation) => {
if (
mutation.variant === "part" &&
mutation.type === "interaction" &&
mutation.part.status === "pending"
) {
render_interaction(mutation.part);
}
});
async function decide_approval(interaction_id: string, decision: "approved" | "denied") {
await session.respond({
interaction_id,
response: { kind: "approval", decision },
});
}Use session.messages() for a canonical history snapshot. It returns the current Active page and a next_before_sequence cursor when older immutable Segments exist. After a disconnect, reload the snapshot and establish a new subscription.
Hosts that render a flat activity feed can use the SDK projection instead of depending on .downcity file names:
import { to_session_message_timeline_events } from "@downcity/agent";
const page = await session.messages();
const timeline = page.items.flatMap(to_session_message_timeline_events);Session JSONL, logs, and scheduled actions all use the Workspace FileSystem for rooted paths, atomic writes, and cross-process file transactions. Applications should use Agent and Session APIs for structured state rather than reading the physical storage layout.
Continue with: