dsh-plugin-soulbridge 0.0.113 → 0.0.115
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 +11 -2
- package/index.js +78 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -107,8 +107,17 @@ DSH 启动器对 `package.json` 直接 `JSON.parse`,不剥 BOM。用 Windows P
|
|
|
107
107
|
|
|
108
108
|
计数来源是宿主的事件流:`step/start`(一次调用)、`assistant/attempt`(重试且没出消息)、
|
|
109
109
|
`assistant/message.usage`(token)。子代理跑在自己的会话里,因此各自一条记录——事件流
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
里没有父子关系,猜一个比写两条更坏。
|
|
111
|
+
|
|
112
|
+
插件**不算钱**(价目有峰谷两档且会变)。账单是另一件事,也只属于宿主侧的运维:
|
|
113
|
+
|
|
114
|
+
```powershell
|
|
115
|
+
node scripts/billing.mjs --days 4 # 读平台导出,按天给金额/请求/三类 token 并复核单价
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
判读与限制见 [docs/cost-visibility.md](docs/cost-visibility.md)。这两样都**不属于 soulbridge**:
|
|
119
|
+
记忆层存"发生了什么",不记"值多少钱"。
|
|
120
|
+
|
|
112
121
|
## 升级提示:新增工具需要新开会话
|
|
113
122
|
|
|
114
123
|
`ctx.inject(['tools'])` 注册的工具在**会话创建时**进入该会话的工具集。插件升级后
|
package/index.js
CHANGED
|
@@ -663,7 +663,7 @@ export function buildFactHooks(text, options = {}) {
|
|
|
663
663
|
for (const fact of [...earliest, ...recent]) {
|
|
664
664
|
if (seen.has(fact.id)) continue;
|
|
665
665
|
seen.add(fact.id);
|
|
666
|
-
const when = fact.
|
|
666
|
+
const when = fact.validFromText ?? (fact.validFrom ? new Date(fact.validFrom).toISOString().slice(0, 10) : '');
|
|
667
667
|
// ADR 0035: a fact from my side of the exchange is inferred. Two characters plus one
|
|
668
668
|
// explanation below the block, because a per-line sentence would double the text.
|
|
669
669
|
const marker = fact.source === 'inferred' ? INFERRED_MARKER : '';
|
|
@@ -694,11 +694,16 @@ export function buildFactHooks(text, options = {}) {
|
|
|
694
694
|
* When the question names a subject, the entity timeline is the answer and the
|
|
695
695
|
* related-pairs search is the contamination: it is how "上次和 a 吃饭" ends up
|
|
696
696
|
* answered with something about b. So the plan chooses one or the other.
|
|
697
|
+
*
|
|
698
|
+
* A time question is different: the window block answers "what happened last week",
|
|
699
|
+
* and the relevance block stays on because it is where relative/unknown facts and
|
|
700
|
+
* not-yet-extracted conversation still surface (0027 §八: they must never be dropped
|
|
701
|
+
* just because their date cannot be located).
|
|
697
702
|
*/
|
|
698
703
|
export function planBlocks(plan) {
|
|
699
|
-
if (!plan) return { related: true, timeline: false };
|
|
700
|
-
if (plan.primitive === 'entity') return { related: false, timeline: true };
|
|
701
|
-
return { related: true, timeline: false };
|
|
704
|
+
if (!plan) return { related: true, timeline: false, window: false };
|
|
705
|
+
if (plan.primitive === 'entity') return { related: false, timeline: true, window: false };
|
|
706
|
+
return { related: true, timeline: false, window: Boolean(plan.window) };
|
|
702
707
|
}
|
|
703
708
|
|
|
704
709
|
const PROFILE_CORE_DIMENSION = '价值观';
|
|
@@ -755,7 +760,52 @@ export function renderRulesBlock(rules) {
|
|
|
755
760
|
return `<rules fired="${ids}">\n${lines}\n</rules>`;
|
|
756
761
|
}
|
|
757
762
|
|
|
758
|
-
|
|
763
|
+
/** How many facts one window may put in the prompt before it is cut. */
|
|
764
|
+
const TIME_WINDOW_LIMIT = 10;
|
|
765
|
+
|
|
766
|
+
/** A calendar day as the user reads it — local, not UTC: a date is a day, not an instant. */
|
|
767
|
+
function isoDay(ms) {
|
|
768
|
+
const date = new Date(ms);
|
|
769
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
770
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** When a fact happened, as the injection may say it: a located date, or the words said. */
|
|
774
|
+
function factWhen(fact) {
|
|
775
|
+
// A range ("10 月下旬") is not a day: printing its start as a date would claim an
|
|
776
|
+
// exactness the model never had. The original words are the honest answer.
|
|
777
|
+
if (fact.validFromPrecision === 'range' && fact.validFromText) return fact.validFromText;
|
|
778
|
+
return fact.validFrom ? isoDay(fact.validFrom) : fact.validFromText ?? '';
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* The facts that happened in one window (0027 §八, reader 1).
|
|
783
|
+
*
|
|
784
|
+
* Only located dates reach this block — the query already excluded the rest. The dates
|
|
785
|
+
* are shown because "when" is the question; an empty window says so explicitly rather
|
|
786
|
+
* than leaving the model to fill the gap. Truncation is stated, never silent: a block
|
|
787
|
+
* that looks complete but is not is worse than a short one.
|
|
788
|
+
*/
|
|
789
|
+
export function renderTimeWindow(window, facts, options = {}) {
|
|
790
|
+
const cue = escapeAttr(String(window.cue ?? ''));
|
|
791
|
+
// `to` is exclusive; the last day in the window is the millisecond before it.
|
|
792
|
+
const head = `<time-window cue="${cue}" from="${isoDay(window.from)}" to="${isoDay(window.to - 1)}">`;
|
|
793
|
+
if (!Array.isArray(facts) || facts.length === 0) {
|
|
794
|
+
return `${head}\n(${escapeBlockText(String(window.cue ?? '这段时间'))}没有记录)\n</time-window>`;
|
|
795
|
+
}
|
|
796
|
+
let sawInferred = false;
|
|
797
|
+
const lines = facts.map((fact) => {
|
|
798
|
+
const marker = fact.source === 'inferred' ? INFERRED_MARKER : '';
|
|
799
|
+
if (marker) sawInferred = true;
|
|
800
|
+
const when = escapeBlockText(String(factWhen(fact)));
|
|
801
|
+
return `- ${marker}[${fact.id}]${when ? ` ${when}` : ''} ${escapeBlockText(fact.statement)}`;
|
|
802
|
+
});
|
|
803
|
+
if (options.truncated) lines.push(`- (只显示最早 ${facts.length} 条)`);
|
|
804
|
+
const block = `${head}\n${lines.join('\n')}\n</time-window>`;
|
|
805
|
+
return sawInferred ? `${block}\n${SOURCE_NOTES}` : block;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export function buildRelatedContext(text, state = {}) {
|
|
759
809
|
const parts = [];
|
|
760
810
|
const used = [];
|
|
761
811
|
// The profile is the merged description of the person; it is the only block that
|
|
@@ -810,7 +860,7 @@ function buildRelatedContext(text, state = {}) {
|
|
|
810
860
|
const body = timeline
|
|
811
861
|
.map(
|
|
812
862
|
(fact) =>
|
|
813
|
-
`- [${fact.id}] ${fact.
|
|
863
|
+
`- [${fact.id}] ${fact.validFromText ? `${escapeBlockText(fact.validFromText)} ` : ''}${escapeBlockText(fact.statement)}`,
|
|
814
864
|
)
|
|
815
865
|
.join('\n');
|
|
816
866
|
parts.push(`<about-now name="${escapeAttr(plan.subject)}">\n${body}\n</about-now>`);
|
|
@@ -824,6 +874,21 @@ function buildRelatedContext(text, state = {}) {
|
|
|
824
874
|
log(`timeline failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
825
875
|
}
|
|
826
876
|
}
|
|
877
|
+
if (policy.window && plan?.window) {
|
|
878
|
+
try {
|
|
879
|
+
// +1 is a probe, not a fact: when it comes back there were more rows than the
|
|
880
|
+
// block may carry, and the renderer says so instead of pretending completeness.
|
|
881
|
+
const hits = ensureSoul().factsInWindow(plan.window, { limit: TIME_WINDOW_LIMIT + 1 });
|
|
882
|
+
const facts = hits.slice(0, TIME_WINDOW_LIMIT);
|
|
883
|
+
const truncated = hits.length > TIME_WINDOW_LIMIT;
|
|
884
|
+
parts.push(renderTimeWindow(plan.window, facts, { truncated }));
|
|
885
|
+
used.push(...facts.map((fact) => fact.id));
|
|
886
|
+
trace({ event: 'time_window', cue: plan.window.cue, from: plan.window.from, to: plan.window.to, hits: facts.length, truncated });
|
|
887
|
+
log(`time-window: ${plan.window.cue} hits=${facts.length}${truncated ? ' (truncated)' : ''}`);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
log(`time window failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
827
892
|
if (policy.related) {
|
|
828
893
|
const pairs = ensureSoul().recallTurnPairs(text);
|
|
829
894
|
if (pairs.length > 0) {
|
|
@@ -2106,7 +2171,7 @@ async function runConsolidationStep() {
|
|
|
2106
2171
|
const batch = context.facts.slice(0, CONSOLIDATION_MAX_FACTS).map((fact) => ({
|
|
2107
2172
|
id: fact.id,
|
|
2108
2173
|
statement: fact.statement,
|
|
2109
|
-
|
|
2174
|
+
validFromText: fact.validFromText,
|
|
2110
2175
|
}));
|
|
2111
2176
|
let raw;
|
|
2112
2177
|
try {
|
|
@@ -2351,8 +2416,9 @@ function detailTool() {
|
|
|
2351
2416
|
properties: {
|
|
2352
2417
|
ok: { type: 'boolean' },
|
|
2353
2418
|
statement: { type: 'string' },
|
|
2354
|
-
|
|
2355
|
-
|
|
2419
|
+
validFrom: { type: 'number' },
|
|
2420
|
+
validFromText: { type: 'string' },
|
|
2421
|
+
validTo: { type: 'number' },
|
|
2356
2422
|
userText: { type: 'string' },
|
|
2357
2423
|
assistantText: { type: 'string' },
|
|
2358
2424
|
reason: { type: 'string' },
|
|
@@ -2373,8 +2439,9 @@ function detailTool() {
|
|
|
2373
2439
|
return {
|
|
2374
2440
|
ok: true,
|
|
2375
2441
|
statement: detail.fact.statement,
|
|
2376
|
-
|
|
2377
|
-
|
|
2442
|
+
validFrom: detail.fact.validFrom,
|
|
2443
|
+
validFromText: detail.fact.validFromText,
|
|
2444
|
+
validTo: detail.fact.validTo,
|
|
2378
2445
|
userText: detail.turn?.userText,
|
|
2379
2446
|
assistantText: detail.turn?.assistantText,
|
|
2380
2447
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-soulbridge",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.115",
|
|
4
4
|
"description": "Inject soulbridge context into DSH sessions and feed user turns back as observations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"setup": "node scripts/install-hooks.mjs"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"soulbridge": ">=0.0.
|
|
45
|
+
"soulbridge": ">=0.0.114 <0.1.0"
|
|
46
46
|
}
|
|
47
47
|
}
|