Skip to main content

Overview

Plugins are the primary way to extend ClarkOS agents. They hook into the tick lifecycle to add custom behavior—posting to social media, fetching data, executing trades, or anything else.

Plugin Interface

Every plugin implements this interface:
PropertyRequiredDescription
nameYesUnique identifier
versionYesSemver version string
descriptionNoHuman-readable description
dependenciesNoOther plugins that must load first
init()NoCalled once when agent starts
cleanup()NoCalled when agent stops
onTick()NoCalled after every tick
actionsNoCallable functions exposed to agent
Reference: Plugin interface in src/plugins/types.ts

Basic Plugin

import type { Plugin } from "./src/plugins";

export const loggerPlugin: Plugin = {
  name: "logger",
  version: "1.0.0",

  onTick(context) {
    console.log(`Tick: mood=${context.state.mood}, health=${context.state.health}`);
  }
};
Use a factory function for configurable plugins:
interface MyPluginConfig {
  enabled: boolean;
  cooldownMs: number;
}

export function createMyPlugin(config: MyPluginConfig): Plugin {
  let lastAction = 0;

  return {
    name: "my-plugin",
    version: "1.0.0",

    onTick(context) {
      if (!config.enabled) return;
      if (Date.now() - lastAction < config.cooldownMs) return;

      // Do something
      lastAction = Date.now();
    }
  };
}

Lifecycle Hooks

init()

Called once when the plugin is registered. Use for setup:
  • Establish connections
  • Validate configuration
  • Load initial state

cleanup()

Called when the agent stops. Use for teardown:
  • Close connections
  • Flush buffers
  • Save state

onTick()

Called after every tick with full context. Use for reactive behavior:
  • Check state conditions before acting
  • Access memories and knowledge
  • Trigger external actions
Reference: TickContext in src/core/types.ts

Plugin Actions

Expose callable functions via the actions property:
actions: {
  async getData(params, agent) {
    return { success: true, data: "..." };
  },

  async doSomething(params, agent) {
    return { success: true };
  }
}
Call actions from code:
const result = await agent.executeAction("my-plugin", "getData", { query: "..." });

Plugin Dependencies

Declare dependencies to ensure load order:
export const enhancedPlugin: Plugin = {
  name: "enhanced",
  dependencies: ["base-plugin", "analytics"],

  init(agent) {
    const base = agent.getPlugin("base-plugin");
    // base is guaranteed to exist
  }
};
Reference: sortPluginsByDependencies() in src/plugins/loader.ts handles topological sorting

Registering Plugins

const agent = new Agent({
  backend,
  plugins: [loggerPlugin, createMyPlugin({ enabled: true, cooldownMs: 60000 })]
});

// Or add later
agent.use(anotherPlugin);

Error Handling

Never let plugin errors crash the agent:
onTick(context) {
  try {
    await this.riskyOperation();
  } catch (error) {
    console.error("Plugin error:", error);
    // Don't re-throw—let agent continue
  }
}

Testing Plugins

Use the MemoryBackend for testing without a real database:
import { MemoryBackend } from "./src/backend/memory";

const backend = new MemoryBackend();
const agent = new Agent({ backend, plugins: [myPlugin] });

await agent.tick();
// Assert expected behavior
Reference: MemoryBackend in src/backend/memory.ts

Best Practices

  • Single responsibility: One plugin, one purpose
  • Graceful failures: Log errors, don’t throw
  • Respect rate limits: Track API usage, implement cooldowns
  • Configurable: Use factory functions with config objects
  • Testable: Use dependency injection

Next Steps