AI Reference
Written by ddxfish on .
- Docs
- Plugin Author Guide
- AI Reference
- All docs ↓
You are reading the dev branch of the docs, which may describe unreleased changes. Read the main docs.
Compact reference for Sapphire's own use when building plugins. For simple tool creation (tool_save/tool_load), see the TOOLMAKER doc — this covers full plugin development.
When creating or modifying plugins:
- Plugin = folder in
user/plugins/{name}/withplugin.jsonmanifest (shipped ones live incore/plugins/{name}/; both import asplugins.{name}; a root-levelplugins/is not scanned) - Project root in plugin code:
from core.paths import PROJECT_ROOT— never count.parents plugin.jsonrequiresnamefield, everything else optional- Hooks = Python functions receiving mutable
HookEventobject - Tools =
TOOLSlist +execute(function_name, arguments, config)returning(str, bool)(optional 4th argplugin_settings, 5thcredentials— inspected by arity) - Tool schema supports
is_local(bool or"endpoint") andnetwork: trueflags - Optional
get_tools()(returnsTOOLS-shaped list from current settings) = dynamic descriptions; rebuilt in place on settings-save, no reload (name stays fixed) - Voice commands = pre_chat hooks with trigger matching,
bypass_llm: truefor instant response - Routes = custom HTTP endpoints at
/api/plugin/{name}/{path}; handler is called with keyword args (each path param +body,settings,credentials,query,request) — END the signature with**_or it raises TypeError. Auth+CSRF+rate-limit enforced by framework - Schedule = cron tasks calling
run(event)handler, event hassystem,config,task,plugin_state - Web settings =
web/index.jsusingregisterPluginSettings(), served at/plugin-web/{name}/ - Plugin scripts =
web/main.jsauto-loaded on app startup, listen forsapphire:tool_startDOM events - Daemon = background thread with
start(plugin_loader, settings)/stop(), emits events viaplugin_loader.emit_daemon_event(source_name, json_payload) - Daemon event sources declared in manifest
capabilities.daemon.event_sources[]—name,label,filter_fields,task_fields - Reply handlers:
plugin_loader.register_reply_handler(plugin_name, handler)— routes LLM responses back to source platform - Providers = register TTS/STT/Embedding/LLM backends via
capabilities.providers— appear in settings dropdowns - App = full-page plugin UI via
capabilities.app—label,icon, optionalnav: truefor navrail promotion (max 3) - Themes = custom CSS themes via
capabilities.themes[]—css,scripts,preview, per-themesettings - State =
plugin_loader.get_plugin_state(name)for persistent key-value storage - Chat-scoped state =
plugin_loader.get_chat_state(name)—get/put/append/read_all/replace/delete/keys/get_all_chats/meta(chat, ...); rows live in the chat DB and rename/encrypt/delete with the chat. Hidden chat (private + sealed): reads empty, writes RAISE. Use it instead of chat-keyed files underuser/ - System access =
event.metadata.get("system")inpost_stt,pre_chat,ghost_inject,post_llm,post_chat,pre_execute, and the fourtts_*streaming hooks prompt_inject,post_execute,pre_ttsdo NOT get system metadata — onlyconfig- System gives access to:
tts(voice/speed/pitch/speak/stop),toggle_stt(),toggle_wakeword(),llm_chat(chat/history/prompt),function_manager(tools/scopes) - Enable/disable live via
PUT /api/webui/plugins/toggle/{name} - All hooks:
post_stt,pre_chat,prompt_inject,ghost_inject,post_llm,post_chat,pre_execute,post_execute,tools_filter,pre_tts,post_tts,on_wake,provider_switched,tts_stream_start,tts_chunk_text,tts_chunk_audio,tts_stream_end,chat_renamed,chat_deleted,chat_cleared,chat_vaulted,plugins_ready tools_filter= per-turn subtract-only tool fence (event.tools intersection; fail-open).chat_cleared= history wiped, chat + chat-scoped rows survive (metadata keychat)- Private chats: hooks are withheld from any plugin that lacks top-level
"privacy_aware": trueinplugin.json(fail-closed if privacy can't be resolved). Runner stampsevent.chat_name/event.chat_private. Always delivered regardless:chat_renamed,chat_deleted,chat_cleared,plugins_ready,provider_switched,on_wake chat_vaultedfires post-seal to privacy_aware plugins only —metadata["name"]is the chat that just went private; scrub plaintext deposits keyed by it, don't log the nameprompt_injectmutates the system prompt (long-lived, breaks Claude cache).ghost_inject(since 2.6.4) injects per-turn ephemera as labeled operator metadata that doesn't break cache — prefer it for time-sensitive context, ambient state, weather, calendar, etc.post_sttfires only for voice input (after STT transcription, before chat pipeline)post_llmfires after LLM response, before history save + TTS — mutateresponseto filter/translate/stylepost_ttsfires after playback completes or is stopped (daemon thread, observational)on_wakefires when wakeword detected, before recording starts (notification only, must return fast)- Error isolation: exceptions logged and skipped, never crash pipeline
- Signing: ed25519 signatures in
plugin.sig, tampered = always blocked, unsigned = blocked unless sideloading enabled - Settings stored at
user/webui/plugins/{name}.json, read viaGET /api/webui/plugins/{name}/settings - Settings files are in
user/(gitignored) — never tracked - Multi-account tools use ContextVar scopes:
scope_email,scope_bitcoin,scope_knowledge, etc. - Web UI modules available:
plugin-registry.js,plugins-api.js,notify.js(rows + toasts),toast.js(legacy),modal.js,danger-confirm.js,fetch.js - CSS variables for theming:
--bg,--text,--border,--trim,--success,--error, etc. - Always guard system access with
hasattr()checks — subsystems may be None if disabled
Thinking blocks (think tags)
ONE reader: core/think.py (web twin interfaces/web/static/shared/think.js). Never write a think-tag regex.
from core import think—think.strip(text)= the answer (use before sending, speaking, parsing JSON, storing);think.split(text)=(answer, thinking);think.has,think.segments,think.wrap(thinking, answer),think.only,think.unfinished(buf),think.PAIRS,think.BLOCK_RE.- Tags:
think,seed:think,thinking,redacted_thinking,reasoning(+ closerseed:cot_budget_reflect). - Rules: a tag in backticks or in a fenced code block is a word; opener + later closer = block; a closer with nothing open folds the text since the last block into thinking (never across a block); an unclosed opener is thinking only at a block start.
- Side jobs:
disable_thinking: Truein generation params, treatresponse.content_is_reasoningas empty, strip anyway. - Full guide: thinking.md. Design rules for plugins: design-principles.md.
Edit this page on GitHub · docs/plugin-author/ai-reference.md @ 96cc222 · synced
