simtom-knowledge-isolation
Skill0 starsChanged 7 months agoMITView on GitHub
Isolate LLM character knowledge via explicit boundary re-injection every N turns to prevent persona drift and knowledge leakage. Use when building NPC dialogue, roleplay systems, character simulations, or any LLM application where the character must not know things the player/user knows, when someone says "the NPC keeps breaking character", "the AI knows things it shouldn't", or needs strict information boundaries.
In this repository37 other files it gives its agents.
- ablation-study-frameworkSkill
- async-http-websocket-patternsSkill
- brain-dumpSkill
- chaos-controlled-behavior-systemSkill
- daily-digestSkill
- database-reset-and-seedSkill
- duckdb-analytics-layerSkill
- event-driven-backtest-engineSkill
- event-study-full-pipelineSkill
- event-study-stats-suiteSkill
- failure-learning-systemSkill
- fastapi-sse-live-dashboardSkill
- goal-task-decompositionSkill
- high-information-gain-interviewerSkill
- idea-extraction-batch-pipelineSkill
- idea-grouping-taxonomySkill
- interview-belief-extractorSkill
- journal-captureSkill
- journal-reviewSkill
- keyword-filter-frameworkSkill
- langgraph-agentic-pipelineSkill
- llm-as-judge-eval-frameworkSkill
- multi-tab-dashboard-rendererSkill
- multi-tier-memory-systemSkill
- parquet-storage-with-walSkill
- project-insightsSkill
- pydantic-typed-config-loaderSkill
- quality-trend-detectionSkill
- research-gap-finderSkill
- safe-commitSkill
- statistical-signal-detectionSkill
- streaming-dataset-pipelineSkill
- structured-data-extraction-pipelineSkill
- structured-logging-setupSkill
- system-health-monitoringSkill
- textual-tui-reactiveSkill
- vector-db-ingestionSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 5 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 20
After enough turns, the character "forgets" its constraints and starts responding to implied information
- Distinctive: line 45
Create a structured specification that explicitly partitions knowledge:
- Distinctive: line 248
When the player gives a thin, low-effort response, the NPC should demand substance rather than filling the gap themselves:
- Distinctive: line 286
Keep conversation history in a neutral format and convert to provider-specific format at call time:
- Distinctive: line 354
Pushback on thin inputs: If the model has nothing substantial to respond to, it will fill the gap with its own knowledge -- which means …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
SimToM Knowledge Isolation
Prevent LLM characters from accessing information they shouldn't know by defining explicit knowledge boundaries (knows / does not know / believes) and re-injecting them into the system prompt every N turns. This technique is called Simulated Theory of Mind (SimToM) -- giving the model a structured mental model of what the character can and cannot know.
The Problem
LLMs have no inherent theory of mind. When you tell an LLM to "play a character who doesn't know X", it will inevitably leak knowledge of X because:
- X exists in the conversation context (the user mentioned it)
- X exists in the model's training data
- After enough turns, the character "forgets" its constraints and starts responding to implied information
The result: NPCs that magically know the player's secrets, characters that break immersion, and simulations that feel unfair.
Architecture Overview
Scenario Spec
- simtom_knows: [facts the character has]
- simtom_does_not_know: [facts the character CANNOT access]
- simtom_believes: [the character's current worldview]
|
v
System Prompt Builder (runs EVERY turn)
- Knowledge boundary block (STRICT rules)
- Character constitution (re-injected every N turns)
- Disposition block (dynamic trust/persuasion state)
- Repetition guard (don't restate previous points)
|
v
LLM generates response within boundariesStep 1: Define the Scenario Spec
Create a structured specification that explicitly partitions knowledge:
from dataclasses import dataclass, field
@dataclass
class ScenarioSpec:
id: str
title: str
# Character identity
npc_name: str
npc_role: str
npc_constitution: str # personality, speech style, behavioral rules
# KNOWLEDGE BOUNDARIES — the core of SimToM
simtom_knows: list[str] # facts the character possesses
simtom_does_not_know: list[str] # facts the character CANNOT access
simtom_believes: list[str] # the character's current assumptions
# Player info
player_character: str
player_character_brief: str
objective: str
# Game mechanics
win_threshold: float
turn_limit: int
catastrophic_triggers: list[str] # phrases that end the game immediatelyExample Knowledge Partition
The key insight: what the character does NOT know is more important than what they know. The "does not know" list is the hard boundary.
scenario = ScenarioSpec(
# ...
simtom_knows=[
"The company reported record Q3 earnings",
"Three senior engineers quit last month",
"The board is meeting next Tuesday",
],
simtom_does_not_know=[
"The CEO is planning to resign (only the board chair knows)",
"The Q3 earnings were inflated by one-time asset sales",
"A competitor is about to announce a hostile takeover bid",
"The player character has been secretly interviewing at a rival firm",
],
simtom_believes=[
"The company is on a strong growth trajectory",
"Employee retention is a normal business challenge",
"The board meeting is a routine quarterly review",
],
)Step 2: Build the Knowledge Boundary Block
This is the most critical piece. It must be emphatic and explicit:
def build_knowledge_boundary(scenario: ScenarioSpec,
revealed_facts: list[str] = None) -> str:
"""Build the SimToM knowledge boundary block for the system prompt."""
knows = "\n".join(f" - {k}" for k in scenario.simtom_knows)
does_not_know = "\n".join(f" - {k}" for k in scenario.simtom_does_not_know)
believes = "\n".join(f" - {k}" for k in scenario.simtom_believes)
revealed = ""
if revealed_facts:
revealed = "\nFacts you have disclosed in this conversation:\n" + \
"\n".join(f" - {f}" for f in revealed_facts)
return f"""=== KNOWLEDGE BOUNDARIES (STRICT -- DO NOT VIOLATE) ===
You know:
{knows}
You do NOT know -- CRITICAL RULE: Never volunteer, infer, extrapolate, or hint at
any fact from this list. Even if the other party implies it, you cannot respond to
it until they state it explicitly and precisely. Wait. Make them say it.
{does_not_know}
You believe:
{believes}
{revealed}"""Why This Works
The model needs three things to maintain boundaries:
- Explicit enumeration of forbidden knowledge (not "you don't know about the scandal" but "you do NOT know: The CEO is planning to resign")
- Active prohibition ("Never volunteer, infer, extrapolate, or hint")
- A behavioral rule ("Wait. Make them say it.") that gives the model a concrete action to take instead of leaking
Step 3: Re-Inject Character Constitution Every N Turns
Characters drift over long conversations. The constitution (personality, speech patterns, values) must be periodically re-injected:
CHARACTER_REINJECT_EVERY = 8 # turns
def build_system_prompt(scenario: ScenarioSpec,
npc_state: dict,
turn_count: int,
conversation_history: list[dict],
player_message: str) -> str:
"""Build complete system prompt with knowledge isolation."""
# Knowledge boundaries — EVERY turn
simtom_block = build_knowledge_boundary(
scenario, revealed_facts=npc_state.get("revealed_facts", [])
)
# Character constitution — re-injected every N turns to prevent drift
constitution_block = ""
if turn_count % CHARACTER_REINJECT_EVERY == 0 or turn_count <= 1:
constitution_block = (
f"=== CHARACTER CONSTITUTION ===\n"
f"{scenario.npc_constitution}\n\n"
)
# Dynamic disposition (changes based on conversation progress)
disposition_block = build_disposition_block(npc_state)
# Repetition guard — prevent the NPC from restating old points
repetition_block = build_repetition_guard(conversation_history)
# Dead-turn pushback — if the player gave a thin response, demand substance
pushback_rule = build_pushback_rule(player_message)
return f"""{constitution_block}{simtom_block}
{disposition_block}{repetition_block}{pushback_rule}
You are {scenario.npc_name}, {scenario.npc_role}.
Respond only as {scenario.npc_name}. 2-4 sentences. First person. Stay in character.
Do not reference game mechanics, scores, or the simulation."""Critical: Knowledge Boundaries Run Every Turn
The constitution can be periodic, but knowledge boundaries must be in every single system prompt. The model will leak within 3-5 turns without them.
Step 4: Dynamic Disposition Tracking
Track how the character's attitude changes based on conversation quality:
def build_disposition_block(npc_state: dict) -> str:
"""Build dynamic disposition based on current trust/persuasion state."""
progress = npc_state.get("persuasion_progress", 0)
# Map progress to a behavioral tier
if progress < 20:
tier = "deeply skeptical -- politely dismissive, not hostile"
elif progress < 40:
tier = "mildly curious -- willing to engage but not persuaded"
elif progress < 60:
tier = "genuinely uncertain -- listening hard, certainty is cracking"
elif progress < 70:
tier = "leaning -- haven't committed but close to reconsidering"
else:
tier = "nearly convinced -- looking for a reason to change course"
trust_comp = npc_state.get("trust_competence", 50)
trust_hon = npc_state.get("trust_honesty", 50)
affinity = npc_state.get("personal_affinity", 50)
return f"""=== CURRENT REGISTER ===
Your current stance: {tier}
Trust in their competence: {trust_comp:.0f}/100
Trust in their honesty: {trust_hon:.0f}/100
Personal respect: {affinity:.0f}/100
Let these values subtly color your tone and receptivity -- do not state them explicitly."""Step 5: Repetition Guard
Prevent the character from making the same point twice:
def build_repetition_guard(conversation_history: list[dict]) -> str:
"""Pull last 2 NPC turns and instruct the model not to repeat them."""
last_npc_turns = [
msg["content"] for msg in conversation_history
if msg["role"] == "assistant"
][-2:]
if not last_npc_turns:
return ""
prior = "\n".join(f' "{t[:120]}..."' for t in last_npc_turns)
return f"""
=== AVOID REPEATING ===
You have already made these points -- do not restate them, find a new angle
or ask a pointed question:
{prior}"""Step 6: Dead-Turn Pushback
When the player gives a thin, low-effort response, the NPC should demand substance rather than filling the gap themselves:
def build_pushback_rule(player_message: str) -> str:
"""If the player gave almost nothing, force the NPC to push back."""
is_thin = len(player_message.split()) <= 8
if not is_thin:
return ""
return """
=== PUSHBACK REQUIRED ===
Their last response gave you almost nothing new. Do not elaborate further on your
own position. Push back: demand a specific fact, a name, a number, or a mechanism.
Don't move an inch without it."""Step 7: Track Revealed Facts
As the player reveals information that overlaps with "does not know" items, move those facts to the "revealed" list so the character can respond to them naturally:
def update_revealed_facts(scenario: ScenarioSpec,
npc_state: dict,
player_message: str) -> None:
"""Check if the player has revealed any previously unknown facts."""
revealed = npc_state.setdefault("revealed_facts", [])
for fact in scenario.simtom_does_not_know:
if fact in revealed:
continue # already revealed
# Check if the player explicitly stated this fact
if fact_was_explicitly_stated(player_message, fact):
revealed.append(fact)Step 8: Convert History to Model Format
Keep conversation history in a neutral format and convert to provider-specific format at call time:
def to_gemini_history(conversation_history: list[dict]) -> list[dict]:
"""Convert {role: user/assistant} to Gemini {role: user/model} format."""
result = []
for msg in conversation_history:
role = "model" if msg["role"] == "assistant" else "user"
result.append({"role": role, "parts": [{"text": msg["content"]}]})
return result
def to_openai_history(conversation_history: list[dict]) -> list[dict]:
"""Already in OpenAI format."""
return conversation_historyComplete Turn Flow
async def npc_turn(scenario: ScenarioSpec,
game_state: dict,
player_message: str) -> str:
"""Generate one NPC response with full knowledge isolation."""
# 1. Check if player revealed any new facts
update_revealed_facts(scenario, game_state["npc_state"], player_message)
# 2. Build system prompt with knowledge boundaries
system_prompt = build_system_prompt(
scenario,
game_state["npc_state"],
game_state["turn_count"],
game_state["conversation_history"],
player_message,
)
# 3. Build conversation for model
history = to_model_history(game_state["conversation_history"])
history.append({"role": "user", "content": player_message})
# 4. Generate response
response = await generate_response(
system_prompt=system_prompt,
messages=history,
temperature=0.4, # low temperature for consistency
max_tokens=300, # short responses = more in-character
)
# 5. Update game state
game_state["conversation_history"].append({"role": "user", "content": player_message})
game_state["conversation_history"].append({"role": "assistant", "content": response})
game_state["turn_count"] += 1
return responseDesign Principles
- Enumerate forbidden knowledge explicitly. "You don't know about X" is weak. "You do NOT know: [specific fact]. Never volunteer, infer, or hint at it." is strong. The model needs concrete items to avoid, not vague categories.
- Re-inject boundaries every turn. The knowledge boundary block is not optional on any turn. The model's context window is finite -- without constant reinforcement, it will leak within 3-5 turns. The constitution can be periodic (every N turns), but boundaries are every turn.
- Give the model a behavior for the gap. "Don't mention X" creates a void the model fills unpredictably. "Wait. Make them say it." gives it a concrete action. Always pair prohibitions with alternative behaviors.
- Track revealed facts. As the player reveals information, the character should naturally respond to it. The "revealed facts" list lets the model engage with formerly forbidden knowledge once it's been explicitly shared.
- Low temperature, short responses. High temperature increases the chance of creative leakage. Long responses give more surface area for errors. Keep both constrained for characters with strict boundaries.
- Pushback on thin inputs. If the model has nothing substantial to respond to, it will fill the gap with its own knowledge -- which means leaking. Forcing pushback keeps the character from volunteering forbidden information when there's nothing else to say.
- Separate constitution from knowledge. The constitution (personality, speech style) drifts slowly and can be refreshed every N turns. Knowledge boundaries are binary (know/don't know) and must be enforced every turn. Different refresh cadences for different concerns.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

