@atrib/recall 0.14.7 → 2.0.0
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/README.md +40 -4
- package/dist/event-type-filter.d.ts +9 -0
- package/dist/event-type-filter.js +15 -0
- package/dist/event-type-filter.js.map +1 -0
- package/dist/index.d.ts +59 -2
- package/dist/index.js +383 -456
- package/dist/index.js.map +1 -1
- package/dist/recall-verb.d.ts +128 -0
- package/dist/recall-verb.js +299 -0
- package/dist/recall-verb.js.map +1 -0
- package/dist/trace-storage.d.ts +36 -0
- package/dist/trace-storage.js +93 -0
- package/dist/trace-storage.js.map +1 -0
- package/dist/trace-tools.d.ts +102 -0
- package/dist/trace-tools.js +314 -0
- package/dist/trace-tools.js.map +1 -0
- package/dist/trace-walk.d.ts +106 -0
- package/dist/trace-walk.js +275 -0
- package/dist/trace-walk.js.map +1 -0
- package/dist/verification.d.ts +108 -0
- package/dist/verification.js +223 -0
- package/dist/verification.js.map +1 -0
- package/package.json +14 -2
package/dist/index.js
CHANGED
|
@@ -33,13 +33,12 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
35
35
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
36
|
-
import { verifyRecord,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
]);
|
|
36
|
+
import { verifyRecord, normalizeEventType, resolveEnvContextId, logReadPrimitiveCall, } from '@atrib/mcp';
|
|
37
|
+
// EventTypeFilterSchema moved to ./event-type-filter.js (leaf module) so
|
|
38
|
+
// recall-verb.ts can use it at module init despite the import cycle with
|
|
39
|
+
// this file. Re-exported here for existing importers.
|
|
40
|
+
import { EventTypeFilterSchema } from './event-type-filter.js';
|
|
41
|
+
export { EventTypeFilterSchema };
|
|
43
42
|
export const IMPORTANCE_NUMERIC = {
|
|
44
43
|
critical: 5,
|
|
45
44
|
high: 4,
|
|
@@ -128,6 +127,9 @@ import { z } from 'zod';
|
|
|
128
127
|
import { aggregateAnnotationsByRecord, aggregateRevisionsByRecord, discoverLoaded, loadLoaded, loadLoadedAppend, loadNewestLoadedFromDir, } from './aggregations.js';
|
|
129
128
|
import { recencyScore, importanceScore, parkScore, buildBM25Index, bm25Score, bm25ScoresForQuery, normalizedBm25Relevance, operationalToolCallScoreFactor, tokenize, indexableTokensForRecord, shouldSuppressLifecycleAnchorForQuery, queryMentionsLifecycle, } from './scoring.js';
|
|
130
129
|
import { buildLocalGraph, shortestDistances, walkFrom } from './graph.js';
|
|
130
|
+
import { registerRecallVerbTool } from './recall-verb.js';
|
|
131
|
+
import { extractRecordHashFieldsFromMcpResult, registerTraceTools } from './trace-tools.js';
|
|
132
|
+
import { registerVerifyTool } from './verification.js';
|
|
131
133
|
import { synthesizeDisplaySummary, resolveDisplayProducer, formatAge } from './legibility.js';
|
|
132
134
|
const ATRIB_RECORD_FILE = process.env.ATRIB_RECORD_FILE;
|
|
133
135
|
const ATRIB_MIRROR_DIR = process.env.ATRIB_MIRROR_DIR ?? join(homedir(), '.atrib', 'records');
|
|
@@ -902,43 +904,6 @@ function fullify(bundles) {
|
|
|
902
904
|
return out;
|
|
903
905
|
});
|
|
904
906
|
}
|
|
905
|
-
function extractRecordHashFieldsFromMcpResult(result) {
|
|
906
|
-
const seen = new Set();
|
|
907
|
-
const pattern = /^sha256:[0-9a-f]{64}$/;
|
|
908
|
-
const content = result?.content;
|
|
909
|
-
const text = Array.isArray(content) &&
|
|
910
|
-
typeof content[0]?.text === 'string'
|
|
911
|
-
? content[0].text
|
|
912
|
-
: undefined;
|
|
913
|
-
let root = result;
|
|
914
|
-
if (text) {
|
|
915
|
-
try {
|
|
916
|
-
root = JSON.parse(text);
|
|
917
|
-
}
|
|
918
|
-
catch {
|
|
919
|
-
root = result;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
walk(root);
|
|
923
|
-
return Array.from(seen);
|
|
924
|
-
function walk(node) {
|
|
925
|
-
if (node === null || node === undefined)
|
|
926
|
-
return;
|
|
927
|
-
if (Array.isArray(node)) {
|
|
928
|
-
for (const item of node)
|
|
929
|
-
walk(item);
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
if (typeof node !== 'object')
|
|
933
|
-
return;
|
|
934
|
-
const obj = node;
|
|
935
|
-
if (typeof obj.record_hash === 'string' && pattern.test(obj.record_hash)) {
|
|
936
|
-
seen.add(obj.record_hash);
|
|
937
|
-
}
|
|
938
|
-
for (const value of Object.values(obj))
|
|
939
|
-
walk(value);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
907
|
/**
|
|
943
908
|
* Discover and load records per the mirror-discovery contract:
|
|
944
909
|
* - If `recordFile` is provided, load just that file.
|
|
@@ -1133,13 +1098,340 @@ function readPackageVersion() {
|
|
|
1133
1098
|
return '0.0.0';
|
|
1134
1099
|
}
|
|
1135
1100
|
}
|
|
1101
|
+
export async function runRecallWalk(args) {
|
|
1102
|
+
const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
|
|
1103
|
+
const graph = buildLocalGraph(loaded);
|
|
1104
|
+
const edgeTypes = args.edge_types ? new Set(args.edge_types) : undefined;
|
|
1105
|
+
const depth = typeof args.depth === 'number' ? args.depth : 3;
|
|
1106
|
+
const walk = walkFrom(graph, args.from_record_hash, edgeTypes, depth);
|
|
1107
|
+
// Layer 1 v2 legibility: join walked hashes back to their loaded
|
|
1108
|
+
// records so each step in the walk carries a readable label
|
|
1109
|
+
// instead of just a hash + distance. Builds the index once for
|
|
1110
|
+
// O(N) lookup across the walk; for typical walks (depth=3, k<50)
|
|
1111
|
+
// this is fast.
|
|
1112
|
+
const byHash = new Map(loaded.map((lr) => [lr.record_hash, lr]));
|
|
1113
|
+
const now = Date.now();
|
|
1114
|
+
const enriched = walk.map((step) => {
|
|
1115
|
+
// walk derives from graph (built from `loaded`); byHash always has a hit.
|
|
1116
|
+
const lr = byHash.get(step.record_hash);
|
|
1117
|
+
const ann = annotationsByRecord.get(step.record_hash);
|
|
1118
|
+
return {
|
|
1119
|
+
record_hash: step.record_hash,
|
|
1120
|
+
distance: step.distance,
|
|
1121
|
+
event_type: lr.record.event_type,
|
|
1122
|
+
timestamp: lr.record.timestamp,
|
|
1123
|
+
display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
|
|
1124
|
+
display_producer: resolveDisplayProducer(lr.record, lr.producer),
|
|
1125
|
+
age: formatAge(lr.record.timestamp, now),
|
|
1126
|
+
};
|
|
1127
|
+
});
|
|
1128
|
+
return {
|
|
1129
|
+
from_record_hash: args.from_record_hash,
|
|
1130
|
+
edge_types: args.edge_types ?? ['CHAIN_PRECEDES', 'INFORMED_BY', 'ANNOTATES', 'REVISES'],
|
|
1131
|
+
depth,
|
|
1132
|
+
count: enriched.length,
|
|
1133
|
+
walk: enriched,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
export async function runRecallAnnotations(args) {
|
|
1137
|
+
const { annotationsByRecord } = getLoadedMirrorSnapshot();
|
|
1138
|
+
const summary = annotationsByRecord.get(args.record_hash) ?? null;
|
|
1139
|
+
return { record_hash: args.record_hash, annotations: summary };
|
|
1140
|
+
}
|
|
1141
|
+
export async function runRecallRevisions(args) {
|
|
1142
|
+
const { loaded, revisionsByRecord } = getLoadedMirrorSnapshot();
|
|
1143
|
+
const byHash = new Map();
|
|
1144
|
+
for (const lr of loaded)
|
|
1145
|
+
byHash.set(lr.record_hash, lr);
|
|
1146
|
+
const chain = [];
|
|
1147
|
+
const seen = new Set();
|
|
1148
|
+
let current = args.record_hash;
|
|
1149
|
+
while (!seen.has(current)) {
|
|
1150
|
+
seen.add(current);
|
|
1151
|
+
const next = revisionsByRecord.get(current);
|
|
1152
|
+
if (!next || next.length === 0)
|
|
1153
|
+
break;
|
|
1154
|
+
// Each entry in the map's value array is a revision pointing at
|
|
1155
|
+
// `current`. The chain follows the first-by-timestamp revision;
|
|
1156
|
+
// the remaining entries are surfaced as `sibling_hashes` so the
|
|
1157
|
+
// agent learns that branches exist without the chain shape
|
|
1158
|
+
// having to explode into a tree.
|
|
1159
|
+
const revHash = next[0];
|
|
1160
|
+
const siblings = next.slice(1);
|
|
1161
|
+
const revLr = byHash.get(revHash);
|
|
1162
|
+
const entry = { record_hash: revHash };
|
|
1163
|
+
if (revLr) {
|
|
1164
|
+
entry.timestamp = revLr.record.timestamp;
|
|
1165
|
+
const c = revLr.content;
|
|
1166
|
+
if (c && typeof c === 'object' && !Array.isArray(c)) {
|
|
1167
|
+
const cObj = c;
|
|
1168
|
+
if (typeof cObj.new_position === 'string')
|
|
1169
|
+
entry.new_position = cObj.new_position;
|
|
1170
|
+
if (typeof cObj.reason === 'string')
|
|
1171
|
+
entry.reason = cObj.reason;
|
|
1172
|
+
if (typeof cObj.importance === 'string')
|
|
1173
|
+
entry.importance = cObj.importance;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (siblings.length > 0) {
|
|
1177
|
+
entry.sibling_hashes = siblings;
|
|
1178
|
+
}
|
|
1179
|
+
chain.push(entry);
|
|
1180
|
+
current = revHash;
|
|
1181
|
+
}
|
|
1182
|
+
return { record_hash: args.record_hash, revision_chain: chain };
|
|
1183
|
+
}
|
|
1184
|
+
export async function runRecallByContent(args) {
|
|
1185
|
+
const evidenceMode = args.evidence_mode === 'require_complete' ? 'require_complete' : 'bounded';
|
|
1186
|
+
const includeToolCallArgs = args.include_tool_call_args === true;
|
|
1187
|
+
const boundedLimit = resolveBoundedContentSearchLimit(args.max_records, Number.MAX_SAFE_INTEGER);
|
|
1188
|
+
const snapshot = getContentSearchSnapshotForRecall(evidenceMode, boundedLimit);
|
|
1189
|
+
const { entryByHash } = snapshot;
|
|
1190
|
+
const totalRecords = snapshot.totalRecords;
|
|
1191
|
+
const loadedLength = snapshot.entries.length;
|
|
1192
|
+
const searchLimit = evidenceMode === 'require_complete'
|
|
1193
|
+
? resolveCompleteContentSearchLimit(args.max_records, loadedLength)
|
|
1194
|
+
: Math.min(boundedLimit, loadedLength);
|
|
1195
|
+
const explicitLimit = hasExplicitRecordLimit(args.max_records);
|
|
1196
|
+
if (evidenceMode === 'require_complete' && explicitLimit && searchLimit < loadedLength) {
|
|
1197
|
+
const k = Math.max(1, Math.min(50, args.k ?? 10));
|
|
1198
|
+
return {
|
|
1199
|
+
query: args.query,
|
|
1200
|
+
k,
|
|
1201
|
+
runtime: recallRuntimeMetadata(),
|
|
1202
|
+
...(includeToolCallArgs ? { include_tool_call_args: true } : {}),
|
|
1203
|
+
evidence_mode: evidenceMode,
|
|
1204
|
+
evidence_status: 'incomplete',
|
|
1205
|
+
fallback_required: true,
|
|
1206
|
+
fallback_reason: `require_complete refused a partial corpus: search_cap=${searchLimit}, ` +
|
|
1207
|
+
`total_records=${loadedLength}.`,
|
|
1208
|
+
fallback: 'Rerun without max_records for full loaded-mirror coverage, or pass an explicit ' +
|
|
1209
|
+
'partition filter through a caller-owned audit plan and treat each partition as its own coverage claim.',
|
|
1210
|
+
total_records: loadedLength,
|
|
1211
|
+
searched_records: 0,
|
|
1212
|
+
search_cap: searchLimit,
|
|
1213
|
+
coverage: recallCoverage(snapshot, 'incomplete_explicit_limit', 0, snapshot.index),
|
|
1214
|
+
candidate_records: 0,
|
|
1215
|
+
truncated_corpus: true,
|
|
1216
|
+
count: 0,
|
|
1217
|
+
results: [],
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
const bm25Index = getContentBm25IndexForNewestLimit(snapshot, searchLimit);
|
|
1221
|
+
const queryTokens = tokenize(args.query);
|
|
1222
|
+
const relevanceByHash = bm25ScoresForQuery(bm25Index, queryTokens);
|
|
1223
|
+
const now = Date.now();
|
|
1224
|
+
const searchPool = queryTokens.length > 0
|
|
1225
|
+
? Array.from(relevanceByHash.keys())
|
|
1226
|
+
.map((hash) => entryByHash.get(hash))
|
|
1227
|
+
.filter((entry) => entry !== undefined)
|
|
1228
|
+
: snapshot.newestEntries.slice(0, searchLimit);
|
|
1229
|
+
const filteredSearchPool = searchPool.filter((entry) => !(entry.lifecycle_anchor && !queryMentionsLifecycle(queryTokens)));
|
|
1230
|
+
const scored = filteredSearchPool.map((entry) => {
|
|
1231
|
+
const ann = entry.annotations;
|
|
1232
|
+
const suppressLifecycleAnchor = entry.lifecycle_anchor && !queryMentionsLifecycle(queryTokens);
|
|
1233
|
+
const toolCallScoreFactor = effectiveToolCallScoreFactor(entry, includeToolCallArgs);
|
|
1234
|
+
const r = recencyScore(entry.timestamp, now, ATRIB_RECALL_TAU_DAYS) * toolCallScoreFactor;
|
|
1235
|
+
const i = suppressLifecycleAnchor ? 0 : importanceScore(ann);
|
|
1236
|
+
const rawRel = !suppressLifecycleAnchor && queryTokens.length > 0
|
|
1237
|
+
? (relevanceByHash.get(entry.record_hash) ?? 0)
|
|
1238
|
+
: 0;
|
|
1239
|
+
const rel = !suppressLifecycleAnchor && queryTokens.length > 0
|
|
1240
|
+
? normalizedBm25Relevance(bm25Index, entry.record_hash, queryTokens, rawRel) *
|
|
1241
|
+
toolCallScoreFactor
|
|
1242
|
+
: 0;
|
|
1243
|
+
const score = parkScore(r, i, rel, ATRIB_RECALL_ALPHA, ATRIB_RECALL_BETA, ATRIB_RECALL_GAMMA);
|
|
1244
|
+
return { entry, score, recency: r, importance: i, relevance: rel };
|
|
1245
|
+
});
|
|
1246
|
+
scored.sort((a, b) => {
|
|
1247
|
+
if (b.score !== a.score)
|
|
1248
|
+
return b.score - a.score;
|
|
1249
|
+
return b.entry.timestamp - a.entry.timestamp;
|
|
1250
|
+
});
|
|
1251
|
+
const k = Math.max(1, Math.min(50, args.k ?? 10));
|
|
1252
|
+
const top = scored.slice(0, k);
|
|
1253
|
+
const completeCoverage = !snapshot.partial && searchLimit >= loadedLength;
|
|
1254
|
+
return {
|
|
1255
|
+
query: args.query,
|
|
1256
|
+
k,
|
|
1257
|
+
runtime: recallRuntimeMetadata(),
|
|
1258
|
+
...(includeToolCallArgs ? { include_tool_call_args: true } : {}),
|
|
1259
|
+
evidence_mode: evidenceMode,
|
|
1260
|
+
evidence_status: completeCoverage ? 'complete' : 'bounded',
|
|
1261
|
+
fallback_required: false,
|
|
1262
|
+
total_records: totalRecords,
|
|
1263
|
+
searched_records: searchLimit,
|
|
1264
|
+
coverage: recallCoverage(snapshot, completeCoverage ? 'complete_full_scan' : 'bounded_newest_first', searchLimit, snapshot.index),
|
|
1265
|
+
candidate_records: filteredSearchPool.length,
|
|
1266
|
+
truncated_corpus: !completeCoverage,
|
|
1267
|
+
count: top.length,
|
|
1268
|
+
results: top.map(({ entry, score, recency, importance, relevance }) => {
|
|
1269
|
+
return {
|
|
1270
|
+
record_hash: entry.record_hash,
|
|
1271
|
+
event_type: entry.event_type,
|
|
1272
|
+
context_id: entry.context_id,
|
|
1273
|
+
timestamp: entry.timestamp,
|
|
1274
|
+
tool_name: entry.tool_name,
|
|
1275
|
+
annotations: entry.annotations,
|
|
1276
|
+
// Layer 1 v2 legibility fields (parity with compactify).
|
|
1277
|
+
display_summary: entry.display_summary,
|
|
1278
|
+
display_producer: entry.display_producer,
|
|
1279
|
+
age: formatAge(entry.timestamp, now),
|
|
1280
|
+
score,
|
|
1281
|
+
components: { recency, importance, relevance },
|
|
1282
|
+
};
|
|
1283
|
+
}),
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
export async function runRecallSessionChain(args) {
|
|
1287
|
+
const ctx = args.context_id ?? resolveEnvContextId();
|
|
1288
|
+
if (!ctx) {
|
|
1289
|
+
return {
|
|
1290
|
+
context_id: null,
|
|
1291
|
+
count: 0,
|
|
1292
|
+
records: [],
|
|
1293
|
+
warning: 'no context_id supplied or resolvable via env',
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
|
|
1297
|
+
const filtered = loaded
|
|
1298
|
+
.filter((lr) => lr.record.context_id === ctx)
|
|
1299
|
+
.sort((a, b) => a.record.timestamp - b.record.timestamp);
|
|
1300
|
+
const limit = Math.max(1, Math.min(500, args.limit ?? 50));
|
|
1301
|
+
const sliced = filtered.slice(0, limit);
|
|
1302
|
+
const now = Date.now();
|
|
1303
|
+
return {
|
|
1304
|
+
context_id: ctx,
|
|
1305
|
+
total: filtered.length,
|
|
1306
|
+
returned: sliced.length,
|
|
1307
|
+
truncated: filtered.length > sliced.length,
|
|
1308
|
+
records: sliced.map((lr) => {
|
|
1309
|
+
const ann = annotationsByRecord.get(lr.record_hash);
|
|
1310
|
+
const entry = {
|
|
1311
|
+
record_hash: lr.record_hash,
|
|
1312
|
+
event_type: lr.record.event_type,
|
|
1313
|
+
timestamp: lr.record.timestamp,
|
|
1314
|
+
display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
|
|
1315
|
+
display_producer: resolveDisplayProducer(lr.record, lr.producer),
|
|
1316
|
+
age: formatAge(lr.record.timestamp, now),
|
|
1317
|
+
};
|
|
1318
|
+
const informedBy = lr.record.informed_by;
|
|
1319
|
+
const toolName = lr.record.tool_name;
|
|
1320
|
+
const argsHash = lr.record.args_hash;
|
|
1321
|
+
const resultHash = lr.record.result_hash;
|
|
1322
|
+
if (Array.isArray(informedBy) && informedBy.length > 0) {
|
|
1323
|
+
entry.informed_by = informedBy;
|
|
1324
|
+
}
|
|
1325
|
+
if (toolName)
|
|
1326
|
+
entry.tool_name = toolName;
|
|
1327
|
+
if (argsHash)
|
|
1328
|
+
entry.args_hash = argsHash;
|
|
1329
|
+
if (resultHash)
|
|
1330
|
+
entry.result_hash = resultHash;
|
|
1331
|
+
if (args.include_content === true && lr.content !== undefined) {
|
|
1332
|
+
entry.local_content = lr.content;
|
|
1333
|
+
}
|
|
1334
|
+
if (args.include_content === true && lr.producer !== undefined) {
|
|
1335
|
+
entry.local_producer = lr.producer;
|
|
1336
|
+
}
|
|
1337
|
+
return entry;
|
|
1338
|
+
}),
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
export async function runRecallOrphans(args) {
|
|
1342
|
+
const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
|
|
1343
|
+
// Build the set of all record_hashes that appear in any record's
|
|
1344
|
+
// informed_by field. Anything in `loaded` whose record_hash is
|
|
1345
|
+
// NOT in this set is an orphan.
|
|
1346
|
+
const cited = new Set();
|
|
1347
|
+
for (const lr of loaded) {
|
|
1348
|
+
const ib = lr.record.informed_by;
|
|
1349
|
+
if (Array.isArray(ib)) {
|
|
1350
|
+
for (const h of ib)
|
|
1351
|
+
if (typeof h === 'string')
|
|
1352
|
+
cited.add(h);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
let orphans = loaded.filter((lr) => !cited.has(lr.record_hash));
|
|
1356
|
+
if (args.context_id) {
|
|
1357
|
+
orphans = orphans.filter((lr) => lr.record.context_id === args.context_id);
|
|
1358
|
+
}
|
|
1359
|
+
if (args.creator_key) {
|
|
1360
|
+
orphans = orphans.filter((lr) => lr.record.creator_key === args.creator_key);
|
|
1361
|
+
}
|
|
1362
|
+
if (args.event_type) {
|
|
1363
|
+
const targetUri = normalizeEventType(args.event_type);
|
|
1364
|
+
orphans = orphans.filter((lr) => lr.record.event_type === targetUri);
|
|
1365
|
+
}
|
|
1366
|
+
orphans.sort((a, b) => b.record.timestamp - a.record.timestamp);
|
|
1367
|
+
const limit = Math.max(1, Math.min(500, args.limit ?? 50));
|
|
1368
|
+
const sliced = orphans.slice(0, limit);
|
|
1369
|
+
const now = Date.now();
|
|
1370
|
+
return {
|
|
1371
|
+
total: orphans.length,
|
|
1372
|
+
returned: sliced.length,
|
|
1373
|
+
truncated: orphans.length > sliced.length,
|
|
1374
|
+
records: sliced.map((lr) => {
|
|
1375
|
+
const ann = annotationsByRecord.get(lr.record_hash);
|
|
1376
|
+
return {
|
|
1377
|
+
record_hash: lr.record_hash,
|
|
1378
|
+
event_type: lr.record.event_type,
|
|
1379
|
+
context_id: lr.record.context_id,
|
|
1380
|
+
timestamp: lr.record.timestamp,
|
|
1381
|
+
display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
|
|
1382
|
+
display_producer: resolveDisplayProducer(lr.record, lr.producer),
|
|
1383
|
+
age: formatAge(lr.record.timestamp, now),
|
|
1384
|
+
};
|
|
1385
|
+
}),
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
export async function runRecallBySigner(args) {
|
|
1389
|
+
const { loaded } = getLoadedMirrorSnapshot();
|
|
1390
|
+
const byKey = new Map();
|
|
1391
|
+
for (const lr of loaded) {
|
|
1392
|
+
const key = lr.record.creator_key;
|
|
1393
|
+
const existing = byKey.get(key);
|
|
1394
|
+
if (existing) {
|
|
1395
|
+
existing.count++;
|
|
1396
|
+
if (lr.record.timestamp > existing.latest_timestamp)
|
|
1397
|
+
existing.latest_timestamp = lr.record.timestamp;
|
|
1398
|
+
if (lr.record.timestamp < existing.earliest_timestamp)
|
|
1399
|
+
existing.earliest_timestamp = lr.record.timestamp;
|
|
1400
|
+
}
|
|
1401
|
+
else {
|
|
1402
|
+
byKey.set(key, {
|
|
1403
|
+
creator_key: key,
|
|
1404
|
+
count: 1,
|
|
1405
|
+
latest_timestamp: lr.record.timestamp,
|
|
1406
|
+
earliest_timestamp: lr.record.timestamp,
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
const minRecords = Math.max(1, args.min_records ?? 1);
|
|
1411
|
+
const stats = [...byKey.values()]
|
|
1412
|
+
.filter((s) => s.count >= minRecords)
|
|
1413
|
+
.sort((a, b) => b.count - a.count);
|
|
1414
|
+
return {
|
|
1415
|
+
total_signers: stats.length,
|
|
1416
|
+
total_records: loaded.length,
|
|
1417
|
+
signers: stats,
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
function jsonToolResult(payload) {
|
|
1421
|
+
return {
|
|
1422
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1136
1425
|
// The recall semantic surface (as defined in the public protocol specification).
|
|
1137
|
-
// Eight distinct MCP tools
|
|
1138
|
-
// filter-and-page
|
|
1139
|
-
//
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
// recall_orphans, and
|
|
1426
|
+
// Eight distinct legacy MCP tools kept as permanent aliases over the shape
|
|
1427
|
+
// runners above: recall_my_attribution_history is the base filter-and-page
|
|
1428
|
+
// tool; recall_annotations + recall_revisions return aggregated annotation
|
|
1429
|
+
// summaries / revision chains for a specific record_hash; recall_walk
|
|
1430
|
+
// traverses the local Layer 1 derived graph; recall_by_content runs BM25
|
|
1431
|
+
// free-form retrieval; recall_session_chain, recall_orphans, and
|
|
1432
|
+
// recall_by_signer cover common agent lookup shapes. New callers should
|
|
1433
|
+
// prefer the `recall` verb (one tool, `shape` argument); results are
|
|
1434
|
+
// JSON-identical by construction.
|
|
1143
1435
|
export function registerAtribRecallTools(server) {
|
|
1144
1436
|
server.registerTool('recall_my_attribution_history', {
|
|
1145
1437
|
description: "Return signed atrib records from the local mirror. The agent's own past, with each record's " +
|
|
@@ -1154,7 +1446,8 @@ export function registerAtribRecallTools(server) {
|
|
|
1154
1446
|
'omit all of them for cross-trace history. Results are sorted newest-first. Pagination uses ' +
|
|
1155
1447
|
'offset; new records appended between calls invalidate offset stability. See the ' +
|
|
1156
1448
|
'pagination_caveat in the response. The filtered_out_by_verification field reports how many ' +
|
|
1157
|
-
'records were dropped due to signature failures (always 0 when include_unverified=true).'
|
|
1449
|
+
'records were dropped due to signature failures (always 0 when include_unverified=true). ' +
|
|
1450
|
+
"Legacy alias: new callers should prefer the `recall` tool with shape='history'.",
|
|
1158
1451
|
inputSchema: {
|
|
1159
1452
|
context_id: z
|
|
1160
1453
|
.string()
|
|
@@ -1269,20 +1562,13 @@ export function registerAtribRecallTools(server) {
|
|
|
1269
1562
|
// min_importance, topic_tags, include_revised, min_signers,
|
|
1270
1563
|
// rank_by, rank_anchor, and toc.
|
|
1271
1564
|
const result = await recall(args);
|
|
1272
|
-
return
|
|
1273
|
-
content: [
|
|
1274
|
-
{
|
|
1275
|
-
type: 'text',
|
|
1276
|
-
text: JSON.stringify(result, null, 2),
|
|
1277
|
-
},
|
|
1278
|
-
],
|
|
1279
|
-
};
|
|
1565
|
+
return jsonToolResult(result);
|
|
1280
1566
|
}, extractRecordHashFieldsFromMcpResult));
|
|
1281
1567
|
// ─── Layer 1 sibling tools ───
|
|
1282
1568
|
// Sibling tools expose common agent lookup shapes beyond the base
|
|
1283
1569
|
// filter-and-page tool.
|
|
1284
1570
|
server.registerTool('recall_walk', {
|
|
1285
|
-
description: 'Walk the local derived graph from a starting record_hash. Returns records reachable via the requested edge types up to the given hop depth, ordered by ascending weighted distance. Layer 1 covers four edge types: CHAIN_PRECEDES (weight 1), INFORMED_BY (weight 1), ANNOTATES (weight 2), REVISES (weight 2). SESSION_PRECEDES, SESSION_PARALLEL, CONVERGES_ON, CROSS_SESSION, and PROVENANCE_OF are deferred to subsequent releases. Useful for tracing the local causal neighborhood of a record before re-attempting a similar action.',
|
|
1571
|
+
description: 'Walk the local derived graph from a starting record_hash. Returns records reachable via the requested edge types up to the given hop depth, ordered by ascending weighted distance. Layer 1 covers four edge types: CHAIN_PRECEDES (weight 1), INFORMED_BY (weight 1), ANNOTATES (weight 2), REVISES (weight 2). SESSION_PRECEDES, SESSION_PARALLEL, CONVERGES_ON, CROSS_SESSION, and PROVENANCE_OF are deferred to subsequent releases. Useful for tracing the local causal neighborhood of a record before re-attempting a similar action. Legacy alias: new callers should prefer the `recall` tool with shape=\'walk\' (no direction).',
|
|
1286
1572
|
inputSchema: {
|
|
1287
1573
|
from_record_hash: z
|
|
1288
1574
|
.string()
|
|
@@ -1296,131 +1582,29 @@ export function registerAtribRecallTools(server) {
|
|
|
1296
1582
|
.optional()
|
|
1297
1583
|
.describe('Maximum hop count (NOT cumulative weight). Default 3. Higher values may return many records; paginate downstream if needed.'),
|
|
1298
1584
|
},
|
|
1299
|
-
}, async (args) => logReadPrimitiveCall('recall_walk', args, async () => {
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
const walk = walkFrom(graph, args.from_record_hash, edgeTypes, depth);
|
|
1305
|
-
// Layer 1 v2 legibility: join walked hashes back to their loaded
|
|
1306
|
-
// records so each step in the walk carries a readable label
|
|
1307
|
-
// instead of just a hash + distance. Builds the index once for
|
|
1308
|
-
// O(N) lookup across the walk; for typical walks (depth=3, k<50)
|
|
1309
|
-
// this is fast.
|
|
1310
|
-
const byHash = new Map(loaded.map((lr) => [lr.record_hash, lr]));
|
|
1311
|
-
const now = Date.now();
|
|
1312
|
-
const enriched = walk.map((step) => {
|
|
1313
|
-
// walk derives from graph (built from `loaded`); byHash always has a hit.
|
|
1314
|
-
const lr = byHash.get(step.record_hash);
|
|
1315
|
-
const ann = annotationsByRecord.get(step.record_hash);
|
|
1316
|
-
return {
|
|
1317
|
-
record_hash: step.record_hash,
|
|
1318
|
-
distance: step.distance,
|
|
1319
|
-
event_type: lr.record.event_type,
|
|
1320
|
-
timestamp: lr.record.timestamp,
|
|
1321
|
-
display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
|
|
1322
|
-
display_producer: resolveDisplayProducer(lr.record, lr.producer),
|
|
1323
|
-
age: formatAge(lr.record.timestamp, now),
|
|
1324
|
-
};
|
|
1325
|
-
});
|
|
1326
|
-
return {
|
|
1327
|
-
content: [
|
|
1328
|
-
{
|
|
1329
|
-
type: 'text',
|
|
1330
|
-
text: JSON.stringify({
|
|
1331
|
-
from_record_hash: args.from_record_hash,
|
|
1332
|
-
edge_types: args.edge_types ?? [
|
|
1333
|
-
'CHAIN_PRECEDES',
|
|
1334
|
-
'INFORMED_BY',
|
|
1335
|
-
'ANNOTATES',
|
|
1336
|
-
'REVISES',
|
|
1337
|
-
],
|
|
1338
|
-
depth,
|
|
1339
|
-
count: enriched.length,
|
|
1340
|
-
walk: enriched,
|
|
1341
|
-
}, null, 2),
|
|
1342
|
-
},
|
|
1343
|
-
],
|
|
1344
|
-
};
|
|
1345
|
-
}, extractRecordHashFieldsFromMcpResult));
|
|
1585
|
+
}, async (args) => logReadPrimitiveCall('recall_walk', args, async () => jsonToolResult(await runRecallWalk({
|
|
1586
|
+
from_record_hash: args.from_record_hash,
|
|
1587
|
+
edge_types: args.edge_types,
|
|
1588
|
+
depth: typeof args.depth === 'number' ? args.depth : undefined,
|
|
1589
|
+
})), extractRecordHashFieldsFromMcpResult));
|
|
1346
1590
|
server.registerTool('recall_annotations', {
|
|
1347
|
-
description: "Return the aggregated annotation summary for a record: maximum annotation importance across all D058 annotation records pointing at it, the union of their topic tags, and the most recent summary string. Useful for surfacing the agent's prior critique on a record before re-attempting a similar action. Returns null annotations field when no annotation points at the record.",
|
|
1591
|
+
description: "Return the aggregated annotation summary for a record: maximum annotation importance across all D058 annotation records pointing at it, the union of their topic tags, and the most recent summary string. Useful for surfacing the agent's prior critique on a record before re-attempting a similar action. Returns null annotations field when no annotation points at the record. Legacy alias: new callers should prefer the `recall` tool with shape='annotations'.",
|
|
1348
1592
|
inputSchema: {
|
|
1349
1593
|
record_hash: z
|
|
1350
1594
|
.string()
|
|
1351
1595
|
.describe('Record hash (sha256:<64-hex>) of the record whose annotations should be retrieved. Annotations are D058 records whose signed annotates field equals this hash.'),
|
|
1352
1596
|
},
|
|
1353
|
-
}, async (args) => logReadPrimitiveCall('recall_annotations', args, async () =>
|
|
1354
|
-
const { annotationsByRecord } = getLoadedMirrorSnapshot();
|
|
1355
|
-
const summary = annotationsByRecord.get(args.record_hash) ?? null;
|
|
1356
|
-
return {
|
|
1357
|
-
content: [
|
|
1358
|
-
{
|
|
1359
|
-
type: 'text',
|
|
1360
|
-
text: JSON.stringify({ record_hash: args.record_hash, annotations: summary }, null, 2),
|
|
1361
|
-
},
|
|
1362
|
-
],
|
|
1363
|
-
};
|
|
1364
|
-
}, extractRecordHashFieldsFromMcpResult));
|
|
1597
|
+
}, async (args) => logReadPrimitiveCall('recall_annotations', args, async () => jsonToolResult(await runRecallAnnotations(args)), extractRecordHashFieldsFromMcpResult));
|
|
1365
1598
|
server.registerTool('recall_revisions', {
|
|
1366
|
-
description: "Return the D059 revision chain for a record, with per-entry content + sibling-fan-out awareness. Walks revises edges forward from the given record_hash, surfacing each revision in turn. Each entry carries the revision's record_hash, timestamp, and content (`new_position`, `reason`, `importance`) so the agent can read the chain inline without follow-up recall calls per entry. When more than one revision targets the same record, the chain follows the first-by-timestamp branch and lists the other branch heads as `sibling_hashes` on that entry so the agent learns about parallel revision branches (common in multi-agent flows). Useful for checking whether a position the agent previously held has been revised before acting on it. Returns an empty chain when no revision points at the record.",
|
|
1599
|
+
description: "Return the D059 revision chain for a record, with per-entry content + sibling-fan-out awareness. Walks revises edges forward from the given record_hash, surfacing each revision in turn. Each entry carries the revision's record_hash, timestamp, and content (`new_position`, `reason`, `importance`) so the agent can read the chain inline without follow-up recall calls per entry. When more than one revision targets the same record, the chain follows the first-by-timestamp branch and lists the other branch heads as `sibling_hashes` on that entry so the agent learns about parallel revision branches (common in multi-agent flows). Useful for checking whether a position the agent previously held has been revised before acting on it. Returns an empty chain when no revision points at the record. Legacy alias: new callers should prefer the `recall` tool with shape='revisions'.",
|
|
1367
1600
|
inputSchema: {
|
|
1368
1601
|
record_hash: z
|
|
1369
1602
|
.string()
|
|
1370
1603
|
.describe('Record hash (sha256:<64-hex>) of the record whose revision chain should be retrieved. Revisions are D059 records whose signed revises field equals this hash (or chain back to it).'),
|
|
1371
1604
|
},
|
|
1372
|
-
}, async (args) => logReadPrimitiveCall('recall_revisions', args, async () =>
|
|
1373
|
-
const { loaded, revisionsByRecord } = getLoadedMirrorSnapshot();
|
|
1374
|
-
const byHash = new Map();
|
|
1375
|
-
for (const lr of loaded)
|
|
1376
|
-
byHash.set(lr.record_hash, lr);
|
|
1377
|
-
const chain = [];
|
|
1378
|
-
const seen = new Set();
|
|
1379
|
-
let current = args.record_hash;
|
|
1380
|
-
while (!seen.has(current)) {
|
|
1381
|
-
seen.add(current);
|
|
1382
|
-
const next = revisionsByRecord.get(current);
|
|
1383
|
-
if (!next || next.length === 0)
|
|
1384
|
-
break;
|
|
1385
|
-
// Each entry in the map's value array is a revision pointing at
|
|
1386
|
-
// `current`. The chain follows the first-by-timestamp revision;
|
|
1387
|
-
// the remaining entries are surfaced as `sibling_hashes` so the
|
|
1388
|
-
// agent learns that branches exist without the chain shape
|
|
1389
|
-
// having to explode into a tree.
|
|
1390
|
-
const revHash = next[0];
|
|
1391
|
-
const siblings = next.slice(1);
|
|
1392
|
-
const revLr = byHash.get(revHash);
|
|
1393
|
-
const entry = { record_hash: revHash };
|
|
1394
|
-
if (revLr) {
|
|
1395
|
-
entry.timestamp = revLr.record.timestamp;
|
|
1396
|
-
const c = revLr.content;
|
|
1397
|
-
if (c && typeof c === 'object' && !Array.isArray(c)) {
|
|
1398
|
-
const cObj = c;
|
|
1399
|
-
if (typeof cObj.new_position === 'string')
|
|
1400
|
-
entry.new_position = cObj.new_position;
|
|
1401
|
-
if (typeof cObj.reason === 'string')
|
|
1402
|
-
entry.reason = cObj.reason;
|
|
1403
|
-
if (typeof cObj.importance === 'string')
|
|
1404
|
-
entry.importance = cObj.importance;
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
if (siblings.length > 0) {
|
|
1408
|
-
entry.sibling_hashes = siblings;
|
|
1409
|
-
}
|
|
1410
|
-
chain.push(entry);
|
|
1411
|
-
current = revHash;
|
|
1412
|
-
}
|
|
1413
|
-
return {
|
|
1414
|
-
content: [
|
|
1415
|
-
{
|
|
1416
|
-
type: 'text',
|
|
1417
|
-
text: JSON.stringify({ record_hash: args.record_hash, revision_chain: chain }, null, 2),
|
|
1418
|
-
},
|
|
1419
|
-
],
|
|
1420
|
-
};
|
|
1421
|
-
}, extractRecordHashFieldsFromMcpResult));
|
|
1605
|
+
}, async (args) => logReadPrimitiveCall('recall_revisions', args, async () => jsonToolResult(await runRecallRevisions(args)), extractRecordHashFieldsFromMcpResult));
|
|
1422
1606
|
server.registerTool('recall_by_content', {
|
|
1423
|
-
description: "Free-form text search over the agent's signed past. Returns top-k records by hybrid retrieval: BM25 over each record's per-event_type indexable text (observation `what + why_noted + intent + rationale + topics`; tool_call `tool_name + args + result`; annotation `summary + topics`; revision `prior_position + new_position + reason + topics`; transaction counterparty + memo; directory_anchor tree_root; extension URIs via generic recursive string-walk per D086/D118) plus the annotation summary + topics when present as a curator-quality lift. Reranked by Park et al. weighted-sum scoring with annotation-derived importance and recency signals. Raw unannotated tool_call records are score-demoted so operation logs do not dominate substantive memory. BM25 contribution is clamped to [0, 1] before coverage scaling so the documented Park-component bound is honored. Responses include runtime metadata plus coverage.index, so callers can detect stale MCP processes and whether the durable content-token sidecar was hit, rebuilt, disabled, or bypassed. Useful when the agent has no specific filter and needs to ask 'what do I know about X?'.",
|
|
1607
|
+
description: "Free-form text search over the agent's signed past. Returns top-k records by hybrid retrieval: BM25 over each record's per-event_type indexable text (observation `what + why_noted + intent + rationale + topics`; tool_call `tool_name + args + result`; annotation `summary + topics`; revision `prior_position + new_position + reason + topics`; transaction counterparty + memo; directory_anchor tree_root; extension URIs via generic recursive string-walk per D086/D118) plus the annotation summary + topics when present as a curator-quality lift. Reranked by Park et al. weighted-sum scoring with annotation-derived importance and recency signals. Raw unannotated tool_call records are score-demoted so operation logs do not dominate substantive memory. BM25 contribution is clamped to [0, 1] before coverage scaling so the documented Park-component bound is honored. Responses include runtime metadata plus coverage.index, so callers can detect stale MCP processes and whether the durable content-token sidecar was hit, rebuilt, disabled, or bypassed. Useful when the agent has no specific filter and needs to ask 'what do I know about X?'. Legacy alias: new callers should prefer the `recall` tool with shape='content'.",
|
|
1424
1608
|
inputSchema: {
|
|
1425
1609
|
query: z
|
|
1426
1610
|
.string()
|
|
@@ -1445,129 +1629,14 @@ export function registerAtribRecallTools(server) {
|
|
|
1445
1629
|
.optional()
|
|
1446
1630
|
.describe('Lift the operational tool_call score suppression for this query so unannotated tool_call records, including their indexed args and result excerpts, rank by ordinary recency and relevance. The default keeps them down-weighted so operational noise does not dominate conversational recall.'),
|
|
1447
1631
|
},
|
|
1448
|
-
}, async (args) => logReadPrimitiveCall('recall_by_content', args, async () =>
|
|
1449
|
-
const evidenceMode = args.evidence_mode === 'require_complete' ? 'require_complete' : 'bounded';
|
|
1450
|
-
const includeToolCallArgs = args.include_tool_call_args === true;
|
|
1451
|
-
const boundedLimit = resolveBoundedContentSearchLimit(args.max_records, Number.MAX_SAFE_INTEGER);
|
|
1452
|
-
const snapshot = getContentSearchSnapshotForRecall(evidenceMode, boundedLimit);
|
|
1453
|
-
const { entryByHash } = snapshot;
|
|
1454
|
-
const totalRecords = snapshot.totalRecords;
|
|
1455
|
-
const loadedLength = snapshot.entries.length;
|
|
1456
|
-
const searchLimit = evidenceMode === 'require_complete'
|
|
1457
|
-
? resolveCompleteContentSearchLimit(args.max_records, loadedLength)
|
|
1458
|
-
: Math.min(boundedLimit, loadedLength);
|
|
1459
|
-
const explicitLimit = hasExplicitRecordLimit(args.max_records);
|
|
1460
|
-
if (evidenceMode === 'require_complete' && explicitLimit && searchLimit < loadedLength) {
|
|
1461
|
-
const k = Math.max(1, Math.min(50, args.k ?? 10));
|
|
1462
|
-
return {
|
|
1463
|
-
content: [
|
|
1464
|
-
{
|
|
1465
|
-
type: 'text',
|
|
1466
|
-
text: JSON.stringify({
|
|
1467
|
-
query: args.query,
|
|
1468
|
-
k,
|
|
1469
|
-
runtime: recallRuntimeMetadata(),
|
|
1470
|
-
...(includeToolCallArgs ? { include_tool_call_args: true } : {}),
|
|
1471
|
-
evidence_mode: evidenceMode,
|
|
1472
|
-
evidence_status: 'incomplete',
|
|
1473
|
-
fallback_required: true,
|
|
1474
|
-
fallback_reason: `require_complete refused a partial corpus: search_cap=${searchLimit}, ` +
|
|
1475
|
-
`total_records=${loadedLength}.`,
|
|
1476
|
-
fallback: 'Rerun without max_records for full loaded-mirror coverage, or pass an explicit ' +
|
|
1477
|
-
'partition filter through a caller-owned audit plan and treat each partition as its own coverage claim.',
|
|
1478
|
-
total_records: loadedLength,
|
|
1479
|
-
searched_records: 0,
|
|
1480
|
-
search_cap: searchLimit,
|
|
1481
|
-
coverage: recallCoverage(snapshot, 'incomplete_explicit_limit', 0, snapshot.index),
|
|
1482
|
-
candidate_records: 0,
|
|
1483
|
-
truncated_corpus: true,
|
|
1484
|
-
count: 0,
|
|
1485
|
-
results: [],
|
|
1486
|
-
}, null, 2),
|
|
1487
|
-
},
|
|
1488
|
-
],
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
const bm25Index = getContentBm25IndexForNewestLimit(snapshot, searchLimit);
|
|
1492
|
-
const queryTokens = tokenize(args.query);
|
|
1493
|
-
const relevanceByHash = bm25ScoresForQuery(bm25Index, queryTokens);
|
|
1494
|
-
const now = Date.now();
|
|
1495
|
-
const searchPool = queryTokens.length > 0
|
|
1496
|
-
? Array.from(relevanceByHash.keys())
|
|
1497
|
-
.map((hash) => entryByHash.get(hash))
|
|
1498
|
-
.filter((entry) => entry !== undefined)
|
|
1499
|
-
: snapshot.newestEntries.slice(0, searchLimit);
|
|
1500
|
-
const filteredSearchPool = searchPool.filter((entry) => !(entry.lifecycle_anchor && !queryMentionsLifecycle(queryTokens)));
|
|
1501
|
-
const scored = filteredSearchPool.map((entry) => {
|
|
1502
|
-
const ann = entry.annotations;
|
|
1503
|
-
const suppressLifecycleAnchor = entry.lifecycle_anchor && !queryMentionsLifecycle(queryTokens);
|
|
1504
|
-
const toolCallScoreFactor = effectiveToolCallScoreFactor(entry, includeToolCallArgs);
|
|
1505
|
-
const r = recencyScore(entry.timestamp, now, ATRIB_RECALL_TAU_DAYS) * toolCallScoreFactor;
|
|
1506
|
-
const i = suppressLifecycleAnchor ? 0 : importanceScore(ann);
|
|
1507
|
-
const rawRel = !suppressLifecycleAnchor && queryTokens.length > 0
|
|
1508
|
-
? (relevanceByHash.get(entry.record_hash) ?? 0)
|
|
1509
|
-
: 0;
|
|
1510
|
-
const rel = !suppressLifecycleAnchor && queryTokens.length > 0
|
|
1511
|
-
? normalizedBm25Relevance(bm25Index, entry.record_hash, queryTokens, rawRel) *
|
|
1512
|
-
toolCallScoreFactor
|
|
1513
|
-
: 0;
|
|
1514
|
-
const score = parkScore(r, i, rel, ATRIB_RECALL_ALPHA, ATRIB_RECALL_BETA, ATRIB_RECALL_GAMMA);
|
|
1515
|
-
return { entry, score, recency: r, importance: i, relevance: rel };
|
|
1516
|
-
});
|
|
1517
|
-
scored.sort((a, b) => {
|
|
1518
|
-
if (b.score !== a.score)
|
|
1519
|
-
return b.score - a.score;
|
|
1520
|
-
return b.entry.timestamp - a.entry.timestamp;
|
|
1521
|
-
});
|
|
1522
|
-
const k = Math.max(1, Math.min(50, args.k ?? 10));
|
|
1523
|
-
const top = scored.slice(0, k);
|
|
1524
|
-
const completeCoverage = !snapshot.partial && searchLimit >= loadedLength;
|
|
1525
|
-
return {
|
|
1526
|
-
content: [
|
|
1527
|
-
{
|
|
1528
|
-
type: 'text',
|
|
1529
|
-
text: JSON.stringify({
|
|
1530
|
-
query: args.query,
|
|
1531
|
-
k,
|
|
1532
|
-
runtime: recallRuntimeMetadata(),
|
|
1533
|
-
...(includeToolCallArgs ? { include_tool_call_args: true } : {}),
|
|
1534
|
-
evidence_mode: evidenceMode,
|
|
1535
|
-
evidence_status: completeCoverage ? 'complete' : 'bounded',
|
|
1536
|
-
fallback_required: false,
|
|
1537
|
-
total_records: totalRecords,
|
|
1538
|
-
searched_records: searchLimit,
|
|
1539
|
-
coverage: recallCoverage(snapshot, completeCoverage ? 'complete_full_scan' : 'bounded_newest_first', searchLimit, snapshot.index),
|
|
1540
|
-
candidate_records: filteredSearchPool.length,
|
|
1541
|
-
truncated_corpus: !completeCoverage,
|
|
1542
|
-
count: top.length,
|
|
1543
|
-
results: top.map(({ entry, score, recency, importance, relevance }) => {
|
|
1544
|
-
return {
|
|
1545
|
-
record_hash: entry.record_hash,
|
|
1546
|
-
event_type: entry.event_type,
|
|
1547
|
-
context_id: entry.context_id,
|
|
1548
|
-
timestamp: entry.timestamp,
|
|
1549
|
-
tool_name: entry.tool_name,
|
|
1550
|
-
annotations: entry.annotations,
|
|
1551
|
-
// Layer 1 v2 legibility fields (parity with compactify).
|
|
1552
|
-
display_summary: entry.display_summary,
|
|
1553
|
-
display_producer: entry.display_producer,
|
|
1554
|
-
age: formatAge(entry.timestamp, now),
|
|
1555
|
-
score,
|
|
1556
|
-
components: { recency, importance, relevance },
|
|
1557
|
-
};
|
|
1558
|
-
}),
|
|
1559
|
-
}, null, 2),
|
|
1560
|
-
},
|
|
1561
|
-
],
|
|
1562
|
-
};
|
|
1563
|
-
}, extractRecordHashFieldsFromMcpResult));
|
|
1632
|
+
}, async (args) => logReadPrimitiveCall('recall_by_content', args, async () => jsonToolResult(await runRecallByContent(args)), extractRecordHashFieldsFromMcpResult));
|
|
1564
1633
|
// recall_session_chain: records in a context_id, ordered chronologically
|
|
1565
1634
|
// (the natural traversal of the CHAIN_PRECEDES topology). Doable via
|
|
1566
1635
|
// recall_my_attribution_history({context_id}) + a manual timestamp sort,
|
|
1567
1636
|
// but agents naturally think "what happened in this session?" and this
|
|
1568
1637
|
// gives them one call.
|
|
1569
1638
|
server.registerTool('recall_session_chain', {
|
|
1570
|
-
description: "Return all records in a context_id, ordered chronologically (oldest-first). The natural traversal of the CHAIN_PRECEDES topology for a single session/trace. Each entry carries record_hash, event_type, timestamp, display_summary (per-event_type human-readable text from D086), and producer label. Useful for 'what happened in this session?' without manually sorting filter results. Sibling tool to recall_my_attribution_history with built-in context_id scoping + chain-ascending order.",
|
|
1639
|
+
description: "Return all records in a context_id, ordered chronologically (oldest-first). The natural traversal of the CHAIN_PRECEDES topology for a single session/trace. Each entry carries record_hash, event_type, timestamp, display_summary (per-event_type human-readable text from D086), and producer label. Useful for 'what happened in this session?' without manually sorting filter results. Sibling tool to recall_my_attribution_history with built-in context_id scoping + chain-ascending order. Legacy alias: new callers should prefer the `recall` tool with shape='chain'.",
|
|
1571
1640
|
inputSchema: {
|
|
1572
1641
|
context_id: z
|
|
1573
1642
|
.string()
|
|
@@ -1582,81 +1651,11 @@ export function registerAtribRecallTools(server) {
|
|
|
1582
1651
|
.optional()
|
|
1583
1652
|
.describe('When true, include D062 local mirror body as local_content on each returned record. Defaults false to keep the session chain cheap.'),
|
|
1584
1653
|
},
|
|
1585
|
-
}, async (args) => logReadPrimitiveCall('recall_session_chain', args, async () =>
|
|
1586
|
-
const ctx = args.context_id ?? resolveEnvContextId();
|
|
1587
|
-
if (!ctx) {
|
|
1588
|
-
return {
|
|
1589
|
-
content: [
|
|
1590
|
-
{
|
|
1591
|
-
type: 'text',
|
|
1592
|
-
text: JSON.stringify({
|
|
1593
|
-
context_id: null,
|
|
1594
|
-
count: 0,
|
|
1595
|
-
records: [],
|
|
1596
|
-
warning: 'no context_id supplied or resolvable via env',
|
|
1597
|
-
}, null, 2),
|
|
1598
|
-
},
|
|
1599
|
-
],
|
|
1600
|
-
};
|
|
1601
|
-
}
|
|
1602
|
-
const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
|
|
1603
|
-
const filtered = loaded
|
|
1604
|
-
.filter((lr) => lr.record.context_id === ctx)
|
|
1605
|
-
.sort((a, b) => a.record.timestamp - b.record.timestamp);
|
|
1606
|
-
const limit = Math.max(1, Math.min(500, args.limit ?? 50));
|
|
1607
|
-
const sliced = filtered.slice(0, limit);
|
|
1608
|
-
const now = Date.now();
|
|
1609
|
-
return {
|
|
1610
|
-
content: [
|
|
1611
|
-
{
|
|
1612
|
-
type: 'text',
|
|
1613
|
-
text: JSON.stringify({
|
|
1614
|
-
context_id: ctx,
|
|
1615
|
-
total: filtered.length,
|
|
1616
|
-
returned: sliced.length,
|
|
1617
|
-
truncated: filtered.length > sliced.length,
|
|
1618
|
-
records: sliced.map((lr) => {
|
|
1619
|
-
const ann = annotationsByRecord.get(lr.record_hash);
|
|
1620
|
-
const entry = {
|
|
1621
|
-
record_hash: lr.record_hash,
|
|
1622
|
-
event_type: lr.record.event_type,
|
|
1623
|
-
timestamp: lr.record.timestamp,
|
|
1624
|
-
display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
|
|
1625
|
-
display_producer: resolveDisplayProducer(lr.record, lr.producer),
|
|
1626
|
-
age: formatAge(lr.record.timestamp, now),
|
|
1627
|
-
};
|
|
1628
|
-
const informedBy = lr.record
|
|
1629
|
-
.informed_by;
|
|
1630
|
-
const toolName = lr.record.tool_name;
|
|
1631
|
-
const argsHash = lr.record.args_hash;
|
|
1632
|
-
const resultHash = lr.record
|
|
1633
|
-
.result_hash;
|
|
1634
|
-
if (Array.isArray(informedBy) && informedBy.length > 0) {
|
|
1635
|
-
entry.informed_by = informedBy;
|
|
1636
|
-
}
|
|
1637
|
-
if (toolName)
|
|
1638
|
-
entry.tool_name = toolName;
|
|
1639
|
-
if (argsHash)
|
|
1640
|
-
entry.args_hash = argsHash;
|
|
1641
|
-
if (resultHash)
|
|
1642
|
-
entry.result_hash = resultHash;
|
|
1643
|
-
if (args.include_content === true && lr.content !== undefined) {
|
|
1644
|
-
entry.local_content = lr.content;
|
|
1645
|
-
}
|
|
1646
|
-
if (args.include_content === true && lr.producer !== undefined) {
|
|
1647
|
-
entry.local_producer = lr.producer;
|
|
1648
|
-
}
|
|
1649
|
-
return entry;
|
|
1650
|
-
}),
|
|
1651
|
-
}, null, 2),
|
|
1652
|
-
},
|
|
1653
|
-
],
|
|
1654
|
-
};
|
|
1655
|
-
}, extractRecordHashFieldsFromMcpResult));
|
|
1654
|
+
}, async (args) => logReadPrimitiveCall('recall_session_chain', args, async () => jsonToolResult(await runRecallSessionChain(args)), extractRecordHashFieldsFromMcpResult));
|
|
1656
1655
|
// recall_orphans: records nothing else cites via informed_by. Useful for
|
|
1657
1656
|
// "what decisions did I make and never act on?": loose-end discovery.
|
|
1658
1657
|
server.registerTool('recall_orphans', {
|
|
1659
|
-
description: "Return records that are NOT cited by any other record via informed_by (loose ends: decisions or observations the agent made but never followed up on). Surfaces records whose record_hash does NOT appear in any other record's informed_by[] array within the local mirror. Optionally scoped to one context_id, one event_type, or one creator_key. Useful for the agent to discover dropped balls (e.g. 'I noted X but never built on it').",
|
|
1658
|
+
description: "Return records that are NOT cited by any other record via informed_by (loose ends: decisions or observations the agent made but never followed up on). Surfaces records whose record_hash does NOT appear in any other record's informed_by[] array within the local mirror. Optionally scoped to one context_id, one event_type, or one creator_key. Useful for the agent to discover dropped balls (e.g. 'I noted X but never built on it'). Legacy alias: new callers should prefer the `recall` tool with shape='orphans'.",
|
|
1660
1659
|
inputSchema: {
|
|
1661
1660
|
context_id: z
|
|
1662
1661
|
.string()
|
|
@@ -1672,119 +1671,47 @@ export function registerAtribRecallTools(server) {
|
|
|
1672
1671
|
.optional()
|
|
1673
1672
|
.describe('Maximum records to return (default 50, max 500), newest-first.'),
|
|
1674
1673
|
},
|
|
1675
|
-
}, async (args) => logReadPrimitiveCall('recall_orphans', args, async () =>
|
|
1676
|
-
const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
|
|
1677
|
-
// Build the set of all record_hashes that appear in any record's
|
|
1678
|
-
// informed_by field. Anything in `loaded` whose record_hash is
|
|
1679
|
-
// NOT in this set is an orphan.
|
|
1680
|
-
const cited = new Set();
|
|
1681
|
-
for (const lr of loaded) {
|
|
1682
|
-
const ib = lr.record.informed_by;
|
|
1683
|
-
if (Array.isArray(ib)) {
|
|
1684
|
-
for (const h of ib)
|
|
1685
|
-
if (typeof h === 'string')
|
|
1686
|
-
cited.add(h);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
let orphans = loaded.filter((lr) => !cited.has(lr.record_hash));
|
|
1690
|
-
if (args.context_id) {
|
|
1691
|
-
orphans = orphans.filter((lr) => lr.record.context_id === args.context_id);
|
|
1692
|
-
}
|
|
1693
|
-
if (args.creator_key) {
|
|
1694
|
-
orphans = orphans.filter((lr) => lr.record.creator_key === args.creator_key);
|
|
1695
|
-
}
|
|
1696
|
-
if (args.event_type) {
|
|
1697
|
-
const targetUri = normalizeEventType(args.event_type);
|
|
1698
|
-
orphans = orphans.filter((lr) => lr.record.event_type === targetUri);
|
|
1699
|
-
}
|
|
1700
|
-
orphans.sort((a, b) => b.record.timestamp - a.record.timestamp);
|
|
1701
|
-
const limit = Math.max(1, Math.min(500, args.limit ?? 50));
|
|
1702
|
-
const sliced = orphans.slice(0, limit);
|
|
1703
|
-
const now = Date.now();
|
|
1704
|
-
return {
|
|
1705
|
-
content: [
|
|
1706
|
-
{
|
|
1707
|
-
type: 'text',
|
|
1708
|
-
text: JSON.stringify({
|
|
1709
|
-
total: orphans.length,
|
|
1710
|
-
returned: sliced.length,
|
|
1711
|
-
truncated: orphans.length > sliced.length,
|
|
1712
|
-
records: sliced.map((lr) => {
|
|
1713
|
-
const ann = annotationsByRecord.get(lr.record_hash);
|
|
1714
|
-
return {
|
|
1715
|
-
record_hash: lr.record_hash,
|
|
1716
|
-
event_type: lr.record.event_type,
|
|
1717
|
-
context_id: lr.record.context_id,
|
|
1718
|
-
timestamp: lr.record.timestamp,
|
|
1719
|
-
display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
|
|
1720
|
-
display_producer: resolveDisplayProducer(lr.record, lr.producer),
|
|
1721
|
-
age: formatAge(lr.record.timestamp, now),
|
|
1722
|
-
};
|
|
1723
|
-
}),
|
|
1724
|
-
}, null, 2),
|
|
1725
|
-
},
|
|
1726
|
-
],
|
|
1727
|
-
};
|
|
1728
|
-
}, extractRecordHashFieldsFromMcpResult));
|
|
1674
|
+
}, async (args) => logReadPrimitiveCall('recall_orphans', args, async () => jsonToolResult(await runRecallOrphans(args)), extractRecordHashFieldsFromMcpResult));
|
|
1729
1675
|
// recall_by_signer: aggregate the mirror by creator_key. Lets the agent
|
|
1730
1676
|
// ask "who else is in this mirror?" before deciding whether to scope
|
|
1731
1677
|
// queries with creator_key filters. Useful when the mirror is shared
|
|
1732
1678
|
// across multi-agent flows (transactions with counterparty signers,
|
|
1733
1679
|
// peer-shared records, etc.).
|
|
1734
1680
|
server.registerTool('recall_by_signer', {
|
|
1735
|
-
description: "Aggregate the local mirror by creator_key. Returns the distinct creators present + per-creator record count + latest record timestamp. Useful when the mirror is multi-signer (multi-agent flows, transactions with counterparty signers) and the agent wants to discover who else's records are in scope before deciding whether to filter with creator_key. Pure aggregation; no records returned directly. Use recall_my_attribution_history with the creator_key filter to drill into one creator's records.",
|
|
1681
|
+
description: "Aggregate the local mirror by creator_key. Returns the distinct creators present + per-creator record count + latest record timestamp. Useful when the mirror is multi-signer (multi-agent flows, transactions with counterparty signers) and the agent wants to discover who else's records are in scope before deciding whether to filter with creator_key. Pure aggregation; no records returned directly. Use recall_my_attribution_history with the creator_key filter to drill into one creator's records. Legacy alias: new callers should prefer the `recall` tool with shape='by_signer'.",
|
|
1736
1682
|
inputSchema: {
|
|
1737
1683
|
min_records: z
|
|
1738
1684
|
.number()
|
|
1739
1685
|
.optional()
|
|
1740
1686
|
.describe('Optional minimum record count to include a creator in the result. Default 1 (include all).'),
|
|
1741
1687
|
},
|
|
1742
|
-
}, async (args) => logReadPrimitiveCall('recall_by_signer', args, async () =>
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
const minRecords = Math.max(1, args.min_records ?? 1);
|
|
1765
|
-
const stats = [...byKey.values()]
|
|
1766
|
-
.filter((s) => s.count >= minRecords)
|
|
1767
|
-
.sort((a, b) => b.count - a.count);
|
|
1768
|
-
return {
|
|
1769
|
-
content: [
|
|
1770
|
-
{
|
|
1771
|
-
type: 'text',
|
|
1772
|
-
text: JSON.stringify({
|
|
1773
|
-
total_signers: stats.length,
|
|
1774
|
-
total_records: loaded.length,
|
|
1775
|
-
signers: stats,
|
|
1776
|
-
}, null, 2),
|
|
1777
|
-
},
|
|
1778
|
-
],
|
|
1779
|
-
};
|
|
1780
|
-
}, extractRecordHashFieldsFromMcpResult));
|
|
1688
|
+
}, async (args) => logReadPrimitiveCall('recall_by_signer', args, async () => jsonToolResult(await runRecallBySigner(args)), extractRecordHashFieldsFromMcpResult));
|
|
1689
|
+
}
|
|
1690
|
+
// ─── Re-export surface for the @atrib/trace and @atrib/verify-mcp shims
|
|
1691
|
+
// and for embedders. The implementations live in this package per the
|
|
1692
|
+
// attest/recall rename; the legacy packages re-export them.
|
|
1693
|
+
export { compactVisited, createAtribTraceServer, registerTraceTools, runTraceWalk, summarizeSidecar, TraceInput, } from './trace-tools.js';
|
|
1694
|
+
export { extractRecordHashFieldsFromMcpResult };
|
|
1695
|
+
export { createAtribVerifyServer, handleAtribVerify, loadVerifyModule, registerVerifyTool, tryHandleAtribVerify, VerifyInput, } from './verification.js';
|
|
1696
|
+
export { registerRecallVerbTool, RecallVerbInput, RECALL_SHAPES, runRecallVerb } from './recall-verb.js';
|
|
1697
|
+
/**
|
|
1698
|
+
* Register the full read union on one server: the `recall` verb plus every
|
|
1699
|
+
* legacy read tool name it absorbs (8 recall_* tools, trace, trace_forward,
|
|
1700
|
+
* atrib-verify). This is what the `atrib-recall` binary serves and what the
|
|
1701
|
+
* primitive runtime / daemon mount for reads (alias-window rule W1).
|
|
1702
|
+
*/
|
|
1703
|
+
export function registerAtribReadTools(server) {
|
|
1704
|
+
registerRecallVerbTool(server);
|
|
1705
|
+
registerAtribRecallTools(server);
|
|
1706
|
+
registerTraceTools(server);
|
|
1707
|
+
registerVerifyTool(server);
|
|
1781
1708
|
}
|
|
1782
1709
|
export function createAtribRecallServer() {
|
|
1783
1710
|
const mcp = new McpServer({
|
|
1784
1711
|
name: 'atrib-recall',
|
|
1785
1712
|
version: readPackageVersion(),
|
|
1786
1713
|
});
|
|
1787
|
-
|
|
1714
|
+
registerAtribReadTools(mcp);
|
|
1788
1715
|
return { mcp };
|
|
1789
1716
|
}
|
|
1790
1717
|
async function main() {
|