Help us improve
Share bugs, ideas, or general feedback.
From all-skills
Entity Memory Management is the systematic extraction, persistence, and retrieval of named entities across agent sessions, enabling long-term contextual awareness that transforms transient conversations into persistent relationships.
npx claudepluginhub itallstartedwithaidea/agent-skills --plugin security-skillsHow this skill is triggered — by the user, by Claude, or both
Slash command
/all-skills:entity-memory-managementThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Part of [Agent Skills™](https://github.com/itallstartedwithaidea/agent-skills) by [googleadsagent.ai™](https://googleadsagent.ai)
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Part of Agent Skills™ by googleadsagent.ai™
Entity Memory Management is the systematic extraction, persistence, and retrieval of named entities across agent sessions, enabling long-term contextual awareness that transforms transient conversations into persistent relationships. While generic memory systems store session summaries, entity memory operates at a granular level — tracking specific campaigns, metrics, decisions, user pain points, and preferences as discrete, queryable objects. This precision enables agents to recall that "Campaign Alpha switched to Target CPA last Tuesday" rather than merely "we discussed bidding changes recently."
This skill is the production entity memory architecture powering Buddy™ at googleadsagent.ai™, where entity memory tracks thousands of named entities across hundreds of user sessions. The system maintains entities of defined types — campaigns (with their strategies, budgets, and performance trajectories), metrics (specific KPIs and their historical values), decisions (changes made and their rationale), and pain points (recurring issues the user has expressed frustration about). Each entity is extracted, typed, scored for importance, stored in Cloudflare KV, and retrieved contextually in future sessions.
The result is an agent that genuinely "remembers" its users — not through conversation replay, but through structured, typed knowledge that persists and evolves. This creates a fundamentally different user experience where the agent builds on prior context rather than starting fresh.
graph TD
A[Conversation Stream] --> B[Entity Extractor]
B --> C{Entity Type}
C -->|Campaign| D[Campaign Schema]
C -->|Metric| E[Metric Schema]
C -->|Decision| F[Decision Schema]
C -->|Pain Point| G[Pain Point Schema]
C -->|Preference| H[Preference Schema]
D --> I[Importance Scorer]
E --> I
F --> I
G --> I
H --> I
I --> J[KV Store Writer]
J --> K[Entity Index]
L[New Session Start] --> M[Context Builder]
M --> N[Entity Retriever]
N --> K
K --> O[Relevance Filter]
O --> P[Context Injection]
The entity extraction pipeline processes the conversation stream through a type-aware extractor that identifies named entities and classifies them into predefined schemas. Each extracted entity receives an importance score based on conversational emphasis, actionability, and novelty (new information scores higher than repeated information). Scored entities are written to Cloudflare KV with structured keys that enable efficient retrieval. At the start of each new session, the context builder queries the entity index for the current user, filters by relevance to the new task, and injects the most relevant entities into the agent's context.
Entity Type Schemas:
interface BaseEntity {
id: string;
userId: string;
type: EntityType;
name: string;
content: string;
importance: number;
createdAt: number;
updatedAt: number;
sessionId: string;
version: number;
}
type EntityType = "campaign" | "metric" | "decision" | "pain_point" | "preference";
interface CampaignEntity extends BaseEntity {
type: "campaign";
metadata: {
campaignId?: string;
strategy?: string;
budget?: number;
status?: string;
lastPerformance?: Record<string, number>;
};
}
interface DecisionEntity extends BaseEntity {
type: "decision";
metadata: {
action: string;
rationale: string;
affectedEntities: string[];
reversible: boolean;
outcome?: string;
};
}
interface PainPointEntity extends BaseEntity {
type: "pain_point";
metadata: {
severity: "low" | "medium" | "high";
frequency: number;
resolved: boolean;
relatedCampaigns: string[];
};
}
interface PreferenceEntity extends BaseEntity {
type: "preference";
metadata: {
category: "communication" | "analysis" | "risk" | "reporting";
value: string;
confidence: number;
};
}
Entity Extraction Prompt:
EXTRACTION_PROMPT = """Analyze this conversation and extract structured entities.
Entity types and what to capture:
- CAMPAIGN: Campaign names, strategies, budgets, performance data, status changes
- METRIC: Specific KPI values mentioned (CPA, ROAS, CTR, etc.) with context
- DECISION: Choices made or agreed upon, with rationale and affected entities
- PAIN_POINT: Frustrations, recurring issues, or problems the user expressed
- PREFERENCE: Stated preferences for communication style, risk tolerance, analysis depth
Conversation:
{conversation}
Extract entities as JSON:
{{
"entities": [
{{
"type": "campaign|metric|decision|pain_point|preference",
"name": "short identifier",
"content": "full description of the entity",
"importance": 0.0-1.0,
"metadata": {{}}
}}
]
}}
Rules:
- Only extract entities with clear evidence in the conversation
- Importance 0.9+: explicit decisions, critical metrics, stated preferences
- Importance 0.5-0.8: mentioned campaigns, context metrics, implied preferences
- Importance <0.5: background information, passing mentions
- Merge with existing entities when updating rather than creating duplicates"""
Entity Store with Cloudflare KV:
class EntityStore {
constructor(private kv: KVNamespace) {}
async upsert(entity: BaseEntity): Promise<void> {
const key = `entity:${entity.userId}:${entity.type}:${entity.id}`;
const existing = await this.kv.get<BaseEntity>(key, "json");
if (existing) {
entity.version = existing.version + 1;
entity.createdAt = existing.createdAt;
if (entity.importance < existing.importance) {
entity.importance = (entity.importance + existing.importance) / 2;
}
}
await this.kv.put(key, JSON.stringify(entity), {
expirationTtl: 60 * 60 * 24 * 180,
metadata: {
type: entity.type,
importance: entity.importance,
updatedAt: entity.updatedAt,
},
});
await this.updateIndex(entity);
}
private async updateIndex(entity: BaseEntity): Promise<void> {
const indexKey = `index:${entity.userId}:entities`;
const index = await this.kv.get<EntityIndex>(indexKey, "json") || { entries: [] };
const existingIdx = index.entries.findIndex(
(e) => e.id === entity.id && e.type === entity.type
);
const entry = {
id: entity.id,
type: entity.type,
name: entity.name,
importance: entity.importance,
updatedAt: entity.updatedAt,
};
if (existingIdx >= 0) {
index.entries[existingIdx] = entry;
} else {
index.entries.push(entry);
}
await this.kv.put(indexKey, JSON.stringify(index));
}
async retrieveForContext(userId: string, taskContext: string, limit = 20): Promise<BaseEntity[]> {
const indexKey = `index:${userId}:entities`;
const index = await this.kv.get<EntityIndex>(indexKey, "json");
if (!index) return [];
const ranked = index.entries
.map((entry) => ({
...entry,
score: this.contextRelevance(entry, taskContext) * 0.6 + entry.importance * 0.3 + this.recencyScore(entry.updatedAt) * 0.1,
}))
.sort((a, b) => b.score - a.score)
.slice(0, limit);
const entities: BaseEntity[] = [];
for (const entry of ranked) {
const key = `entity:${userId}:${entry.type}:${entry.id}`;
const entity = await this.kv.get<BaseEntity>(key, "json");
if (entity) entities.push(entity);
}
return entities;
}
private contextRelevance(entry: { name: string; type: string }, context: string): number {
const contextLower = context.toLowerCase();
const nameWords = entry.name.toLowerCase().split(/\s+/);
const matches = nameWords.filter((w) => contextLower.includes(w)).length;
return Math.min(1, matches / Math.max(nameWords.length, 1));
}
private recencyScore(timestamp: number): number {
const ageMs = Date.now() - timestamp;
const ageDays = ageMs / (1000 * 60 * 60 * 24);
return Math.max(0, 1 - ageDays / 180);
}
}
Context Assembly from Entities:
def assemble_entity_context(entities: list[dict], max_tokens: int = 4000) -> str:
grouped = {}
for entity in entities:
grouped.setdefault(entity["type"], []).append(entity)
type_labels = {
"campaign": "Known Campaigns",
"metric": "Historical Metrics",
"decision": "Prior Decisions",
"pain_point": "Known Pain Points",
"preference": "User Preferences",
}
sections = ["<entity_memory>"]
token_count = 0
for entity_type in ["preference", "pain_point", "decision", "campaign", "metric"]:
items = grouped.get(entity_type, [])
if not items:
continue
label = type_labels.get(entity_type, entity_type.title())
section = f"\n### {label}\n"
for item in items[:5]:
entry = f"- **{item['name']}**: {item['content']}\n"
entry_tokens = len(entry.split()) * 1.3
if token_count + entry_tokens > max_tokens:
break
section += entry
token_count += entry_tokens
sections.append(section)
sections.append("</entity_memory>")
return "\n".join(sections)
| Feature | Claude Code | Cursor | Codex | Gemini CLI |
|---|---|---|---|---|
| Entity extraction | ✅ Full | ✅ Full | ✅ Full | ✅ Full |
| KV storage | ✅ Any KV/file | ✅ Any KV/file | ✅ Any KV/file | ✅ Any KV/file |
| Session hooks | ✅ Stop hooks | ✅ Extensions | ⚠️ Custom | ⚠️ Custom |
| Context injection | ✅ CLAUDE.md | ✅ Skills | ✅ Instructions | ✅ System prompts |
| Index management | ✅ Full | ✅ Full | ✅ Full | ✅ Full |
entity-memory, named-entities, cross-session-continuity, cloudflare-kv, entity-extraction, entity-types, importance-scoring, contextual-retrieval, user-preferences, agent-skills
© 2026 googleadsagent.ai™ | Agent Skills™ | MIT License