@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.
Files changed (239) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/memory.md +180 -119
  4. package/package.json +2 -2
  5. package/src/__tests__/agent-loop.test.ts +3 -1
  6. package/src/__tests__/anthropic-provider.test.ts +114 -23
  7. package/src/__tests__/approval-cascade.test.ts +1 -15
  8. package/src/__tests__/approval-routes-http.test.ts +2 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  10. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  11. package/src/__tests__/checker.test.ts +13 -0
  12. package/src/__tests__/config-schema.test.ts +1 -68
  13. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  14. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  15. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  16. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  17. package/src/__tests__/credential-vault-unit.test.ts +4 -0
  18. package/src/__tests__/credential-vault.test.ts +13 -1
  19. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  20. package/src/__tests__/date-context.test.ts +93 -77
  21. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  22. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  23. package/src/__tests__/history-repair.test.ts +245 -0
  24. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  25. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  26. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  27. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  28. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  29. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  30. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  31. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  32. package/src/__tests__/memory-regressions.test.ts +477 -2841
  33. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  34. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  35. package/src/__tests__/mime-builder.test.ts +28 -0
  36. package/src/__tests__/native-web-search.test.ts +1 -0
  37. package/src/__tests__/oauth-cli.test.ts +572 -5
  38. package/src/__tests__/oauth-store.test.ts +120 -6
  39. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  40. package/src/__tests__/registry.test.ts +0 -1
  41. package/src/__tests__/relay-server.test.ts +46 -1
  42. package/src/__tests__/schedule-tools.test.ts +32 -0
  43. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  44. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  45. package/src/__tests__/secure-keys.test.ts +7 -2
  46. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  47. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  48. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  49. package/src/__tests__/session-agent-loop.test.ts +19 -15
  50. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  51. package/src/__tests__/session-error.test.ts +124 -2
  52. package/src/__tests__/session-history-web-search.test.ts +918 -0
  53. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  54. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  55. package/src/__tests__/session-queue.test.ts +37 -27
  56. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  57. package/src/__tests__/session-slash-known.test.ts +1 -15
  58. package/src/__tests__/session-slash-queue.test.ts +1 -15
  59. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  60. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  61. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  62. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  63. package/src/__tests__/skills-install-extract.test.ts +93 -0
  64. package/src/__tests__/skillssh-registry.test.ts +451 -0
  65. package/src/__tests__/trust-store.test.ts +15 -0
  66. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  67. package/src/agent/ax-tree-compaction.test.ts +51 -0
  68. package/src/agent/loop.ts +39 -12
  69. package/src/approvals/AGENTS.md +1 -1
  70. package/src/approvals/guardian-request-resolvers.ts +14 -2
  71. package/src/bundler/compiler-tools.ts +66 -2
  72. package/src/calls/call-domain.ts +132 -0
  73. package/src/calls/call-store.ts +6 -0
  74. package/src/calls/relay-server.ts +43 -5
  75. package/src/calls/relay-setup-router.ts +17 -1
  76. package/src/calls/twilio-config.ts +1 -1
  77. package/src/calls/types.ts +3 -1
  78. package/src/cli/commands/doctor.ts +4 -3
  79. package/src/cli/commands/mcp.ts +46 -59
  80. package/src/cli/commands/memory.ts +16 -165
  81. package/src/cli/commands/oauth/apps.ts +31 -2
  82. package/src/cli/commands/oauth/connections.ts +431 -97
  83. package/src/cli/commands/oauth/providers.ts +15 -1
  84. package/src/cli/commands/sessions.ts +5 -2
  85. package/src/cli/commands/skills.ts +173 -1
  86. package/src/cli/http-client.ts +0 -20
  87. package/src/cli/main-screen.tsx +2 -2
  88. package/src/cli/program.ts +5 -6
  89. package/src/cli.ts +4 -10
  90. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  91. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  92. package/src/config/bundled-tool-registry.ts +2 -5
  93. package/src/config/schema.ts +1 -12
  94. package/src/config/schemas/memory-lifecycle.ts +0 -9
  95. package/src/config/schemas/memory-processing.ts +0 -180
  96. package/src/config/schemas/memory-retrieval.ts +32 -104
  97. package/src/config/schemas/memory.ts +0 -10
  98. package/src/config/types.ts +0 -4
  99. package/src/context/window-manager.ts +4 -1
  100. package/src/daemon/config-watcher.ts +61 -3
  101. package/src/daemon/daemon-control.ts +1 -1
  102. package/src/daemon/date-context.ts +114 -31
  103. package/src/daemon/handlers/sessions.ts +18 -13
  104. package/src/daemon/handlers/skills.ts +20 -1
  105. package/src/daemon/history-repair.ts +72 -8
  106. package/src/daemon/host-cu-proxy.ts +55 -26
  107. package/src/daemon/lifecycle.ts +31 -3
  108. package/src/daemon/mcp-reload-service.ts +2 -2
  109. package/src/daemon/message-types/computer-use.ts +1 -12
  110. package/src/daemon/message-types/memory.ts +4 -16
  111. package/src/daemon/message-types/messages.ts +1 -0
  112. package/src/daemon/message-types/sessions.ts +4 -0
  113. package/src/daemon/server.ts +12 -1
  114. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  115. package/src/daemon/session-agent-loop.ts +334 -48
  116. package/src/daemon/session-error.ts +89 -6
  117. package/src/daemon/session-history.ts +17 -7
  118. package/src/daemon/session-media-retry.ts +6 -2
  119. package/src/daemon/session-memory.ts +69 -149
  120. package/src/daemon/session-process.ts +10 -1
  121. package/src/daemon/session-runtime-assembly.ts +49 -19
  122. package/src/daemon/session-surfaces.ts +4 -1
  123. package/src/daemon/session-tool-setup.ts +7 -1
  124. package/src/daemon/session.ts +12 -2
  125. package/src/instrument.ts +61 -1
  126. package/src/memory/admin.ts +2 -191
  127. package/src/memory/canonical-guardian-store.ts +38 -2
  128. package/src/memory/conversation-crud.ts +0 -33
  129. package/src/memory/conversation-queries.ts +22 -3
  130. package/src/memory/db-init.ts +28 -0
  131. package/src/memory/embedding-backend.ts +84 -8
  132. package/src/memory/embedding-types.ts +9 -1
  133. package/src/memory/indexer.ts +7 -46
  134. package/src/memory/items-extractor.ts +274 -76
  135. package/src/memory/job-handlers/backfill.ts +2 -127
  136. package/src/memory/job-handlers/cleanup.ts +2 -16
  137. package/src/memory/job-handlers/extraction.ts +2 -138
  138. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  139. package/src/memory/job-handlers/summarization.ts +3 -148
  140. package/src/memory/job-utils.ts +21 -59
  141. package/src/memory/jobs-store.ts +1 -159
  142. package/src/memory/jobs-worker.ts +9 -52
  143. package/src/memory/migrations/104-core-indexes.ts +3 -3
  144. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  145. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  146. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  147. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  148. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  149. package/src/memory/migrations/154-drop-fts.ts +20 -0
  150. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  151. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  152. package/src/memory/migrations/index.ts +7 -0
  153. package/src/memory/qdrant-client.ts +148 -51
  154. package/src/memory/raw-query.ts +1 -1
  155. package/src/memory/retriever.test.ts +294 -273
  156. package/src/memory/retriever.ts +421 -645
  157. package/src/memory/schema/calls.ts +2 -0
  158. package/src/memory/schema/memory-core.ts +3 -48
  159. package/src/memory/schema/oauth.ts +2 -0
  160. package/src/memory/search/formatting.ts +263 -176
  161. package/src/memory/search/lexical.ts +1 -254
  162. package/src/memory/search/ranking.ts +0 -455
  163. package/src/memory/search/semantic.ts +100 -14
  164. package/src/memory/search/staleness.ts +47 -0
  165. package/src/memory/search/tier-classifier.ts +21 -0
  166. package/src/memory/search/types.ts +15 -77
  167. package/src/memory/task-memory-cleanup.ts +4 -6
  168. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  169. package/src/oauth/byo-connection.test.ts +8 -1
  170. package/src/oauth/oauth-store.ts +113 -27
  171. package/src/oauth/seed-providers.ts +6 -0
  172. package/src/oauth/token-persistence.ts +11 -3
  173. package/src/permissions/defaults.ts +1 -0
  174. package/src/permissions/trust-store.ts +23 -1
  175. package/src/playbooks/playbook-compiler.ts +1 -1
  176. package/src/prompts/system-prompt.ts +18 -2
  177. package/src/providers/anthropic/client.ts +56 -126
  178. package/src/providers/types.ts +7 -1
  179. package/src/runtime/AGENTS.md +9 -0
  180. package/src/runtime/auth/route-policy.ts +6 -3
  181. package/src/runtime/guardian-reply-router.ts +24 -22
  182. package/src/runtime/http-server.ts +2 -2
  183. package/src/runtime/invite-redemption-service.ts +19 -1
  184. package/src/runtime/invite-service.ts +25 -0
  185. package/src/runtime/pending-interactions.ts +2 -2
  186. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  187. package/src/runtime/routes/conversation-routes.ts +9 -1
  188. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  189. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  190. package/src/runtime/routes/memory-item-routes.ts +503 -0
  191. package/src/runtime/routes/session-management-routes.ts +3 -3
  192. package/src/runtime/routes/settings-routes.ts +2 -2
  193. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  194. package/src/runtime/routes/workspace-routes.ts +2 -1
  195. package/src/security/keychain-broker-client.ts +17 -4
  196. package/src/security/secure-keys.ts +25 -3
  197. package/src/security/token-manager.ts +36 -36
  198. package/src/skills/catalog-install.ts +74 -18
  199. package/src/skills/skillssh-registry.ts +503 -0
  200. package/src/tools/assets/search.ts +5 -1
  201. package/src/tools/computer-use/definitions.ts +0 -10
  202. package/src/tools/computer-use/registry.ts +1 -1
  203. package/src/tools/credentials/vault.ts +1 -3
  204. package/src/tools/memory/definitions.ts +4 -13
  205. package/src/tools/memory/handlers.test.ts +83 -103
  206. package/src/tools/memory/handlers.ts +50 -85
  207. package/src/tools/schedule/create.ts +8 -1
  208. package/src/tools/schedule/update.ts +8 -1
  209. package/src/tools/skills/load.ts +25 -2
  210. package/src/__tests__/clarification-resolver.test.ts +0 -193
  211. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  212. package/src/__tests__/conflict-policy.test.ts +0 -269
  213. package/src/__tests__/conflict-store.test.ts +0 -372
  214. package/src/__tests__/contradiction-checker.test.ts +0 -361
  215. package/src/__tests__/entity-extractor.test.ts +0 -211
  216. package/src/__tests__/entity-search.test.ts +0 -1117
  217. package/src/__tests__/profile-compiler.test.ts +0 -392
  218. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  219. package/src/__tests__/session-profile-injection.test.ts +0 -557
  220. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  221. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  222. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  223. package/src/daemon/session-conflict-gate.ts +0 -167
  224. package/src/daemon/session-dynamic-profile.ts +0 -77
  225. package/src/memory/clarification-resolver.ts +0 -417
  226. package/src/memory/conflict-intent.ts +0 -205
  227. package/src/memory/conflict-policy.ts +0 -127
  228. package/src/memory/conflict-store.ts +0 -410
  229. package/src/memory/contradiction-checker.ts +0 -508
  230. package/src/memory/entity-extractor.ts +0 -535
  231. package/src/memory/format-recall.ts +0 -47
  232. package/src/memory/fts-reconciler.ts +0 -165
  233. package/src/memory/job-handlers/conflict.ts +0 -200
  234. package/src/memory/profile-compiler.ts +0 -195
  235. package/src/memory/recall-cache.ts +0 -117
  236. package/src/memory/search/entity.ts +0 -535
  237. package/src/memory/search/query-expansion.test.ts +0 -70
  238. package/src/memory/search/query-expansion.ts +0 -118
  239. 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
- }