Agent

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 } from "@downcity/agent";
import { Workspace } from "@downcity/workspace";
import { Shell } from "@downcity/workspace";
import { MacOsSeatbeltSandbox } from "@downcity/sandbox-macos";

const workspace = new Workspace({
  id: "project",
  path: process.cwd(),
  shell: new Shell({ sandbox: new MacOsSeatbeltSandbox() }),
  env: { API_KEY: process.env.API_KEY ?? "" },
});

const agent = new Agent({
  id: "repo-helper",
  model,
  tools: { search: search_tool },
  plugins: [plugin],
});

// Session 在创建时选择 Workspace。
const session = await agent.sessions.create({ workspace });
const turn = await session.prompt({ query: "Inspect this project" });
const result = await turn.finished;

await agent.dispose();

Workspace comes from @downcity/workspace and owns project files, search tools, Env, private storage capability, and an optional Shell. It does not own Agent identity, Plugins, or Session semantics. agent.sessions.create({ workspace }) creates the SessionStore inside the Agent-specific private storage scope.

An Agent is not configured with a Workspace. The same Agent may enter multiple Workspace instances, and multiple Agents may operate on the same physical directory by creating separate Workspace instances. agent.dispose() leaves all entered Workspaces and releases their Shell resources.

Local runtime state is centralized outside the project directory:

~/.downcity/agents/<agent_id>/workspaces/<workspace_id>/

Construction starts plugin lifecycle and background work. Session execution waits for initialization automatically; use await agent.dispose() to release resources. HTTP and RPC are supplied by the containing City through city.http() and city.rpc().

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: { type: "approval", outcome: decision === "approved" ? "resolved" : "denied", payload: { 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 use the AgentWorkspace private FileSystem for rooted paths, atomic writes, and cross-process file transactions. Project File/Search tools cannot access this private state. Applications should use AgentWorkspace and Session APIs rather than depend on the physical layout.

Continue with: