Plugin Development
Build a unified City Plugin module with @downcity/city/plugin.
Plugin Development
Plugin author APIs live in @downcity/city/plugin. A Plugin is a long-lived instance owned by City;
do not create one instance per Agent.
Execution instance
import { Plugin, create_action } from "@downcity/city/plugin";
import { z } from "zod";
class ExamplePlugin extends Plugin {
readonly name = "example";
readonly title = "Example";
readonly description = "An example Plugin.";
constructor(private readonly client: ExampleClient) {
super();
}
readonly actions = {
status: create_action({
description: "Return the current scope.",
input_schema: { zod: z.object({}) },
execute: async ({ context, execution }) => ({
success: true,
data: {
agent_id: context.agent.id,
workspace_id: context.workspace.id,
session_id: context.session?.id ?? null,
call_id: execution.call_id,
},
}),
}),
};
}Use context.workspace.files for user Workspace files and context.storage.files for private Plugin
state. context.session and context.agent.sessions are directly callable restricted handles; a
Plugin does not need to resolve objects from IDs alone.
Unified City module
export default new ExamplePlugin();An SDK host places the instance in City and adds Agents:
const city = new City({ plugins: [new ExamplePlugin()], workspaces: [workspace] });
city.agents.add(agent);Lifecycle
initialize/dispose belong to City's unique Plugin instance. Agent and Workspace are dynamic call
context, not lifecycle boundaries. Key long-lived resources by their actual domain owner and close
all of them in dispose. Every asynchronous Action should propagate execution.abort_signal.
Installable package
example-plugin/
├── plugin.json
├── package.json
├── README.md
├── dist/main.js
└── dist/renderer.js{
"schema_version": 1,
"id": "example",
"version": "1.0.0",
"description": "A small example Plugin.",
"readme": "./README.md",
"main": "./dist/main.js",
"renderer": {
"entry": "./dist/renderer.js",
"sidebar": true,
"mainview": true,
"config": false
}
}main default-exports a Plugin instance or { plugin, main }; there is no separate agent entry. A Renderer
communicates only through the host action gateway and cannot access Node, Electron, Agent objects, or
Plugin Config files directly.
Testing focus
- Multiple Agents using one City Plugin initialize one instance and read the same configuration.
- Repeated calls receive fresh Context objects with the correct Agent and Workspace.
disposewaits for the final execution lease.- Concurrent main calls activate once, and shutdown fully deactivates it.
- Action input, JSON results, and cancellation signals remain valid.