| 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. Routes registry-relational edges to relationship_assert_fact(); reserves memory_store_fact() for narrative edges. Includes question answering flow and 8 complete examples. |
| version | 3.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 Registry-Relational Edge-Facts)
When storing a registry-relational 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 calling relationship_assert_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"]
relationship_assert_fact(
subject="<uuid-sarah>",
predicate="works-at",
object=org_entity_id,
src="relationship",
object_kind="entity",
conf=0.9,
weight=5,
)
Never store a registry-relational edge using memory_store_fact(). See the
"Canonical fact-store boundary" section below for the full discriminator.
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.
Canonical fact-store boundary: where each kind of fact lives
Three categories of facts, three destinations (relationship-entity-lifecycle
"Canonical fact-store layering"; module-memory "Registry-relational edges are
out of scope for the memory facts store"):
Category 1 — Identity-contact triples → relationship_assert_fact()
Channel identifiers and identity predicates (has-email, has-phone,
has-handle, has-address, has-birthday, has-website) live ONLY in
relationship.entity_facts, written through relationship_assert_fact().
The butler's contact_create / contact_update / date_add tools route
these automatically — you do not assert them by hand.
Do NOT call memory_store_fact(predicate="has-email", ...) The writer
rejects identity-contact predicates with a ValueError.
Category 2 — Registry-relational edges → relationship_assert_fact(object_kind="entity")
An edge between two tracked entities is registry-relational when its
predicate is a durable standing relationship type registered in
relationship.entity_predicate_registry (relational family):
| Predicate (canonical hyphenated) | Meaning |
|---|
knows | general acquaintance |
friend-of | friendship |
family-of | kinship (siblings use this) |
partner-of | spousal / partner |
parent-of | parent → child |
child-of | child → parent |
colleague-of | professional peer |
works-at | person → organization (employment) |
member-of | person → organization (membership) |
co-attended | event co-attendance |
purchased-from | transactional |
subscribed-to | subscription |
visited | place visit |
manages | person manages another person or organization |
managed-by | person is managed by another person (inverse of manages) |
manages-property | person manages a place or organization |
participant-of | durable participation in a group, org, or recurring event |
invited-by | person was invited or referred by another person |
rental-agent | person acts as rental agent for an organization or place |
rental-location | person has a rental relationship with a place |
These MUST be written through relationship_assert_fact(object_kind="entity").
The memory writer rejects them with a ValueError if you try memory_store_fact.
Underscore aliases (works_at, friend_of, managed_by, rental_agent, etc.)
are resolved automatically by relationship_assert_fact() — but always write
the hyphenated canonical form in this skill to be explicit.
Category 3 — Narrative and episodic edges → memory_store_fact(object_entity_id=...)
An edge is narrative when it is episodic or coordination context that happens
to reference two entities but is NOT a durable standing relationship type — for
example planned_dinner_with, wake_coordination, social_exchange_with,
job_opportunity, talked_to, meetup_coordination.
These live in {schema}.facts via memory_store_fact(object_entity_id=...).
Discriminator rule:
If the predicate is in the registry relational family above → relationship_assert_fact.
Otherwise (episodic, one-off, coordination) → memory_store_fact(object_entity_id=...).
Do NOT call memory_store_fact(predicate="works-at", object_entity_id=..., ...)
(or any other registry-relational predicate with object_entity_id). The writer
rejects those with a ValueError directing you to relationship_assert_fact().
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), determine which store owns the edge using the discriminator in the
"Canonical fact-store boundary" section above, then follow the appropriate path.
When to use edge-facts vs property-facts:
- Edge-fact (registry-relational): The fact is a durable standing relationship between two tracked entities with a registry predicate → use
relationship_assert_fact(object_kind="entity").
- Edge-fact (narrative): The fact is episodic, one-off, or coordination context referencing two entities → use
memory_store_fact(object_entity_id=...).
- Property-fact: 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) → use memory_store_fact() without object_entity_id.
Registry-relational edges → relationship_assert_fact
Resolve both entities first; then assert the edge through the central writer.
relationship_assert_fact(
subject="uuid-sarah",
predicate="works-at",
object="uuid-google",
src="relationship",
object_kind="entity",
conf=0.9,
weight=5,
)
relationship_assert_fact(
subject="uuid-john",
predicate="family-of",
object="uuid-lisa",
src="relationship",
object_kind="entity",
conf=1.0,
weight=8,
)
relationship_assert_fact(
subject="uuid-alice",
predicate="parent-of",
object="uuid-bob",
src="relationship",
object_kind="entity",
conf=1.0,
weight=9,
)
If the object entity doesn't exist yet (e.g., a new organization), create it with
memory_entity_create first (see Step 4b), then assert the edge.
Narrative edges → memory_store_fact
Episodic or coordination context that references two entities but is NOT a registry
predicate uses memory_store_fact with object_entity_id.
memory_store_fact(
subject="owner",
predicate="planned_dinner_with",
content="planning dinner next week",
entity_id="uuid-user",
object_entity_id="uuid-alex",
permanence="volatile",
importance=4.0,
tags=["social"]
)
Step 5c: Dual-Emit Rule — Property Facts That Imply Standing Relationships
Some property predicates describe what a standing relationship IS rather than
being pure attribute facts. When you extract a property fact whose content
names another person and describes a durable relationship, you MUST emit BOTH:
- The property fact via
memory_store_fact() (for narrative record / search).
Always include object_entity_id in this call — set it to the resolved entity UUID
of the person/org named in the content. This lets the scheduled memory_curation
job pick up the prose fact and promote it to a structured edge even when the
dual-emit step below is skipped or repeated in a future session.
- The registry-relational edge via
relationship_assert_fact(object_kind='entity') (for
the entity graph).
Emitting only the property fact silently drops the structured edge and leaves the
relationship graph impoverished (zero kinship/partner/colleague edges despite known
relationships).
Property predicates that ALWAYS require a dual-emit
| Property predicate | Implied relational edge | Example content |
|---|
living_arrangement with partner/cohabiting content | partner-of | "Cohabiting partner with Chloe Wong" |
relationship_status with partner/spouse content | partner-of | "Married to [Name]" / "Dating [Name]" |
relationship_to_user with family value | see table below | "Mom" / "Brother" / "Son" |
family_relationship | see table below | "Mummy is Tze How Lee's mother" |
children with a named person | parent-of (subject → named child) | "Has a daughter, Emma" |
parents / mother / father | child-of (subject → named parent) | "Father is [Name]" |
sibling / siblings | family-of | "Sister is [Name]" |
colleague / coworker with named person | colleague-of | "Colleagues with [Name] at Acme" |
manager / reports_to | managed-by | "Reports to [Name]" |
Relationship-to-user value → edge predicate mapping
relationship_to_user value | Edge to emit (subject → object) | Notes |
|---|
| "Mom" / "Mother" / "Mum" | parent-of (contact → owner) | Owner is child; contact is parent |
| "Dad" / "Father" | parent-of (contact → owner) | Owner is child; contact is parent |
| "Son" / "Daughter" / "Child" | child-of (contact → owner) | Owner is parent; contact is child |
| "Brother" / "Sister" / "Sibling" | family-of (bidirectional) | |
| "Partner" / "Spouse" / "Wife" / "Husband" / "Boyfriend" / "Girlfriend" | partner-of (contact → owner) | |
| "Boss" / "Manager" | manages (contact → owner) | |
| "Colleague" / "Co-worker" | colleague-of (contact → owner) | |
| "Friend" | friend-of (contact → owner) | |
Dual-emit code pattern
candidates = memory_entity_resolve(name="Chloe Wong", entity_type="person")
if candidates:
chloe_entity_id = candidates[0]["entity_id"]
else:
chloe = memory_entity_create(
canonical_name="Chloe Wong",
entity_type="person",
metadata={"unidentified": True, "source": "fact_storage",
"source_butler": "relationship", "source_scope": "relationship"}
)
chloe_entity_id = chloe["entity_id"]
memory_store_fact(
subject="owner",
predicate="living_arrangement",
content="Cohabiting partner with Chloe Wong",
entity_id="<owner-entity-id>",
object_entity_id=chloe_entity_id,
permanence="stable",
importance=8.0,
tags=["relationship"],
)
relationship_assert_fact(
subject="<owner-entity-id>",
predicate="partner-of",
object=chloe_entity_id,
src="relationship",
object_kind=,
conf=,
weight=,
)
Step 5d: Confidence Gate for Family Predicates
Kinship edges (parent-of, child-of, family-of) are prone to LLM mis-extraction
when the model infers a relationship from context rather than reading an explicit
statement. A live mis-extraction: "has a son" was stored as a parent-of edge when
the owner has no son.
The central writer (relationship_assert_fact) enforces a confidence gate:
conf ≥ 0.8 → writes the edge directly (explicit-assertion tier).
conf < 0.8 → routes to pending_approval for human confirmation (no edge written).
When to use which confidence level
| Scenario | conf to use |
|---|
| Explicit statement: "Mummy is X's mother" / "X and Y are siblings" | conf=1.0 |
| Near-explicit: name is mentioned in a clear relational context ("my brother Jake called") | conf=0.9 |
| Reasonably confident inference: prior facts corroborate the relationship | conf=0.8 |
| Inferred / ambiguous: implied by context without direct statement | conf=0.5–0.7 → gated |
| Speculative: one possible interpretation among others | conf < 0.5 → gated |
Practical rule
NEVER assert parent-of, child-of, or family-of with conf < 0.8.
At conf < 0.8, the central writer gates the assertion to pending approval automatically —
you do NOT need to skip the call. But choosing conf < 0.8 is the correct signal that you
are not certain, and the owner will be prompted to confirm.
If you cannot make a confident determination from the message, omit the kinship edge
entirely (do not force-assert with an artificially high conf).
Non-kinship predicates are NOT gated
partner-of, friend-of, knows, colleague-of, and all other non-kinship predicates
follow the normal upsert path at any confidence level. Only the three kinship predicates
(parent-of, child-of, family-of) trigger the gate.
Step 5e: Correct Existing Registry-Relational Edge-Facts (Retract + Re-assert)
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. Registry-relational edges live in
relationship.entity_facts (via relationship_assert_fact), so the correction workflow
also goes through relationship_assert_fact.
Correction is signaled by language like: "not", "actually", "instead", "correction",
"no longer", "moved to", "now at".
The central writer supports automatic supersession: calling relationship_assert_fact with
the same (subject, predicate, object_kind='entity') triple and updated provenance will
supersede the old row atomically. For a changed object (different organization), you can
simply assert the new fact — if the old and new objects differ, both rows remain active
(there's no automatic retraction of the old relationship for a different object). Explicitly
assert the retraction when needed; for new-object corrections, asserting the corrected edge
via relationship_assert_fact is sufficient to record the new canonical value.
Correction workflow for employment/workplace
person_entity_id = memory_entity_resolve("Yousof", entity_type="person")[0]["entity_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"]
relationship_assert_fact(
subject=person_entity_id,
predicate="works-at",
object=new_org_entity_id,
src="relationship",
object_kind="entity",
conf=0.95,
weight=5,
)
old_props = memory_search(
query="Yousof workplace",
types=["fact"],
filters={"entity_id": person_entity_id, "predicate": "workplace"}
)
for fact in old_props:
fact.get() == :
memory_forget(memory_type=, memory_id=fact[])
Critical rules for corrections:
- Never use
memory_store_fact for works-at or other registry-relational predicates.
The writer will reject them with a ValueError.
- Never store an audit predicate like
workplace_correction. The corrected
relationship_assert_fact call already records the new authoritative value.
- Always resolve or create the new organization entity before asserting the edge.
- When in doubt about whether the user is correcting vs adding new info, assert
the new edge via
relationship_assert_fact — the writer handles supersession
automatically, so re-asserting the same triple is safe and idempotent.
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
Registry-relational edge predicates (require object_entity_id; use relationship_assert_fact(object_kind="entity") — hyphenated canonical names):
works-at: Employment relationship (person → organization)
member-of: Group/org membership (person → organization)
friend-of: Friendship link (person → person)
family-of: Kinship — siblings, cousins, etc. (person → person)
partner-of: Spousal / partner relationship (person → person)
parent-of: Parent → child (person → person)
child-of: Child → parent (person → person)
colleague-of: Professional peer (person → person)
knows: General acquaintance (person → person / entity)
co-attended: Event co-attendance (person → entity)
purchased-from: Transactional (person → organization)
subscribed-to: Subscription (person → entity)
visited: Place visit (person → place)
manages: Person manages another person or org (person → person/org)
managed-by: Person is managed by another person (person → person)
manages-property: Person manages a place or organization (person → entity)
participant-of: Durable participation in a group, org, or recurring event (person → entity)
invited-by: Person was invited or referred by another person (person → person)
rental-agent: Person acts as rental agent for an org or place (person → entity)
rental-location: Person has a rental relationship with a place (person → entity)
Narrative edge predicates (require object_entity_id; use memory_store_fact(object_entity_id=...) — free-form, not in registry):
planned_dinner_with: Episodic coordination context
wake_coordination: Scheduling coordination
social_exchange_with: Episodic social interaction
job_opportunity: Episodic job lead — not a standing relationship
invited_to: One-time event invitation — episodic
outreach_replied: Interaction response — episodic
party_location: Event context detail — episodic
likes: Property-like preference with an entity object — not a durable standing type
announced_by: Episodic event attribution
surprise_meeting_point: Episodic coordination detail
move_coordination: Logistics for a one-time move event
talked_to: Interaction context — not a durable standing relationship
dinner_invitation: Episodic dinner invitation
meetup_coordination: Episodic meetup logistics
- Any other episodic / one-off / coordination predicate not in the registry above
Note: reports_to and lives_with have no registry entry — use colleague-of/managed-by
or a narrative predicate until a registry migration adds them.
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 Registry-Relational Edge-Facts → relationship_assert_fact
relationship_assert_fact(
subject="uuid-sarah",
predicate="works-at",
object="uuid-google",
src="relationship",
object_kind="entity",
conf=0.9,
weight=5,
)
relationship_assert_fact(
subject="uuid-jake",
predicate="partner-of",
object="uuid-emma",
src="relationship",
object_kind="entity",
conf=1.0,
weight=9,
)
Example Narrative Edge-Facts → memory_store_fact
memory_store_fact(
subject="owner",
predicate="planned_dinner_with",
content="dinner next Friday",
entity_id="uuid-owner",
object_entity_id="uuid-alex",
permanence="volatile",
importance=4.0,
tags=["social"]
)
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)
relationship_assert_fact(subject="<uuid-john>", predicate="works-at", object="<uuid-amazon>", src="relationship", object_kind="entity", conf=0.9, weight=5) — registry-relational edge: 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>"
relationship_assert_fact(subject="<uuid-marcus>", predicate="works-at", object="<uuid-figma>", src="relationship", object_kind="entity", conf=0.9, weight=5) — registry-relational edge: Marcus → Figma
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"}))
relationship_assert_fact(subject="<uuid-jake>", predicate="family-of", object="<uuid-user>", src="relationship", object_kind="entity", conf=1.0, weight=9) — registry-relational edge: Jake → owner (siblings use family-of)
relationship_assert_fact(subject="<uuid-jake>", predicate="works-at", object="<uuid-stripe>", src="relationship", object_kind="entity", conf=0.9, weight=5) — registry-relational edge: Jake → Stripe
relationship_assert_fact(subject="<uuid-sarah>", predicate="works-at", object="<uuid-stripe>", src="relationship", object_kind="entity", conf=0.9, weight=5) — registry-relational edge: 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 — Re-assert via Central Writer
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
- Resolve new org:
memory_entity_resolve("Citadel", entity_type="organization") → existing or create with memory_entity_create(canonical_name="Citadel", ...) → entity_id="<uuid-citadel>"
relationship_assert_fact(subject="<uuid-yousof>", predicate="works-at", object="<uuid-citadel>", src="relationship", object_kind="entity", conf=0.95, weight=5) — asserts the new Citadel edge (central writer supersedes if same object, otherwise new active row)
- Also retract stale
workplace property-facts: memory_search(query="Yousof workplace", types=["fact"], filters={"entity_id": "<uuid-yousof>", "predicate": "workplace"}) → retract each with memory_forget
notify(channel="telegram", message="Updated: Yousof now works at Citadel (corrected from QRT).", intent="reply", request_context=...)
Wrong: memory_store_fact(predicate="works_at", object_entity_id=..., ...) — the writer rejects registry-relational predicates with object_entity_id set. Use relationship_assert_fact for all works-at edges.
Example 13: Dual-Emit — Property Fact That Implies a Relational Edge
User message: "Chloe and I have been cohabiting partners for 3 years"
Context: owner is sending (identified by preamble), Chloe Wong is already a contact.
Actions:
memory_entity_resolve("Chloe", entity_type="person", context_hints={...}) → entity_id="<uuid-chloe>", single match (HIGH conf)
memory_store_fact(subject="owner", predicate="living_arrangement", content="Cohabiting partner with Chloe Wong for 3 years", entity_id="<uuid-owner>", object_entity_id="<uuid-chloe>", permanence="stable", importance=9.0, tags=["relationship", "family"]) — narrative property fact with object_entity_id so memory_curation can promote it
relationship_assert_fact(subject="<uuid-owner>", predicate="partner-of", object="<uuid-chloe>", src="relationship", object_kind="entity", conf=1.0, weight=9) — MANDATORY dual-emit: explicit partnership → full confidence registry-relational edge (owner entity → pending_approval via RFC 0017 §2.3, not written directly)
notify(channel="telegram", intent="react", emoji="✅", request_context=...)
Why dual-emit? The living_arrangement property fact alone leaves the entity graph with zero partner-of edges. Searching for "who is the owner's partner?" finds nothing. The registry-relational edge is what populates /entities/concentration and makes the relationship graph useful.
Why include object_entity_id in memory_store_fact? Prose predicates like living_arrangement are not registry-relational predicates so the writer accepts object_entity_id on them. Including it lets the memory_curation scheduled job find and promote this fact independently of the explicit relationship_assert_fact call, providing a safety net if the dual-emit is ever missed in a session.
Example 14: Inferred Family Claim — Low-Confidence Gate
User message: "Someone in the office mentioned I look like I could have kids"
Actions:
- No named person mentioned. No explicit kinship statement.
- This is ambiguous / speculative — do NOT emit any kinship edge.
- No
relationship_assert_fact call for parent-of / child-of.
notify(channel="telegram", intent="react", emoji="✅", request_context=...) — react silently or acknowledge.
Why not assert a kinship edge? The statement is a vague comment, not a relationship claim. Asserting parent-of here would be a mis-extraction (conf would be ~0.2–0.3, well below the 0.8 gate). Even if you called relationship_assert_fact(conf=0.3), the central writer would gate it to pending_approval — but do not manufacture kinship edges from non-relational prose.
Contrast: "My son Jake called today" → parent-of(owner → Jake) with conf=0.9 — explicit statement, bypass gate, write edge.
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 any write; never store facts with only a raw subject string
- Route edges correctly — registry-relational edges (works-at, friend-of, family-of, …) →
relationship_assert_fact(object_kind="entity"); narrative edges → memory_store_fact(object_entity_id=...); see "Canonical fact-store boundary" section
- Dual-emit standing relationships — when a property fact's content asserts a durable relationship (living_arrangement/partner, family_relationship/parent, relationship_status/spouse, children, siblings), ALSO emit the registry-relational edge via
relationship_assert_fact(object_kind='entity'); see Step 5c
- Gate inferred kinship —
parent-of, child-of, family-of require conf ≥ 0.8 to write a direct edge; lower-confidence inferences are gated to pending approval by the central writer; never force-assert kinship you cannot confirm; see Step 5d
- 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