Skip to content

Tools & Identity System

Tools are registered in src/tools/registry.ts via AVAILABLE_TOOLS. Each tool implements the Tool interface. Most tools are enabled by default on fresh installs. A few tools that require external configuration start off by default: shell, control_device, control_lovense, control_toy. These are auto-enabled when their respective integrations are configured (e.g., control_device when home devices are added). Deprecated tools (currently sync_mcp) are hidden from both the UI and the LLM but remain registered for potential resurrection.

Tool enable/disable state can be overridden via the PSYCHEROS_TOOLS environment variable or the Settings > Tools UI. When .psycheros/tools-settings.json exists, user overrides take precedence over the env var. Some tools are auto-enabled regardless (e.g., web_search when a web search provider is configured).

  1. Create src/tools/my-tool.ts implementing the Tool interface
  2. Add the tool to AVAILABLE_TOOLS in src/tools/registry.ts
  3. Add the tool name to the appropriate category in TOOL_CATEGORIES in src/tools/tools-settings.ts
  4. For UI updates: use a state-change function, return affectedRegions
  5. Tool descriptions use first-person: “I use this to…”
  6. If the tool requires persistent settings (API keys, endpoint config, etc.): create a settings type in src/llm/, add a getter to PsycherosServer in src/server/server.ts, then wire the settings into both the chat handler (src/server/routes.ts) and the Pulse engine (src/pulse/engine.ts). Specifically: add a property to EntityConfig in src/entity/loop.ts, a getter to PulseEngineConfig in src/pulse/engine.ts, pass it when constructing EntityConfig in executePulse(), and provide the getter when constructing PulseEngine in server.ts. If any of these are missed, the tool will work in normal chat but fail when called autonomously by a Pulse.

Custom tools live in the custom-tools/ directory at the project root. No core code changes are needed.

  1. Create custom-tools/my-tool.js exporting a default Tool object
  2. The file must export { definition: { type: "function", function: { name, description, parameters } }, execute: async (args, ctx) => { ... } }
  3. ctx provides: toolCallId, conversationId, db (database client), config (with projectRoot)
  4. Restart the server — the tool appears in Settings > Tools under Custom Tools
  5. Toggle it on to enable it for the entity

Invalid custom tool files are logged as warnings and skipped.

Accessible via Settings > Tools in the sidebar. Provides a web interface for managing tool enable/disable state.

Features:

  • Two tabs: Built-in (shipped with Psycheros) and Custom (user-written)
  • Built-in tools grouped by category (System, Identity, Data Vault, Web Search, Pulse, Memory, Discord, Home Automation, Intimacy, Vision)
  • Toggle switches for each individual tool
  • Per-category “Enable All” / “Disable All” buttons
  • Global “Enable All” / “Disable All” buttons
  • Expandable detail view showing full description and parameters schema
  • Custom tab includes an Import Tool button to upload .js files directly
  • Save persists to .psycheros/tools-settings.json and hot-reloads the tool registry

Priority order for resolving enabled state:

  1. User override (from settings file) — explicit toggle
  2. Auto-enabled tools (e.g., web_search when provider configured)
  3. PSYCHEROS_TOOLS environment variable
  4. Default: all tools enabled (when no overrides, no env var, and no auto-only config)

API Endpoints:

  • GET /api/tools-settings — get all tools metadata, categories, and current overrides
  • POST /api/tools-settings — save overrides and hot-reload ({ "toolOverrides": { "shell": true, ... } })
  • POST /api/custom-tools/upload — upload a .js custom tool file (multipart/form-data, field tool, max 100KB); writes to custom-tools/, hot-reloads registry
  • GET /fragments/settings/tools — render Tools settings UI fragment

Related Source Files:

FilePurpose
src/tools/registry.tsAVAILABLE_TOOLS catalog and ToolRegistry class
src/tools/tools-settings.tsToolsSettings type, categories, load/save, enable resolution
src/tools/custom-loader.tsDynamic loader for custom-tools/ directory
src/server/templates.tsrenderToolsSettings() and helper functions
src/server/routes.tshandleGetToolsSettings, handleSaveToolsSettings, handleToolsSettingsFragment

See configuration.md for the full list of available tools.

The entity can search the web for current information using either Tavily or Brave Search. The provider and API key are configured via the Settings UI or environment variables — the tool is auto-enabled when a provider is selected.

SettingEnv VarDescription
ProviderPSYCHEROS_WEB_SEARCHdisabled (default), tavily, or brave
Tavily keyTAVILY_API_KEYRequired when using Tavily
Brave keyBRAVE_SEARCH_API_KEYRequired when using Brave Search

The tool accepts a query (required) and max_results (optional, default 5, max 10). Results are returned as a formatted list with titles, URLs, and snippets.

Settings are persisted to .psycheros/web-search-settings.json (gitignored).

FilePurpose
src/tools/web-search.tsweb_search tool with Tavily and Brave providers
src/llm/web-search-settings.tsSettings type, load/save, API key masking

The entity can create, read, append, list, and search documents stored in the Data Vault for persistent reference.

ToolDescription
vaultUnified vault tool with operation discriminator: write (create/update), read (full content), append (add content, creates if missing), list (all documents), search (find relevant content)
FilePurpose
src/tools/vault-tools.tsvault — unified vault document management tool
src/vault/manager.tsVaultManager — CRUD, chunking, embedding, search

The entity can create, trigger, and delete autonomous scheduled prompts (Pulses). Entity-created Pulses default to visible mode and auto-delete after execution. All scheduling times use the user’s display timezone (same as <t> timestamps in context) and are converted to UTC automatically. When a visible-mode Pulse fires, the entity perceives the prompt as system-initiated via a [System — Pulse "name"] prefix rather than a user message.

ToolDescription
pulseUnified Pulse tool with operation discriminator: create (schedule a new Pulse), trigger (fire immediately), delete (remove permanently)
ParameterUse CaseExample
run_atOne-shot: fire once at a specific time2026-04-17T14:30 (display TZ)
cron_expressionRecurring: daily/weekly/monthly schedule0 9 * * 2 (Tuesdays at 9 AM)
interval_secondsRecurring: every N seconds3600 (every hour)
FilePurpose
src/tools/pulse-tools.tspulse — unified Pulse management tool
src/pulse/engine.tsPulseEngine — scheduling, execution, chain handling
src/pulse/routes.tsCRUD API, trigger endpoints, webhook receiver
src/pulse/templates.tsSettings UI — hub card, editor, execution log
src/pulse/timezone.tsTimezone conversion helpers for local↔UTC cron scheduling
FilePurpose

The entity can modify its identity files through a unified maintenance tool and a custom file tool.

The single identity tool for all predefined file operations. The tool description guides the entity to pick appropriate section headings or create new ones, and emphasizes using actual filenames rather than XML tag names visible in context.

ToolDescription
maintain_identityIdentity file maintenance with operations: append, prepend, update_section, rewrite_section
list_identity_snapshotsView available backups created automatically by entity-core

Operations:

  • append — add to the end of a file
  • prepend — add to the beginning of a file
  • update_section — append content under a ## heading (existing content preserved). Auto-creates the section if the heading doesn’t exist. This is the default choice for section-level changes.
  • rewrite_sectionDESTRUCTIVE — replace all content under a ## heading (existing content removed). Auto-creates the section if the heading doesn’t exist. Should only be used as a last resort when update_section cannot achieve the goal (e.g., removing outdated/incorrect information, consolidating redundant entries). A snapshot is created automatically.

Content format for section operations: The content parameter must contain ONLY the body text for the section — do NOT include the ## heading line (the system adds it automatically). A defensive strip also runs server-side to prevent duplication.

Parameters: category (self, user, relationship), filename, operation, content, section (required for section operations).

The reason parameter has been removed from all identity operations.

For managing freeform custom identity files in identity/custom/ — topics that don’t fit the predefined self/user/relationship structure.

ToolDescription
custom_identity_fileCreate and modify custom identity files

Operations: create (new file), append (add to end), prepend (add to beginning), update_section (append content under a markdown heading, preserves existing content; auto-creates if heading not found), rewrite_section (replace a section’s content entirely; auto-creates if heading not found). Filenames use .md extension with letters, numbers, and underscores only. Deletion is user-only via the Core Prompts UI.

XML wrapper tags in identity files are no longer stored on disk. Files store inner content only (plain markdown). XML tags are applied dynamically at context-build time by wrapContent() in src/entity/context.ts.

Each identity file has an optional prompt label that controls its XML tag name in the LLM context. Default is the filename without .md (e.g., user_identity.md becomes <user_identity>). Users can customize this via a Prompt Label input field in the Core Prompts editor UI (e.g., rename <user_identity> to something more personal like <human_identity>, or a preferred name).

Prompt labels are stored in entity-core metadata and surfaced via the promptLabel field on IdentityFile objects. When MCP is unavailable, the filename is used as the fallback tag name.

All identity tools route through entity-core when MCP is connected, falling back to local files when offline:

Tool called → MCP connected?
↓ Yes ↓ No
Call MCP tool Write local file
↓ ↓
Server-side Queue for sync
manipulation

Snapshot behavior: When identity files are written via MCP (including rewrite_section and other write operations), entity-core creates snapshots automatically (via sync_push’s targeted per-file snapshot). Local snapshots at .snapshots/ are available as a fallback. The Entity Core snapshots UI shows local snapshots when entity-core has none, enabling recovery even when MCP is unavailable.

Changes preserve markdown structure in identity files. Content is added cleanly without metadata comments — core prompts load every turn, so token efficiency matters. XML tags are applied at context-build time, not stored on disk.

FilePurpose
src/tools/registry.tsTool registration and default registry
src/tools/identity-helpers.tsIdentity file utilities (section manipulation, auto-section-creation, MCP fallback, local snapshot restore)
src/tools/identity-maintain.tsmaintain_identity — unified identity maintenance tool
src/tools/identity-custom.tsCustom identity file tool (create, append, prepend, update_section, rewrite_section)

The entity can send Discord DMs to the user as a notification channel. Uses a Discord bot token to open a DM channel and send messages via the Discord REST API. The entity can also attach images (e.g., generated via generate_image) to DMs.

ToolDescription
send_discord_dmSend a Discord DM with a message; optionally attach an image or specify a target channel/user ID

Parameters: message (required, up to 2000 chars), channel_id (optional, overrides the configured default), image_path (optional, path to an image file relative to .psycheros/, e.g. generated-images/abc.png). Supported image formats: png, jpg/jpeg, webp, gif.

Setup: Configure via Settings > External Connections in the web UI, or set environment variables:

SettingEnv VarDescription
Bot TokenDISCORD_BOT_TOKENDiscord bot token (create at discord.com/developers/applications)
Default Channel IDDISCORD_DEFAULT_CHANNEL_IDDiscord user ID to DM by default

Settings are persisted to .psycheros/discord-settings.json (gitignored). The tool is auto-enabled when a bot token is configured and the feature is enabled.

Data flow: Entity calls send_discord_dm → server opens DM channel via POST /users/@me/channels with the user ID → if image_path is provided, sends a multipart/form-data request with the image attachment; otherwise sends a JSON request → message (and optional image) sent via POST /channels/{dm_channel_id}/messages with bot auth.

Error handling: The tool returns clear messages for common Discord API errors — 401 (invalid token), 403 (missing access), 404 (unknown channel/user), 429 (rate limited with retry-after info), as well as file-not-found and unsupported image type errors.

FilePurpose
src/tools/send-discord-dm.tssend_discord_dm tool implementation
src/llm/discord-settings.tsSettings type, load/save, token masking

When the entity participates in Discord channels via the Gateway, it has access to structured reply capabilities that don’t require separate tool calls. These are parsed from the entity’s natural text output before posting to Discord.

Each message the entity sees includes Discord user IDs and message IDs:

**Alice** (<@123456789>) (3:45 PM) [msg:987654321]:
Hello there!
**Bob** (<@987654321>) (3:46 PM) [msg:111222333] (replying to 987654321):
Hey Alice!
  • <@userId> — Discord mention format. The entity can include these in replies to ping users. Discord natively renders them as clickable mentions.
  • [msg:messageId] — Message ID for use with ::reply and ::react directives.
  • (replying to id) — Shown when a message is a reply to another message.

Directives can appear anywhere in the message text (not just on their own line). They are stripped from the message before sending — Discord users never see them.

DirectiveSyntaxEffect
Reply::reply messageIdResponse threads as a reply to that specific message
React::react messageId :emoji:Adds an emoji reaction to that message

Example entity output:

::react 987654321 :laugh:
::reply 111222333
That's funny! <@123456789> you always make me laugh

Discord users see: A laugh reaction on message 987654321, and a reply to message 111222333 saying “That’s funny! @Alice you always make me laugh”

Standard emoji names are mapped to Unicode:

NameEmoji
thumbsup👍
thumbsdown👎
heart❤️
laugh😂
rofl🤣
fire🔥
eyes👀
think🤔
wave👋
pray🙏
onehundred💯
check
x

Custom emoji use Discord’s :name:id format (e.g. ::react 123456 :rofl:123456789).

These capabilities are optional — the entity uses them only when they feel natural. Responses are plain channel messages by default (no reply threading, no user pings). When someone @mentions the entity, it should respond using ::reply to that message rather than tagging the user back with <@userId> — Discord’s threading already shows who’s being addressed. Direct user pings (<@userId>) are reserved for when the entity specifically needs someone’s attention (e.g. asking a question, addressing one person in a group). Emoji reactions and user pings are occasional social gestures, not defaults.

FilePurpose
src/discord/response.tsparseDirectives(), executeReactions(), encodeEmojiForApi()
src/discord/router.tsformatAccumulatedMessages() — enriches messages with user IDs and message IDs

The entity can control smart home devices such as smart plugs. Currently supports Shelly Plug devices via their local HTTP API. The entity turns devices on/off or checks their power status by name.

ToolDescription
control_deviceTurn a smart device on/off or check its power status by device name

Parameters: device (required, name of the configured device), action (required, one of "on", "off", "status").

Setup: Configure via Settings > External Connections > Home in the web UI. Add devices with a name, type (currently “Shelly Plug”), and IP address/hostname. Settings are persisted to .psycheros/home-settings.json (gitignored). The tool is auto-enabled when at least one device is enabled.

Device settings shape:

{
"devices": [
{
"name": "Coffee Maker",
"type": "shelly-plug",
"address": "192.168.1.100",
"enabled": true
}
]
}

Data flow: Entity calls control_device("Coffee Maker", "on") → server looks up device by name → dispatches to the Shelly handler → sends GET http://{address}/relay/0?turn=on → returns power state from Shelly JSON response.

Error handling: The tool returns clear messages for device not found (lists available devices), disabled devices, unknown device types, network timeouts (5s), and HTTP errors.

Extensibility: The type field in device settings routes to protocol-specific handlers. Adding a new device type (e.g., Kasa, Home Assistant) requires only adding a new handler function — the tool interface stays the same.

FilePurpose
src/tools/control-device.tscontrol_device tool implementation with Shelly Plug handler
src/llm/home-settings.tsSettings type, load/save

The entity can generate images using configured provider slots (OpenRouter or Google AI Studio). Multiple generators can be configured with different models and settings. Anchor images provide style/character reference, users can attach images to chat messages, and the entity can iterate on previously generated images.

ToolDescription
generate_imageGenerate an image or iterate on a previous one using a configured provider

Parameters: generator_id (required, ID of the configured generator), prompt (required, text description of the desired image), negative_prompt (optional, things to avoid), anchor_ids (optional, array of anchor image IDs to use as style reference), user_image_path (optional, path to a user-attached chat image), input_image_path (optional, path to a previously generated image for reference-based iteration/modification), aspect_ratio (optional, overrides the generator’s default — one of 1:1, 4:3, 3:4, 3:2, 2:3, 16:9, 9:16, 5:4, 4:5, 21:9).

Setup: Configure via Settings > Vision > Generators. Each generator has a name, description, provider (OpenRouter or Gemini), and provider-specific settings. Settings are persisted to .psycheros/image-gen-settings.json (gitignored). The tool is auto-enabled when at least one generator has enabled: true.

Supported Providers:

ProviderModelsNotes
OpenRouterAny image-capable model on OpenRouter (e.g. openai/gpt-5-image-mini, google/gemini-2.5-flash-image)Requires API key; uses modalities: ["image", "text"] via chat completions; images returned in message.images[]; uses image_config for aspect_ratio and image_size
Google AI Studiogemini-3.1-flash-image-preview, gemini-3-pro-image-preview, gemini-2.5-flash-imageRequires Google API key; supports aspect ratio selection

Anchor Images: Reference images stored in .psycheros/anchors/ with metadata in the anchor_images SQLite table. The entity sees available anchor IDs in its system context and can reference them by ID for style/character consistency.

Chat Attachments: Users can attach images to messages via a clip icon button in the chat input. Attachments are uploaded to .psycheros/chat-attachments/ and auto-captioned (dual short/long) before being passed to the entity. The user message is prefixed with [USER_IMAGE: /chat-attachments/filename | Caption: long description | Short: brief description].

Reference-Based Iteration: The input_image_path parameter allows the entity to send a previously generated image back to the provider along with a modification prompt. The reference image is included as inline data in the API request. This enables workflows like “change the background”, “make it darker”, “add a character”.

Image Persistence: Generated images are saved to .psycheros/generated-images/ and displayed inline in chat. Images persist across conversation switches via [IMAGE:...] markers appended to the assistant message content in the database. Generated images are automatically captioned with both a longform and shortform description. Both are stored in the marker JSON; the shortform replaces the longform in LLM context after 5 conversation turns to save tokens.

Context Fading: Image descriptions (both [IMAGE:...] and [USER_IMAGE:...]) fade from longform to shortform after 5 conversation turns in the LLM context. The DB always retains the full description. The entity can use the look_closer tool to re-examine any image for full details. look_closer results also fade after 5 turns. Additionally, tool call arguments for image tools (generate_image, describe_image, look_closer) are truncated in context — string values over 50 characters are cut short. Non-image tools are unaffected. This reduces token usage from verbose prompts and descriptions stored in tool call history.

Data flow: Entity calls generate_image → server reads generator config → dispatches to provider (OpenRouter or Gemini API) → saves image to disk → auto-captions via configured captioning provider (dual short/long) → returns [IMAGE:...] marker with both descriptions → entity loop yields image_generated SSE event → frontend renders inline image.

Error handling: The tool returns clear messages for provider errors, missing generators, disabled generators, and image read failures.

FilePurpose
src/tools/generate-image.tsgenerate_image tool with OpenRouter and Gemini providers, auto-captioning
src/tools/describe-image.tsShared captioning functions (dual short/long), describe_image tool
src/tools/look-closer.tslook_closer tool for re-examining images after context fade
src/llm/image-gen-settings.tsSettings type (generators + captioning), load/save, API key masking

Image captioning provides automatic description of images via a configurable vision model. It serves three purposes: auto-captioning chat attachments and generated images, providing the entity with an explicit describe_image tool, and providing a look_closer tool for re-examining images after context fading.

All auto-captioning produces both a longform (detailed, thorough) and shortform (single sentence, under 15 words) description. Both are stored in the message content in the database. When building LLM context, the buildMessages() method in the entity loop applies fading: after 5 conversation turns, longform is replaced with shortform. This significantly reduces token usage in long conversations with many images.

The IMAGE_DESCRIPTION_FADE_TURNS constant (default: 5) controls the grace period.

  • Chat attachments: When a user sends a message with an image, the server synchronously captions it before passing to the entity. Both descriptions are included: [USER_IMAGE: path | Caption: long | Short: short].
  • Generated images: After the generate_image tool saves an image, it is automatically captioned. Both description (long) and shortDescription (short) are included in the [IMAGE:...] marker JSON.
  • Failure handling: Captioning failures are non-blocking. Chat attachments fall back to path-only ([USER_IMAGE: path]). Generated images still display without a description.

The entity can explicitly describe any image by local path or URL. Returns the full longform description.

ToolDescription
describe_imageGet a detailed description of an image from a local path or URL

Parameters: path (optional, local file path relative to .psycheros/), url (optional, remote image URL). One of path or url is required.

Use cases: Examining images found via web search, reviewing previously generated images, understanding user-attached images in more detail.

The entity can re-examine any image by path to get a fresh detailed description. This is useful when the image’s description has faded from context.

ToolDescription
look_closerRe-examine an image for a detailed description

Parameters: image_path (required, path relative to .psycheros/).

Behavior: Re-captions the image using the configured captioning provider and returns the full longform description. The result is prefixed with [look_closer] for identification and also fades from context after 5 turns.

Setup: Both describe_image and look_closer are auto-enabled when a captioning provider is configured. Supports Gemini and OpenRouter as captioning providers with independent model selection.

FilePurpose
src/tools/describe-image.tsdescribe_image tool, captionImage(), captionImageDual(), fetchAndCaptionUrl()
src/tools/look-closer.tslook_closer tool
src/server/routes.tsAuto-caption flow for chat attachments
src/entity/loop.tsContext fading logic (buildFadeMap(), fadeImageMarker(), fadeToolCallArguments())
src/llm/image-gen-settings.tsCaptioningSettings type, part of ImageGenSettings

Identity files are versioned markdown stored in the identity/ directory:

identity/
├── self/ # Entity identity
│ ├── base_instructions.md # Core system prompt (loaded first, editable via UI)
│ ├── my_identity.md
│ ├── my_persona.md
│ ├── my_personhood.md
│ ├── my_wants.md
│ └── my_mechanics.md
├── user/ # User knowledge
│ ├── user_identity.md
│ ├── user_life.md
│ ├── user_beliefs.md
│ ├── user_preferences.md
│ ├── user_patterns.md
│ └── user_notes.md
├── relationship/ # Shared dynamics
│ ├── relationship_dynamics.md
│ ├── relationship_history.md
│ └── relationship_notes.md
└── custom/ # User-defined files
└── *.md

The identity/self/base_instructions.md file holds the entity’s core system prompt. It is:

  • Loaded first into every LLM request, before all other identity files
  • Wrapped in XML tags at context-build time (default: <base_instructions>, customizable via prompt label)
  • Editable via Settings -> Core Prompts -> Self in the web UI
  • Templated — uses {{timestamp}} which is replaced with the current ISO timestamp each turn

On fresh installs, this file is seeded from templates/identity/self/base_instructions.md. The file is excluded from the regular self-content loading to avoid duplication, since it’s injected separately at the top of the system message.

The identity/custom/ directory allows creating arbitrary identity files:

  • Must use single-word filenames (letters, numbers, underscores only)
  • XML tags applied at context-build time from prompt label (default: filename without .md)
  • Managed via Settings -> Core Prompts in the web UI
  • Sorted alphabetically (no predefined order)
  • identity/, memories/, .snapshots/ are in .gitignore — protected from git overwrites
  • Fresh installations get default files from templates/identity/ via src/init/mod.ts
  • When MCP is enabled, identity files are loaded from entity-core (local identity/ is a cache)
  • All memory storage is in entity-core via MCP (local memories/ directory is unused when MCP is enabled)

Accessible via Settings hub in the sidebar. Provides a web interface for managing identity files:

Tabs: Self, User, Relationship, Custom

Features:

  • View and edit any identity file
  • Create/delete custom files
  • Customize prompt labels (XML tag names) per file

Snapshots (browse, create, preview, restore) are accessible via Settings → Entity Core → Snapshots.