@jinn-network/plugin 0.1.0 → 0.1.1-canary.87457e48
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/dist/index.d.ts +1 -1
- package/dist/pickup.d.ts +12 -3
- package/dist/pickup.js +45 -9
- package/dist/plugin.d.ts +5 -1
- package/dist/plugin.js +121 -32
- package/dist/ports/corpus-port.d.ts +5 -0
- package/dist/schemas/knowledge-hit.d.ts +2 -0
- package/dist/schemas/knowledge-hit.js +9 -1
- package/dist/schemas/knowledge-packet.d.ts +5 -4
- package/dist/schemas/knowledge-packet.js +14 -7
- package/dist/testing/in-memory-corpus.d.ts +3 -1
- package/dist/testing/in-memory-corpus.js +5 -1
- package/package.json +6 -1
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type { ContributionPort, ContributionLedgerEntry, ContributionLocalState,
|
|
|
19
19
|
export type { LocalLearningPort, LocalLearningRun, LocalLearningSkill, } from './ports/local-learning-port.js';
|
|
20
20
|
export type { SkillsPort, SkillRecord } from './ports/skills-port.js';
|
|
21
21
|
export { createJinnPlugin, JINN_PLUGIN_CONTRACT_VERSION, PluginSession } from './plugin.js';
|
|
22
|
-
export type { CompleteSessionEligibilityInputs, CompleteSessionInput, JinnPlugin, JinnPluginDeps, SessionMeta, FirstTurnPickupResult, ToolCallEvent, SessionOutcome, SessionEndResult, ContributionCompletionReceipt, ContributionPreview, } from './plugin.js';
|
|
22
|
+
export type { CompleteSessionEligibilityInputs, CompleteSessionInput, JinnPlugin, JinnPluginDeps, SessionMeta, FirstTurnPickupOptions, FirstTurnPickupResult, ToolCallEvent, SessionOutcome, SessionEndResult, ContributionCompletionReceipt, ContributionPreview, } from './plugin.js';
|
|
23
23
|
export { PickupConfigSchema, DEFAULT_PICKUP_CONFIG, parsePickupConfig, TIER_ORDER } from './schemas/pickup-config.js';
|
|
24
24
|
export type { PickupConfig, Tier } from './schemas/pickup-config.js';
|
|
25
25
|
export { deriveRepositorySearchTerms, discriminatingTerms, deriveSearchTerms, classifyPayload, dedupeKnowledgeHits, scoreKnowledgeHit, selectKnowledgeHits, rankKnowledgeHits, MAX_SELECTED_PACKETS, } from './pickup.js';
|
package/dist/pickup.d.ts
CHANGED
|
@@ -62,7 +62,15 @@ export declare function scoreKnowledgeHit(hit: KnowledgeHit, terms: string[]): n
|
|
|
62
62
|
* cannot manufacture relevance.
|
|
63
63
|
*/
|
|
64
64
|
export declare function scoreKnowledgeRecord(hit: KnowledgeHit, record: CorpusRecord, terms: string[]): number;
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Return a copied, deterministically ranked scored-candidate list.
|
|
67
|
+
*
|
|
68
|
+
* Recency is a group property, not a pairwise comparator: within each equal
|
|
69
|
+
* score+tier group, use recency only when every hit declares the same domain
|
|
70
|
+
* (including the all-omitted legacy group). A mixed-domain group preserves
|
|
71
|
+
* its stable score/tier input order rather than comparing incomparable
|
|
72
|
+
* recency values or inventing a source priority.
|
|
73
|
+
*/
|
|
66
74
|
export declare function rankScoredKnowledgeHits(candidates: ScoredKnowledgeHit[]): ScoredKnowledgeHit[];
|
|
67
75
|
/**
|
|
68
76
|
* Eligible, deduplicated candidates that have at least one metadata match.
|
|
@@ -73,8 +81,9 @@ export declare function rankKnowledgeCandidates(hits: KnowledgeHit[], terms: str
|
|
|
73
81
|
/**
|
|
74
82
|
* Full ranked candidate pool (rescope §3.3 selection policy): drop skill
|
|
75
83
|
* hits, dedup, score, apply the relevance floor (honest nothing-found below
|
|
76
|
-
* it), rank score desc → tier desc → recency desc
|
|
77
|
-
* clears the floor, not
|
|
84
|
+
* it), rank score desc → tier desc → comparable-domain recency desc (or ref
|
|
85
|
+
* for a mixed-domain tie group) — every candidate that clears the floor, not
|
|
86
|
+
* sliced to `MAX_SELECTED_PACKETS`.
|
|
78
87
|
*
|
|
79
88
|
* Content-level guards that can only run after a candidate's content is
|
|
80
89
|
* fetched (mono #1782: post-fetch skill-payload classification, empty-packet
|
package/dist/pickup.js
CHANGED
|
@@ -293,17 +293,52 @@ function tierRank(tier) {
|
|
|
293
293
|
return -1;
|
|
294
294
|
return TIER_ORDER.indexOf(tier);
|
|
295
295
|
}
|
|
296
|
-
function
|
|
296
|
+
function compareScoreAndTier(a, b) {
|
|
297
297
|
if (b.score !== a.score)
|
|
298
298
|
return b.score - a.score;
|
|
299
|
-
|
|
300
|
-
if (tierDiff !== 0)
|
|
301
|
-
return tierDiff;
|
|
302
|
-
return (b.hit.publishedAt ?? 0) - (a.hit.publishedAt ?? 0);
|
|
299
|
+
return tierRank(b.hit.tier) - tierRank(a.hit.tier);
|
|
303
300
|
}
|
|
304
|
-
|
|
301
|
+
function compareRef(a, b) {
|
|
302
|
+
if (a.hit.ref < b.hit.ref)
|
|
303
|
+
return -1;
|
|
304
|
+
if (a.hit.ref > b.hit.ref)
|
|
305
|
+
return 1;
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Return a copied, deterministically ranked scored-candidate list.
|
|
310
|
+
*
|
|
311
|
+
* Recency is a group property, not a pairwise comparator: within each equal
|
|
312
|
+
* score+tier group, use recency only when every hit declares the same domain
|
|
313
|
+
* (including the all-omitted legacy group). A mixed-domain group preserves
|
|
314
|
+
* its stable score/tier input order rather than comparing incomparable
|
|
315
|
+
* recency values or inventing a source priority.
|
|
316
|
+
*/
|
|
305
317
|
export function rankScoredKnowledgeHits(candidates) {
|
|
306
|
-
|
|
318
|
+
const grouped = [...candidates].sort(compareScoreAndTier);
|
|
319
|
+
const ranked = [];
|
|
320
|
+
let start = 0;
|
|
321
|
+
while (start < grouped.length) {
|
|
322
|
+
let end = start + 1;
|
|
323
|
+
while (end < grouped.length
|
|
324
|
+
&& compareScoreAndTier(grouped[start], grouped[end]) === 0) {
|
|
325
|
+
end += 1;
|
|
326
|
+
}
|
|
327
|
+
const tieGroup = grouped.slice(start, end);
|
|
328
|
+
const recencyDomain = tieGroup[0].hit.recencyDomain;
|
|
329
|
+
const comparableRecency = tieGroup.every((candidate) => candidate.hit.recencyDomain === recencyDomain);
|
|
330
|
+
if (comparableRecency) {
|
|
331
|
+
tieGroup.sort((a, b) => {
|
|
332
|
+
const recencyDiff = (b.hit.publishedAt ?? 0) - (a.hit.publishedAt ?? 0);
|
|
333
|
+
if (recencyDiff !== 0)
|
|
334
|
+
return recencyDiff;
|
|
335
|
+
return compareRef(a, b);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
ranked.push(...tieGroup);
|
|
339
|
+
start = end;
|
|
340
|
+
}
|
|
341
|
+
return ranked;
|
|
307
342
|
}
|
|
308
343
|
/**
|
|
309
344
|
* Eligible, deduplicated candidates that have at least one metadata match.
|
|
@@ -320,8 +355,9 @@ export function rankKnowledgeCandidates(hits, terms) {
|
|
|
320
355
|
/**
|
|
321
356
|
* Full ranked candidate pool (rescope §3.3 selection policy): drop skill
|
|
322
357
|
* hits, dedup, score, apply the relevance floor (honest nothing-found below
|
|
323
|
-
* it), rank score desc → tier desc → recency desc
|
|
324
|
-
* clears the floor, not
|
|
358
|
+
* it), rank score desc → tier desc → comparable-domain recency desc (or ref
|
|
359
|
+
* for a mixed-domain tie group) — every candidate that clears the floor, not
|
|
360
|
+
* sliced to `MAX_SELECTED_PACKETS`.
|
|
325
361
|
*
|
|
326
362
|
* Content-level guards that can only run after a candidate's content is
|
|
327
363
|
* fetched (mono #1782: post-fetch skill-payload classification, empty-packet
|
package/dist/plugin.d.ts
CHANGED
|
@@ -50,10 +50,14 @@ export interface FirstTurnPickupResult {
|
|
|
50
50
|
retrievalFired: boolean;
|
|
51
51
|
eligibleRefs: string[];
|
|
52
52
|
deliveredRefs: string[];
|
|
53
|
+
deliveredCanonicalEpisodeIds: string[];
|
|
53
54
|
deliveryMode: 'delivered' | 'disabled' | 'degraded' | 'withheld';
|
|
54
55
|
deliveredContentHash?: string;
|
|
55
56
|
degraded?: string;
|
|
56
57
|
}
|
|
58
|
+
export interface FirstTurnPickupOptions {
|
|
59
|
+
excludeCanonicalEpisodeIds?: readonly string[];
|
|
60
|
+
}
|
|
57
61
|
export interface ToolCallEvent {
|
|
58
62
|
spanId: string;
|
|
59
63
|
parentSpanId: string | null;
|
|
@@ -136,7 +140,7 @@ export declare class PluginSession {
|
|
|
136
140
|
private deliveredContentHash;
|
|
137
141
|
private readonly capturedAt;
|
|
138
142
|
constructor(deps: JinnPluginDeps, meta: SessionMeta);
|
|
139
|
-
firstTurnPickup(firstMessage: string): Promise<FirstTurnPickupResult>;
|
|
143
|
+
firstTurnPickup(firstMessage: string, options?: FirstTurnPickupOptions): Promise<FirstTurnPickupResult>;
|
|
140
144
|
noteUserTurn(content: string): void;
|
|
141
145
|
noteAssistantTurn(content: string): void;
|
|
142
146
|
private pushTurn;
|
package/dist/plugin.js
CHANGED
|
@@ -227,7 +227,8 @@ export class PluginSession {
|
|
|
227
227
|
this.deps = deps;
|
|
228
228
|
this.meta = meta;
|
|
229
229
|
}
|
|
230
|
-
async firstTurnPickup(firstMessage) {
|
|
230
|
+
async firstTurnPickup(firstMessage, options = {}) {
|
|
231
|
+
const excludedCanonicalEpisodeIds = new Set(options.excludeCanonicalEpisodeIds ?? []);
|
|
231
232
|
const config = parsePickupConfig(this.meta.pickup);
|
|
232
233
|
if (!config.enabled) {
|
|
233
234
|
return {
|
|
@@ -237,6 +238,7 @@ export class PluginSession {
|
|
|
237
238
|
retrievalFired: false,
|
|
238
239
|
eligibleRefs: [],
|
|
239
240
|
deliveredRefs: [],
|
|
241
|
+
deliveredCanonicalEpisodeIds: [],
|
|
240
242
|
deliveryMode: 'disabled',
|
|
241
243
|
};
|
|
242
244
|
}
|
|
@@ -257,6 +259,7 @@ export class PluginSession {
|
|
|
257
259
|
retrievalFired: true,
|
|
258
260
|
eligibleRefs: [],
|
|
259
261
|
deliveredRefs: [],
|
|
262
|
+
deliveredCanonicalEpisodeIds: [],
|
|
260
263
|
deliveryMode: this.deliveryMode,
|
|
261
264
|
};
|
|
262
265
|
}
|
|
@@ -283,7 +286,19 @@ export class PluginSession {
|
|
|
283
286
|
byRef.set(hit.ref, hit);
|
|
284
287
|
}
|
|
285
288
|
}
|
|
286
|
-
|
|
289
|
+
// Only the production local search adapter exposes this hit-level hint in
|
|
290
|
+
// this release. Federated search is stable local-then-public, so the first
|
|
291
|
+
// hit for an identity is the deterministic local form to prefer once a
|
|
292
|
+
// fetched public record reveals that same identity.
|
|
293
|
+
const preferredHitByCanonicalEpisodeId = new Map();
|
|
294
|
+
for (const hit of byRef.values()) {
|
|
295
|
+
if (hit.canonicalEpisodeId !== undefined
|
|
296
|
+
&& !preferredHitByCanonicalEpisodeId.has(hit.canonicalEpisodeId)) {
|
|
297
|
+
preferredHitByCanonicalEpisodeId.set(hit.canonicalEpisodeId, hit);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const candidates = rankKnowledgeCandidates([...byRef.values()].filter((hit) => hit.canonicalEpisodeId === undefined
|
|
301
|
+
|| !excludedCanonicalEpisodeIds.has(hit.canonicalEpisodeId)), scoringTerms);
|
|
287
302
|
if (candidates.length === 0) {
|
|
288
303
|
this.deliveryMode = degradedReason === undefined ? 'withheld' : 'degraded';
|
|
289
304
|
return {
|
|
@@ -293,6 +308,7 @@ export class PluginSession {
|
|
|
293
308
|
retrievalFired: true,
|
|
294
309
|
eligibleRefs: [],
|
|
295
310
|
deliveredRefs: [],
|
|
311
|
+
deliveredCanonicalEpisodeIds: [],
|
|
296
312
|
deliveryMode: this.deliveryMode,
|
|
297
313
|
...(degradedReason !== undefined ? { degraded: degradedReason } : {}),
|
|
298
314
|
};
|
|
@@ -307,25 +323,78 @@ export class PluginSession {
|
|
|
307
323
|
// floor and records an honest degraded reason (fail open).
|
|
308
324
|
const fetchedRefs = [];
|
|
309
325
|
const prefetchedByRef = new Map();
|
|
326
|
+
const fetchRecord = (hit) => {
|
|
327
|
+
const prefetched = prefetchedByRef.get(hit.ref);
|
|
328
|
+
if (prefetched !== undefined)
|
|
329
|
+
return prefetched;
|
|
330
|
+
fetchedRefs.push(hit.ref);
|
|
331
|
+
const pending = this.deps.corpus.get(hit.ref).catch((error) => degraded(`corpus get rejected for ${hit.ref}: ${errorReason(error)}`));
|
|
332
|
+
prefetchedByRef.set(hit.ref, pending);
|
|
333
|
+
return pending;
|
|
334
|
+
};
|
|
335
|
+
const resolvePreferredRecord = async (hit, fetchedRecord) => {
|
|
336
|
+
let selectedHit = hit;
|
|
337
|
+
let record = fetchedRecord;
|
|
338
|
+
let preferredFailureReason;
|
|
339
|
+
const canonicalEpisodeId = record.canonicalEpisodeId;
|
|
340
|
+
const preferredHit = canonicalEpisodeId === undefined
|
|
341
|
+
? undefined
|
|
342
|
+
: preferredHitByCanonicalEpisodeId.get(canonicalEpisodeId);
|
|
343
|
+
if (preferredHit !== undefined && preferredHit.ref !== hit.ref) {
|
|
344
|
+
const preferredResult = await fetchRecord(preferredHit);
|
|
345
|
+
if (preferredResult.status === 'unavailable') {
|
|
346
|
+
preferredFailureReason = preferredResult.reason;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
if (preferredResult.status === 'degraded') {
|
|
350
|
+
preferredFailureReason = preferredResult.reason;
|
|
351
|
+
}
|
|
352
|
+
const preferredRecord = valueOr(preferredResult, null);
|
|
353
|
+
if (preferredRecord !== null) {
|
|
354
|
+
selectedHit = preferredHit;
|
|
355
|
+
record = preferredRecord;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
selectedHit,
|
|
361
|
+
record,
|
|
362
|
+
...(preferredFailureReason !== undefined ? { preferredFailureReason } : {}),
|
|
363
|
+
};
|
|
364
|
+
};
|
|
310
365
|
const directCandidates = candidates.filter((candidate) => candidate.score >= RELEVANCE_FLOOR);
|
|
311
366
|
const nearMisses = candidates
|
|
312
367
|
.filter((candidate) => candidate.score < RELEVANCE_FLOOR)
|
|
313
368
|
.slice(0, MAX_CONTENT_RESCORE_CANDIDATES);
|
|
314
|
-
const nearMissResults = await Promise.all(nearMisses.map(({ hit }) => {
|
|
315
|
-
|
|
316
|
-
|
|
369
|
+
const nearMissResults = await Promise.all(nearMisses.map(async ({ hit }) => {
|
|
370
|
+
const result = await fetchRecord(hit);
|
|
371
|
+
if (result.status === 'unavailable') {
|
|
372
|
+
return { result, selectedHit: hit, record: null };
|
|
373
|
+
}
|
|
374
|
+
const fetchedRecord = valueOr(result, null);
|
|
375
|
+
if (fetchedRecord === null)
|
|
376
|
+
return { result, selectedHit: hit, record: null };
|
|
377
|
+
if (fetchedRecord.canonicalEpisodeId !== undefined
|
|
378
|
+
&& excludedCanonicalEpisodeIds.has(fetchedRecord.canonicalEpisodeId)) {
|
|
379
|
+
return { result, selectedHit: hit, record: null };
|
|
380
|
+
}
|
|
381
|
+
const preferred = await resolvePreferredRecord(hit, fetchedRecord);
|
|
382
|
+
return { result, ...preferred };
|
|
317
383
|
}));
|
|
318
384
|
const promotedCandidates = [];
|
|
319
385
|
for (let index = 0; index < nearMisses.length; index += 1) {
|
|
320
386
|
const candidate = nearMisses[index];
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
if (result.status !== 'ok') {
|
|
387
|
+
const nearMissResult = nearMissResults[index];
|
|
388
|
+
if (nearMissResult.result.status === 'unavailable') {
|
|
324
389
|
if (degradedReason === undefined)
|
|
325
|
-
degradedReason = result.reason;
|
|
390
|
+
degradedReason = nearMissResult.result.reason;
|
|
326
391
|
continue;
|
|
327
392
|
}
|
|
328
|
-
|
|
393
|
+
if (nearMissResult.result.status === 'degraded') {
|
|
394
|
+
degradedReason ??= nearMissResult.result.reason;
|
|
395
|
+
}
|
|
396
|
+
degradedReason ??= nearMissResult.preferredFailureReason;
|
|
397
|
+
const record = nearMissResult.record;
|
|
329
398
|
if (record === null)
|
|
330
399
|
continue;
|
|
331
400
|
if (record.isSkillPayload === true)
|
|
@@ -350,40 +419,55 @@ export class PluginSession {
|
|
|
350
419
|
retrievalFired: true,
|
|
351
420
|
eligibleRefs: [],
|
|
352
421
|
deliveredRefs: [],
|
|
422
|
+
deliveredCanonicalEpisodeIds: [],
|
|
353
423
|
deliveryMode: this.deliveryMode,
|
|
354
424
|
...(degradedReason !== undefined ? { degraded: degradedReason } : {}),
|
|
355
425
|
};
|
|
356
426
|
}
|
|
357
427
|
// Fetch full content for ranked candidates and project packets, walking
|
|
358
428
|
// down the ranked list until MAX_SELECTED_PACKETS valid packets are
|
|
359
|
-
// found or candidates are exhausted (mono #1782).
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
// a legacy skill-shaped record
|
|
363
|
-
// jinn.skill.v1-backed record that slipped
|
|
364
|
-
// as a wire kind:'skill' hit is excluded at
|
|
365
|
-
// retrieval-visibility content verification (#1824,
|
|
366
|
-
// where the other
|
|
367
|
-
// projection with zero excerpts and no synthesis
|
|
368
|
-
// projection failure degrades that one ref to
|
|
369
|
-
// throwing into the caller (§3.5).
|
|
429
|
+
// found or candidates are exhausted (mono #1782). Post-fetch guards can
|
|
430
|
+
// disqualify a candidate without spending its slot, promoting the
|
|
431
|
+
// next-ranked one: (1) canonical exclusion/dedup; (2) content-level
|
|
432
|
+
// skill classification — which excludes a legacy skill-shaped record
|
|
433
|
+
// (skill.md step attribute) or a jinn.skill.v1-backed record that slipped
|
|
434
|
+
// the wire kind filter, exactly as a wire kind:'skill' hit is excluded at
|
|
435
|
+
// selection time; (3) retrieval-visibility content verification (#1824,
|
|
436
|
+
// W2) — fail-closed where the other guards are fail-open; (4)
|
|
437
|
+
// empty-packet honesty — a projection with zero excerpts and no synthesis
|
|
438
|
+
// is not evidence. A projection failure degrades that one ref to
|
|
439
|
+
// nothing-found rather than throwing into the caller (§3.5).
|
|
370
440
|
const packets = [];
|
|
441
|
+
const deliveredCanonicalEpisodeIds = [];
|
|
442
|
+
const deliveredCanonicalSet = new Set();
|
|
371
443
|
for (const hit of ranked) {
|
|
372
444
|
if (packets.length >= MAX_SELECTED_PACKETS)
|
|
373
445
|
break;
|
|
374
|
-
|
|
375
|
-
if (result ===
|
|
376
|
-
fetchedRefs.push(hit.ref);
|
|
377
|
-
result = await this.deps.corpus.get(hit.ref).catch((error) => degraded(`corpus get rejected for ${hit.ref}: ${errorReason(error)}`));
|
|
378
|
-
}
|
|
379
|
-
if (result.status !== 'ok') {
|
|
446
|
+
const result = await fetchRecord(hit);
|
|
447
|
+
if (result.status === 'unavailable') {
|
|
380
448
|
if (degradedReason === undefined)
|
|
381
449
|
degradedReason = result.reason;
|
|
382
450
|
continue;
|
|
383
451
|
}
|
|
384
|
-
|
|
385
|
-
|
|
452
|
+
if (result.status === 'degraded')
|
|
453
|
+
degradedReason ??= result.reason;
|
|
454
|
+
const fetchedRecord = valueOr(result, null);
|
|
455
|
+
if (fetchedRecord === null)
|
|
456
|
+
continue;
|
|
457
|
+
const fetchedCanonicalEpisodeId = fetchedRecord.canonicalEpisodeId;
|
|
458
|
+
if (fetchedCanonicalEpisodeId !== undefined
|
|
459
|
+
&& (excludedCanonicalEpisodeIds.has(fetchedCanonicalEpisodeId)
|
|
460
|
+
|| deliveredCanonicalSet.has(fetchedCanonicalEpisodeId))) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const { selectedHit, record, preferredFailureReason, } = await resolvePreferredRecord(hit, fetchedRecord);
|
|
464
|
+
degradedReason ??= preferredFailureReason;
|
|
465
|
+
const canonicalEpisodeId = record.canonicalEpisodeId;
|
|
466
|
+
if (canonicalEpisodeId !== undefined
|
|
467
|
+
&& (excludedCanonicalEpisodeIds.has(canonicalEpisodeId)
|
|
468
|
+
|| deliveredCanonicalSet.has(canonicalEpisodeId))) {
|
|
386
469
|
continue;
|
|
470
|
+
}
|
|
387
471
|
if (record.isSkillPayload === true)
|
|
388
472
|
continue;
|
|
389
473
|
// Post-fetch content guard (#1824, W2): content is the truth, the
|
|
@@ -397,12 +481,16 @@ export class PluginSession {
|
|
|
397
481
|
packet = projectKnowledgePacket(record);
|
|
398
482
|
}
|
|
399
483
|
catch (error) {
|
|
400
|
-
degradedReason ??= `packet projection failed for ${
|
|
484
|
+
degradedReason ??= `packet projection failed for ${selectedHit.ref}: ${errorReason(error)}`;
|
|
401
485
|
continue;
|
|
402
486
|
}
|
|
403
487
|
if (packet.excerpts.length === 0 && packet.synthesis === undefined)
|
|
404
488
|
continue;
|
|
405
489
|
packets.push(packet);
|
|
490
|
+
if (canonicalEpisodeId !== undefined) {
|
|
491
|
+
deliveredCanonicalSet.add(canonicalEpisodeId);
|
|
492
|
+
deliveredCanonicalEpisodeIds.push(canonicalEpisodeId);
|
|
493
|
+
}
|
|
406
494
|
}
|
|
407
495
|
this.fetchedRefs = fetchedRefs;
|
|
408
496
|
this.providedRefs = packets.map((packet) => packet.ref);
|
|
@@ -417,6 +505,7 @@ export class PluginSession {
|
|
|
417
505
|
retrievalFired: true,
|
|
418
506
|
eligibleRefs: this.eligibleRefs,
|
|
419
507
|
deliveredRefs: [],
|
|
508
|
+
deliveredCanonicalEpisodeIds: [],
|
|
420
509
|
deliveryMode: this.deliveryMode,
|
|
421
510
|
...(degradedReason !== undefined ? { degraded: degradedReason } : {}),
|
|
422
511
|
};
|
|
@@ -431,6 +520,7 @@ export class PluginSession {
|
|
|
431
520
|
retrievalFired: true,
|
|
432
521
|
eligibleRefs: this.eligibleRefs,
|
|
433
522
|
deliveredRefs: this.providedRefs,
|
|
523
|
+
deliveredCanonicalEpisodeIds,
|
|
434
524
|
deliveryMode: this.deliveryMode,
|
|
435
525
|
deliveredContentHash: this.deliveredContentHash,
|
|
436
526
|
...(degradedReason !== undefined ? { degraded: degradedReason } : {}),
|
|
@@ -536,8 +626,7 @@ function renderKnowledgePacket(packet) {
|
|
|
536
626
|
for (const excerpt of packet.excerpts)
|
|
537
627
|
lines.push(`- ${excerpt.label}: ${excerpt.text}`);
|
|
538
628
|
const capturedDate = packet.attribution.capturedAt.slice(0, 10);
|
|
539
|
-
lines.push(` source: ${packet.ref} · ${packet.attribution.origin} · captured ${capturedDate}
|
|
540
|
-
+ `full episode: corpus_fetch ${packet.ref}`);
|
|
629
|
+
lines.push(` source: ${packet.ref} · ${packet.attribution.origin} · captured ${capturedDate}`);
|
|
541
630
|
return lines.join('\n');
|
|
542
631
|
}
|
|
543
632
|
function renderKnowledgePackets(packets) {
|
|
@@ -19,6 +19,11 @@ export interface CorpusRecordStep {
|
|
|
19
19
|
*/
|
|
20
20
|
export interface CorpusRecord {
|
|
21
21
|
ref: string;
|
|
22
|
+
/**
|
|
23
|
+
* Canonical EpisodeV1 episodeId, or the legacy trace sessionId used as its
|
|
24
|
+
* read-compatible identity. Application metadata for deduplication only.
|
|
25
|
+
*/
|
|
26
|
+
canonicalEpisodeId?: string;
|
|
22
27
|
task: {
|
|
23
28
|
summary: string;
|
|
24
29
|
repositorySlug?: string;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
export declare const KnowledgeHitSchema: z.ZodObject<{
|
|
4
4
|
ref: z.ZodString;
|
|
5
|
+
canonicalEpisodeId: z.ZodOptional<z.ZodString>;
|
|
5
6
|
kind: z.ZodEnum<{
|
|
6
7
|
seed: "seed";
|
|
7
8
|
trace: "trace";
|
|
@@ -22,6 +23,7 @@ export declare const KnowledgeHitSchema: z.ZodObject<{
|
|
|
22
23
|
tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
23
24
|
origin: z.ZodOptional<z.ZodString>;
|
|
24
25
|
publishedAt: z.ZodOptional<z.ZodNumber>;
|
|
26
|
+
recencyDomain: z.ZodOptional<z.ZodString>;
|
|
25
27
|
retrievalVisible: z.ZodOptional<z.ZodBoolean>;
|
|
26
28
|
}, z.core.$strict>;
|
|
27
29
|
export type KnowledgeHit = z.infer<typeof KnowledgeHitSchema>;
|
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { TIER_ORDER } from './pickup-config.js';
|
|
4
4
|
export const KnowledgeHitSchema = z.strictObject({
|
|
5
5
|
ref: z.string().min(1),
|
|
6
|
+
canonicalEpisodeId: z.string().min(1).optional(),
|
|
6
7
|
kind: z.enum(['seed', 'trace', 'skill']),
|
|
7
8
|
title: z.string().min(1).optional(),
|
|
8
9
|
snippet: z.string().min(1).optional(),
|
|
@@ -14,8 +15,15 @@ export const KnowledgeHitSchema = z.strictObject({
|
|
|
14
15
|
/** Trustworthy content-dedup identity: on-chain agentId when known, otherwise
|
|
15
16
|
* the record ref. Never a manifest-supplied safeAddress. */
|
|
16
17
|
origin: z.string().min(1).optional(),
|
|
17
|
-
/**
|
|
18
|
+
/** Adapter-native recency value — comparable only within `recencyDomain`. */
|
|
18
19
|
publishedAt: z.number().int().nonnegative().optional(),
|
|
20
|
+
/**
|
|
21
|
+
* Adapter-declared comparison domain for `publishedAt` (for example,
|
|
22
|
+
* `unix-ms` or `block-number`). Recency is comparable only within one
|
|
23
|
+
* explicit domain. When both hits omit this additive field, ranking keeps
|
|
24
|
+
* the legacy raw-number behavior.
|
|
25
|
+
*/
|
|
26
|
+
recencyDomain: z.string().min(1).optional(),
|
|
19
27
|
/** Computed by the adapter from the wire hit's tags (issue #1824) — presence
|
|
20
28
|
* of RETRIEVAL_VISIBLE_TAG. Absent = treat as not visible (fail-closed at
|
|
21
29
|
* ranking). */
|
|
@@ -65,11 +65,12 @@ export interface KnowledgePacketBudget {
|
|
|
65
65
|
maxChars?: number;
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
|
-
* Line-boundary-aware truncation ending with
|
|
69
|
-
*
|
|
70
|
-
*
|
|
68
|
+
* Line-boundary-aware truncation ending with a neutral marker. The `ref`
|
|
69
|
+
* parameter remains required for source compatibility with the public helper.
|
|
70
|
+
* Returns an empty string when the budget cannot fit both meaningful source
|
|
71
|
+
* text and the complete tail.
|
|
71
72
|
*/
|
|
72
|
-
export declare function truncateLineBoundary(text: string, maxChars: number,
|
|
73
|
+
export declare function truncateLineBoundary(text: string, maxChars: number, _ref: string): string;
|
|
73
74
|
/**
|
|
74
75
|
* Pure deterministic projection of a `CorpusRecord` into a `KnowledgePacket`
|
|
75
76
|
* (rescope §3.2). Selects and truncates; never paraphrases. Enforces the
|
|
@@ -60,6 +60,9 @@ function stepDiff(step) {
|
|
|
60
60
|
function stepNote(step) {
|
|
61
61
|
return attrString(step.attributes, 'note') ?? attrString(step.attributes, 'turn.text');
|
|
62
62
|
}
|
|
63
|
+
function isAssistantTurn(step) {
|
|
64
|
+
return attrString(step.attributes, 'role') === 'assistant';
|
|
65
|
+
}
|
|
63
66
|
const EXCERPT_LABELS = new Set([
|
|
64
67
|
'failure', 'fix', 'command', 'diff', 'note',
|
|
65
68
|
]);
|
|
@@ -90,7 +93,9 @@ function seedStepExcerpt(step) {
|
|
|
90
93
|
* output, the next command after it that is not itself a failure (the
|
|
91
94
|
* correction), the last passing command overall (the final backstop), and a
|
|
92
95
|
* diff step when present. Falls back to a single free-form note when
|
|
93
|
-
* nothing command-shaped is found
|
|
96
|
+
* nothing command-shaped is found, preferring an assistant turn so a
|
|
97
|
+
* conversational episode supplies the retained answer rather than merely
|
|
98
|
+
* repeating the user's prompt. Selects only — never paraphrases.
|
|
94
99
|
*/
|
|
95
100
|
function selectExcerpts(steps) {
|
|
96
101
|
const seedExcerpts = steps
|
|
@@ -135,21 +140,23 @@ function selectExcerpts(steps) {
|
|
|
135
140
|
if (diffStep !== undefined)
|
|
136
141
|
excerpts.push({ label: 'diff', text: stepDiff(diffStep) });
|
|
137
142
|
if (excerpts.length === 0) {
|
|
138
|
-
const noteStep = steps.find((step) => stepNote(step) !== undefined)
|
|
143
|
+
const noteStep = steps.find((step) => isAssistantTurn(step) && stepNote(step) !== undefined)
|
|
144
|
+
?? steps.find((step) => stepNote(step) !== undefined);
|
|
139
145
|
if (noteStep !== undefined)
|
|
140
146
|
excerpts.push({ label: 'note', text: stepNote(noteStep) });
|
|
141
147
|
}
|
|
142
148
|
return excerpts;
|
|
143
149
|
}
|
|
144
150
|
/**
|
|
145
|
-
* Line-boundary-aware truncation ending with
|
|
146
|
-
*
|
|
147
|
-
*
|
|
151
|
+
* Line-boundary-aware truncation ending with a neutral marker. The `ref`
|
|
152
|
+
* parameter remains required for source compatibility with the public helper.
|
|
153
|
+
* Returns an empty string when the budget cannot fit both meaningful source
|
|
154
|
+
* text and the complete tail.
|
|
148
155
|
*/
|
|
149
|
-
export function truncateLineBoundary(text, maxChars,
|
|
156
|
+
export function truncateLineBoundary(text, maxChars, _ref) {
|
|
150
157
|
if (text.length <= maxChars)
|
|
151
158
|
return text;
|
|
152
|
-
const tail =
|
|
159
|
+
const tail = '\n[truncated]';
|
|
153
160
|
const budget = maxChars - tail.length;
|
|
154
161
|
if (budget <= 0)
|
|
155
162
|
return '';
|
|
@@ -6,7 +6,7 @@ import type { KnowledgeHit } from '../schemas/knowledge-hit.js';
|
|
|
6
6
|
* search would return for it (kind/title/tier/payloadKind/publishedAt — the
|
|
7
7
|
* fields not derivable from `CorpusRecord` alone).
|
|
8
8
|
*/
|
|
9
|
-
export type InMemoryCorpusSeed = CorpusRecord & Pick<KnowledgeHit, 'kind'> & Partial<Pick<KnowledgeHit, 'title' | 'tier' | 'payloadKind' | 'publishedAt'>>;
|
|
9
|
+
export type InMemoryCorpusSeed = CorpusRecord & Pick<KnowledgeHit, 'kind'> & Partial<Pick<KnowledgeHit, 'title' | 'tier' | 'payloadKind' | 'publishedAt' | 'recencyDomain'>>;
|
|
10
10
|
/** Map/array-backed, seedable — architecture spec §8. Seeds carry full
|
|
11
11
|
* content (`CorpusRecord`) so `get()` can return it directly; `search()`
|
|
12
12
|
* derives the lightweight `KnowledgeHit` view. */
|
|
@@ -17,6 +17,7 @@ export declare class InMemoryCorpusPort implements CorpusPort {
|
|
|
17
17
|
ref: string;
|
|
18
18
|
kind: "seed" | "trace" | "skill";
|
|
19
19
|
tags: string[];
|
|
20
|
+
canonicalEpisodeId?: string | undefined;
|
|
20
21
|
title?: string | undefined;
|
|
21
22
|
snippet?: string | undefined;
|
|
22
23
|
score?: number | undefined;
|
|
@@ -24,6 +25,7 @@ export declare class InMemoryCorpusPort implements CorpusPort {
|
|
|
24
25
|
payloadKind?: "unknown" | "skill" | undefined;
|
|
25
26
|
origin?: string | undefined;
|
|
26
27
|
publishedAt?: number | undefined;
|
|
28
|
+
recencyDomain?: string | undefined;
|
|
27
29
|
retrievalVisible?: boolean | undefined;
|
|
28
30
|
}[]>>;
|
|
29
31
|
get(ref: string): Promise<{
|
|
@@ -2,6 +2,9 @@ import { ok } from '../outcome.js';
|
|
|
2
2
|
function toKnowledgeHit(seed) {
|
|
3
3
|
return {
|
|
4
4
|
ref: seed.ref,
|
|
5
|
+
...(seed.canonicalEpisodeId !== undefined
|
|
6
|
+
? { canonicalEpisodeId: seed.canonicalEpisodeId }
|
|
7
|
+
: {}),
|
|
5
8
|
kind: seed.kind,
|
|
6
9
|
...(seed.title !== undefined ? { title: seed.title } : {}),
|
|
7
10
|
snippet: seed.task.summary,
|
|
@@ -10,6 +13,7 @@ function toKnowledgeHit(seed) {
|
|
|
10
13
|
tags: seed.tags,
|
|
11
14
|
origin: seed.origin,
|
|
12
15
|
...(seed.publishedAt !== undefined ? { publishedAt: seed.publishedAt } : {}),
|
|
16
|
+
...(seed.recencyDomain !== undefined ? { recencyDomain: seed.recencyDomain } : {}),
|
|
13
17
|
retrievalVisible: seed.retrievalVisible,
|
|
14
18
|
};
|
|
15
19
|
}
|
|
@@ -48,7 +52,7 @@ export class InMemoryCorpusPort {
|
|
|
48
52
|
if (!record)
|
|
49
53
|
return ok(null);
|
|
50
54
|
// Strip the KnowledgeHit-only fields — get() returns the CorpusRecord shape.
|
|
51
|
-
const { kind: _kind, title: _title, tier: _tier, payloadKind: _payloadKind, publishedAt: _publishedAt, ...corpusRecord } = record;
|
|
55
|
+
const { kind: _kind, title: _title, tier: _tier, payloadKind: _payloadKind, publishedAt: _publishedAt, recencyDomain: _recencyDomain, ...corpusRecord } = record;
|
|
52
56
|
return ok(corpusRecord);
|
|
53
57
|
}
|
|
54
58
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jinn-network/plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1-canary.87457e48",
|
|
4
4
|
"description": "Product core for the Jinn Plugin — ports, product schemas, and the createJinnPlugin factory. Stage 1 foundation package (contracts + in-memory kit only).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "yarn@4.13.0",
|
|
7
7
|
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/Jinn-Network/mono.git",
|
|
11
|
+
"directory": "packages/plugin"
|
|
12
|
+
},
|
|
8
13
|
"main": "./dist/index.js",
|
|
9
14
|
"types": "./dist/index.d.ts",
|
|
10
15
|
"files": [
|