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 { City } from "@downcity/city";
import { create_builtin_plugin_registrations } from "@downcity/plugins";
import { Workspace } from "@downcity/city";
import { Shell } from "@downcity/city";
import { MicrosandboxProvider } from "@downcity/sandbox-microsandbox";

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

const agent = new Agent({
  id: "repo-helper",
  model,
  tools: { search: search_tool },
});
const city = new City({
  workspaces: [workspace],
  plugins: create_builtin_plugin_registrations(),
});
city.agents.add(agent);

// 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/city and owns project files, search tools, Env, and an optional Shell. It does not own Agent identity, storage, Plugins, or Session semantics. agent.sessions.create({ workspace }) creates the SessionStore inside the Agent scope supplied by City Storage.

An Agent is not configured with a Workspace. Each Session may use a Workspace, and multiple Agents may operate on the same physical directory by creating separate Workspace instances. agent.dispose() releases Agent resources and the Sessions' Workspace resources.

Local runtime state is centralized outside the project directory:

~/.downcity/agents/<agent_id>/
├── sessions/<origin_type>/<session_id>/
└── archived-sessions/<origin_type>/<session_id>/

chat is the default origin partition. Callers may attach any non-empty origin.type at creation and must pass the same type to get(session_id, origin_type) when restoring the Session.

City starts shared Plugin instances and background work when it binds them. Session execution waits for initialization automatically. Use await city.close() to close the host or city.agents.remove() to remove one Agent. City also supplies HTTP and 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"; a pending Interaction lives on its Tool Part's interactions while that 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.part.type !== "tool") return;
  const interaction = (mutation.part.interactions ?? []).find(
    (item) => item.status === "pending",
  );
  if (interaction) render_interaction(interaction);
});

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 paginates on Message boundaries and returns a next_before_sequence cursor when older history exists. 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 StorageProvider scope injected by City. If no City storage is injected, the Agent uses an in-memory provider. Project File/Search tools cannot access this private state. Applications should use Agent.sessions and Session APIs rather than depend on the physical layout.

Continue with: