Plugins

Plugin Lifecycle

Understand City-owned Plugin instances, dynamic call contexts, and execution leases.

Plugin Lifecycle

City owns Plugins. city.plugins.add(new MyPlugin()) registers one unique instance. A Plugin ID cannot be registered twice in one City, and Agents do not store Plugin instances or Registries.

Two lifecycle checkpoints

  • initialize(context) runs once when the Plugin enters City. Register host Actions and initialize resources owned by the Plugin here.
  • dispose(context) runs once when the Plugin is removed or City closes. Release those resources here.
class ProviderPlugin extends Plugin {
  readonly name = "provider";

  async initialize({ storage }: PluginLifecycleContext) {
    await this.provider.open(storage.path);
  }

  async dispose() {
    await this.provider.close();
  }
}

Both methods receive the same stable PluginLifecycleContext. Agent, Session, and Workspace entry or removal do not trigger Plugin lifecycle callbacks. There is no generic connect/disconnect protocol.

city.plugins.add() explicitly rejects when initialization fails. The failed instance never enters the execution Registry and cannot contribute Actions, Hooks, or System content. City retains an observable status: "error" snapshot with last_error in city.plugins.snapshots(). A host should isolate that Plugin and continue starting other subjects instead of treating an optional Plugin as an application-wide startup dependency. A successful retry for the same Plugin ID replaces the error snapshot with ready.

Call context

City freshly projects a PluginContext for every Action, Hook, System, and Availability call. Its agent, workspace, and optional session/turn handles can be used directly. Workspace is the project resource for the current call; it is not a Plugin lifecycle boundary or a default resource-isolation key.

Long-lived connections, timers, schedulers, and workers must follow their actual domain ownership. A browser Provider may be created lazily for an Agent, a chat channel may be keyed by account, and a scheduler may follow task definitions. dispose must ultimately close every resource.

Execution leases

A Session captures an immutable Plugin execution view at each Step checkpoint. New Steps stop seeing a removed Plugin immediately. An existing Step may finish with its captured instance, and City calls dispose only after the final execution lease is released.

city.close() stops Sessions before releasing Plugins and Workspaces, and aggregates cleanup errors. A single Plugin dispose failure does not prevent other Plugins from cleaning up.