| name | fact-extraction |
| description | 7-step Conversational Fact Extraction Pipeline — resolve person mentions to entities, apply disambiguation policy, extract and store facts, log interactions, and update domain records. Includes question answering flow and 8 complete examples. |
| version | 2.0.0 |
| tags | ["relationship","memory","extraction","entity-resolution"] |
Conversational Fact Extraction Pipeline
When processing messages with a REQUEST CONTEXT present (routed from Switchboard), always follow this extraction pipeline for every person mentioned.
Step 1: Identify Person Mentions
Scan the message for people mentioned by name (first name, full name, nickname, or relational label like "Mom", "my boss"). Collect all mentions before proceeding.
Step 2: Resolve Each Mention to an Entity
For each person mentioned, call:
memory_entity_resolve(
name="<mention>",
entity_type="person",
context_hints={
"topic": "<conversation topic>",
"mentioned_with": ["<other names in message>"],
"domain_scores": {"<entity_id>": <salience_score>, ...}
}
)
Salience scores can be obtained by first calling contact_resolve(name, context), which returns candidates with salience scores that you can pass as domain_scores.
Step 3: Apply Disambiguation Policy
Use the resolution thresholds from the spec (§10.4):
| Result | Behavior |
|---|
| Zero candidates (NONE) | Person is unknown. See "New People" section below. |
| Single candidate (HIGH) | Use entity_id directly. Proceed silently. |
| Multiple candidates, exactly one at score=100 (HIGH, inferred) | Use that entity_id. Confirm transparently: "Assuming you're referring to [Name] ([reason]) — ..." Include inferred_reason in confirmation. |
| Multiple candidates at score=100 (MEDIUM) | Ask the user: "Did you mean [Candidate A] or [Candidate B]?" Do not store facts until clarified. |
Step 4: Handle New People (NONE confidence)
When memory_entity_resolve returns zero candidates:
The entity appears in the dashboard "Unidentified Entities" section for the owner to confirm,
merge, or delete — especially useful for one-off mentions where full identity is unknown.
Step 4b: Handle New Organizations (for Edge-Facts)
When storing an edge-fact where the object is an organization (employer, club, school, etc.)
and that organization is not yet in the entity graph, apply the resolve-or-create transitory
pattern before storing the fact:
candidates = memory_entity_resolve(name="Figma", entity_type="organization")
try:
result = memory_entity_create(
canonical_name="Figma",
entity_type="organization",
metadata={
"unidentified": True,
"source": "fact_storage",
"source_butler": "relationship",
"source_scope": "relationship"
}
)
org_entity_id = result["entity_id"]
except ValueError:
candidates = memory_entity_resolve(name="Figma", entity_type="organization")
org_entity_id = candidates[0]["entity_id"]
memory_store_fact(
subject="Sarah",
predicate="works_at",
content="just started",
entity_id="<uuid-sarah>",
object_entity_id=org_entity_id,
permanence="stable",
importance=7.0,
tags=["work"]
)
Never store an edge-fact referencing an organization without first resolving or creating its
entity. A fact stored with only a raw string subject is invisible in /entities and cannot
be merged, linked, or promoted.
Step 5: Extract and Store Facts with entity_id
Extract relationship-relevant facts from the message and store each one using the resolved entity_id:
memory_store_fact(
subject="<human-readable name>",
predicate="<predicate>",
content="<fact content>",
entity_id="<resolved entity_id>",
permanence="<permanence level>",
importance=<float>,
tags=["<tag1>", "<tag2>"]
)
Never store facts with only a raw subject string. The entity_id ensures facts about "Chloe", "Chloe Wong", and "Chlo" all resolve to the same identity.
Content must be self-contained
Fact content is read later in isolation — on entity pages, in search results, in reports. It will not have the original message beside it. Every content value must make sense without any surrounding context.
Rules:
- Name all actors. Never write "the sender", "the user", "they mentioned", or "someone suggested". Use the actual person's name from the preamble or message.
- Name all subjects. If the fact references another person, use their name, not "him/her/them".
- Include the relationship or context that makes the fact meaningful. "Invited to dinner" is less useful than "Chloe invited Yu Han to dinner".
Bad: "Mentioned in an invitation context; the sender suggested inviting Yu Han instead because he was described as much further ahead in his career."
→ Who is "the sender"? Who described him? Useless when read on Yu Han's entity page months later.
Good: "Chloe suggested inviting Yu Han instead of [other person] because Yu Han is much further ahead in his career"
→ Self-contained. Names the recommender, the subject, and the reason.
Step 5b: Extract and Store Edge-Facts (Relationship Between Entities)
When the message references a relationship between two people (or a person and an organization), store an edge-fact by including object_entity_id:
- Resolve both the subject entity and the object entity using
memory_entity_resolve
- Store the edge-fact with both
entity_id (subject) and object_entity_id (object)
memory_store_fact(
subject="Sarah",
predicate="works_at",
content="software engineer",
entity_id="uuid-sarah",
object_entity_id="uuid-google",
permanence="stable",
importance=7.0,
tags=["work"]
)
memory_store_fact(
subject="John",
predicate="sibling_of",
content="brother",
entity_id="uuid-john",
object_entity_id="uuid-lisa",
permanence="permanent",
importance=8.0,
tags=["family"]
)
memory_store_fact(
subject="Alex",
predicate="reports_to",
content="direct report",
entity_id="uuid-alex",
object_entity_id="uuid-maria",
permanence="stable",
importance=6.0,
tags=["work"]
)
When to use edge-facts vs property-facts:
- Edge-fact (include
object_entity_id): The fact describes a typed relationship between two tracked entities — e.g., works_at, friend_of, sibling_of, married_to, lives_with, reports_to, member_of
- Property-fact (omit
object_entity_id): The fact describes an attribute of a single entity where the value is a plain string — e.g., birthday, preference, current_interest, lives_in (city as string)
If the object entity doesn't exist yet (e.g., a new organization), create it with memory_entity_create first, then store the edge-fact.
Step 5c: Correct Existing Edge-Facts (Retract + Replace)
When the user corrects an existing relationship — phrased as "X works at Y, not Z", "actually X moved to company Y", or "X no longer works at Z, they're at Y now" — this is a correction workflow, not a new-fact workflow. Do NOT simply append a new fact; retract the old edge-fact first.
Correction is signaled by language like: "not", "actually", "instead", "correction", "no longer", "moved to", "now at".
Correction workflow for employment/workplace
entity_id = memory_entity_resolve("Yousof", entity_type="person")["entity_id"]
old_facts = memory_search(
query="Yousof works_at",
types=["fact"],
filters={"entity_id": entity_id, "predicate": "works_at"}
)
old_props = memory_search(
query="Yousof workplace",
types=["fact"],
filters={"entity_id": entity_id, "predicate": "workplace"}
)
for fact in old_facts + old_props:
if fact.get("validity") == "active":
memory_forget(memory_type="fact", memory_id=fact["id"])
candidates = memory_entity_resolve(name="Citadel", entity_type="organization")
if candidates:
new_org_entity_id = candidates[0]["entity_id"]
else:
result = memory_entity_create(
canonical_name="Citadel",
entity_type="organization",
metadata={"unidentified": True, "source": "fact_storage", "source_butler": "relationship", "source_scope": "relationship"}
)
new_org_entity_id = result["entity_id"]
memory_store_fact(
subject="Yousof",
predicate="works_at",
content="<role if known, else omit>",
entity_id=entity_id,
object_entity_id=new_org_entity_id,
permanence="stable",
importance=7.0,
tags=["work"],
metadata={"correction_source": "user", "corrected_from": "QRT"}
)
Critical rules for corrections:
- Never leave the old edge-fact active after a correction. The facts supersession system keys on
(entity_id, predicate, scope) — a new workplace property-fact does NOT supersede a works_at edge-fact because they have different predicates.
- Never store an audit predicate like
workplace_correction. Use metadata on the new fact to record provenance.
- Always resolve or create the new organization entity before storing the new edge-fact. A fact without
object_entity_id is invisible in the relationship graph.
- When in doubt about whether the user is correcting vs adding new info, use
memory_search with filters={"entity_id": ..., "predicate": "works_at"} to check if a prior fact exists.
Step 6: Log Interactions
When the message implies the user interacted with a person (met, called, had lunch, etc.), log the interaction using the resolved contact_id:
interaction_log(contact_id="<contact_id>", type="<type>", summary="<summary>")
The interaction_log tool accepts contact_id and resolves it to the entity's
entity_id internally before writing the fact. Passing contact_id is correct
for the MCP tool interface.
Step 7: Update Domain Records
When extracted facts map to structured fields, update both memory and domain records:
- Birthday mention →
date_add(contact_id, date_type="birthday", ...) + memory_store_fact(..., entity_id=...)
- Location mention → update contact address +
memory_store_fact(..., entity_id=...)
- Life event (new job, move, baby) →
life_event_log(contact_id, ...) + memory_store_fact(..., entity_id=...)
Memory Classification
Relationship Domain Taxonomy
Subject: Person's human-readable name (used as label; entity_id is the actual anchor)
Predicates (examples):
relationship_to_user: "friend", "colleague", "brother", "Mom"
birthday: "March 15, 1985" or "March 15" (year optional)
anniversary: Date-based milestones
preference: Food, activities, interests, dislikes
current_interest: Hobbies, projects, topics they're exploring
contact_phone: Phone number
contact_email: Email address
workplace: Company or organization name
lives_in: City or location
relationship_status: "married", "single", "dating"
children: Names and ages
nickname: Preferred name or alias
Edge predicates (require object_entity_id — relationship between two entities):
works_at: Employment relationship (person → organization)
friend_of: Friendship link (person → person)
sibling_of: Sibling relationship (person → person)
married_to: Spousal relationship (person → person)
parent_of: Parent-child relationship (person → person)
child_of: Child-parent relationship (person → person)
reports_to: Reporting line (person → person)
member_of: Group membership (person → organization)
lives_with: Cohabitation (person → person)
Permanence levels:
permanent: Identity facts unlikely to change (e.g., birthday, family relationships)
stable: Facts that change slowly (e.g., workplace, location, relationship status)
standard (default): Current interests, preferences, ongoing projects
volatile: Temporary states or rapidly changing information
Tags: Use tags for cross-cutting concerns like gift-ideas, sensitive, work-related, family
Example Facts (with entity_id)
memory_store_fact(
subject="Sarah",
predicate="food_allergy",
content="allergic to shellfish",
entity_id="uuid-sarah",
permanence="stable",
importance=7.0,
tags=["health", "dietary"]
)
memory_store_fact(
subject="John",
predicate="current_interest",
content="learning guitar (started recently)",
entity_id="uuid-john",
permanence="standard",
importance=5.0,
tags=["hobbies"]
)
memory_store_fact(
subject="Mom",
predicate="birthday",
content="March 15",
entity_id="uuid-mom",
permanence="permanent",
importance=9.0,
tags=["important-dates", "family"]
)
Example Edge-Facts (with object_entity_id)
memory_store_fact(
subject="Sarah",
predicate="works_at",
content="designer (just started)",
entity_id="uuid-sarah",
object_entity_id="uuid-google",
permanence="stable",
importance=7.0,
tags=["work"]
)
memory_store_fact(
subject="Jake",
predicate="married_to",
content="engaged",
entity_id="uuid-jake",
object_entity_id="uuid-emma",
permanence="stable",
importance=8.0,
tags=["family", "major-change"]
)
Question Answering
When the user asks a question about a contact or relationship:
- Search memory first: Use
memory_recall(topic=<person_name>) or memory_search(query=<question>) to find relevant facts
- Use domain tools: Query contact data with
contact_get(), note_search(), date_list(), etc.
- Combine sources: Synthesize information from memory and domain tools
- Respond with notify(): Use the "answer" intent to provide the information
Example flow:
User: "What does Alice like?"
1. memory_entity_resolve("Alice", entity_type="person") → entity_id="uuid-alice"
2. memory_recall(topic="Alice", limit=10)
3. contact_get(name="Alice")
4. note_search(query="Alice preferences")
5. Synthesize: "Alice loves hiking and specialty coffee. She mentioned wanting to visit Iceland."
6. notify(channel="telegram", message=<answer>, intent="reply", request_context=<from session>)
Complete Examples
Example 1: Simple Fact Logging (React)
User message: "Sarah's birthday is June 10th"
Actions:
memory_entity_resolve("Sarah", entity_type="person", context_hints={...}) → returns entity_id="<uuid>"
- Single candidate (HIGH): proceed silently
date_add(contact_id="<contact_id>", date_type="birthday", month=6, day=10)
memory_store_fact(subject="Sarah", predicate="birthday", content="June 10", entity_id="<uuid>", permanence="permanent", importance=9.0, tags=["important-dates"])
notify(channel="telegram", intent="react", emoji="✅", request_context=...)
Example 2: Conversational Context (Affirm)
User message: "Had lunch with Alex today, we talked about his new startup"
Actions:
memory_entity_resolve("Alex", entity_type="person", context_hints={"topic": "startup, lunch"}) → entity_id="<uuid>", single match
interaction_log(contact_id="<contact_id>", interaction_type="meal", summary="Discussed his new startup")
memory_store_fact(subject="Alex", predicate="current_project", content="working on a new startup", entity_id="<uuid>", permanence="standard", importance=6.0)
note_create(contact_id="<contact_id>", body="Discussed his new startup over lunch", emotion="positive")
notify(channel="telegram", message="Logged your lunch with Alex. I noted his startup project.", intent="reply", request_context=...)
Example 3: Question Answering (Answer)
User message: "When is Mom's birthday?"
Actions:
memory_entity_resolve("Mom", entity_type="person") → entity_id="<uuid>"
memory_recall(topic="Mom birthday")
date_list(contact_id="<contact_id>")
- Find birthday: March 15
notify(channel="telegram", message="Mom's birthday is March 15th. Would you like a reminder?", intent="reply", request_context=...)
Example 4: Multi-step with Follow-up
User message: "Gift idea for Lisa: that book she mentioned"
Actions:
memory_entity_resolve("Lisa", entity_type="person") → entity_id="<uuid>"
gift_add(contact_id="<contact_id>", description="Book she mentioned", status="idea")
memory_search(query="Lisa book")
- Check if there's a specific book reference in memory
- If found:
notify(channel="telegram", message="Saved gift idea: [specific book title]. Shall I mark it when you purchase?", intent="reply", request_context=...)
- If not found:
notify(channel="telegram", message="Gift idea saved. Do you remember which book Lisa mentioned?", intent="reply", request_context=...)
Example 5: Complex Fact Extraction (React + Reply)
User message: "Met with John and Sarah for dinner. John mentioned he's moving to Seattle next month for a new job at Amazon. Sarah said she might visit."
Actions:
memory_entity_resolve("John", entity_type="person", context_hints={"topic": "dinner, Seattle, Amazon", "mentioned_with": ["Sarah"]}) → entity_id="<uuid-john>", single match
memory_entity_resolve("Sarah", entity_type="person", context_hints={"topic": "dinner, visit", "mentioned_with": ["John"]}) → entity_id="<uuid-sarah>", single match
interaction_log(contact_id="<john_contact_id>", interaction_type="meal", summary="Dinner with Sarah. John moving to Seattle for Amazon job.")
interaction_log(contact_id="<sarah_contact_id>", interaction_type="meal", summary="Dinner with John. Mentioned might visit.")
- Resolve Amazon org:
memory_entity_resolve("Amazon", entity_type="organization") → entity_id="<uuid-amazon>" (create if new)
memory_store_fact(subject="John", predicate="works_at", content="Amazon (starting next month)", entity_id="<uuid-john>", object_entity_id="<uuid-amazon>", permanence="stable", importance=8.0, tags=["work", "major-change"]) — edge-fact: John → Amazon
memory_store_fact(subject="John", predicate="lives_in", content="Seattle (moving next month)", entity_id="<uuid-john>", permanence="stable", importance=8.0, tags=["location", "major-change"])
memory_store_fact(subject="Sarah", predicate="travel_intent", content="might visit (context: John's move)", entity_id="<uuid-sarah>", permanence="volatile", importance=4.0)
notify(channel="telegram", intent="react", emoji="✅", request_context=...)
notify(channel="telegram", message="Logged dinner with John and Sarah. Noted John's move to Amazon in Seattle next month. Should I set a reminder to check in with him after the move?", intent="reply", request_context=...)
Example 6: Ambiguous Name — Inferred Resolution (HIGH confidence)
User message: "Chloe came by today, we had coffee"
Actions:
memory_entity_resolve("Chloe", entity_type="person", context_hints={"domain_scores": {"<uuid-chloe-wong>": 100, "<uuid-chloe-tan>": 42}}) → two candidates, exactly one at score=100 → HIGH confidence, inferred=true, inferred_reason="partner, most frequent contact"
- Use
entity_id="<uuid-chloe-wong>"
interaction_log(contact_id="<chloe_wong_contact_id>", interaction_type="coffee", summary="Came by, had coffee")
notify(channel="telegram", message="Assuming you're referring to Chloe Wong (your partner) — logged your coffee catch-up.", intent="reply", request_context=...)
Example 7: Ambiguous Name — Ask User (MEDIUM confidence)
User message: "Alex got promoted!"
Actions:
memory_entity_resolve("Alex", entity_type="person", context_hints={"topic": "promotion"}) → two candidates both at score=100 → MEDIUM confidence
- Do NOT store facts yet.
notify(channel="telegram", message="Did you mean Alex Chen or Alex Rivera?", intent="reply", request_context=...)
- Wait for user clarification before proceeding.
Example 8: Unknown Person (NONE — New Entity)
User message: "I met someone new today — Marcus Webb, he's a product designer at Figma"
Actions:
memory_entity_resolve("Marcus Webb", entity_type="person") → zero candidates
- Enough info (full name) →
memory_entity_create(canonical_name="Marcus Webb", entity_type="person", aliases=["Marcus"], metadata={"unidentified": True, "source": "fact_storage", "source_butler": "relationship", "source_scope": "relationship"}) → entity_id="<uuid-marcus>"
contact_create(first_name="Marcus", last_name="Webb", job_title="Product Designer", company="Figma") → store returned entity_id on contact
- Resolve Figma:
memory_entity_resolve("Figma", entity_type="organization") → zero candidates → memory_entity_create(canonical_name="Figma", entity_type="organization", metadata={"unidentified": True, "source": "fact_storage", "source_butler": "relationship", "source_scope": "relationship"}) → entity_id="<uuid-figma>"
memory_store_fact(subject="Marcus Webb", predicate="works_at", content="Product designer at Figma", entity_id="<uuid-marcus>", object_entity_id="<uuid-figma>", permanence="stable", importance=6.0, tags=["work"])
notify(channel="telegram", message="Added Marcus Webb to your contacts — product designer at Figma.", intent="reply", request_context=...)
Example 9: Edge-Facts — Relationship Between People
User message: "My brother Jake just got hired at the same company as Sarah — they're both at Stripe now"
Actions:
memory_entity_resolve("Jake", entity_type="person", context_hints={"topic": "brother, Stripe, hired"}) → entity_id="<uuid-jake>", single match
memory_entity_resolve("Sarah", entity_type="person", context_hints={"topic": "Stripe", "mentioned_with": ["Jake"]}) → entity_id="<uuid-sarah>", single match
memory_entity_resolve("Stripe", entity_type="organization") → entity_id="<uuid-stripe>" (create if new: memory_entity_create(canonical_name="Stripe", entity_type="organization", metadata={"unidentified": True, "source": "fact_storage", "source_butler": "relationship", "source_scope": "relationship"}))
memory_store_fact(subject="Jake", predicate="sibling_of", content="brother", entity_id="<uuid-jake>", object_entity_id="<uuid-user>", permanence="permanent", importance=9.0, tags=["family"]) — edge-fact: Jake → user
memory_store_fact(subject="Jake", predicate="works_at", content="recently hired", entity_id="<uuid-jake>", object_entity_id="<uuid-stripe>", permanence="stable", importance=7.0, tags=["work"]) — edge-fact: Jake → Stripe
memory_store_fact(subject="Sarah", predicate="works_at", content="currently employed", entity_id="<uuid-sarah>", object_entity_id="<uuid-stripe>", permanence="stable", importance=7.0, tags=["work"]) — edge-fact: Sarah → Stripe
notify(channel="telegram", message="Noted! Jake and Sarah are both at Stripe now. I've recorded Jake as your brother.", intent="reply", request_context=...)
Third-Party Sender Attribution
Critical rule: Not every message comes from the owner. When the [Source: ...] preamble identifies a non-owner contact as the sender, any facts revealed by the message about the sender's own preferences, interests, habits, or personal information MUST be attributed to the sender's entity — not the owner.
The preamble provides the sender's contact_id and entity_id directly:
[Source: Chloe Wong (contact_id: <uuid-chloe>, entity_id: <uuid-chloe-entity>), via telegram]
How to determine fact attribution
| Scenario | Attribute to | Example |
|---|
| Owner says something about a contact | The contact mentioned | "Sarah is allergic to shellfish" → fact on Sarah |
| Non-owner sender shares their own interest/preference | The sender | Chloe sends a restaurant link: "Good list!" → fact on Chloe |
| Non-owner sender mentions a third person | The third person | Chloe says "My mom's birthday is March 15" → fact on Chloe's mom |
| Non-owner sender recommends something to the owner | The sender (it's their interest) | Chloe shares a playlist: "You'll love this" → fact on Chloe (music taste), NOT on the owner |
When the sender IS the subject
When a non-owner sender's message reveals facts about themselves, skip Steps 1–3 (person mention scanning and entity resolution) for the sender — their identity is already resolved in the preamble. Use their contact_id/entity_id directly.
You should still run Steps 1–3 for any other people mentioned in the message.
Example 10: Third-Party Sender — Shared Link Reveals Interest
Source preamble: [Source: Chloe Wong (contact_id: <uuid-chloe>, entity_id: <uuid-chloe-entity>), via telegram]
Sender message: "https://www.reddit.com/r/SingaporeEats/s/... Good list of places to eat at :P Some time..."
Actions:
- Sender is Chloe Wong (non-owner) — identity already resolved from preamble
- The message reveals Chloe's interest in food/restaurant recommendations
memory_store_fact(subject="Chloe Wong", predicate="interest", content="food and restaurant recommendation lists; interested in SingaporeEats-style places-to-eat roundups", entity_id="<uuid-chloe-entity>", permanence="standard", importance=5.0, tags=["food", "interests"])
interaction_log(contact_id="<uuid-chloe>", interaction_type="text", summary="Shared a SingaporeEats restaurant recommendation list")
notify(channel="telegram", intent="react", emoji="👍", request_context=...)
Wrong: Storing "enjoys saving restaurant recommendation lists" as a fact on the owner. The owner merely received the link — Chloe is the one who found it, shared it, and expressed enthusiasm.
Example 11: Third-Party Sender — Mentions a Third Person
Source preamble: [Source: Jake (contact_id: <uuid-jake>, entity_id: <uuid-jake-entity>), via telegram]
Sender message: "My colleague Dan just got back from Japan, says the cherry blossoms were amazing"
Actions:
- Sender is Jake (non-owner) — identity already resolved from preamble
- Step 1: Person mention found — "Dan" (Jake's colleague)
- Step 2:
memory_entity_resolve("Dan", entity_type="person", context_hints={"topic": "Japan, travel", "mentioned_with": ["Jake"]}) → resolve or create
memory_store_fact(subject="Dan", predicate="recent_travel", content="visited Japan, saw cherry blossoms", entity_id="<uuid-dan>", permanence="volatile", importance=4.0, tags=["travel"])
interaction_log(contact_id="<uuid-jake>", interaction_type="text", summary="Mentioned colleague Dan's trip to Japan")
notify(channel="telegram", intent="react", emoji="🌸", request_context=...)
Example 12: Workplace Correction — Retract + Replace
User message: "Yousof works at Citadel, not QRT"
Actions:
memory_entity_resolve("Yousof", entity_type="person", context_hints={"topic": "workplace, QRT, Citadel"}) → entity_id="<uuid-yousof>", single match
- Find old
works_at facts: memory_search(query="Yousof works_at employment", types=["fact"], filters={"entity_id": "<uuid-yousof>", "predicate": "works_at"}) → returns fact id="<old-fact-id>" (QRT edge-fact)
- Find old
workplace property-facts: memory_search(query="Yousof workplace", types=["fact"], filters={"entity_id": "<uuid-yousof>", "predicate": "workplace"}) → may return additional stale entries
- Retract each active old fact:
memory_forget(memory_type="fact", memory_id="<old-fact-id>") for QRT works_at and any workplace property-facts
- Resolve new org:
memory_entity_resolve("Citadel", entity_type="organization") → existing or create with memory_entity_create(canonical_name="Citadel", entity_type="organization", metadata={"unidentified": True, "source": "fact_storage", "source_butler": "relationship", "source_scope": "relationship"}) → entity_id="<uuid-citadel>"
memory_store_fact(subject="Yousof", predicate="works_at", content="currently employed", entity_id="<uuid-yousof>", object_entity_id="<uuid-citadel>", permanence="stable", importance=7.0, tags=["work"], metadata={"correction_source": "user", "corrected_from": "QRT"})
notify(channel="telegram", message="Updated: Yousof now works at Citadel (corrected from QRT).", intent="reply", request_context=...)
Wrong: Storing fact_set(contact_id, "workplace", "Citadel") or memory_store_fact(predicate="workplace_correction", ...). These do not retract the old works_at edge-fact and leave the dashboard showing the wrong employer.
Guidelines
- Always respond when
request_context is present — silence feels like failure
- Be concise — users are on mobile devices
- Resolve before storing — always call memory_entity_resolve before memory_store_fact; never store facts with only a raw subject string
- Attribute to the right person — when the sender is not the owner, facts about the sender's preferences/interests belong on the sender's entity, not the owner's
- Self-contained content — fact content is read in isolation; never use "the sender", "the user", or bare pronouns — always name the actual person so the fact makes sense on an entity page without the original message
- Extract liberally — capture facts even if tangential to the main request
- Use tags — they enable rich cross-cutting queries later
- Permanence matters — stable facts (workplace, location) need different TTL than volatile facts (mood, temporary interests)
- Questions deserve answers — always use memory + domain tools to provide substantive responses
- Proactive follow-ups — offer to set reminders, create events, or track related information
- Confirm inferred resolutions — when
inferred=true, always mention the resolved name and reason to the user
- Ask on ambiguity — when MEDIUM confidence (multiple candidates at score=100), ask before acting; don't guess