Local Agent

Agent Constructor Options

Detailed explanation of Workspace env, WorkspaceTools, and local Agent id, instruction, model, tools, and plugins

Agent Constructor Options

The local Agent is constructed like this:

const workspace = new Workspace({
  path,
  shell,
  env,
});

new Agent({
  id,
  workspace,
  model,
  instruction,
  tools,
  plugins,
  Session,
})

id

The stable identifier of the agent.

It affects the SDK session storage path:

<project_root>/.downcity/agents/<agent_id>/...

Recommended properties:

  • stable
  • URL-safe
  • not changed casually

That is because it directly controls the session storage partition.

workspace

The project resource and security boundary available to the Agent. Workspace owns the project root, file and search tools, and an optional Shell:

const workspace = new Workspace({
  path: "/path/to/project",
  shell,
});

Key points:

  • macOS, Linux, and Windows use the same Workspace
  • path is resolved to a real absolute directory during construction
  • file and search tools remain confined to the Workspace
  • Shell and file tools share the same project root
  • one Workspace instance binds exactly one Agent
  • multiple Agents may target the same directory through separate Workspace instances
  • agent.dispose() also disposes its exclusive Workspace

Workspace construction fails immediately when path is empty, missing, or not a directory.

instruction

Static caller-provided instructions for the local SDK Agent.

new Agent({
  id: "repo-helper",
  workspace: new Workspace({ path: "/path/to/project" }),
  instruction: [
    "You are a concise code assistant.",
    "Prefer direct file references when explaining code.",
  ],
});

Key points:

  • instruction is static and cache-friendly
  • the SDK does not render dynamic variables inside it
  • the SDK does not read project prompt config files
  • if you omit instruction, the SDK uses its minimal core instruction fallback

Hosts that need additional base instructions should pass them explicitly through instruction.

model

The default AgentModel instance held by the Agent. AgentModel can be an AI SDK LanguageModel or a City CityModel; callers do not need to convert it first.

new Agent({
  id: "repo-helper",
  workspace: new Workspace({ path: "/path/to/project" }),
  model: openai.responses("gpt-5"),
});

Key points:

  • the SDK does not select, persist, or restore model IDs; it only holds AgentModel instances
  • CityModel implements LanguageModelV3 itself, so Agent calls it directly
  • a Session without its own model falls back to the Agent model
  • a local Session can override the Agent model with session.set({ model })
  • effective model resolution always checks the Session model before the Agent model

Workspace env

The project execution environment owned by the Workspace.

const workspace = new Workspace({
  path: "/path/to/project",
  env: {
    OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? "",
  },
});

Key points:

  • the SDK reads the project .env first, then applies explicit Workspace env overrides
  • env belongs to Workspace rather than Agent
  • if a host needs layered env merging, it should merge first and then pass the final result through env
  • these values become the Workspace runtime env, but are not written back to the system environment automatically

Updating env at runtime

You can update or replace Workspace environment variables at runtime:

workspace.set_env({ FOO: "1" });
workspace.get_env();                // => { FOO: "1" }
workspace.patch_env({ BAR: "2" }); // merge
workspace.patch_env({ FOO: null }); // null means delete
workspace.set_env({ ONLY: "ok" }); // replace all

Key points:

  • get_env() returns a shallow snapshot
  • patch_env() treats null / undefined as delete
  • set_env() replaces the current Workspace env
  • agent.set_instruction() affects only Sessions created afterwards and does not retroactively change existing in-memory Sessions
  • restored Sessions regenerate from the Agent's current instruction and plugins when instruction.md is absent; use session.syncshot() to refresh an existing Session and session.snapshot() to persist it
  • Workspace env and plugin changes commit in existing Sessions at the next Session step checkpoint
  • an in-flight provider request and its tool calls keep the env that was effective for the current step
  • if the current Session turn has no later step, the queued change is committed when the next Session turn starts
  • the Session timeline emits a completed action message when the change becomes effective
  • Runtime changes live in memory only; they are not written back to the project .env or to the system environment

tools

Additional caller-defined tools registered on the Agent.

Key points:

  • Workspace provides file, search, and optional Shell tools as WorkspaceTools
  • PluginRegistry provides plugin_read and plugin_call
  • AgentOptions.tools provides caller-defined tools
  • optional tools such as ask_question must be imported from @downcity/agent/tools and passed explicitly
  • Agent owns the final merged collection shared by Sessions
  • duplicate names across any source throw instead of silently overriding tools

This is a good fit when multiple sessions should share a stable default tool set.

Workspace shell

Optional built-in shell capability.

import { Agent, Workspace } from "@downcity/agent";
import { Shell } from "@downcity/shell";
import { MacOsSeatbeltSandbox } from "@downcity/sandbox-macos";

const agent = new Agent({
  id: "repo-helper",
  workspace: new Workspace({
    path: "/path/to/project",
    shell: new Shell({ sandbox: new MacOsSeatbeltSandbox() }),
  }),
});

Key points:

  • Shell is not a plugin
  • Shell only mounts shell_exec and shell_session
  • Workspace mounts grep, find, read, write, and edit, even without Shell
  • session and turn context are wired internally
  • approvals belong to a specific Session; use session.interactions(), session.respond(...), and session.set({ security })
  • file and search tools are always restricted to the Agent project root and have no unrestricted mode

plugins

This accepts already-instantiated BasePlugin objects.

For example:

import { Agent, Workspace } from "@downcity/agent";
import { SkillPlugin } from "@downcity/plugins/skill";

const agent = new Agent({
  id: "repo-helper",
  workspace: new Workspace({ path: "/path/to/project" }),
  tools: {},
  plugins: [new SkillPlugin()],
});

Web capabilities are opt-in and require an explicit provider; see the Web Plugin guide.

The SDK creates an independent plugin registry for the current Agent.

Key points:

  • pass instances such as new SkillPlugin() rather than raw plugin definitions
  • it does not register every built-in plugin by default
  • it does not reuse the global plugin manager
  • duplicate plugin names throw immediately
  • agent.plugins and PluginContext.plugins use this registry
  • instantiate the exact plugin set the Agent needs and pass those instances in plugins

Session

Advanced local-only custom Session class for sessions created by this agent.

Use this when you want to replace the local session implementation or inject custom Session-level Composers. Agent only uses the class to create and restore sessions; it does not inspect Composer behavior.

import {
  Agent,
  Workspace,
  DefaultSessionComposer,
  Session,
  type SessionOptions,
  type SessionComposeInput,
} from "@downcity/agent";

class CustomSessionComposer extends DefaultSessionComposer {
  override async compose(input: SessionComposeInput) {
    const step = await super.compose(input);
    return {
      ...step,
      system: [
        ...step.system,
        { role: "system" as const, content: "Always answer tersely." },
      ],
    };
  }
}

class CustomSession extends Session {
  constructor(options: SessionOptions) {
    super({
      ...options,
      composer: new CustomSessionComposer(),
    });
  }
}

const agent = new Agent({
  id: "repo-helper",
  workspace: new Workspace({ path: "/path/to/project" }),
  session_class: CustomSession,
});

Key points:

  • pass a class, not an already-created session instance
  • Agent can then create multiple sessions with different session_id values
  • Composer customization stays inside the custom Session class
  • Composer reads Session snapshots and composes system, history, tools, or compaction plans; it never persists messages directly
  • RemoteAgent does not support local Session classes because it only accesses a remote runtime

What a Session Composer can customize

One SessionComposer replaces the previous System, History, Context, and Compaction composer pipeline. A custom Session only needs one composer:

MethodPurpose
compose(input)Return the system, messages, and tools for the current model Step
compact(input)Return a SessionCompactionPlan from a read-only Message snapshot, or null when no compaction is needed
should_compact(error)Decide whether a model error should retry after persistent compaction

compose(input) may:

  • add, remove, or reorder system messages
  • filter, transform, or inject model messages for the current Step
  • change the tools visible to the current Step

A Composer does not consume the Prompt Queue, control Turns, or write Messages, Metadata, Mutations, or JSONL. The Session persists the User Message before invoking the Composer, so a custom Composer does not need to append the current query to history.

Compaction is also split into policy and commit stages. The Composer only returns a plan; the Session commits the Segment after the Assistant draft has closed. Custom policy therefore cannot bypass Session Message consistency and recovery.