@vellumai/assistant 0.4.49 → 0.4.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +24 -33
- package/README.md +3 -3
- package/docs/architecture/memory.md +180 -119
- package/package.json +2 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -0
- package/src/__tests__/credential-vault.test.ts +13 -1
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/invite-redemption-service.test.ts +65 -1
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +572 -5
- package/src/__tests__/oauth-store.test.ts +120 -6
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +46 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secure-keys.test.ts +7 -2
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/voice-invite-redemption.test.ts +32 -1
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +132 -0
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +43 -5
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/doctor.ts +4 -3
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +31 -2
- package/src/cli/commands/oauth/connections.ts +431 -97
- package/src/cli/commands/oauth/providers.ts +15 -1
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +4 -10
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +61 -3
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +31 -3
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -0
- package/src/daemon/server.ts +12 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/instrument.ts +61 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +22 -3
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/oauth/byo-connection.test.ts +8 -1
- package/src/oauth/oauth-store.ts +113 -27
- package/src/oauth/seed-providers.ts +6 -0
- package/src/oauth/token-persistence.ts +11 -3
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/invite-redemption-service.ts +19 -1
- package/src/runtime/invite-service.ts +25 -0
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/conversation-routes.ts +9 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +2 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +2 -1
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/secure-keys.ts +25 -3
- package/src/security/token-manager.ts +36 -36
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +1 -3
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/schedule/create.ts +8 -1
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -1,535 +0,0 @@
|
|
|
1
|
-
import { eq, sql } from "drizzle-orm";
|
|
2
|
-
|
|
3
|
-
import type { MemoryEntityConfig } from "../config/types.js";
|
|
4
|
-
import {
|
|
5
|
-
createTimeout,
|
|
6
|
-
extractToolUse,
|
|
7
|
-
getConfiguredProvider,
|
|
8
|
-
userMessage,
|
|
9
|
-
} from "../providers/provider-send-message.js";
|
|
10
|
-
import { getLogger } from "../util/logger.js";
|
|
11
|
-
import { truncate } from "../util/truncate.js";
|
|
12
|
-
import { getDb, rawAll } from "./db.js";
|
|
13
|
-
import {
|
|
14
|
-
memoryEntities,
|
|
15
|
-
memoryEntityRelations,
|
|
16
|
-
memoryItemEntities,
|
|
17
|
-
} from "./schema.js";
|
|
18
|
-
|
|
19
|
-
const log = getLogger("memory-entity-extractor");
|
|
20
|
-
|
|
21
|
-
const ENTITY_EXTRACTION_TIMEOUT_MS = 15_000;
|
|
22
|
-
|
|
23
|
-
export type EntityType =
|
|
24
|
-
| "person"
|
|
25
|
-
| "project"
|
|
26
|
-
| "tool"
|
|
27
|
-
| "company"
|
|
28
|
-
| "concept"
|
|
29
|
-
| "location"
|
|
30
|
-
| "organization";
|
|
31
|
-
|
|
32
|
-
export type EntityRelationType =
|
|
33
|
-
| "works_on"
|
|
34
|
-
| "uses"
|
|
35
|
-
| "owns"
|
|
36
|
-
| "member_of"
|
|
37
|
-
| "located_in"
|
|
38
|
-
| "depends_on"
|
|
39
|
-
| "collaborates_with"
|
|
40
|
-
| "reports_to"
|
|
41
|
-
| "related_to";
|
|
42
|
-
|
|
43
|
-
const VALID_ENTITY_TYPES = new Set<string>([
|
|
44
|
-
"person",
|
|
45
|
-
"project",
|
|
46
|
-
"tool",
|
|
47
|
-
"company",
|
|
48
|
-
"concept",
|
|
49
|
-
"location",
|
|
50
|
-
"organization",
|
|
51
|
-
]);
|
|
52
|
-
|
|
53
|
-
const VALID_RELATION_TYPES = new Set<string>([
|
|
54
|
-
"works_on",
|
|
55
|
-
"uses",
|
|
56
|
-
"owns",
|
|
57
|
-
"member_of",
|
|
58
|
-
"located_in",
|
|
59
|
-
"depends_on",
|
|
60
|
-
"collaborates_with",
|
|
61
|
-
"reports_to",
|
|
62
|
-
"related_to",
|
|
63
|
-
]);
|
|
64
|
-
|
|
65
|
-
export interface ExtractedEntity {
|
|
66
|
-
name: string;
|
|
67
|
-
type: EntityType;
|
|
68
|
-
aliases: string[];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface ExtractedEntityRelation {
|
|
72
|
-
sourceEntityName: string;
|
|
73
|
-
targetEntityName: string;
|
|
74
|
-
relation: EntityRelationType;
|
|
75
|
-
evidence: string | null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface ExtractedEntityGraph {
|
|
79
|
-
entities: ExtractedEntity[];
|
|
80
|
-
relations: ExtractedEntityRelation[];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface UpsertEntityRelationInput {
|
|
84
|
-
sourceEntityId: string;
|
|
85
|
-
targetEntityId: string;
|
|
86
|
-
relation: EntityRelationType;
|
|
87
|
-
evidence?: string | null;
|
|
88
|
-
seenAt?: number;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
interface LLMExtractedEntity {
|
|
92
|
-
name: string;
|
|
93
|
-
type: string;
|
|
94
|
-
aliases: string[];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
interface LLMExtractedRelation {
|
|
98
|
-
sourceEntityName: string;
|
|
99
|
-
targetEntityName: string;
|
|
100
|
-
relation: string;
|
|
101
|
-
evidence?: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const ENTITY_EXTRACTION_SYSTEM_PROMPT = `You are an entity extraction system. Given text from a conversation, extract named entities that are worth tracking across conversations.
|
|
105
|
-
|
|
106
|
-
Extract entities in these categories:
|
|
107
|
-
- person: People mentioned by name (users, colleagues, contacts)
|
|
108
|
-
- project: Named projects, repositories, products, apps
|
|
109
|
-
- tool: Software tools, libraries, frameworks, languages
|
|
110
|
-
- company: Companies, organizations with commercial identity
|
|
111
|
-
- concept: Technical concepts, methodologies, design patterns
|
|
112
|
-
- location: Cities, offices, regions relevant to the user
|
|
113
|
-
- organization: Non-commercial orgs, teams, groups, communities
|
|
114
|
-
|
|
115
|
-
If relation extraction is enabled, also extract directional entity relations using:
|
|
116
|
-
- works_on, uses, owns, member_of, located_in, depends_on, collaborates_with, reports_to, related_to
|
|
117
|
-
|
|
118
|
-
For each entity, provide:
|
|
119
|
-
- name: The canonical name (proper casing, full name preferred)
|
|
120
|
-
- type: One of the categories above
|
|
121
|
-
- aliases: Array of alternate names, abbreviations, or nicknames (empty array if none)
|
|
122
|
-
|
|
123
|
-
If relation extraction is enabled, for each relation provide:
|
|
124
|
-
- sourceEntityName: canonical source entity name
|
|
125
|
-
- targetEntityName: canonical target entity name
|
|
126
|
-
- relation: one of the allowed relation types
|
|
127
|
-
- evidence: short evidence phrase from the text (optional)
|
|
128
|
-
|
|
129
|
-
Rules:
|
|
130
|
-
- Only extract concrete, named entities. Skip generic terms like "the project" or "that tool".
|
|
131
|
-
- Prefer the most specific and complete name as the canonical name.
|
|
132
|
-
- Include common abbreviations and nicknames as aliases.
|
|
133
|
-
- Do NOT extract the assistant itself or generic conversation participants.
|
|
134
|
-
- If there are no extractable entities, return an empty entities array.
|
|
135
|
-
- Only emit relations that are explicitly or strongly implied by the text.`;
|
|
136
|
-
|
|
137
|
-
export async function extractEntitiesWithLLM(
|
|
138
|
-
text: string,
|
|
139
|
-
entityConfig: MemoryEntityConfig,
|
|
140
|
-
): Promise<ExtractedEntityGraph> {
|
|
141
|
-
const provider = getConfiguredProvider();
|
|
142
|
-
if (!provider) {
|
|
143
|
-
log.debug("Configured provider unavailable for entity extraction");
|
|
144
|
-
return { entities: [], relations: [] };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const extractRelations = entityConfig.extractRelations?.enabled ?? false;
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
const { signal, cleanup } = createTimeout(ENTITY_EXTRACTION_TIMEOUT_MS);
|
|
151
|
-
try {
|
|
152
|
-
const response = await provider.sendMessage(
|
|
153
|
-
[userMessage(text)],
|
|
154
|
-
[
|
|
155
|
-
{
|
|
156
|
-
name: "store_entities",
|
|
157
|
-
description: "Store extracted entities from the text",
|
|
158
|
-
input_schema: buildToolInputSchema(extractRelations),
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
ENTITY_EXTRACTION_SYSTEM_PROMPT,
|
|
162
|
-
{
|
|
163
|
-
config: {
|
|
164
|
-
modelIntent: entityConfig.modelIntent,
|
|
165
|
-
max_tokens: 1024,
|
|
166
|
-
tool_choice: { type: "tool" as const, name: "store_entities" },
|
|
167
|
-
},
|
|
168
|
-
signal,
|
|
169
|
-
},
|
|
170
|
-
);
|
|
171
|
-
cleanup();
|
|
172
|
-
|
|
173
|
-
const toolBlock = extractToolUse(response);
|
|
174
|
-
if (!toolBlock) {
|
|
175
|
-
log.warn("No tool_use block in entity extraction response");
|
|
176
|
-
return { entities: [], relations: [] };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const input = toolBlock.input as {
|
|
180
|
-
entities?: LLMExtractedEntity[];
|
|
181
|
-
relations?: LLMExtractedRelation[];
|
|
182
|
-
};
|
|
183
|
-
if (!Array.isArray(input.entities)) {
|
|
184
|
-
log.warn("Invalid entities in entity extraction response");
|
|
185
|
-
return { entities: [], relations: [] };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const entities = parseExtractedEntities(input.entities);
|
|
189
|
-
const relations = extractRelations
|
|
190
|
-
? parseExtractedRelations(input.relations)
|
|
191
|
-
: [];
|
|
192
|
-
|
|
193
|
-
return { entities, relations };
|
|
194
|
-
} finally {
|
|
195
|
-
cleanup();
|
|
196
|
-
}
|
|
197
|
-
} catch (err) {
|
|
198
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
199
|
-
log.warn({ err: message }, "Entity extraction LLM call failed");
|
|
200
|
-
return { entities: [], relations: [] };
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Resolve an extracted entity against existing entities in the database.
|
|
206
|
-
* Returns the existing entity ID if a match is found, or null if no match.
|
|
207
|
-
*/
|
|
208
|
-
export function resolveEntity(entity: ExtractedEntity): string | null {
|
|
209
|
-
const candidates = findEntityCandidates(entity.name);
|
|
210
|
-
if (candidates.length > 0) {
|
|
211
|
-
const sameType = candidates.find(
|
|
212
|
-
(candidate) => candidate.type === entity.type,
|
|
213
|
-
);
|
|
214
|
-
return sameType?.id ?? candidates[0].id;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
for (const alias of entity.aliases) {
|
|
218
|
-
const aliasCandidates = findEntityCandidates(alias);
|
|
219
|
-
if (aliasCandidates.length > 0) {
|
|
220
|
-
const sameType = aliasCandidates.find(
|
|
221
|
-
(candidate) => candidate.type === entity.type,
|
|
222
|
-
);
|
|
223
|
-
return sameType?.id ?? aliasCandidates[0].id;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Resolve an entity by canonical name or alias.
|
|
231
|
-
* Prefers exact canonical name matches over alias-only matches so that
|
|
232
|
-
* relations are attached to the correct node.
|
|
233
|
-
*/
|
|
234
|
-
export function resolveEntityName(entityName: string): string | null {
|
|
235
|
-
const candidates = findEntityCandidates(entityName);
|
|
236
|
-
if (candidates.length === 0) return null;
|
|
237
|
-
const nameLower = entityName.trim().toLowerCase();
|
|
238
|
-
const exactNameMatch = candidates.find(
|
|
239
|
-
(c) => c.name.toLowerCase() === nameLower,
|
|
240
|
-
);
|
|
241
|
-
return exactNameMatch?.id ?? candidates[0].id;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Upsert an entity into the database: resolve against existing entities,
|
|
246
|
-
* update if found, or insert a new one.
|
|
247
|
-
* Returns the entity ID.
|
|
248
|
-
*/
|
|
249
|
-
export function upsertEntity(entity: ExtractedEntity): string {
|
|
250
|
-
const db = getDb();
|
|
251
|
-
const now = Date.now();
|
|
252
|
-
const existingId = resolveEntity(entity);
|
|
253
|
-
|
|
254
|
-
if (existingId) {
|
|
255
|
-
const existing = db
|
|
256
|
-
.select()
|
|
257
|
-
.from(memoryEntities)
|
|
258
|
-
.where(eq(memoryEntities.id, existingId))
|
|
259
|
-
.get();
|
|
260
|
-
|
|
261
|
-
if (existing) {
|
|
262
|
-
const existingAliases: string[] = existing.aliases
|
|
263
|
-
? (JSON.parse(existing.aliases) as string[])
|
|
264
|
-
: [];
|
|
265
|
-
const mergedAliases = mergeAliases(
|
|
266
|
-
existingAliases,
|
|
267
|
-
entity.aliases,
|
|
268
|
-
existing.name,
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
db.update(memoryEntities)
|
|
272
|
-
.set({
|
|
273
|
-
lastSeenAt: now,
|
|
274
|
-
mentionCount: sql`${memoryEntities.mentionCount} + 1`,
|
|
275
|
-
aliases:
|
|
276
|
-
mergedAliases.length > 0 ? JSON.stringify(mergedAliases) : null,
|
|
277
|
-
})
|
|
278
|
-
.where(eq(memoryEntities.id, existingId))
|
|
279
|
-
.run();
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return existingId;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const id = crypto.randomUUID();
|
|
286
|
-
db.insert(memoryEntities)
|
|
287
|
-
.values({
|
|
288
|
-
id,
|
|
289
|
-
name: entity.name,
|
|
290
|
-
type: entity.type,
|
|
291
|
-
aliases:
|
|
292
|
-
entity.aliases.length > 0 ? JSON.stringify(entity.aliases) : null,
|
|
293
|
-
description: null,
|
|
294
|
-
firstSeenAt: now,
|
|
295
|
-
lastSeenAt: now,
|
|
296
|
-
mentionCount: 1,
|
|
297
|
-
})
|
|
298
|
-
.run();
|
|
299
|
-
|
|
300
|
-
return id;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Upsert an entity relation edge using (source, target, relation) as a stable uniqueness key.
|
|
305
|
-
*/
|
|
306
|
-
export function upsertEntityRelation(input: UpsertEntityRelationInput): void {
|
|
307
|
-
if (input.sourceEntityId === input.targetEntityId) return;
|
|
308
|
-
|
|
309
|
-
const db = getDb();
|
|
310
|
-
const seenAt = input.seenAt ?? Date.now();
|
|
311
|
-
const normalizedEvidence = normalizeEvidence(input.evidence);
|
|
312
|
-
|
|
313
|
-
db.insert(memoryEntityRelations)
|
|
314
|
-
.values({
|
|
315
|
-
id: crypto.randomUUID(),
|
|
316
|
-
sourceEntityId: input.sourceEntityId,
|
|
317
|
-
targetEntityId: input.targetEntityId,
|
|
318
|
-
relation: input.relation,
|
|
319
|
-
evidence: normalizedEvidence,
|
|
320
|
-
firstSeenAt: seenAt,
|
|
321
|
-
lastSeenAt: seenAt,
|
|
322
|
-
})
|
|
323
|
-
.onConflictDoUpdate({
|
|
324
|
-
target: [
|
|
325
|
-
memoryEntityRelations.sourceEntityId,
|
|
326
|
-
memoryEntityRelations.targetEntityId,
|
|
327
|
-
memoryEntityRelations.relation,
|
|
328
|
-
],
|
|
329
|
-
set:
|
|
330
|
-
normalizedEvidence === undefined
|
|
331
|
-
? {
|
|
332
|
-
firstSeenAt: sql`MIN(${memoryEntityRelations.firstSeenAt}, ${seenAt})`,
|
|
333
|
-
lastSeenAt: sql`MAX(${memoryEntityRelations.lastSeenAt}, ${seenAt})`,
|
|
334
|
-
}
|
|
335
|
-
: {
|
|
336
|
-
firstSeenAt: sql`MIN(${memoryEntityRelations.firstSeenAt}, ${seenAt})`,
|
|
337
|
-
lastSeenAt: sql`MAX(${memoryEntityRelations.lastSeenAt}, ${seenAt})`,
|
|
338
|
-
evidence: normalizedEvidence,
|
|
339
|
-
},
|
|
340
|
-
})
|
|
341
|
-
.run();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Link a memory item to an entity via the join table.
|
|
346
|
-
*/
|
|
347
|
-
export function linkMemoryItemToEntity(
|
|
348
|
-
memoryItemId: string,
|
|
349
|
-
entityId: string,
|
|
350
|
-
): void {
|
|
351
|
-
const db = getDb();
|
|
352
|
-
db.insert(memoryItemEntities)
|
|
353
|
-
.values({
|
|
354
|
-
memoryItemId,
|
|
355
|
-
entityId,
|
|
356
|
-
})
|
|
357
|
-
.onConflictDoNothing()
|
|
358
|
-
.run();
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
type ToolInputSchema = Record<string, unknown> & {
|
|
362
|
-
type: "object";
|
|
363
|
-
properties: Record<string, unknown>;
|
|
364
|
-
required: string[];
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
function buildToolInputSchema(includeRelations: boolean): ToolInputSchema {
|
|
368
|
-
const properties: Record<string, unknown> = {
|
|
369
|
-
entities: {
|
|
370
|
-
type: "array",
|
|
371
|
-
items: {
|
|
372
|
-
type: "object",
|
|
373
|
-
properties: {
|
|
374
|
-
name: {
|
|
375
|
-
type: "string",
|
|
376
|
-
description: "Canonical name of the entity",
|
|
377
|
-
},
|
|
378
|
-
type: {
|
|
379
|
-
type: "string",
|
|
380
|
-
enum: [...VALID_ENTITY_TYPES],
|
|
381
|
-
description: "Category of the entity",
|
|
382
|
-
},
|
|
383
|
-
aliases: {
|
|
384
|
-
type: "array",
|
|
385
|
-
items: { type: "string" },
|
|
386
|
-
description: "Alternate names or abbreviations",
|
|
387
|
-
},
|
|
388
|
-
},
|
|
389
|
-
required: ["name", "type", "aliases"],
|
|
390
|
-
},
|
|
391
|
-
},
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
const required: string[] = ["entities"];
|
|
395
|
-
|
|
396
|
-
if (includeRelations) {
|
|
397
|
-
properties.relations = {
|
|
398
|
-
type: "array",
|
|
399
|
-
items: {
|
|
400
|
-
type: "object",
|
|
401
|
-
properties: {
|
|
402
|
-
sourceEntityName: { type: "string" },
|
|
403
|
-
targetEntityName: { type: "string" },
|
|
404
|
-
relation: {
|
|
405
|
-
type: "string",
|
|
406
|
-
enum: [...VALID_RELATION_TYPES],
|
|
407
|
-
},
|
|
408
|
-
evidence: { type: "string" },
|
|
409
|
-
},
|
|
410
|
-
required: ["sourceEntityName", "targetEntityName", "relation"],
|
|
411
|
-
},
|
|
412
|
-
};
|
|
413
|
-
required.push("relations");
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return {
|
|
417
|
-
type: "object",
|
|
418
|
-
properties,
|
|
419
|
-
required,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function parseExtractedEntities(
|
|
424
|
-
rawEntities: LLMExtractedEntity[],
|
|
425
|
-
): ExtractedEntity[] {
|
|
426
|
-
const entities: ExtractedEntity[] = [];
|
|
427
|
-
const seen = new Set<string>();
|
|
428
|
-
for (const raw of rawEntities) {
|
|
429
|
-
if (!VALID_ENTITY_TYPES.has(raw.type)) continue;
|
|
430
|
-
const name = normalizeEntityName(raw.name);
|
|
431
|
-
if (!name) continue;
|
|
432
|
-
|
|
433
|
-
const aliases = Array.isArray(raw.aliases)
|
|
434
|
-
? dedupeAliasList(raw.aliases, name)
|
|
435
|
-
: [];
|
|
436
|
-
const dedupeKey = `${name.toLowerCase()}|${raw.type}`;
|
|
437
|
-
if (seen.has(dedupeKey)) continue;
|
|
438
|
-
seen.add(dedupeKey);
|
|
439
|
-
entities.push({
|
|
440
|
-
name,
|
|
441
|
-
type: raw.type as EntityType,
|
|
442
|
-
aliases,
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
return entities;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function parseExtractedRelations(
|
|
449
|
-
rawRelations: LLMExtractedRelation[] | undefined,
|
|
450
|
-
): ExtractedEntityRelation[] {
|
|
451
|
-
if (!Array.isArray(rawRelations)) return [];
|
|
452
|
-
const relations: ExtractedEntityRelation[] = [];
|
|
453
|
-
const seen = new Set<string>();
|
|
454
|
-
for (const raw of rawRelations) {
|
|
455
|
-
if (!VALID_RELATION_TYPES.has(raw.relation)) continue;
|
|
456
|
-
const sourceEntityName = normalizeEntityName(raw.sourceEntityName);
|
|
457
|
-
const targetEntityName = normalizeEntityName(raw.targetEntityName);
|
|
458
|
-
if (!sourceEntityName || !targetEntityName) continue;
|
|
459
|
-
if (sourceEntityName.toLowerCase() === targetEntityName.toLowerCase())
|
|
460
|
-
continue;
|
|
461
|
-
const relation = raw.relation as EntityRelationType;
|
|
462
|
-
const dedupeKey = `${sourceEntityName.toLowerCase()}|${targetEntityName.toLowerCase()}|${relation}`;
|
|
463
|
-
if (seen.has(dedupeKey)) continue;
|
|
464
|
-
seen.add(dedupeKey);
|
|
465
|
-
relations.push({
|
|
466
|
-
sourceEntityName,
|
|
467
|
-
targetEntityName,
|
|
468
|
-
relation,
|
|
469
|
-
evidence: normalizeEvidence(raw.evidence),
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
return relations;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function findEntityCandidates(
|
|
476
|
-
nameOrAlias: string,
|
|
477
|
-
): Array<typeof memoryEntities.$inferSelect> {
|
|
478
|
-
const normalized = normalizeEntityName(nameOrAlias);
|
|
479
|
-
if (!normalized) return [];
|
|
480
|
-
const nameLower = normalized.toLowerCase();
|
|
481
|
-
|
|
482
|
-
return rawAll<typeof memoryEntities.$inferSelect>(
|
|
483
|
-
`
|
|
484
|
-
SELECT DISTINCT me.* FROM memory_entities me
|
|
485
|
-
WHERE LOWER(me.name) = ?
|
|
486
|
-
UNION
|
|
487
|
-
SELECT DISTINCT me.* FROM memory_entities me, json_each(me.aliases) je
|
|
488
|
-
WHERE me.aliases IS NOT NULL AND LOWER(je.value) = ?
|
|
489
|
-
`,
|
|
490
|
-
nameLower,
|
|
491
|
-
nameLower,
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Merge alias lists, deduplicating and excluding the canonical name.
|
|
497
|
-
*/
|
|
498
|
-
function mergeAliases(
|
|
499
|
-
existing: string[],
|
|
500
|
-
incoming: string[],
|
|
501
|
-
canonicalName: string,
|
|
502
|
-
): string[] {
|
|
503
|
-
const seen = new Set<string>();
|
|
504
|
-
const canonicalLower = canonicalName.toLowerCase();
|
|
505
|
-
const merged: string[] = [];
|
|
506
|
-
for (const alias of [...existing, ...incoming]) {
|
|
507
|
-
const normalizedAlias = normalizeEntityName(alias);
|
|
508
|
-
if (!normalizedAlias) continue;
|
|
509
|
-
const lower = normalizedAlias.toLowerCase();
|
|
510
|
-
if (lower === canonicalLower) continue;
|
|
511
|
-
if (seen.has(lower)) continue;
|
|
512
|
-
seen.add(lower);
|
|
513
|
-
merged.push(normalizedAlias);
|
|
514
|
-
}
|
|
515
|
-
return merged;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function dedupeAliasList(
|
|
519
|
-
rawAliases: string[],
|
|
520
|
-
canonicalName: string,
|
|
521
|
-
): string[] {
|
|
522
|
-
return mergeAliases([], rawAliases, canonicalName);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function normalizeEntityName(value: string | null | undefined): string | null {
|
|
526
|
-
if (!value) return null;
|
|
527
|
-
const normalized = truncate(String(value).trim(), 200, "");
|
|
528
|
-
return normalized.length > 0 ? normalized : null;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function normalizeEvidence(value: string | null | undefined): string | null {
|
|
532
|
-
if (!value) return null;
|
|
533
|
-
const normalized = truncate(String(value).trim(), 500, "");
|
|
534
|
-
return normalized.length > 0 ? normalized : null;
|
|
535
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { estimateTextTokens } from "../context/token-estimator.js";
|
|
2
|
-
import { buildInjectedText } from "./search/formatting.js";
|
|
3
|
-
import { markItemUsage, trimToTokenBudget } from "./search/ranking.js";
|
|
4
|
-
import type { Candidate } from "./search/types.js";
|
|
5
|
-
|
|
6
|
-
export interface FormatRecallTextOptions {
|
|
7
|
-
/** Injection format: 'markdown' or 'structured_v1'. */
|
|
8
|
-
format: string;
|
|
9
|
-
/** Maximum token budget for the formatted output. */
|
|
10
|
-
maxTokens: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface FormatRecallTextResult {
|
|
14
|
-
/** The formatted text ready for injection. */
|
|
15
|
-
text: string;
|
|
16
|
-
/** Candidates that fit within the token budget. */
|
|
17
|
-
selected: Candidate[];
|
|
18
|
-
/** Token count of the final injected text. */
|
|
19
|
-
tokenCount: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Format scored recall candidates into injectable text.
|
|
24
|
-
*
|
|
25
|
-
* Trims candidates to the token budget, groups by section with temporal
|
|
26
|
-
* grounding, applies "Lost in the Middle" ordering, and marks item usage.
|
|
27
|
-
*
|
|
28
|
-
* Extracted from `formatRecallResult()` in `retriever.ts` so both the
|
|
29
|
-
* auto-injection path and the on-demand memory_recall tool can reuse it.
|
|
30
|
-
*/
|
|
31
|
-
export function formatRecallText(
|
|
32
|
-
candidates: Candidate[],
|
|
33
|
-
opts: FormatRecallTextOptions,
|
|
34
|
-
): FormatRecallTextResult {
|
|
35
|
-
const { format, maxTokens } = opts;
|
|
36
|
-
|
|
37
|
-
const selected = trimToTokenBudget(candidates, maxTokens, format);
|
|
38
|
-
markItemUsage(selected);
|
|
39
|
-
|
|
40
|
-
const text = buildInjectedText(selected, format);
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
text,
|
|
44
|
-
selected,
|
|
45
|
-
tokenCount: estimateTextTokens(text),
|
|
46
|
-
};
|
|
47
|
-
}
|