@theseus-whale/dsh-continuity 0.0.0-stage → 4.12.2
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/INSTALL.md +25 -0
- package/PANEL.md +92 -0
- package/README.md +286 -2
- package/RELEASE-NOTES.md +732 -0
- package/bin/bc-acceptance-full.mjs +91 -0
- package/bin/bc-acceptance-tight.mjs +66 -0
- package/bin/bc-acceptance.mjs +22 -0
- package/bin/bc-add-keywords.mjs +21 -0
- package/bin/bc-demo.mjs +47 -0
- package/bin/bc-expand-hint.mjs +179 -0
- package/bin/bc-ingest-index.mjs +35 -0
- package/bin/bc-ingest.mjs +31 -0
- package/bin/bc-label-demo.mjs +18 -0
- package/bin/bc-relabel.mjs +150 -0
- package/bin/bc-sessionize.mjs +59 -0
- package/bin/coref_pipeline.py +47 -0
- package/bin/coref_service.py +57 -0
- package/bin/debug_ckip.py +16 -0
- package/bin/debug_ner_zh.py +18 -0
- package/bin/doctor.mjs +275 -0
- package/bin/en_ner_error_audit.py +33 -0
- package/bin/eval-label-rules.mjs +342 -0
- package/bin/export-turns.py +68 -0
- package/bin/extract-user-paragraph.mjs +14 -0
- package/bin/extraction-composite-smoke.mjs +28 -0
- package/bin/extraction-latency.mjs +58 -0
- package/bin/extraction-user-smoke.mjs +28 -0
- package/bin/extraction_service.py +164 -0
- package/bin/facet-active-experiment.mjs +110 -0
- package/bin/facet-covid.mjs +8 -0
- package/bin/facet-curve.mjs +27 -0
- package/bin/facet-embedding-train.mjs +20 -0
- package/bin/facet-embedding-validate.mjs +83 -0
- package/bin/facet-eval-roleplay.mjs +42 -0
- package/bin/facet-eval.mjs +38 -0
- package/bin/facet-fold.mjs +31 -0
- package/bin/facet-load.mjs +16 -0
- package/bin/facet-mlp-scan.mjs +45 -0
- package/bin/facet-mvp.mjs +34 -0
- package/bin/facet-online-experiments.mjs +78 -0
- package/bin/facet-online-race.mjs +99 -0
- package/bin/facet-online-validate.mjs +51 -0
- package/bin/facet-panel.mjs +22 -0
- package/bin/facet-passage.mjs +41 -0
- package/bin/facet-random-validate.mjs +48 -0
- package/bin/facet-roleplay.mjs +33 -0
- package/bin/facet-scan.mjs +56 -0
- package/bin/facet-segment.mjs +34 -0
- package/bin/facet-train.mjs +19 -0
- package/bin/facet-validate.mjs +103 -0
- package/bin/facet-zeroshot.mjs +14 -0
- package/bin/install-templates.mjs +72 -0
- package/bin/memory-bench.mjs +53 -0
- package/bin/memory-build-summary.mjs +43 -0
- package/bin/memory-build-vector.mjs +31 -0
- package/bin/memory-build.mjs +41 -0
- package/bin/memory-calibration.mjs +70 -0
- package/bin/memory-closed-loop.mjs +119 -0
- package/bin/memory-dry-run.mjs +70 -0
- package/bin/memory-eval.mjs +104 -0
- package/bin/memory-event-alias-test.mjs +89 -0
- package/bin/memory-extraction-bench.mjs +58 -0
- package/bin/memory-load-gate-replay.mjs +137 -0
- package/bin/memory-loop-test.mjs +90 -0
- package/bin/memory-pre-turn-harness.mjs +63 -0
- package/bin/memory-query.mjs +39 -0
- package/bin/memory-replay-hints.mjs +84 -0
- package/bin/memory-scale.mjs +58 -0
- package/bin/memory-scenario-test.mjs +146 -0
- package/bin/memory-user-loop.mjs +95 -0
- package/bin/ner_bench_tiny_zh.py +59 -0
- package/bin/ner_bench_zh.py +60 -0
- package/bin/ner_conll_en.py +139 -0
- package/bin/ner_direct_en.py +123 -0
- package/bin/ner_direct_zh.py +100 -0
- package/bin/ner_pipeline.py +208 -0
- package/bin/noise-selfcheck.mjs +50 -0
- package/bin/pack-integrity.mjs +47 -0
- package/bin/panel-check.mjs +57 -0
- package/bin/plugin-integrity.mjs +74 -0
- package/bin/preview-live.mjs +13 -0
- package/bin/public-install-check.mjs +222 -0
- package/bin/relabel-apply.mjs +272 -0
- package/bin/relabel-dry-run.mjs +381 -0
- package/bin/relik_pipeline.py +39 -0
- package/bin/relik_service.py +52 -0
- package/bin/srl_pipeline.py +55 -0
- package/bin/srl_service.py +68 -0
- package/bin/summary-index.mjs +151 -0
- package/bin/summary-query.mjs +79 -0
- package/bin/vec-check.mjs +27 -0
- package/bin/zh_ner_error_audit.py +36 -0
- package/bin/zh_ner_slot_eval.py +38 -0
- package/client.js +760 -0
- package/cordis.patch.yml +378 -0
- package/docs/API-NOTES.md +1743 -0
- package/docs/ARCHITECTURE.md +355 -0
- package/docs/CACHE.md +253 -0
- package/docs/CONDUCT.md +30 -0
- package/docs/CREATOR-HANDOFF.md +102 -0
- package/docs/EXPERIMENT.md +136 -0
- package/docs/FACET-MVP-REPORT.md +473 -0
- package/docs/GUIDE-0.4.66.md +195 -0
- package/docs/GUIDE.md +127 -0
- package/docs/MECHANISM.md +243 -0
- package/docs/NEW-INSTANCE-HANDOFF.md +219 -0
- package/docs/README.md +37 -0
- package/docs/SESSION-LAYOUT.md +151 -0
- package/docs/history-map.md +249 -0
- package/docs/relabel-migration-wiring.md +366 -0
- package/guides/00-READ-FIRST.md +206 -0
- package/guides/01-INSTALL.md +207 -0
- package/guides/02-TABLES.md +225 -0
- package/guides/03-AUDIT.md +201 -0
- package/guides/04-REBUILD.md +306 -0
- package/guides/05-FIRST-RUN.md +191 -0
- package/guides/06-EMBEDDING.md +146 -0
- package/guides/07-PTC-AND-ASSETS.md +221 -0
- package/icon.svg +7 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/models/README.md +13 -0
- package/models/Xenova/bge-small-zh-v1.5/config.json +31 -0
- package/models/Xenova/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
- package/models/Xenova/bge-small-zh-v1.5/tokenizer.json +21278 -0
- package/models/Xenova/bge-small-zh-v1.5/tokenizer_config.json +15 -0
- package/package.json +139 -4
- package/src/asset-tool.mjs +237 -0
- package/src/assets.mjs +244 -0
- package/src/backfill.mjs +74 -0
- package/src/backup.mjs +80 -0
- package/src/deadend.mjs +95 -0
- package/src/deleted-store.mjs +35 -0
- package/src/engine.mjs +3831 -0
- package/src/facets/classifier.mjs +146 -0
- package/src/facets/embedding-service.mjs +53 -0
- package/src/facets/embedding-worker.mjs +27 -0
- package/src/facets/embedding.mjs +163 -0
- package/src/facets/extract.mjs +80 -0
- package/src/facets/fold-plan.mjs +84 -0
- package/src/facets/hybrid.mjs +29 -0
- package/src/facets/index.mjs +113 -0
- package/src/facets/linear-probe.mjs +43 -0
- package/src/facets/load.mjs +37 -0
- package/src/facets/mlp-probe.mjs +54 -0
- package/src/facets/mlp-service.mjs +53 -0
- package/src/facets/mlp-worker.mjs +26 -0
- package/src/facets/online-knn.mjs +58 -0
- package/src/facets/online-models.mjs +227 -0
- package/src/facets/panel-state.mjs +23 -0
- package/src/facets/panel.mjs +73 -0
- package/src/facets/random.mjs +26 -0
- package/src/facets/scanner.mjs +35 -0
- package/src/facets/schema.mjs +75 -0
- package/src/facets/seed-examples.mjs +58 -0
- package/src/facets/sensitivity.mjs +98 -0
- package/src/facets/service.mjs +53 -0
- package/src/facets/store.mjs +39 -0
- package/src/facets/worker.mjs +23 -0
- package/src/folds-store.mjs +109 -0
- package/src/groups.mjs +167 -0
- package/src/handoff-tool.mjs +51 -0
- package/src/handoff.mjs +127 -0
- package/src/index.mjs +649 -0
- package/src/inject-policy.mjs +17 -0
- package/src/learning.mjs +88 -0
- package/src/memory/alias-proposal.mjs +40 -0
- package/src/memory/bc-audit.mjs +245 -0
- package/src/memory/bc-index-updater.mjs +404 -0
- package/src/memory/bc-index.mjs +48 -0
- package/src/memory/bc-label.mjs +220 -0
- package/src/memory/bc-panel-state.mjs +107 -0
- package/src/memory/bc-pre-step.mjs +463 -0
- package/src/memory/bc-recall.mjs +470 -0
- package/src/memory/bc-shift.mjs +123 -0
- package/src/memory/bc-store.mjs +41 -0
- package/src/memory/bm25.mjs +112 -0
- package/src/memory/calibration.mjs +54 -0
- package/src/memory/chatter.mjs +29 -0
- package/src/memory/cmap-file.mjs +107 -0
- package/src/memory/concepts-file.mjs +41 -0
- package/src/memory/coref-slots.mjs +40 -0
- package/src/memory/event-alias-ledger.mjs +87 -0
- package/src/memory/event-log.mjs +95 -0
- package/src/memory/event-model.mjs +132 -0
- package/src/memory/event-segmenter.mjs +36 -0
- package/src/memory/event-summary-tree.mjs +122 -0
- package/src/memory/extraction-adapter.mjs +76 -0
- package/src/memory/extraction-composite.mjs +75 -0
- package/src/memory/extraction-core.mjs +76 -0
- package/src/memory/extraction-pre-step.mjs +161 -0
- package/src/memory/extraction-provider.mjs +300 -0
- package/src/memory/extraction-score.mjs +36 -0
- package/src/memory/facet-stabilizer.mjs +59 -0
- package/src/memory/history-map.mjs +139 -0
- package/src/memory/language-router.mjs +30 -0
- package/src/memory/load-gate.mjs +161 -0
- package/src/memory/mechanical-note.mjs +104 -0
- package/src/memory/onboarding.mjs +163 -0
- package/src/memory/persona.mjs +78 -0
- package/src/memory/planner.mjs +249 -0
- package/src/memory/pre-turn.mjs +80 -0
- package/src/memory/query-aliases.mjs +39 -0
- package/src/memory/recall-cue.mjs +43 -0
- package/src/memory/recall.mjs +65 -0
- package/src/memory/referent-candidates.mjs +55 -0
- package/src/memory/referent-ledger.mjs +86 -0
- package/src/memory/relabel-migrate.mjs +376 -0
- package/src/memory/relik-adapter.mjs +25 -0
- package/src/memory/special-keywords.mjs +60 -0
- package/src/memory/structured-adapters.mjs +75 -0
- package/src/memory/summary-index.mjs +335 -0
- package/src/memory/summary-tree.mjs +110 -0
- package/src/memory/term-extractor.mjs +44 -0
- package/src/memory/tokenize.mjs +9 -0
- package/src/memory/vector-index.mjs +54 -0
- package/src/memory/window-summary.mjs +133 -0
- package/src/mood.mjs +133 -0
- package/src/peer-tool.mjs +63 -0
- package/src/peer.mjs +144 -0
- package/src/policy.mjs +170 -0
- package/src/preflight.mjs +113 -0
- package/src/provenance.mjs +94 -0
- package/src/range.mjs +75 -0
- package/src/recall-exact-tool.mjs +267 -0
- package/src/recall-tool.mjs +521 -0
- package/src/recheck.mjs +99 -0
- package/src/save-start.mjs +29 -0
- package/src/session-guard.mjs +140 -0
- package/src/session-log-tool.mjs +322 -0
- package/src/session-log.mjs +606 -0
- package/src/skills.mjs +111 -0
- package/src/split-range.mjs +115 -0
- package/src/state.mjs +198 -0
- package/src/t0.mjs +149 -0
- package/src/tag-confirm.mjs +78 -0
- package/src/tag-detect.mjs +42 -0
- package/src/tag-maintenance.mjs +285 -0
- package/src/tag-reject.mjs +73 -0
- package/src/tags-store.mjs +94 -0
- package/src/tags.mjs +692 -0
- package/src/threads-backend.mjs +26 -0
- package/src/threads-core.mjs +1887 -0
- package/src/threads-store.mjs +207 -0
- package/src/threads-tool.mjs +522 -0
- package/src/threads.mjs +929 -0
- package/src/tool-preview.mjs +427 -0
- package/src/topic.mjs +63 -0
- package/src/training.mjs +75 -0
- package/src/turn-tag.mjs +62 -0
- package/src/user-turn.mjs +39 -0
- package/templates/cmap.md +11 -0
- package/templates/concepts.md +12 -0
- package/templates/habits.md +40 -0
- package/templates/keywords.md +6 -0
- package/templates/rebuild.md +134 -0
- package/templates/rules-safe.md +184 -0
- package/templates/rules.md +177 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventModel: stable situation slots and boundary strength.
|
|
3
|
+
*
|
|
4
|
+
* Unlike facets, these slots describe the current situation. A topic change
|
|
5
|
+
* alone is not a boundary; location/characters/goal changes are stronger.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const WEIGHTS = Object.freeze({
|
|
9
|
+
location: 0.35,
|
|
10
|
+
characters: 0.30,
|
|
11
|
+
goal: 0.25,
|
|
12
|
+
objects: 0.10,
|
|
13
|
+
activity: 0.10,
|
|
14
|
+
topic: 0,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const LOCATION_STOP = new Set(['这里', '那里', '现在', '刚才', '地方', '时候', '什么', '一个']);
|
|
18
|
+
const LOCATION_TERMS = ['灯室', '钟楼', '矿口', '石屋', '旧镇', '库房', '码头', '地窖', '院子', '木棚', '悬崖'];
|
|
19
|
+
|
|
20
|
+
export function validateEventSlots(input = {}) {
|
|
21
|
+
const errors = [];
|
|
22
|
+
const allowed = new Set(['location', 'characters', 'goal', 'objects', 'activity', 'topic']);
|
|
23
|
+
for (const key of Object.keys(input)) {
|
|
24
|
+
if (!allowed.has(key)) errors.push({ key, reason: 'unknown-slot' });
|
|
25
|
+
}
|
|
26
|
+
for (const key of ['location', 'goal', 'activity', 'topic']) {
|
|
27
|
+
const value = input[key];
|
|
28
|
+
if (value !== undefined && value !== null && typeof value !== 'string') {
|
|
29
|
+
errors.push({ key, reason: 'not-a-string' });
|
|
30
|
+
} else if (typeof value === 'string' && value.length > 40) {
|
|
31
|
+
errors.push({ key, reason: 'too-long' });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const key of ['characters', 'objects']) {
|
|
35
|
+
const value = input[key];
|
|
36
|
+
if (value !== undefined && value !== null && !Array.isArray(value)) {
|
|
37
|
+
errors.push({ key, reason: 'not-an-array' });
|
|
38
|
+
} else if (Array.isArray(value)) {
|
|
39
|
+
if (value.length > 20) errors.push({ key, reason: 'too-many' });
|
|
40
|
+
for (const item of value) {
|
|
41
|
+
if (typeof item !== 'string') errors.push({ key, reason: 'item-not-string' });
|
|
42
|
+
else if (item.length > 40) errors.push({ key, reason: 'item-too-long' });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { ok: errors.length === 0, errors, slots: normalizeEventSlots(input) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function normalizeEventSlots(input = {}) {
|
|
50
|
+
const array = (value) => (Array.isArray(value) ? value : value === undefined || value === null ? [] : [value])
|
|
51
|
+
.map((item) => String(item).trim())
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
const scalar = (value) => {
|
|
54
|
+
const text = value === undefined || value === null ? '' : String(value).trim();
|
|
55
|
+
return text.length === 0 ? null : text;
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
location: scalar(input.location),
|
|
59
|
+
characters: [...new Set(array(input.characters))],
|
|
60
|
+
goal: scalar(input.goal),
|
|
61
|
+
objects: [...new Set(array(input.objects))],
|
|
62
|
+
activity: scalar(input.activity),
|
|
63
|
+
topic: scalar(input.topic),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scalarChanged(a, b) {
|
|
68
|
+
if (a === null || b === null) return 0;
|
|
69
|
+
return a === b ? 0 : 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function setChanged(a, b) {
|
|
73
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
74
|
+
const left = new Set(a);
|
|
75
|
+
const right = new Set(b);
|
|
76
|
+
let same = 0;
|
|
77
|
+
for (const item of left) if (right.has(item)) same += 1;
|
|
78
|
+
const union = new Set([...left, ...right]).size;
|
|
79
|
+
return union === 0 ? 0 : 1 - same / union;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function boundaryScore(previous, next) {
|
|
83
|
+
if (previous === null || previous === undefined) return 1;
|
|
84
|
+
const a = normalizeEventSlots(previous);
|
|
85
|
+
const b = normalizeEventSlots(next);
|
|
86
|
+
const changed = {
|
|
87
|
+
location: scalarChanged(a.location, b.location),
|
|
88
|
+
characters: setChanged(a.characters, b.characters),
|
|
89
|
+
goal: scalarChanged(a.goal, b.goal),
|
|
90
|
+
objects: setChanged(a.objects, b.objects),
|
|
91
|
+
activity: scalarChanged(a.activity, b.activity),
|
|
92
|
+
topic: scalarChanged(a.topic, b.topic),
|
|
93
|
+
};
|
|
94
|
+
const score = Object.entries(WEIGHTS).reduce((sum, [slot, weight]) => sum + weight * changed[slot], 0);
|
|
95
|
+
return { score: +score.toFixed(4), changed };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class EventModelState {
|
|
99
|
+
constructor({ eventId = 0, slots = null, threshold = 0.35 } = {}) {
|
|
100
|
+
this.eventId = eventId;
|
|
101
|
+
this.slots = slots === null ? null : normalizeEventSlots(slots);
|
|
102
|
+
this.threshold = threshold;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
observe({ slots, explicitBoundary = false } = {}) {
|
|
106
|
+
const checked = validateEventSlots(slots);
|
|
107
|
+
if (!checked.ok) {
|
|
108
|
+
return { eventId: this.eventId, boundary: false, score: 0, error: 'invalid-slots', errors: checked.errors, slots: this.slots };
|
|
109
|
+
}
|
|
110
|
+
const next = checked.slots;
|
|
111
|
+
const result = boundaryScore(this.slots, next);
|
|
112
|
+
const boundary = explicitBoundary || this.slots === null || result.score >= this.threshold;
|
|
113
|
+
if (boundary && this.slots !== null) this.eventId += 1;
|
|
114
|
+
this.slots = next;
|
|
115
|
+
return {
|
|
116
|
+
eventId: this.eventId,
|
|
117
|
+
boundary,
|
|
118
|
+
score: this.slots === null ? 1 : result.score,
|
|
119
|
+
changed: this.slots === null ? {} : result.changed,
|
|
120
|
+
slots: next,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function extractEventSlots(text, { characters = [] } = {}) {
|
|
126
|
+
const source = String(text ?? '');
|
|
127
|
+
const location = [...LOCATION_TERMS].sort((a, b) => b.length - a.length).find((term) => source.includes(term)) ?? null;
|
|
128
|
+
const foundCharacters = characters.filter((name) => name.length > 0 && source.includes(name));
|
|
129
|
+
const objects = ['钥匙', '账册', '信', '名册', '收条', '罐子', '灯', '门', '钟', '船', '布标']
|
|
130
|
+
.filter((object) => source.includes(object));
|
|
131
|
+
return normalizeEventSlots({ location, characters: foundCharacters, objects });
|
|
132
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event segmentation for alias formation.
|
|
3
|
+
* A boundary is opened when activity or topic changes, or when the caller
|
|
4
|
+
* supplies an explicit boundary. Event IDs are stable for the session.
|
|
5
|
+
*/
|
|
6
|
+
export class EventSegmenter {
|
|
7
|
+
constructor({ eventId = 0, lastFacets = null } = {}) {
|
|
8
|
+
this.eventId = eventId;
|
|
9
|
+
this.lastFacets = lastFacets;
|
|
10
|
+
this.boundaries = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
observe({ facets = {}, nowTurn = 0, explicitBoundary = false, reason = null } = {}) {
|
|
14
|
+
const changed = this.lastFacets !== null
|
|
15
|
+
&& (this.lastFacets.activity !== facets.activity || this.lastFacets.topic !== facets.topic);
|
|
16
|
+
const boundary = this.lastFacets === null || changed || explicitBoundary;
|
|
17
|
+
if (boundary && this.lastFacets !== null) {
|
|
18
|
+
this.eventId += 1;
|
|
19
|
+
this.boundaries.push({ eventId: this.eventId, nowTurn, reason: reason ?? (changed ? 'facet-change' : 'explicit') });
|
|
20
|
+
}
|
|
21
|
+
this.lastFacets = { ...facets };
|
|
22
|
+
return { eventId: this.eventId, boundary, reason: boundary ? reason ?? 'start' : null };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function segmentEvents(turns) {
|
|
27
|
+
const segmenter = new EventSegmenter();
|
|
28
|
+
return turns.map((turn, index) => {
|
|
29
|
+
const result = segmenter.observe({
|
|
30
|
+
facets: turn.facets,
|
|
31
|
+
nowTurn: turn.turn ?? index,
|
|
32
|
+
explicitBoundary: turn.boundary === true,
|
|
33
|
+
});
|
|
34
|
+
return { ...turn, eventId: result.eventId, boundary: result.boundary };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-aligned summary tree.
|
|
3
|
+
*
|
|
4
|
+
* Leaves are turns. Segment nodes are event groups, never fixed-size windows.
|
|
5
|
+
* Chapters group consecutive event segments. Extractive and deterministic.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
function sha256(text) {
|
|
10
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function clip(text, max) {
|
|
14
|
+
const clean = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
15
|
+
return clean.length <= max ? clean : `${clean.slice(0, max)}…`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function nodeId(level, index) {
|
|
19
|
+
return `${level}-${String(index + 1).padStart(5, '0')}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildEventSummaryTree(records, { eventIds = null, chapterSize = 8 } = {}) {
|
|
23
|
+
const ordered = [...records].sort((a, b) => a.turn - b.turn);
|
|
24
|
+
const eventOf = (record, index) => String(eventIds?.[record.turn] ?? eventIds?.[index] ?? `turn:${record.turn}`);
|
|
25
|
+
|
|
26
|
+
const leaves = ordered.map((record, index) => ({
|
|
27
|
+
id: nodeId('turn', index),
|
|
28
|
+
level: 'turn',
|
|
29
|
+
parentId: null,
|
|
30
|
+
childIds: [],
|
|
31
|
+
turn: record.turn,
|
|
32
|
+
eventId: eventOf(record, index),
|
|
33
|
+
seqStart: record.seqStart,
|
|
34
|
+
seqEnd: record.seqEnd,
|
|
35
|
+
summary: clip(record.text, 280),
|
|
36
|
+
sourceHash: record.hash,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const groups = [];
|
|
40
|
+
for (const leaf of leaves) {
|
|
41
|
+
const last = groups[groups.length - 1];
|
|
42
|
+
if (last !== undefined && last.eventId === leaf.eventId) last.leaves.push(leaf);
|
|
43
|
+
else groups.push({ eventId: leaf.eventId, leaves: [leaf] });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const segments = groups.map((group, index) => {
|
|
47
|
+
const id = nodeId('segment', index);
|
|
48
|
+
const children = group.leaves;
|
|
49
|
+
const node = {
|
|
50
|
+
id,
|
|
51
|
+
level: 'segment',
|
|
52
|
+
parentId: null,
|
|
53
|
+
childIds: children.map((child) => child.id),
|
|
54
|
+
eventId: group.eventId,
|
|
55
|
+
seqStart: children[0].seqStart,
|
|
56
|
+
seqEnd: children[children.length - 1].seqEnd,
|
|
57
|
+
firstTurn: children[0].turn,
|
|
58
|
+
lastTurn: children[children.length - 1].turn,
|
|
59
|
+
summary: clip(children.map((child) => child.summary).join(' / '), 700),
|
|
60
|
+
sourceHash: sha256(children.map((child) => child.sourceHash).join('')),
|
|
61
|
+
};
|
|
62
|
+
for (const child of children) child.parentId = id;
|
|
63
|
+
return node;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const chapters = [];
|
|
67
|
+
for (let start = 0; start < segments.length; start += chapterSize) {
|
|
68
|
+
const children = segments.slice(start, start + chapterSize);
|
|
69
|
+
const id = nodeId('chapter', chapters.length);
|
|
70
|
+
const node = {
|
|
71
|
+
id,
|
|
72
|
+
level: 'chapter',
|
|
73
|
+
parentId: null,
|
|
74
|
+
childIds: children.map((child) => child.id),
|
|
75
|
+
eventIds: children.map((child) => child.eventId),
|
|
76
|
+
seqStart: children[0].seqStart,
|
|
77
|
+
seqEnd: children[children.length - 1].seqEnd,
|
|
78
|
+
firstTurn: children[0].firstTurn,
|
|
79
|
+
lastTurn: children[children.length - 1].lastTurn,
|
|
80
|
+
summary: clip(children.map((child) => child.summary).join(' / '), 1200),
|
|
81
|
+
sourceHash: sha256(children.map((child) => child.sourceHash).join('')),
|
|
82
|
+
};
|
|
83
|
+
for (const child of children) child.parentId = id;
|
|
84
|
+
chapters.push(node);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
schema: 1,
|
|
89
|
+
version: 1,
|
|
90
|
+
mode: 'event-aligned',
|
|
91
|
+
chapterSize,
|
|
92
|
+
nodeCount: leaves.length + segments.length + chapters.length,
|
|
93
|
+
levels: { turn: leaves.length, segment: segments.length, chapter: chapters.length },
|
|
94
|
+
treeRootHash: sha256([...leaves, ...segments, ...chapters].map((node) => node.sourceHash).join('')),
|
|
95
|
+
nodes: [...leaves, ...segments, ...chapters],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function verifyEventSummaryTree(tree) {
|
|
100
|
+
const byId = new Map(tree.nodes.map((node) => [node.id, node]));
|
|
101
|
+
const errors = [];
|
|
102
|
+
for (const node of tree.nodes) {
|
|
103
|
+
for (const childId of node.childIds) {
|
|
104
|
+
const child = byId.get(childId);
|
|
105
|
+
if (child === undefined) {
|
|
106
|
+
errors.push({ id: node.id, reason: 'missing-child', childId });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (child.parentId !== node.id) errors.push({ id: child.id, reason: 'parent-mismatch' });
|
|
110
|
+
if (child.seqStart < node.seqStart || child.seqEnd > node.seqEnd) {
|
|
111
|
+
errors.push({ id: child.id, reason: 'child-outside-parent' });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const segment of tree.nodes.filter((node) => node.level === 'segment')) {
|
|
116
|
+
const childEvents = new Set(segment.childIds.map((childId) => byId.get(childId)?.eventId));
|
|
117
|
+
if (childEvents.size !== 1 || !childEvents.has(segment.eventId)) {
|
|
118
|
+
errors.push({ id: segment.id, reason: 'segment-crosses-event' });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { ok: errors.length === 0, errors };
|
|
122
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter from extractor output to EventModel slots and the pre-turn gate.
|
|
3
|
+
* Extractor output is validated before it can affect state.
|
|
4
|
+
*/
|
|
5
|
+
import { mapEntitiesToSlots } from './extraction-core.mjs';
|
|
6
|
+
import { validateEventSlots } from './event-model.mjs';
|
|
7
|
+
import { applyCorefToSlots } from './coref-slots.mjs';
|
|
8
|
+
import { srlToSlots, normalizeEntityLinks } from './structured-adapters.mjs';
|
|
9
|
+
|
|
10
|
+
export function attachExtraction(controller, { text, role = 'user', entities = [], slots = null, facets = {}, confidence = 1, nowTurn = 0, corefClusters = [] }) {
|
|
11
|
+
const mapped = slots ?? mapEntitiesToSlots(entities);
|
|
12
|
+
const withCoref = applyCorefToSlots(mapped, corefClusters);
|
|
13
|
+
const checked = validateEventSlots(withCoref.slots);
|
|
14
|
+
if (!checked.ok) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
reason: 'invalid-slots',
|
|
18
|
+
errors: checked.errors,
|
|
19
|
+
entities,
|
|
20
|
+
slots: null,
|
|
21
|
+
gate: null,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const gate = controller.step({
|
|
25
|
+
text,
|
|
26
|
+
role,
|
|
27
|
+
facets,
|
|
28
|
+
confidence,
|
|
29
|
+
eventSlots: checked.slots,
|
|
30
|
+
nowTurn,
|
|
31
|
+
});
|
|
32
|
+
return { ok: true, entities, slots: checked.slots, referents: withCoref.referents, gate };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function attachStructuredExtraction(controller, {
|
|
36
|
+
text,
|
|
37
|
+
role = 'user',
|
|
38
|
+
facets = {},
|
|
39
|
+
confidence = 1,
|
|
40
|
+
nowTurn = 0,
|
|
41
|
+
slots = null,
|
|
42
|
+
srl = null,
|
|
43
|
+
corefClusters = [],
|
|
44
|
+
entityLinks = [],
|
|
45
|
+
}) {
|
|
46
|
+
const fromSrl = srl ? srlToSlots(srl) : null;
|
|
47
|
+
const withCoref = applyCorefToSlots(slots ?? fromSrl?.slots ?? {}, corefClusters);
|
|
48
|
+
const checked = validateEventSlots(withCoref.slots);
|
|
49
|
+
if (!checked.ok) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: 'invalid-slots',
|
|
53
|
+
errors: checked.errors,
|
|
54
|
+
slots: null,
|
|
55
|
+
referents: withCoref.referents,
|
|
56
|
+
entityLinks: normalizeEntityLinks(entityLinks),
|
|
57
|
+
gate: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const gate = controller.step({
|
|
61
|
+
text,
|
|
62
|
+
role,
|
|
63
|
+
facets,
|
|
64
|
+
confidence,
|
|
65
|
+
eventSlots: checked.slots,
|
|
66
|
+
nowTurn,
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
predicate: fromSrl?.predicate ?? null,
|
|
71
|
+
slots: checked.slots,
|
|
72
|
+
referents: withCoref.referents,
|
|
73
|
+
entityLinks: normalizeEntityLinks(entityLinks),
|
|
74
|
+
gate,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composite provider: NER is required; coref, SRL, and EL are optional.
|
|
3
|
+
* Optional failures are recorded but do not discard NER slots.
|
|
4
|
+
*/
|
|
5
|
+
function mergeSlots(base = {}, extra = {}) {
|
|
6
|
+
const slots = {
|
|
7
|
+
characters: [...(base.characters ?? [])],
|
|
8
|
+
location: base.location ?? null,
|
|
9
|
+
goal: base.goal ?? null,
|
|
10
|
+
objects: [...(base.objects ?? [])],
|
|
11
|
+
};
|
|
12
|
+
for (const name of extra.characters ?? []) {
|
|
13
|
+
if (!slots.characters.includes(name)) slots.characters.push(name);
|
|
14
|
+
}
|
|
15
|
+
for (const item of extra.objects ?? []) {
|
|
16
|
+
if (!slots.objects.includes(item)) slots.objects.push(item);
|
|
17
|
+
}
|
|
18
|
+
slots.location ??= extra.location ?? null;
|
|
19
|
+
slots.goal ??= extra.goal ?? null;
|
|
20
|
+
return slots;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function candidatesFrom(data) {
|
|
24
|
+
const names = [
|
|
25
|
+
...(data?.slots?.characters ?? []),
|
|
26
|
+
...(data?.slots?.objects ?? []),
|
|
27
|
+
...(data?.entities ?? []).map((entity) => entity.text),
|
|
28
|
+
];
|
|
29
|
+
return [...new Set(names.filter((item) => typeof item === 'string' && item.trim()))];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createCompositeProvider({ ner, coref = null, srl = null, el = null, candidates = candidatesFrom } = {}) {
|
|
33
|
+
if (!ner) throw new TypeError('ner provider is required');
|
|
34
|
+
return {
|
|
35
|
+
mode: 'composite',
|
|
36
|
+
close() {
|
|
37
|
+
for (const provider of [ner, coref, srl, el]) provider?.close?.();
|
|
38
|
+
},
|
|
39
|
+
async extract({ text } = {}) {
|
|
40
|
+
const started = Date.now();
|
|
41
|
+
const nerResult = await ner.extract({ text });
|
|
42
|
+
if (!nerResult.ok) return { ...nerResult, source: 'composite', latencyMs: Date.now() - started };
|
|
43
|
+
const parts = { ner: nerResult };
|
|
44
|
+
const optional = {};
|
|
45
|
+
if (coref) optional.coref = await coref.extract({ text });
|
|
46
|
+
if (srl) optional.srl = await srl.extract({ text });
|
|
47
|
+
if (el) optional.el = await el.extract({ text, candidates: candidates(nerResult.data) });
|
|
48
|
+
|
|
49
|
+
let slots = { ...(nerResult.data?.slots ?? {}) };
|
|
50
|
+
let srlData = null;
|
|
51
|
+
if (optional.srl?.ok) {
|
|
52
|
+
srlData = optional.srl.data?.srl ?? optional.srl.data;
|
|
53
|
+
const { srlToSlots } = await import('./structured-adapters.mjs');
|
|
54
|
+
slots = mergeSlots(slots, srlToSlots(srlData).slots);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
source: 'composite',
|
|
59
|
+
latencyMs: Date.now() - started,
|
|
60
|
+
data: {
|
|
61
|
+
...nerResult.data,
|
|
62
|
+
slots,
|
|
63
|
+
srl: srlData,
|
|
64
|
+
corefClusters: optional.coref?.ok ? (optional.coref.data?.clusters ?? []) : [],
|
|
65
|
+
entityLinks: optional.el?.ok ? (optional.el.data?.links ?? []) : [],
|
|
66
|
+
partial: {
|
|
67
|
+
coref: optional.coref ? { ok: optional.coref.ok, reason: optional.coref.reason ?? null, stderr: optional.coref.stderr ?? null } : null,
|
|
68
|
+
srl: optional.srl ? { ok: optional.srl.ok, reason: optional.srl.reason ?? null, stderr: optional.srl.stderr ?? null } : null,
|
|
69
|
+
el: optional.el ? { ok: optional.el.ok, reason: optional.el.reason ?? null, stderr: optional.el.stderr ?? null } : null,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extraction core: route language, call a provider, validate entities and map
|
|
3
|
+
* them to EventModel slots. Providers are replaceable (HanLP, spaCy, XLM-R...).
|
|
4
|
+
*/
|
|
5
|
+
import { routeLanguage } from './language-router.mjs';
|
|
6
|
+
import { validateEventSlots } from './event-model.mjs';
|
|
7
|
+
|
|
8
|
+
const SLOT_BY_LABEL = Object.freeze({
|
|
9
|
+
PERSON: 'characters',
|
|
10
|
+
PER: 'characters',
|
|
11
|
+
NAME: 'characters',
|
|
12
|
+
LOC: 'location',
|
|
13
|
+
LOCATION: 'location',
|
|
14
|
+
GPE: 'location',
|
|
15
|
+
FAC: 'location',
|
|
16
|
+
ADDRESS: 'location',
|
|
17
|
+
SCENE: 'location',
|
|
18
|
+
OBJECT: 'objects',
|
|
19
|
+
MISC: 'objects',
|
|
20
|
+
PROPER: 'objects',
|
|
21
|
+
ORG: 'objects',
|
|
22
|
+
COMPANY: 'objects',
|
|
23
|
+
ORGANIZATION: 'objects',
|
|
24
|
+
GOVERNMENT: 'objects',
|
|
25
|
+
BOOK: 'objects',
|
|
26
|
+
MOVIE: 'objects',
|
|
27
|
+
GAME: 'objects',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function validEntity(entity) {
|
|
31
|
+
return entity !== null
|
|
32
|
+
&& typeof entity === 'object'
|
|
33
|
+
&& typeof entity.text === 'string'
|
|
34
|
+
&& entity.text.length > 0
|
|
35
|
+
&& typeof entity.label === 'string'
|
|
36
|
+
&& (entity.start === undefined || Number.isInteger(entity.start))
|
|
37
|
+
&& (entity.end === undefined || Number.isInteger(entity.end));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function mapEntitiesToSlots(entities) {
|
|
41
|
+
const slots = { characters: [], location: null, objects: [] };
|
|
42
|
+
for (const entity of entities) {
|
|
43
|
+
const slot = SLOT_BY_LABEL[entity.label.toUpperCase()];
|
|
44
|
+
if (slot === 'characters') slots.characters.push(entity.text);
|
|
45
|
+
else if (slot === 'location' && slots.location === null) slots.location = entity.text;
|
|
46
|
+
else if (slot === 'objects') slots.objects.push(entity.text);
|
|
47
|
+
}
|
|
48
|
+
slots.characters = [...new Set(slots.characters)];
|
|
49
|
+
slots.objects = [...new Set(slots.objects)];
|
|
50
|
+
return slots;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createExtractionCore({ providers = {}, fallback = null } = {}) {
|
|
54
|
+
return {
|
|
55
|
+
extract(text) {
|
|
56
|
+
const route = routeLanguage(text);
|
|
57
|
+
const provider = providers[route.language] ?? providers.mixed ?? fallback;
|
|
58
|
+
if (typeof provider !== 'function') {
|
|
59
|
+
return { ok: false, language: route.language, reason: 'no-provider', entities: [], slots: null };
|
|
60
|
+
}
|
|
61
|
+
let output;
|
|
62
|
+
try {
|
|
63
|
+
output = provider(text, route);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return { ok: false, language: route.language, reason: 'provider-error', error: String(error), entities: [], slots: null };
|
|
66
|
+
}
|
|
67
|
+
const entities = Array.isArray(output?.entities) ? output.entities.filter(validEntity) : [];
|
|
68
|
+
const mapped = output?.slots ?? mapEntitiesToSlots(entities);
|
|
69
|
+
const checked = validateEventSlots(mapped);
|
|
70
|
+
if (!checked.ok) {
|
|
71
|
+
return { ok: false, language: route.language, reason: 'invalid-slots', errors: checked.errors, entities, slots: null };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, language: route.language, route, entities, slots: checked.slots };
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-step hook prototype for extraction.
|
|
3
|
+
* Not registered by index.mjs yet; registration is a separate, explicit switch.
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { userMessage } from '../threads.mjs';
|
|
8
|
+
import { createExtractionProviderFromEnv } from './extraction-provider.mjs';
|
|
9
|
+
import { attachStructuredExtraction } from './extraction-adapter.mjs';
|
|
10
|
+
import { buildRecallIndex, findRecall, renderRecall } from './recall.mjs';
|
|
11
|
+
|
|
12
|
+
const MACHINE_PREFIX = /^(?:Current runtime context|Current working directory|Current DSH file policy|Approval prompts|Time sampled|\[(?:抽取|工作线|标签|规则|习惯|技能|归位|候选|存\?|闸|回正|学习|追忆|记忆|continuity:[a-z-]+)\])/;
|
|
13
|
+
|
|
14
|
+
function isHumanMessage(message, text) {
|
|
15
|
+
const source = message?.source ?? message?.data?.source;
|
|
16
|
+
if (source && typeof source.kind === 'string' && source.kind !== 'user') return false;
|
|
17
|
+
if (source && source.peer === true) return false;
|
|
18
|
+
if (MACHINE_PREFIX.test(text.trimStart())) return false;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function textOf(message) {
|
|
23
|
+
if (!message || message.role !== 'user') return '';
|
|
24
|
+
const content = message.content;
|
|
25
|
+
if (typeof content === 'string') return isHumanMessage(message, content) ? content : '';
|
|
26
|
+
if (!Array.isArray(content)) return '';
|
|
27
|
+
const text = content
|
|
28
|
+
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
29
|
+
.map((part) => part.text)
|
|
30
|
+
.join('\n')
|
|
31
|
+
.trim();
|
|
32
|
+
return isHumanMessage(message, text) ? text : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function lastUserText(messages = []) {
|
|
36
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
37
|
+
const text = textOf(messages[index]);
|
|
38
|
+
if (text) return text;
|
|
39
|
+
}
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createExtractionPreStep({ provider = null, mode = 'off', prefix = '[抽取]', controller = null, recall = null } = {}) {
|
|
44
|
+
return async function extractionPreStep(payload, next) {
|
|
45
|
+
const decision = await next();
|
|
46
|
+
if (mode === 'off' || !provider || !decision || !Array.isArray(decision.messages)) return decision;
|
|
47
|
+
if (Number.isInteger(payload?.step) && payload.step !== 1) return decision;
|
|
48
|
+
const text = lastUserText(decision.messages);
|
|
49
|
+
if (!text) return decision;
|
|
50
|
+
let recallLine = null;
|
|
51
|
+
if (typeof recall === 'function') {
|
|
52
|
+
try {
|
|
53
|
+
recallLine = await recall({ text, agent: payload?.agent ?? null });
|
|
54
|
+
} catch {
|
|
55
|
+
recallLine = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const extraction = await provider.extract({ text });
|
|
59
|
+
if (!extraction?.ok) {
|
|
60
|
+
if (!recallLine) return { ...decision, extraction };
|
|
61
|
+
return {
|
|
62
|
+
...decision,
|
|
63
|
+
messages: [...decision.messages, userMessage(recallLine)],
|
|
64
|
+
extraction,
|
|
65
|
+
recall: recallLine,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const data = extraction.data ?? {};
|
|
69
|
+
let attached = null;
|
|
70
|
+
if (controller) {
|
|
71
|
+
attached = attachStructuredExtraction(controller, {
|
|
72
|
+
text,
|
|
73
|
+
slots: data.slots ?? {},
|
|
74
|
+
srl: data.srl ?? null,
|
|
75
|
+
corefClusters: data.corefClusters ?? [],
|
|
76
|
+
entityLinks: data.entityLinks ?? [],
|
|
77
|
+
});
|
|
78
|
+
if (!attached.ok) return { ...decision, extraction, attached };
|
|
79
|
+
}
|
|
80
|
+
const slots = attached?.slots ?? data.slots ?? {};
|
|
81
|
+
const hasSignal = (slots.characters?.length ?? 0) > 0
|
|
82
|
+
|| Boolean(slots.location)
|
|
83
|
+
|| (slots.objects?.length ?? 0) > 0
|
|
84
|
+
|| Boolean(slots.goal)
|
|
85
|
+
|| Boolean(data.srl?.verb)
|
|
86
|
+
|| (data.entityLinks?.length ?? 0) > 0;
|
|
87
|
+
if (!hasSignal) {
|
|
88
|
+
if (!recallLine) return { ...decision, extraction, gate: attached?.gate ?? null };
|
|
89
|
+
return {
|
|
90
|
+
...decision,
|
|
91
|
+
messages: [...decision.messages, userMessage(recallLine)],
|
|
92
|
+
extraction,
|
|
93
|
+
recall: recallLine,
|
|
94
|
+
gate: attached?.gate ?? null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const lines = [
|
|
98
|
+
`${prefix} ${data.lang ?? ''}`.trim(),
|
|
99
|
+
`characters: ${(slots.characters ?? []).join(', ') || '-'}`,
|
|
100
|
+
`location: ${slots.location ?? '-'}`,
|
|
101
|
+
`objects: ${(slots.objects ?? []).join(', ') || '-'}`,
|
|
102
|
+
`goal: ${slots.goal ?? '-'}`,
|
|
103
|
+
];
|
|
104
|
+
if (data.srl?.verb) lines.push(`srl: ${data.srl.verb} (${data.srl.frame ?? '-'})`);
|
|
105
|
+
if (data.entityLinks?.length) lines.push(`links: ${data.entityLinks.map((link) => `${link.text}=${link.referent ?? link.entityId ?? link.id ?? 'NIL'}`).join(', ')}`);
|
|
106
|
+
if (recallLine) lines.push(recallLine);
|
|
107
|
+
return {
|
|
108
|
+
...decision,
|
|
109
|
+
messages: [...decision.messages, userMessage(lines.join('\n'))],
|
|
110
|
+
extraction,
|
|
111
|
+
recall: recallLine,
|
|
112
|
+
gate: attached?.gate ?? null,
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function registerExtractionPreStep(ctx, options = {}) {
|
|
118
|
+
if (options.mode === 'off' || !options.provider) return false;
|
|
119
|
+
if (!ctx || typeof ctx.on !== 'function') return false;
|
|
120
|
+
ctx.on('agent/pre-step', createExtractionPreStep(options));
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function installExtractionPreStep(ctx, env = process.env, { sessionQuery = null } = {}) {
|
|
125
|
+
const mode = String(env.DSH_EXTRACTION_MODE ?? 'off').toLowerCase();
|
|
126
|
+
if (mode !== 'cache' && mode !== 'model') return { mode, registered: false, provider: null };
|
|
127
|
+
try {
|
|
128
|
+
const provider = createExtractionProviderFromEnv(env);
|
|
129
|
+
const recall = sessionQuery && typeof sessionQuery.readSession === 'function'
|
|
130
|
+
? async ({ text, agent }) => {
|
|
131
|
+
const sessionId = agent?.session?.id;
|
|
132
|
+
if (sessionId === undefined || sessionId === null) return null;
|
|
133
|
+
const snapshot = await sessionQuery.readSession(sessionId);
|
|
134
|
+
const built = buildRecallIndex(snapshot?.events ?? [], {
|
|
135
|
+
beforeTurn: Number.isInteger(agent?.lastTurn) ? agent.lastTurn : Infinity,
|
|
136
|
+
});
|
|
137
|
+
const hits = findRecall(built, text, { topK: 1, minCoverage: 0.25 });
|
|
138
|
+
return hits.length > 0 ? renderRecall(hits[0], text) : null;
|
|
139
|
+
}
|
|
140
|
+
: null;
|
|
141
|
+
const registered = registerExtractionPreStep(ctx, { provider, mode, recall });
|
|
142
|
+
return { mode, registered, provider };
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return { mode, registered: false, provider: null, error: error?.message ?? String(error) };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function recordExtractionInstallError(error, { home = process.env.USERPROFILE ?? process.env.HOME ?? '', log = console } = {}) {
|
|
149
|
+
const message = String(error?.message ?? error);
|
|
150
|
+
try {
|
|
151
|
+
const file = join(home, '.dsh', 'continuity', 'gate-errors.jsonl');
|
|
152
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
153
|
+
appendFileSync(file, JSON.stringify({
|
|
154
|
+
at: new Date().toISOString(),
|
|
155
|
+
name: error?.name ?? 'Error',
|
|
156
|
+
code: 'extraction-pre-step-register',
|
|
157
|
+
message,
|
|
158
|
+
}) + '\n', 'utf8');
|
|
159
|
+
} catch { /* best effort, same as the install catch */ }
|
|
160
|
+
log?.warn?.(`[dsh-continuity] extraction pre-step failed: ${message}`);
|
|
161
|
+
}
|