@vellumai/assistant 0.3.3 → 0.3.5
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/Dockerfile +2 -0
- package/README.md +45 -18
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +13 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +391 -1
- package/src/__tests__/call-routes-http.test.ts +27 -2
- package/src/__tests__/channel-approval-routes.test.ts +397 -135
- package/src/__tests__/channel-approvals.test.ts +99 -3
- package/src/__tests__/channel-delivery-store.test.ts +30 -4
- package/src/__tests__/channel-guardian.test.ts +261 -22
- package/src/__tests__/channel-readiness-service.test.ts +257 -0
- package/src/__tests__/config-schema.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +636 -0
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
- package/src/__tests__/handlers-twilio-config.test.ts +480 -0
- package/src/__tests__/ipc-snapshot.test.ts +63 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
- package/src/__tests__/run-orchestrator.test.ts +22 -0
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-runtime-assembly.test.ts +85 -1
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/sms-messaging-provider.test.ts +125 -0
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/twilio-routes.test.ts +39 -3
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/calls/call-domain.ts +8 -5
- package/src/calls/call-orchestrator.ts +85 -22
- package/src/calls/twilio-config.ts +17 -11
- package/src/calls/twilio-rest.ts +276 -0
- package/src/calls/twilio-routes.ts +39 -1
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
- package/src/config/bundled-skills/messaging/SKILL.md +24 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/defaults.ts +2 -1
- package/src/config/schema.ts +9 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +40 -0
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +58 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +819 -22
- package/src/daemon/handlers/dictation.ts +182 -0
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +2 -0
- package/src/daemon/handlers/sessions.ts +2 -0
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/skills.ts +6 -7
- package/src/daemon/handlers/work-items.ts +15 -7
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +114 -4
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +111 -504
- package/src/daemon/session-agent-loop.ts +10 -15
- package/src/daemon/session-runtime-assembly.ts +115 -44
- package/src/daemon/session-tool-setup.ts +2 -0
- package/src/daemon/session.ts +19 -2
- package/src/inbound/public-ingress-urls.ts +3 -3
- package/src/memory/channel-guardian-store.ts +2 -1
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1163 -0
- package/src/memory/db.ts +2 -2007
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-handlers/media-processing.ts +100 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-store.ts +2 -1
- package/src/memory/jobs-worker.ts +11 -1
- package/src/memory/media-store.ts +759 -0
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +36 -2
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +99 -0
- package/src/memory/search/entity.ts +208 -25
- package/src/memory/search/ranking.ts +6 -1
- package/src/memory/search/types.ts +26 -0
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +204 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/permissions/checker.ts +16 -2
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/approval-message-composer.ts +143 -0
- package/src/runtime/channel-approvals.ts +29 -7
- package/src/runtime/channel-guardian-service.ts +44 -18
- package/src/runtime/channel-readiness-service.ts +292 -0
- package/src/runtime/channel-readiness-types.ts +29 -0
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +65 -28
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-routes.ts +237 -103
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +43 -3
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/executor.ts +10 -2
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/skills/vellum-catalog.ts +61 -156
- package/src/tools/terminal/parser.ts +21 -5
- package/src/tools/types.ts +2 -0
- package/src/twitter/router.ts +1 -1
- package/src/util/platform.ts +43 -1
- package/src/util/retry.ts +4 -4
package/src/memory/schema.ts
CHANGED
|
@@ -129,6 +129,7 @@ export const memoryEmbeddings = sqliteTable('memory_embeddings', {
|
|
|
129
129
|
model: text('model').notNull(),
|
|
130
130
|
dimensions: integer('dimensions').notNull(),
|
|
131
131
|
vectorJson: text('vector_json').notNull(),
|
|
132
|
+
contentHash: text('content_hash'),
|
|
132
133
|
createdAt: integer('created_at').notNull(),
|
|
133
134
|
updatedAt: integer('updated_at').notNull(),
|
|
134
135
|
});
|
|
@@ -679,3 +680,101 @@ export const channelGuardianRateLimits = sqliteTable('channel_guardian_rate_limi
|
|
|
679
680
|
createdAt: integer('created_at').notNull(),
|
|
680
681
|
updatedAt: integer('updated_at').notNull(),
|
|
681
682
|
});
|
|
683
|
+
|
|
684
|
+
// ── Media Assets ─────────────────────────────────────────────────────
|
|
685
|
+
|
|
686
|
+
export const mediaAssets = sqliteTable('media_assets', {
|
|
687
|
+
id: text('id').primaryKey(),
|
|
688
|
+
title: text('title').notNull(),
|
|
689
|
+
filePath: text('file_path').notNull(),
|
|
690
|
+
mimeType: text('mime_type').notNull(),
|
|
691
|
+
durationSeconds: real('duration_seconds'),
|
|
692
|
+
fileHash: text('file_hash').notNull(),
|
|
693
|
+
status: text('status').notNull().default('registered'), // registered | processing | indexed | failed
|
|
694
|
+
mediaType: text('media_type').notNull(), // video | audio | image
|
|
695
|
+
metadata: text('metadata'), // JSON
|
|
696
|
+
createdAt: integer('created_at').notNull(),
|
|
697
|
+
updatedAt: integer('updated_at').notNull(),
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
export const processingStages = sqliteTable('processing_stages', {
|
|
701
|
+
id: text('id').primaryKey(),
|
|
702
|
+
assetId: text('asset_id').notNull()
|
|
703
|
+
.references(() => mediaAssets.id, { onDelete: 'cascade' }),
|
|
704
|
+
stage: text('stage').notNull(),
|
|
705
|
+
status: text('status').notNull().default('pending'), // pending | running | completed | failed
|
|
706
|
+
progress: integer('progress').notNull().default(0), // 0-100
|
|
707
|
+
lastError: text('last_error'),
|
|
708
|
+
startedAt: integer('started_at'),
|
|
709
|
+
completedAt: integer('completed_at'),
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
export const mediaKeyframes = sqliteTable('media_keyframes', {
|
|
713
|
+
id: text('id').primaryKey(),
|
|
714
|
+
assetId: text('asset_id').notNull()
|
|
715
|
+
.references(() => mediaAssets.id, { onDelete: 'cascade' }),
|
|
716
|
+
timestamp: real('timestamp').notNull(),
|
|
717
|
+
filePath: text('file_path').notNull(),
|
|
718
|
+
metadata: text('metadata'), // JSON
|
|
719
|
+
createdAt: integer('created_at').notNull(),
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
export const mediaVisionOutputs = sqliteTable('media_vision_outputs', {
|
|
723
|
+
id: text('id').primaryKey(),
|
|
724
|
+
assetId: text('asset_id').notNull()
|
|
725
|
+
.references(() => mediaAssets.id, { onDelete: 'cascade' }),
|
|
726
|
+
keyframeId: text('keyframe_id').notNull()
|
|
727
|
+
.references(() => mediaKeyframes.id, { onDelete: 'cascade' }),
|
|
728
|
+
analysisType: text('analysis_type').notNull(),
|
|
729
|
+
output: text('output').notNull(), // JSON
|
|
730
|
+
confidence: real('confidence'),
|
|
731
|
+
createdAt: integer('created_at').notNull(),
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
export const mediaTimelines = sqliteTable('media_timelines', {
|
|
735
|
+
id: text('id').primaryKey(),
|
|
736
|
+
assetId: text('asset_id').notNull()
|
|
737
|
+
.references(() => mediaAssets.id, { onDelete: 'cascade' }),
|
|
738
|
+
startTime: real('start_time').notNull(),
|
|
739
|
+
endTime: real('end_time').notNull(),
|
|
740
|
+
segmentType: text('segment_type').notNull(),
|
|
741
|
+
attributes: text('attributes'), // JSON
|
|
742
|
+
confidence: real('confidence'),
|
|
743
|
+
createdAt: integer('created_at').notNull(),
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
export const mediaEvents = sqliteTable('media_events', {
|
|
747
|
+
id: text('id').primaryKey(),
|
|
748
|
+
assetId: text('asset_id').notNull()
|
|
749
|
+
.references(() => mediaAssets.id, { onDelete: 'cascade' }),
|
|
750
|
+
eventType: text('event_type').notNull(),
|
|
751
|
+
startTime: real('start_time').notNull(),
|
|
752
|
+
endTime: real('end_time').notNull(),
|
|
753
|
+
confidence: real('confidence').notNull(),
|
|
754
|
+
reasons: text('reasons').notNull(), // JSON array
|
|
755
|
+
metadata: text('metadata'), // JSON
|
|
756
|
+
createdAt: integer('created_at').notNull(),
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
export const mediaTrackingProfiles = sqliteTable('media_tracking_profiles', {
|
|
760
|
+
id: text('id').primaryKey(),
|
|
761
|
+
assetId: text('asset_id').notNull()
|
|
762
|
+
.references(() => mediaAssets.id, { onDelete: 'cascade' }),
|
|
763
|
+
capabilities: text('capabilities').notNull(), // JSON: { [capName]: { enabled, tier } }
|
|
764
|
+
createdAt: integer('created_at').notNull(),
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
export const mediaEventFeedback = sqliteTable('media_event_feedback', {
|
|
768
|
+
id: text('id').primaryKey(),
|
|
769
|
+
assetId: text('asset_id').notNull()
|
|
770
|
+
.references(() => mediaAssets.id, { onDelete: 'cascade' }),
|
|
771
|
+
eventId: text('event_id').notNull()
|
|
772
|
+
.references(() => mediaEvents.id, { onDelete: 'cascade' }),
|
|
773
|
+
feedbackType: text('feedback_type').notNull(), // correct | incorrect | boundary_edit | missed
|
|
774
|
+
originalStartTime: real('original_start_time'),
|
|
775
|
+
originalEndTime: real('original_end_time'),
|
|
776
|
+
correctedStartTime: real('corrected_start_time'),
|
|
777
|
+
correctedEndTime: real('corrected_end_time'),
|
|
778
|
+
notes: text('notes'),
|
|
779
|
+
createdAt: integer('created_at').notNull(),
|
|
780
|
+
});
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
memoryItems,
|
|
9
9
|
memoryItemSources,
|
|
10
10
|
} from '../schema.js';
|
|
11
|
-
import type { Candidate, CandidateSource, CandidateType, EntitySearchResult, MatchedEntityRow } from './types.js';
|
|
11
|
+
import type { Candidate, CandidateSource, CandidateType, EntitySearchResult, MatchedEntityRow, TraversalOptions, TraversalResult, TraversalStep } from './types.js';
|
|
12
12
|
import { computeRecencyScore } from './ranking.js';
|
|
13
13
|
|
|
14
14
|
const log = getLogger('memory-retriever');
|
|
@@ -51,15 +51,16 @@ export function entitySearch(
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const relationSeedEntityCount = seedEntityIds.length;
|
|
54
|
+
|
|
54
55
|
const {
|
|
55
56
|
neighborEntityIds,
|
|
56
57
|
traversedEdgeCount: relationTraversedEdgeCount,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
relationConfig.maxEdges,
|
|
60
|
-
relationConfig.maxNeighborEntities,
|
|
61
|
-
relationConfig.maxDepth,
|
|
62
|
-
);
|
|
58
|
+
neighborDepths,
|
|
59
|
+
} = findNeighborEntities(seedEntityIds, {
|
|
60
|
+
maxEdges: relationConfig.maxEdges,
|
|
61
|
+
maxNeighborEntities: relationConfig.maxNeighborEntities,
|
|
62
|
+
maxDepth: relationConfig.maxDepth,
|
|
63
|
+
});
|
|
63
64
|
const relationNeighborEntityCount = neighborEntityIds.length;
|
|
64
65
|
const directItemIds = new Set(directCandidates.map((candidate) => candidate.id));
|
|
65
66
|
const relationCandidates = getEntityLinkedItemCandidates(neighborEntityIds, {
|
|
@@ -70,12 +71,47 @@ export function entitySearch(
|
|
|
70
71
|
});
|
|
71
72
|
const relationExpandedItemCount = relationCandidates.length;
|
|
72
73
|
|
|
74
|
+
// Build candidate key → BFS depth map so ranking can apply distance-based decay
|
|
75
|
+
const candidateDepths = new Map<string, number>();
|
|
76
|
+
if (relationCandidates.length > 0 && neighborDepths.size > 0) {
|
|
77
|
+
const db = getDb();
|
|
78
|
+
const itemIds = relationCandidates.map((c) => c.id);
|
|
79
|
+
const links = db
|
|
80
|
+
.select({
|
|
81
|
+
memoryItemId: memoryItemEntities.memoryItemId,
|
|
82
|
+
entityId: memoryItemEntities.entityId,
|
|
83
|
+
})
|
|
84
|
+
.from(memoryItemEntities)
|
|
85
|
+
.where(inArray(memoryItemEntities.memoryItemId, itemIds))
|
|
86
|
+
.all();
|
|
87
|
+
|
|
88
|
+
// For each item, find the minimum depth among its linked neighbor entities
|
|
89
|
+
const itemDepthMap = new Map<string, number>();
|
|
90
|
+
for (const link of links) {
|
|
91
|
+
const depth = neighborDepths.get(link.entityId);
|
|
92
|
+
if (depth !== undefined) {
|
|
93
|
+
const existing = itemDepthMap.get(link.memoryItemId);
|
|
94
|
+
if (existing === undefined || depth < existing) {
|
|
95
|
+
itemDepthMap.set(link.memoryItemId, depth);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const candidate of relationCandidates) {
|
|
101
|
+
const depth = itemDepthMap.get(candidate.id);
|
|
102
|
+
if (depth !== undefined) {
|
|
103
|
+
candidateDepths.set(candidate.key, depth);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
73
108
|
return {
|
|
74
109
|
candidates: [...directCandidates, ...relationCandidates],
|
|
75
110
|
relationSeedEntityCount,
|
|
76
111
|
relationTraversedEdgeCount,
|
|
77
112
|
relationNeighborEntityCount,
|
|
78
113
|
relationExpandedItemCount,
|
|
114
|
+
candidateDepths,
|
|
79
115
|
};
|
|
80
116
|
}
|
|
81
117
|
|
|
@@ -86,6 +122,7 @@ export function emptyEntitySearchResult(): EntitySearchResult {
|
|
|
86
122
|
relationTraversedEdgeCount: 0,
|
|
87
123
|
relationNeighborEntityCount: 0,
|
|
88
124
|
relationExpandedItemCount: 0,
|
|
125
|
+
candidateDepths: new Map(),
|
|
89
126
|
};
|
|
90
127
|
}
|
|
91
128
|
|
|
@@ -152,18 +189,19 @@ export function findMatchedEntities(query: string, maxMatches: number): MatchedE
|
|
|
152
189
|
*/
|
|
153
190
|
export function findNeighborEntities(
|
|
154
191
|
seedEntityIds: string[],
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
maxDepth
|
|
158
|
-
): { neighborEntityIds: string[]; traversedEdgeCount: number } {
|
|
192
|
+
opts: TraversalOptions,
|
|
193
|
+
): TraversalResult {
|
|
194
|
+
const { maxEdges, maxNeighborEntities, maxDepth = 3, relationTypes, entityTypes, directed } = opts;
|
|
159
195
|
if (seedEntityIds.length === 0 || maxEdges <= 0 || maxNeighborEntities <= 0 || maxDepth <= 0) {
|
|
160
|
-
return { neighborEntityIds: [], traversedEdgeCount: 0 };
|
|
196
|
+
return { neighborEntityIds: [], traversedEdgeCount: 0, neighborDepths: new Map() };
|
|
161
197
|
}
|
|
162
198
|
|
|
163
199
|
const db = getDb();
|
|
164
200
|
const visited = new Set<string>(seedEntityIds);
|
|
165
201
|
const neighbors: string[] = [];
|
|
202
|
+
const neighborDepths = new Map<string, number>();
|
|
166
203
|
let totalEdgesTraversed = 0;
|
|
204
|
+
const filterByEntityType = entityTypes && entityTypes.length > 0;
|
|
167
205
|
|
|
168
206
|
// BFS frontier starts with seed entities
|
|
169
207
|
let frontier = [...seedEntityIds];
|
|
@@ -174,19 +212,91 @@ export function findNeighborEntities(
|
|
|
174
212
|
const edgeBudget = maxEdges - totalEdgesTraversed;
|
|
175
213
|
if (edgeBudget <= 0) break;
|
|
176
214
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
))
|
|
187
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
215
|
+
let rows: Array<{ sourceEntityId: string; targetEntityId: string }>;
|
|
216
|
+
|
|
217
|
+
if (filterByEntityType) {
|
|
218
|
+
// When filtering by entity type, JOIN with memoryEntities on the neighbor
|
|
219
|
+
// side so non-matching edges are excluded at the SQL level and don't
|
|
220
|
+
// consume the edge budget.
|
|
221
|
+
const relationTypeCondition = relationTypes && relationTypes.length > 0
|
|
222
|
+
? `AND r.relation IN (${relationTypes.map(() => '?').join(',')})`
|
|
223
|
+
: '';
|
|
224
|
+
const entityTypeFilter = `AND me.type IN (${entityTypes.map(() => '?').join(',')})`;
|
|
225
|
+
const frontierPlaceholders = frontier.map(() => '?').join(',');
|
|
226
|
+
const limit = Math.max(1, edgeBudget);
|
|
227
|
+
|
|
228
|
+
const raw = (db as unknown as { $client: { query: (q: string) => { all: (...params: unknown[]) => unknown[] } } }).$client;
|
|
229
|
+
const relationParams = relationTypes && relationTypes.length > 0 ? relationTypes : [];
|
|
230
|
+
|
|
231
|
+
if (directed) {
|
|
232
|
+
// GROUP BY deduplicates entity pairs that have multiple relation rows
|
|
233
|
+
const q1 = `
|
|
234
|
+
SELECT r.source_entity_id AS sourceEntityId, r.target_entity_id AS targetEntityId
|
|
235
|
+
FROM memory_entity_relations r
|
|
236
|
+
INNER JOIN memory_entities me ON me.id = r.target_entity_id
|
|
237
|
+
WHERE r.source_entity_id IN (${frontierPlaceholders})
|
|
238
|
+
${relationTypeCondition} ${entityTypeFilter}
|
|
239
|
+
GROUP BY r.source_entity_id, r.target_entity_id
|
|
240
|
+
ORDER BY MAX(r.last_seen_at) DESC
|
|
241
|
+
LIMIT ?
|
|
242
|
+
`;
|
|
243
|
+
const params1 = [...frontier, ...relationParams, ...entityTypes, limit];
|
|
244
|
+
rows = raw.query(q1).all(...params1) as Array<{ sourceEntityId: string; targetEntityId: string }>;
|
|
245
|
+
} else {
|
|
246
|
+
// Combine both directions in a single query with global recency
|
|
247
|
+
// ordering so the edge budget isn't direction-biased.
|
|
248
|
+
const q = `
|
|
249
|
+
SELECT sourceEntityId, targetEntityId FROM (
|
|
250
|
+
SELECT r.source_entity_id AS sourceEntityId, r.target_entity_id AS targetEntityId, r.last_seen_at
|
|
251
|
+
FROM memory_entity_relations r
|
|
252
|
+
INNER JOIN memory_entities me ON me.id = r.target_entity_id
|
|
253
|
+
WHERE r.source_entity_id IN (${frontierPlaceholders})
|
|
254
|
+
${relationTypeCondition} ${entityTypeFilter}
|
|
255
|
+
UNION ALL
|
|
256
|
+
SELECT r.source_entity_id AS sourceEntityId, r.target_entity_id AS targetEntityId, r.last_seen_at
|
|
257
|
+
FROM memory_entity_relations r
|
|
258
|
+
INNER JOIN memory_entities me ON me.id = r.source_entity_id
|
|
259
|
+
WHERE r.target_entity_id IN (${frontierPlaceholders})
|
|
260
|
+
${relationTypeCondition} ${entityTypeFilter}
|
|
261
|
+
)
|
|
262
|
+
GROUP BY sourceEntityId, targetEntityId
|
|
263
|
+
ORDER BY MAX(last_seen_at) DESC
|
|
264
|
+
LIMIT ?
|
|
265
|
+
`;
|
|
266
|
+
const params = [
|
|
267
|
+
...frontier,
|
|
268
|
+
...relationParams,
|
|
269
|
+
...entityTypes,
|
|
270
|
+
...frontier,
|
|
271
|
+
...relationParams,
|
|
272
|
+
...entityTypes,
|
|
273
|
+
limit,
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
rows = raw.query(q).all(...params) as Array<{ sourceEntityId: string; targetEntityId: string }>;
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
const frontierCondition = directed
|
|
280
|
+
? inArray(memoryEntityRelations.sourceEntityId, frontier)
|
|
281
|
+
: or(
|
|
282
|
+
inArray(memoryEntityRelations.sourceEntityId, frontier),
|
|
283
|
+
inArray(memoryEntityRelations.targetEntityId, frontier),
|
|
284
|
+
);
|
|
285
|
+
const whereCondition = relationTypes && relationTypes.length > 0
|
|
286
|
+
? and(frontierCondition, inArray(memoryEntityRelations.relation, relationTypes))
|
|
287
|
+
: frontierCondition;
|
|
288
|
+
|
|
289
|
+
rows = db
|
|
290
|
+
.select({
|
|
291
|
+
sourceEntityId: memoryEntityRelations.sourceEntityId,
|
|
292
|
+
targetEntityId: memoryEntityRelations.targetEntityId,
|
|
293
|
+
})
|
|
294
|
+
.from(memoryEntityRelations)
|
|
295
|
+
.where(whereCondition)
|
|
296
|
+
.orderBy(desc(memoryEntityRelations.lastSeenAt))
|
|
297
|
+
.limit(Math.max(1, edgeBudget))
|
|
298
|
+
.all();
|
|
299
|
+
}
|
|
190
300
|
|
|
191
301
|
totalEdgesTraversed += rows.length;
|
|
192
302
|
|
|
@@ -194,16 +304,20 @@ export function findNeighborEntities(
|
|
|
194
304
|
const frontierSet = new Set(frontier);
|
|
195
305
|
for (const row of rows) {
|
|
196
306
|
if (neighbors.length >= maxNeighborEntities) break;
|
|
307
|
+
// In directed mode, only follow source→target (frontier is always on source side)
|
|
197
308
|
if (frontierSet.has(row.sourceEntityId) && !visited.has(row.targetEntityId)) {
|
|
198
309
|
visited.add(row.targetEntityId);
|
|
199
310
|
neighbors.push(row.targetEntityId);
|
|
200
311
|
nextFrontier.push(row.targetEntityId);
|
|
312
|
+
neighborDepths.set(row.targetEntityId, depth + 1);
|
|
201
313
|
}
|
|
314
|
+
if (directed) continue;
|
|
202
315
|
if (neighbors.length >= maxNeighborEntities) break;
|
|
203
316
|
if (frontierSet.has(row.targetEntityId) && !visited.has(row.sourceEntityId)) {
|
|
204
317
|
visited.add(row.sourceEntityId);
|
|
205
318
|
neighbors.push(row.sourceEntityId);
|
|
206
319
|
nextFrontier.push(row.sourceEntityId);
|
|
320
|
+
neighborDepths.set(row.sourceEntityId, depth + 1);
|
|
207
321
|
}
|
|
208
322
|
}
|
|
209
323
|
|
|
@@ -213,6 +327,7 @@ export function findNeighborEntities(
|
|
|
213
327
|
return {
|
|
214
328
|
neighborEntityIds: neighbors.slice(0, maxNeighborEntities),
|
|
215
329
|
traversedEdgeCount: totalEdgesTraversed,
|
|
330
|
+
neighborDepths,
|
|
216
331
|
};
|
|
217
332
|
}
|
|
218
333
|
|
|
@@ -296,3 +411,71 @@ export function getEntityLinkedItemCandidates(
|
|
|
296
411
|
finalScore: 0,
|
|
297
412
|
}));
|
|
298
413
|
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Multi-step typed traversal: each step expands the frontier through
|
|
417
|
+
* edges matching the step's relation/entity type filters.
|
|
418
|
+
* Returns entity IDs reachable after all steps are applied in sequence.
|
|
419
|
+
*/
|
|
420
|
+
export function collectTypedNeighbors(
|
|
421
|
+
seedEntityIds: string[],
|
|
422
|
+
steps: TraversalStep[],
|
|
423
|
+
opts?: { maxResultsPerStep?: number; maxEdgesPerStep?: number },
|
|
424
|
+
): string[] {
|
|
425
|
+
if (seedEntityIds.length === 0 || steps.length === 0) return [];
|
|
426
|
+
|
|
427
|
+
const maxResults = opts?.maxResultsPerStep ?? 20;
|
|
428
|
+
const maxEdges = opts?.maxEdgesPerStep ?? 40;
|
|
429
|
+
|
|
430
|
+
let currentSeeds = seedEntityIds;
|
|
431
|
+
|
|
432
|
+
for (const step of steps) {
|
|
433
|
+
if (currentSeeds.length === 0) break;
|
|
434
|
+
|
|
435
|
+
const result = findNeighborEntities(currentSeeds, {
|
|
436
|
+
maxEdges,
|
|
437
|
+
maxNeighborEntities: maxResults,
|
|
438
|
+
maxDepth: 1,
|
|
439
|
+
relationTypes: step.relationTypes,
|
|
440
|
+
entityTypes: step.entityTypes,
|
|
441
|
+
directed: true,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
currentSeeds = result.neighborEntityIds;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return currentSeeds;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Find entities reachable from ALL given seeds via their respective
|
|
452
|
+
* typed traversal steps, then return the intersection.
|
|
453
|
+
*/
|
|
454
|
+
export function intersectReachable(
|
|
455
|
+
queries: Array<{
|
|
456
|
+
seedEntityIds: string[];
|
|
457
|
+
steps: TraversalStep[];
|
|
458
|
+
}>,
|
|
459
|
+
opts?: { maxResultsPerStep?: number; maxEdgesPerStep?: number },
|
|
460
|
+
): string[] {
|
|
461
|
+
if (queries.length === 0) return [];
|
|
462
|
+
|
|
463
|
+
const resultSets: Set<string>[] = [];
|
|
464
|
+
for (const query of queries) {
|
|
465
|
+
const result = collectTypedNeighbors(
|
|
466
|
+
query.seedEntityIds,
|
|
467
|
+
query.steps,
|
|
468
|
+
opts,
|
|
469
|
+
);
|
|
470
|
+
resultSets.push(new Set(result));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (resultSets.length === 0) return [];
|
|
474
|
+
|
|
475
|
+
// Intersect all sets: keep only entities present in ALL sets
|
|
476
|
+
const intersection = [...resultSets[0]].filter(id =>
|
|
477
|
+
resultSets.every(set => set.has(id)),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
return intersection;
|
|
481
|
+
}
|
|
@@ -53,6 +53,7 @@ export function mergeCandidates(
|
|
|
53
53
|
entity: Candidate[] = [],
|
|
54
54
|
freshnessConfig?: { enabled: boolean; maxAgeDays: Record<string, number>; staleDecay: number; reinforcementShieldDays: number },
|
|
55
55
|
relationScoreMultiplier?: number,
|
|
56
|
+
candidateDepthMap?: Map<string, number>,
|
|
56
57
|
): Candidate[] {
|
|
57
58
|
// Build effective weight map that reflects the actual scoring weight for
|
|
58
59
|
// each source. For entity_relation the static SOURCE_WEIGHTS entry is 1.0
|
|
@@ -126,7 +127,11 @@ export function mergeCandidates(
|
|
|
126
127
|
const lastUsedAt = meta?.lastUsedAt ?? null;
|
|
127
128
|
const freshnessWeight = computeFreshnessWeight(row, accessCount, lastUsedAt, freshnessConfig);
|
|
128
129
|
|
|
129
|
-
|
|
130
|
+
let sourceWeight = effectiveWeights[row.source] ?? 1.0;
|
|
131
|
+
if (row.source === 'entity_relation' && candidateDepthMap && relationScoreMultiplier != null) {
|
|
132
|
+
const depth = candidateDepthMap.get(row.key) ?? 1;
|
|
133
|
+
sourceWeight = Math.pow(relationScoreMultiplier, depth);
|
|
134
|
+
}
|
|
130
135
|
row.finalScore = rrfScore * (0.5 + 0.5 * effectiveImportance) * trustWeight * freshnessWeight * sourceWeight;
|
|
131
136
|
}
|
|
132
137
|
|
|
@@ -94,6 +94,8 @@ export interface CollectedCandidates {
|
|
|
94
94
|
relationNeighborEntityCount: number;
|
|
95
95
|
relationExpandedItemCount: number;
|
|
96
96
|
earlyTerminated: boolean;
|
|
97
|
+
/** True when semantic search was attempted but threw an error. */
|
|
98
|
+
semanticSearchFailed: boolean;
|
|
97
99
|
merged: Candidate[];
|
|
98
100
|
}
|
|
99
101
|
|
|
@@ -103,6 +105,7 @@ export interface EntitySearchResult {
|
|
|
103
105
|
relationTraversedEdgeCount: number;
|
|
104
106
|
relationNeighborEntityCount: number;
|
|
105
107
|
relationExpandedItemCount: number;
|
|
108
|
+
candidateDepths?: Map<string, number>; // candidate key → BFS hop depth (1-based)
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
export interface MatchedEntityRow {
|
|
@@ -135,3 +138,26 @@ export interface MemorySearchResult {
|
|
|
135
138
|
recency: number;
|
|
136
139
|
};
|
|
137
140
|
}
|
|
141
|
+
|
|
142
|
+
import type { EntityRelationType, EntityType } from '../entity-extractor.js';
|
|
143
|
+
|
|
144
|
+
export interface TraversalOptions {
|
|
145
|
+
maxEdges: number;
|
|
146
|
+
maxNeighborEntities: number;
|
|
147
|
+
maxDepth?: number; // default 3
|
|
148
|
+
relationTypes?: EntityRelationType[];
|
|
149
|
+
entityTypes?: EntityType[];
|
|
150
|
+
/** When true, only follow source→target edges (frontier must be on source side). */
|
|
151
|
+
directed?: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface TraversalResult {
|
|
155
|
+
neighborEntityIds: string[];
|
|
156
|
+
traversedEdgeCount: number;
|
|
157
|
+
neighborDepths: Map<string, number>; // entityId → depth (1-based)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface TraversalStep {
|
|
161
|
+
relationTypes?: EntityRelationType[];
|
|
162
|
+
entityTypes?: EntityType[];
|
|
163
|
+
}
|