Tools and Plugins
Configure Agent Tools and provide Plugins through City
Tools and Plugins
Tools and Plugins both participate in Session execution, but ownership differs: Agent owns custom Tools; City owns unique Plugin instances, configuration projection, and lifecycle.
Custom Tools
A local Agent directly receives custom Tools:
const agent = new Agent({
id: "repo-helper",
tools: {
my_tool,
},
});They are merged with the Tools of the Workspace selected by the Session. Name conflicts throw instead of silently replacing a Tool.
Workspace and Shell
Workspace provides grep, find, read, write, and edit. Configure Shell explicitly when command execution is required:
import { Agent } from "@downcity/agent";
import { MicrosandboxProvider } from "@downcity/sandbox-microsandbox";
import { Shell, Workspace } from "@downcity/city";
const agent = new Agent({ id: "repo-helper" });
const workspace = new Workspace({
id: "project",
path: "/path/to/project",
shell: new Shell({
sandbox_provider: new MicrosandboxProvider(),
}),
runtime_path: "/path/to/downcity-runtime/project",
});Shell adds shell_exec and shell_session. File and search Tools remain restricted to the Workspace root. Approvals and asynchronous questions belong to a specific Session and use session.interactions() and session.respond(...).
City provides Plugins
The Agent constructor does not accept Plugins. Once registrations are provided to City, every Agent receives them automatically:
import { Agent } from "@downcity/agent";
import { City } from "@downcity/city";
import { create_builtin_plugin_registrations } from "@downcity/plugins";
import { Workspace } from "@downcity/city";
const workspace = new Workspace({
id: "project",
path: "/path/to/project",
});
const agent = new Agent({ id: "repo-helper", model });
const city = new City({
workspaces: [workspace],
plugins: create_builtin_plugin_registrations(),
});
city.agents.add(agent);City automatically exposes every registered Plugin to every Agent. A running City can update the
collection through city.plugins.add() and city.plugins.remove().
Each Plugin ID identifies one instance inside a City with a City-level initialize/dispose lifecycle.
Agents and Workspaces provide call context; they do not own Plugin lifecycle.
City-level configuration
The host provides one Config store for each Plugin. Configuration does not enter Agent or determine instance count:
const city = new City({
plugin_host: {
config: (plugin_id) => open_plugin_config_store(plugin_id),
},
});Each Plugin ID has one configuration inside a City. A Plugin models its own account collection and selection rules when needed; City does not provide a generic named-configuration layer. After saving Config, the Plugin refreshes any affected long-lived resources.
Calling Plugins
Host code calls Actions and Hooks through an Agent/Workspace execution entry:
const result = await city.plugins.scope({ agent_id: agent.id, workspace_id: workspace.id }).run_action({
plugin: "memory",
action: "search",
payload: { query: "project conventions" },
});
const enriched = await city.plugins.scope({ agent_id: agent.id, workspace_id: workspace.id }).pipeline("chat.enrich_message", input);
await city.plugins.scope({ agent_id: agent.id, workspace_id: workspace.id }).guard("review.require_checked", enriched);When Actions exist, Session receives the plugin_read and plugin_call Tools. The model can inspect an Action schema before sending a validated payload. Plugin system(context, execution_context) text is injected at Session prompt checkpoints.
Direct communication through PluginContext
Plugin author APIs come from @downcity/city/plugin. Action, Hook, System, and Availability receive a PluginContext with restricted city, agent, workspace, session, turn, config, and storage handles.
context.session and context.agent.sessions are directly callable in-process objects; a Plugin does not need to retain only session_id and query the host again. Use context.workspace.files for Workspace files and context.storage.files for private Plugin files.
HTTP
A Plugin may declare structured HTTP routes. City mounts and authenticates them and injects the request-specific PluginContext. Plugins do not create independent servers.