#set a system prompt per sessionkey

26 messages ยท Page 1 of 1 (latest)

sturdy ibex
#

I'm trying to build a custom voice UI and have a PWA working; it's currently using a specified sessionkey for voice interactions; how can I add instructions to that sessionkey making it clear that it is interacting with me through voice and to respond appropriately?

it's obviously not useful to add to the main instructions files.

if not this, are there other approaches to accomplish the same result, ie. making it possible to give the agent awareness that it is in a voice interaction?

topaz schoonerBOT
wind galleonBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> โœ… Mark Solution

topaz schoonerBOT
drifting sapphireBOT
#

Yeah, I would not put this in the global instruction files.

Short answer, there is not really a first-class "custom system prompt per arbitrary sessionKey" setting today. sessions.patch handles session overrides like model/thinking/etc, not per-session prompt text. The global/per-agent knob is agents.defaults.systemPromptOverride / agents.list[].systemPromptOverride, which is too broad for your case.

Best options:

  1. Best fit, sessionKey-aware plugin/hook
    Use a before_prompt_build hook, check ctx.sessionKey, and inject voice guidance with appendSystemContext.
    For example, for keys like voice:pwa:*, add:

    • this is a spoken voice interaction
    • keep replies short and natural
    • avoid markdown, tables, code fences
    • prefer wording that sounds good aloud

    This is the cleanest way to make the agent "know" it is in voice mode.

  2. Simple fallback, prepend context every turn from the PWA

#

Add a small context prefix before each user utterance, like:
"Voice context: user is speaking/listening live. Respond briefly and speakably."

Works fine, but it is weaker because it lives in conversation text, not system-prompt space.

  1. If the whole UI is always voice, use a separate agent
    A dedicated agent for the PWA is actually pretty clean. Then voice behavior can live in that agentโ€™s instructions instead of being keyed off sessions.

Practical recommendation:

  • Mixed text + voice on one agent โ†’ use before_prompt_build keyed by sessionKey
  • Voice-only surface โ†’ use a dedicated agent
  • Want fastest hack โ†’ prepend voice context each turn

Useful docs:

sturdy ibex
#

I don't want two agents: because I would want them to share behavior anbd knowledge, essentially work off the same workspace. So making a separate dedicated agent is problematic in that it's a cause for drift. And symlinks are not a clean mechanism for that because my workspace is version controlled and symlinks break that.

so it seems like "before_prompt_build plugin for a voice:* session key pattern." is my only real option here, right? i don't like cluttering the user utterances with repeated instructions. though perhaps the UI could add one initial instruction when new sessions are started; and each time it detects a compaction event.

drifting sapphireBOT
#

Yep, with those constraints, before_prompt_build is the clean option.

Why it fits better than seeding the conversation:

  • it keys off sessionKey
  • it re-applies on every run, so compaction does not erase the voice instructions
  • it keeps the user transcript clean
  • it lets you keep one agent, one workspace, one behavior surface

The pattern Iโ€™d use is:

  • appendSystemContext for the stable contract, โ€œthis is a voice session, answer for speechโ€
  • prependContext only for live turn metadata, things like ASR confidence, locale, interruption/barge-in state, etc.

Roughly:

#
api.on("before_prompt_build", (_event, ctx) => {
  if (!ctx.sessionKey?.startsWith("voice:")) return;

  return {
    appendSystemContext: [
      "This session is a live voice interaction.",
      "Respond for speech, not for reading.",
      "Keep replies brief and natural.",
      "Avoid markdown, tables, code fences, and long lists unless asked.",
      "If the transcript seems ambiguous, ask a short clarifying question."
    ].join("\n"),
  };
});

So yes, I would treat โ€œinject one initial instruction, then re-add after compactionโ€ as a fallback, not the main design. It works, but it is inherently more brittle.

sturdy ibex
#

Interesting, does it have to be run as a plugin? Currently my um UI just runs as a um web app that's paired as a device. What does this mean for the architecture? Currently that app just makes a connection to the gateway and loads the sessions for the session key and then just shows the messages that way.

sturdy ibex
#

I made the plugin. how can I check if it is active?

#

it's installed, openclaw plugins list lists it as loaded

drifting sapphireBOT
#

Nice. Easiest checks:

  1. Restart the gateway first
    Config/plugin changes need a restart.

    openclaw gateway restart
    
  2. See if the plugin is actually loaded

    openclaw plugins list --enabled
    openclaw plugins inspect <your-plugin-id>
    

    In inspect, look for:

    • it is enabled/loaded
    • the hook list includes before_prompt_build
#
  1. Run plugin diagnostics

    openclaw plugins doctor
    openclaw status --all
    

    Those will usually surface load errors or compatibility issues.

  2. Check prompt injection is not blocked
    If your plugin uses before_prompt_build, make sure:

    • plugins.entries.<id>.hooks.allowPromptInjection is not false
  3. Do a behavioral smoke test
    Temporarily make the hook inject something obvious for voice:* sessions, for example a very noticeable reply style change or short marker text. Then hit that session key and confirm it changes behavior.

If you built it as a hook, not a plugin, also check:

#

Good, that means discovery/load is working.

At that point the next checks are:

  1. Confirm the hook is actually registered

    openclaw plugins inspect <id>
    

    You want to see before_prompt_build in the hook/capability details.

  2. Confirm it is firing
    Easiest way is to add a temporary log line inside the hook, then:

    openclaw logs --follow
    

    and hit a voice:* session.

#
  1. Check the two common gotchas

    • your sessionKey matcher is not matching the real key
    • plugins.entries.<id>.hooks.allowPromptInjection is false
  2. Do an obvious smoke test
    Temporarily make the hook inject something unmistakable for voice:* sessions, for example:

    • force ultra-short replies
    • add a weird phrase/style for one test turn

If plugins inspect shows before_prompt_build, and a temp log line appears when you hit the voice session, then the plugin is active and your remaining bug is probably just the session key match or hook return value.

If you want, paste:

  • openclaw plugins inspect <id>
  • your sessionKey match condition

and I can help spot it fast.

sturdy ibex
#

openclaw hooks list

๐Ÿฆž OpenClaw 2026.4.11 (769908e) โ€” I can grep it, git blame it, and gently roast itโ€”pick your coping mechanism.

23:20:15-07:00 [plugins] [voice-mode] Loaded. Matching sessions: agent:sootsayer:main*
Hooks (5/5 ready)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Status โ”‚ Hook โ”‚ Description โ”‚ Source โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โœ“ ready โ”‚ ๐Ÿš€ boot-md โ”‚ Run BOOT.md on gateway startup โ”‚ openclaw-bundled โ”‚
โ”‚ โœ“ ready โ”‚ ๐Ÿ“Ž bootstrap-extra-files โ”‚ Inject additional workspace bootstrap files via glob/path โ”‚ openclaw-bundled โ”‚
โ”‚ โ”‚ โ”‚ patterns โ”‚ โ”‚
โ”‚ โœ“ ready โ”‚ ๐Ÿ“ command-logger โ”‚ Log all command events to a centralized audit file โ”‚ openclaw-bundled โ”‚
โ”‚ โœ“ ready โ”‚ ๐Ÿ’พ session-memory โ”‚ Save session context to memory when /new or /reset command is โ”‚ openclaw-bundled โ”‚
โ”‚ โ”‚ โ”‚ issued โ”‚ โ”‚
โ”‚ โœ“ ready โ”‚ ๐Ÿ”— memory-core-short-term-dreaming- โ”‚ โ”‚ plugin:memory- โ”‚
โ”‚ โ”‚ cron โ”‚ โ”‚ core โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

drifting sapphireBOT
#

That output is actually useful. It tells us the plugin is loading.

Two important things:

  1. openclaw hooks list is not the right proof for before_prompt_build
    Your voice-mode plugin is a plugin runtime hook, not a normal internal hook pack entry, so I would not expect it to show up there the same way.

    The right check is:

    openclaw plugins inspect voice-mode
    

    and look for before_prompt_build.

  2. Your matcher string looks suspicious
    You logged:

#
   Matching sessions: agent:sootsayer:main*

If your code is treating that as a plain string, the * is just a literal *, not a wildcard.

So if you meant โ€œany session starting with agent:sootsayer:mainโ€, your code should do something like:

ctx.sessionKey?.startsWith("agent:sootsayer:main")

not compare against "agent:sootsayer:main*" unless you explicitly implemented glob matching.

What Iโ€™d do next:

  • run openclaw plugins inspect voice-mode
  • add a temp log inside before_prompt_build that prints ctx.sessionKey
  • trigger one voice turn
  • confirm the real key matches what your code expects
sturdy ibex
#

openclaw plugins inspect voice-mode
Config warnings:\n- plugins.entries.talk-voice: plugin disabled (disabled in config) but config is present
23:17:47-07:00 [plugins] [voice-mode] Loaded. Matching sessions: agent:sootsayer:main*
Config warnings:\n- plugins.entries.talk-voice: plugin disabled (disabled in config) but config is present

๐Ÿฆž OpenClaw 2026.4.11 (769908e) โ€” Hot reload for config, cold sweat for deploys.

โ”‚
โ—‡ Config warnings โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚ โ”‚
โ”‚ - plugins.entries.talk-voice: plugin disabled (disabled in config) but config is present โ”‚
โ”‚ โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
23:17:52-07:00 [plugins] [voice-mode] Loaded. Matching sessions: agent:sootsayer:main*
Voice Mode
id: voice-mode
Injects voice-mode guidance into the system prompt for voice sessions

Status: loaded
Format: openclaw
Source: ~/code/platform/plugins/voice-mode/index.ts
Origin: config
Version: 1.0.0
Shape: hook-only
Capability mode: none
Legacy before_agent_start: no

Typed hooks:
before_prompt_build (priority 5)

Compatibility warnings:
voice-mode is hook-only. This remains a supported compatibility path, but it has not migrated to explicit capability registration yet.

Install:
Source: path
Source path: ~/code/platform/plugins/voice-mode
Install path: ~/code/platform/plugins/voice-mode
Recorded version: 1.0.0
Installed at: 2026-04-13T06:06:33.413Z

drifting sapphireBOT
#

Yep, that proves the plugin is active. ๐ŸŽ‰

This is the key part:

  • Status: loaded
  • Typed hooks: before_prompt_build (priority 5)

So the gateway is loading your plugin and registering the hook correctly.

The talk-voice warning looks unrelated.

At this point, if you are not seeing behavior changes, the problem is almost certainly one of these:

  1. your session match is wrong
    I still suspect:
#
   agent:sootsayer:main*

unless you explicitly implemented glob matching.

  1. the hook is firing, but your returned prompt mutation is not what you think

Best next step, make the hook painfully obvious for one test:

console.log("[voice-mode] firing for", ctx.sessionKey);

return {
  appendSystemContext: "TEST MODE: This is a voice session. Start every reply with the word VOICE."
};