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 pathis 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:
instructionis 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
AgentModelinstances CityModelimplementsLanguageModelV3itself, 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
.envfirst, then applies explicit Workspaceenvoverrides - 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 allKey points:
get_env()returns a shallow snapshotpatch_env()treatsnull/undefinedas deleteset_env()replaces the current Workspace envagent.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.mdis absent; usesession.syncshot()to refresh an existing Session andsession.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
.envor 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_readandplugin_call AgentOptions.toolsprovides caller-defined tools- optional tools such as
ask_questionmust be imported from@downcity/agent/toolsand 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_execandshell_session - Workspace mounts
grep,find,read,write, andedit, even without Shell - session and turn context are wired internally
- approvals belong to a specific Session; use
session.interactions(),session.respond(...), andsession.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.pluginsandPluginContext.pluginsuse 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
Agentcan then create multiple sessions with differentsession_idvalues- 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
RemoteAgentdoes 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:
| Method | Purpose |
|---|---|
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.