@jjh32456/dsh-longlive 1.6.8 → 1.8.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/CHANGELOG.md +69 -1539
- package/LICENSE +1 -1
- package/QUICKSTART.md +33 -12
- package/README.en.md +17 -7
- package/README.md +29 -10
- package/SPEC.md +5 -5
- package/cordis.patch.yml +17 -12
- package/docs/ANSWER-GUARD.md +92 -0
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-RECALL-WIRING.md +1 -1
- package/docs/AUTO-RECALL.md +15 -9
- package/docs/CONFIG.md +143 -37
- package/docs/ERRORS.md +17 -5
- package/docs/FAQ.md +7 -7
- package/docs/FEATURES.md +66 -29
- package/docs/MIGRATION-1.6.0.md +2 -2
- package/docs/OPERATIONS.md +6 -6
- package/docs/PANEL-INTEGRATION.md +6 -0
- package/docs/PRIVACY.md +88 -0
- package/docs/QUICKSTART.md +5 -5
- package/docs/README.en.md +3 -2
- package/docs/RESIDENT-WATCHER.md +1 -1
- package/docs/SESSION-WATCH.md +6 -6
- package/docs/TROUBLESHOOTING.md +4 -4
- package/package.json +42 -117
- package/runbook.md +2 -2
- package/scripts/backfill-auto-cards.mjs +98 -0
- package/scripts/install-surgery-task.ps1 +5 -2
- package/scripts/lib/session-watch-fixture.mjs +1 -1
- package/scripts/longlive-doctor.mjs +6 -0
- package/scripts/migrate-180.mjs +163 -0
- package/scripts/recall-index-build.mjs +128 -0
- package/scripts/session-watch-drill.mjs +1 -1
- package/scripts/surgery-run.cmd +1 -1
- package/scripts/unattended-surgery.ps1 +5 -2
- package/scripts/verify-card-anchors.mjs +138 -0
- package/src/actions/audit.js +125 -0
- package/src/actions/evidence.js +122 -0
- package/src/actions/fs.js +162 -0
- package/src/activity/index.js +129 -18
- package/src/activity/model.js +11 -1
- package/src/activity/store.js +22 -3
- package/src/activity/thinking.js +176 -0
- package/src/adapters/constants.js +11 -0
- package/src/adapters/engram/data/aliases.json +51 -0
- package/src/adapters/engram/data/policy.json +8 -0
- package/src/adapters/engram/data/routing-rules.json +546 -0
- package/src/adapters/engram/index.js +235 -0
- package/src/adapters/registry.js +315 -0
- package/src/answer/audit.js +193 -0
- package/src/answer/evidence.js +30 -0
- package/src/answer/guard.js +1370 -0
- package/src/answer/index.js +1067 -0
- package/src/answer/query.js +111 -0
- package/src/answer/second-hop.mjs +101 -0
- package/src/auto-truncate.js +1 -1
- package/src/config.js +145 -48
- package/src/deps.js +6 -4
- package/src/features.js +158 -8
- package/src/identity/index.js +13 -5
- package/src/identity/inject-position.js +2 -2
- package/src/index.js +97 -7
- package/src/lifecycle/gap-clocks.js +328 -0
- package/src/lifecycle/index.js +269 -0
- package/src/memory/activity-capture.js +229 -0
- package/src/memory/anchors.js +194 -0
- package/src/memory/audit-records.js +222 -0
- package/src/memory/auto-cards.js +401 -0
- package/src/memory/capture.js +442 -0
- package/src/memory/card-anchors.js +203 -0
- package/src/memory/conflicts.js +106 -0
- package/src/memory/decay.js +311 -0
- package/src/memory/distill.js +471 -0
- package/src/memory/event-index.js +917 -0
- package/src/memory/evidence-chain.js +295 -0
- package/src/memory/evidence.js +145 -0
- package/src/memory/filter-runner.js +1 -1
- package/src/memory/filter.js +5 -1
- package/src/memory/forget-v2.js +410 -0
- package/src/memory/forget.js +71 -0
- package/src/memory/frames-activation.js +247 -0
- package/src/memory/fts-query.js +174 -0
- package/src/memory/governance.js +604 -0
- package/src/memory/index.js +1 -0
- package/src/memory/l2-gate.js +169 -0
- package/src/memory/lock.js +121 -0
- package/src/memory/migrate.js +139 -0
- package/src/memory/recall-v2.js +223 -0
- package/src/memory/recall.js +65 -2
- package/src/memory/records.js +425 -0
- package/src/memory/retention.js +128 -0
- package/src/memory/review.js +124 -0
- package/src/memory/trust.js +88 -0
- package/src/memory/version-chain.js +264 -0
- package/src/memory/wal.js +137 -0
- package/src/memory/working-frame.js +516 -0
- package/src/metrics/classify.mjs +5 -3
- package/src/metrics/index.mjs +21 -1
- package/src/metrics/prestep-latency.mjs +300 -0
- package/src/metrics/store.mjs +25 -0
- package/src/policy-profile.js +18 -3
- package/src/recall/anaphora.mjs +87 -0
- package/src/recall/cards.mjs +7 -0
- package/src/recall/embedding.mjs +8 -8
- package/src/recall/entities.mjs +112 -0
- package/src/recall/index.js +235 -19
- package/src/recall/inject-order.mjs +10 -8
- package/src/recall/privacy.mjs +1 -1
- package/src/recall/recall-core.mjs +116 -10
- package/src/recall/shared-visibility.js +302 -0
- package/src/recall/time-normalizer.mjs +196 -0
- package/src/role/activation.js +191 -0
- package/src/role/attention.js +318 -0
- package/src/role/base.js +36 -0
- package/src/role/channels.js +423 -0
- package/src/role/index.js +425 -0
- package/src/role/interfaces.js +115 -0
- package/src/role/personality.js +370 -0
- package/src/role/readers.js +151 -0
- package/src/role/relationships.js +477 -0
- package/src/role/store.js +202 -0
- package/src/role/writeprotect.js +373 -0
- package/src/router/aliases.json +176 -25
- package/src/router/background/agent-runner.mjs +1 -1
- package/src/router/background/anchor.mjs +1 -1
- package/src/router/background/audit.mjs +2 -2
- package/src/router/background/budget-ledger.mjs +2 -2
- package/src/router/background/host-sim.mjs +1 -1
- package/src/router/background/memory.mjs +4 -4
- package/src/router/background/queue.mjs +1 -1
- package/src/router/background/retry.mjs +1 -1
- package/src/router/background/router.mjs +1 -1
- package/src/router/background/stream.mjs +1 -1
- package/src/router/background/tool-policy.mjs +1 -1
- package/src/router/design-headings.json +5 -5
- package/src/router/face/config.mjs +1 -1
- package/src/router/face/identity.mjs +3 -3
- package/src/router/judge.mjs +8 -7
- package/src/router/policies/generic.json +21 -4
- package/src/router/route.js +109 -22
- package/src/router/router.mjs +10 -6
- package/src/router/routing-rules.json +260 -445
- package/src/router/run-sim.mjs +8 -8
- package/src/router/runtime.js +23 -12
- package/src/router/service/config.mjs +5 -5
- package/src/router/service/context.mjs +1 -1
- package/src/router/service/queue.mjs +1 -1
- package/src/router/shadow.mjs +4 -3
- package/src/surgery-recovery.js +2 -2
- package/src/tool-output.js +2 -2
- package/src/tools.js +39 -7
- package/src/truncate-visible.js +2 -2
- package/src/ui/actions.js +1312 -0
- package/src/ui/panel-api.js +367 -0
- package/src/ui/panel-auth.js +251 -0
- package/src/ui/panel-integrity.js +243 -0
- package/src/ui/routes-readonly.js +695 -0
- package/src/ui/routes.js +3 -0
- package/docs/DEV.md +0 -279
- package/docs/PRESENCE-BUS-WIRING.md +0 -491
- package/docs/PRESENCE-BUS.md +0 -694
- package/docs/SURGERY.md +0 -186
- package/scripts/mark-core-cards.mjs +0 -289
- package/scripts/pack.mjs +0 -392
- package/scripts/router-proposals.mjs +0 -386
- package/src/router/fixtures/M1.json +0 -16
- package/src/router/fixtures/M2.json +0 -16
- package/src/router/fixtures/M3.json +0 -20
- package/src/router/fixtures/M4.json +0 -20
- package/src/router/fixtures/M5.json +0 -45
- package/src/router/fixtures/M6.json +0 -53
- package/src/router/fixtures/M7.json +0 -46
- package/src/router/fixtures/S1.json +0 -20
- package/src/router/fixtures/S2.json +0 -23
- package/src/router/fixtures/S3.json +0 -20
- package/src/router/fixtures/S4.json +0 -34
- package/src/router/fixtures/S5.json +0 -43
- package/src/router/fixtures/S6.json +0 -47
- package/src/router/fixtures/T1.json +0 -23
- package/src/router/fixtures/T2.json +0 -16
- package/src/router/fixtures/T3.json +0 -19
- package/src/router/fixtures/T4.json +0 -16
- package/src/router/fixtures/X1.json +0 -17
- package/src/router/fixtures/X2.json +0 -20
- package/src/router/gen-design-headings.mjs +0 -107
- package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
- package/test/activity-realtime.test.mjs +0 -959
- package/test/activity-stream.test.mjs +0 -512
- package/test/auto-recall.test.mjs +0 -474
- package/test/auto-truncate.test.mjs +0 -931
- package/test/autobind.test.mjs +0 -399
- package/test/bus.test.mjs +0 -1572
- package/test/cli-guard.test.mjs +0 -198
- package/test/conclusion.test.mjs +0 -408
- package/test/config-schema.test.mjs +0 -210
- package/test/d6-output.test.mjs +0 -194
- package/test/d6.test.mjs +0 -395
- package/test/deps.test.mjs +0 -342
- package/test/doctor-setup.test.mjs +0 -297
- package/test/embedding-recall.test.mjs +0 -347
- package/test/feature-flags.test.mjs +0 -585
- package/test/filter-shadow.test.mjs +0 -170
- package/test/fixtures/activity-append-child.mjs +0 -29
- package/test/fixtures/baseline-0.8.0-registration.json +0 -556
- package/test/fixtures/d6-header-tools.json +0 -1
- package/test/fixtures/lock-child.mjs +0 -67
- package/test/fixtures/pre-step-container.mjs +0 -100
- package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
- package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
- package/test/fixtures/profile-session-longlive-config.json +0 -20
- package/test/fixtures/proposal-cas-child.mjs +0 -66
- package/test/host-engine-status.test.mjs +0 -307
- package/test/hot-truncate.test.mjs +0 -1349
- package/test/identity-card-marker.test.mjs +0 -307
- package/test/identity-router/run.mjs +0 -721
- package/test/integration-pre-step.test.mjs +0 -553
- package/test/live.test.mjs +0 -1023
- package/test/maintenance-schedule.test.mjs +0 -538
- package/test/memory.test.mjs +0 -2828
- package/test/metrics.test.mjs +0 -478
- package/test/orchestrator.test.mjs +0 -266
- package/test/p1_probe.mjs +0 -97
- package/test/packaging.test.mjs +0 -184
- package/test/portability.test.mjs +0 -343
- package/test/rc4-regressions.test.mjs +0 -562
- package/test/rc5-regressions.test.mjs +0 -197
- package/test/rc6-chinese.test.mjs +0 -394
- package/test/router-1100/apply-off.test.mjs +0 -175
- package/test/router-1100/budget-reconcile.test.mjs +0 -215
- package/test/router-1100/config-context.test.mjs +0 -211
- package/test/router-1100/helpers.mjs +0 -129
- package/test/router-1100/hint-envelope.test.mjs +0 -150
- package/test/router-1100/lock-crossproc.test.mjs +0 -206
- package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
- package/test/router-1100/router-apply.test.mjs +0 -372
- package/test/router-1100/router-fixb.test.mjs +0 -602
- package/test/router-1100/router-proposals.test.mjs +0 -405
- package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
- package/test/router-1100/router-security-cr.test.mjs +0 -333
- package/test/router-1100/service-lifecycle.test.mjs +0 -220
- package/test/router-1100/tools-contract.test.mjs +0 -164
- package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
- package/test/router-1100/v6-contract.test.mjs +0 -158
- package/test/router-1100/v6b-contract.test.mjs +0 -293
- package/test/router-1100/v6c-contract.test.mjs +0 -249
- package/test/router-1100/wal-rotation.test.mjs +0 -375
- package/test/router-1100/wp1-config.test.mjs +0 -170
- package/test/router-1100/wp1-default-off.test.mjs +0 -77
- package/test/router-1100/wp1-model-status.test.mjs +0 -90
- package/test/router-1100/wp1-status.test.mjs +0 -220
- package/test/router-1100/wp1-tools.test.mjs +0 -146
- package/test/router-1100/wp5-cli.test.mjs +0 -240
- package/test/router-1100/wp5-model-auto.test.mjs +0 -418
- package/test/router-1100/wp6-merge.test.mjs +0 -386
- package/test/router-1100/wp6-wake.test.mjs +0 -502
- package/test/router-background/audit.test.mjs +0 -37
- package/test/router-background/budget-ledger.test.mjs +0 -330
- package/test/router-background/cross-process.test.mjs +0 -222
- package/test/router-background/fixtures/cross-child.mjs +0 -85
- package/test/router-background/fixtures/real-shapes.mjs +0 -239
- package/test/router-background/router.test.mjs +0 -1076
- package/test/router-background/stream-retry.test.mjs +0 -358
- package/test/router-background/tool-agent.test.mjs +0 -384
- package/test/router-face/bridge.test.mjs +0 -169
- package/test/router-face/face.test.mjs +0 -316
- package/test/router-failclosed.test.mjs +0 -466
- package/test/router-identity.test.mjs +0 -384
- package/test/router-l1-l2.contract.test.mjs +0 -1075
- package/test/router-memory-tools.test.mjs +0 -262
- package/test/router-policy.test.mjs +0 -225
- package/test/router-runtime.test.mjs +0 -438
- package/test/router-shadow.test.mjs +0 -287
- package/test/router.test.mjs +0 -903
- package/test/run-rc.mjs +0 -315
- package/test/run.mjs +0 -2469
- package/test/safety-defaults.test.mjs +0 -181
- package/test/seeded-load.test.mjs +0 -436
- package/test/session-original.test.mjs +0 -338
- package/test/state-truth.test.mjs +0 -937
- package/test/surgery-recovery.test.mjs +0 -936
- package/test/t7_drive.mjs +0 -151
- package/test/tool-output.test.mjs +0 -393
- package/test/tool-schema.test.mjs +0 -177
- package/test/tools_contract.test.mjs +0 -716
- package/test/truncate-visible.test.mjs +0 -1150
- package/test/ui-flags.test.mjs +0 -477
- package/test/v4-hardening.test.mjs +0 -427
- package/test/version-consistency.test.mjs +0 -286
package/test/t7_drive.mjs
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
// T7 driver: boot the real host (sdk profile) against an isolated DSH_HOME,
|
|
2
|
-
// resume a persisted session, send one prompt and wait for the assistant reply.
|
|
3
|
-
// Usage: T7_DSH_HOME=... T7_SESSION_FILE=... node test/t7_drive.mjs <sessionId> [prompt]
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import { createHash } from 'node:crypto';
|
|
7
|
-
import { parseSession } from '../src/codec.js';
|
|
8
|
-
|
|
9
|
-
// No install path is hard-coded: a machine that does not opt in gets a clean
|
|
10
|
-
// skip (exit 2) instead of a driver that happens to work only on one drive.
|
|
11
|
-
const EXE = process.env.DSH_EXE;
|
|
12
|
-
const CLI = process.env.DSH_CLI_JS;
|
|
13
|
-
if (!EXE || !CLI) {
|
|
14
|
-
console.error(JSON.stringify({
|
|
15
|
-
ok: false,
|
|
16
|
-
skipped: true,
|
|
17
|
-
error: 'DSH_EXE and DSH_CLI_JS are required; no default install path is hard-coded',
|
|
18
|
-
}));
|
|
19
|
-
process.exit(2);
|
|
20
|
-
}
|
|
21
|
-
const HOME = process.env.T7_DSH_HOME;
|
|
22
|
-
if (!HOME || !process.env.T7_SESSION_FILE) {
|
|
23
|
-
console.error(JSON.stringify({ ok: false, error: 'T7_DSH_HOME and T7_SESSION_FILE are required' }));
|
|
24
|
-
process.exit(2);
|
|
25
|
-
}
|
|
26
|
-
const SESSION = process.argv[2];
|
|
27
|
-
const PROMPT = process.argv[3] ?? '只回两个字:收到';
|
|
28
|
-
const CWD = process.env.T7_CWD ?? process.cwd();
|
|
29
|
-
const SESSION_FILE = process.env.T7_SESSION_FILE;
|
|
30
|
-
const WAIT_MS = Number(process.env.T7_WAIT_MS ?? 120000);
|
|
31
|
-
const POLL_MS = 500;
|
|
32
|
-
|
|
33
|
-
const sha = (f) => createHash('sha256').update(fs.readFileSync(f)).digest('hex');
|
|
34
|
-
const readEvents = () => parseSession(fs.readFileSync(SESSION_FILE)).events;
|
|
35
|
-
const before = { bytes: fs.statSync(SESSION_FILE).size, sha256: sha(SESSION_FILE), events: readEvents().length };
|
|
36
|
-
|
|
37
|
-
const child = spawn(EXE, ['--expose-internals', CLI, 'sdk'], {
|
|
38
|
-
env: { ...process.env, DSH_HOME: HOME, ELECTRON_RUN_AS_NODE: '1' },
|
|
39
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
let buffer = '';
|
|
43
|
-
const pending = new Map();
|
|
44
|
-
let stderr = '';
|
|
45
|
-
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
46
|
-
child.stdout.on('data', (chunk) => {
|
|
47
|
-
buffer += chunk.toString();
|
|
48
|
-
let nl;
|
|
49
|
-
while ((nl = buffer.indexOf('\n')) >= 0) {
|
|
50
|
-
const line = buffer.slice(0, nl).trim();
|
|
51
|
-
buffer = buffer.slice(nl + 1);
|
|
52
|
-
if (line) onLine(line);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
function onLine(line) {
|
|
57
|
-
let msg;
|
|
58
|
-
try { msg = JSON.parse(line); } catch { return; }
|
|
59
|
-
if (msg.id && !msg.method) {
|
|
60
|
-
const waiter = pending.get(msg.id);
|
|
61
|
-
if (!waiter) return;
|
|
62
|
-
pending.delete(msg.id);
|
|
63
|
-
if (msg.error) waiter.reject(new Error(msg.error.message ?? JSON.stringify(msg.error)));
|
|
64
|
-
else waiter.resolve(msg.result);
|
|
65
|
-
}
|
|
66
|
-
// notifications (session/event etc.) are observed through the session file
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let nextId = 0;
|
|
70
|
-
function request(method, params) {
|
|
71
|
-
const id = `t7_${nextId += 1}`;
|
|
72
|
-
return new Promise((resolve, reject) => {
|
|
73
|
-
pending.set(id, { resolve, reject });
|
|
74
|
-
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function assistantTexts(events, from) {
|
|
79
|
-
const out = [];
|
|
80
|
-
for (const e of events.slice(from)) {
|
|
81
|
-
if (e.type !== 'assistant/message') continue;
|
|
82
|
-
const content = e.data?.message?.content;
|
|
83
|
-
const text = Array.isArray(content)
|
|
84
|
-
? content.filter((x) => x.type === 'text').map((x) => x.text).join('')
|
|
85
|
-
: '';
|
|
86
|
-
if (text.trim()) out.push(text.trim());
|
|
87
|
-
}
|
|
88
|
-
return out;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function seqContiguous(events) {
|
|
92
|
-
for (let i = 0; i < events.length; i += 1) if (events[i].seq !== i) return false;
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const driverTimeout = setTimeout(() => {
|
|
97
|
-
console.error(JSON.stringify({ ok: false, error: 'driver timeout', stderrTail: stderr.slice(-800) }));
|
|
98
|
-
child.kill();
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}, 240000);
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
await request('initialize', { cwd: CWD, provider: 'deepseek-official', model: 'deepseek-chat' });
|
|
104
|
-
await request('session/resume', { sessionId: SESSION });
|
|
105
|
-
await request('session/prompt', { sessionId: SESSION, contentBlocks: [{ type: 'text', text: PROMPT }] });
|
|
106
|
-
|
|
107
|
-
const deadline = Date.now() + WAIT_MS;
|
|
108
|
-
let events = readEvents();
|
|
109
|
-
let texts = assistantTexts(events, before.events);
|
|
110
|
-
while (Date.now() < deadline && texts.length === 0) {
|
|
111
|
-
const turnEnded = events.slice(before.events).some((e) => e.type === 'turn/end');
|
|
112
|
-
if (turnEnded) break;
|
|
113
|
-
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
114
|
-
events = readEvents();
|
|
115
|
-
texts = assistantTexts(events, before.events);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const after = { bytes: fs.statSync(SESSION_FILE).size, sha256: sha(SESSION_FILE), events: events.length };
|
|
119
|
-
const result = {
|
|
120
|
-
ok: texts.length > 0,
|
|
121
|
-
assistantTexts: texts.slice(-3),
|
|
122
|
-
seqContiguous: seqContiguous(events),
|
|
123
|
-
eventsBefore: before.events,
|
|
124
|
-
eventsAfter: after.events,
|
|
125
|
-
bytesBefore: before.bytes,
|
|
126
|
-
bytesAfter: after.bytes,
|
|
127
|
-
sha256Before: before.sha256,
|
|
128
|
-
sha256After: after.sha256,
|
|
129
|
-
lastTypes: events.slice(-3).map((e) => e.type),
|
|
130
|
-
stderrTail: stderr.slice(-300),
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
await Promise.race([request('shutdown', {}), new Promise((r) => setTimeout(r, 3000))]);
|
|
135
|
-
} catch { /* ignore shutdown errors */ }
|
|
136
|
-
clearTimeout(driverTimeout);
|
|
137
|
-
child.stdin.end();
|
|
138
|
-
await new Promise((resolve) => {
|
|
139
|
-
if (child.exitCode !== null) return resolve();
|
|
140
|
-
child.once('exit', resolve);
|
|
141
|
-
setTimeout(resolve, 5000);
|
|
142
|
-
});
|
|
143
|
-
result.exitCode = child.exitCode;
|
|
144
|
-
console.log(JSON.stringify(result, null, 2));
|
|
145
|
-
process.exit(result.ok && result.seqContiguous && child.exitCode === 0 ? 0 : 3);
|
|
146
|
-
} catch (error) {
|
|
147
|
-
clearTimeout(driverTimeout);
|
|
148
|
-
console.log(JSON.stringify({ ok: false, error: String(error.message ?? error), stderrTail: stderr.slice(-1500) }, null, 2));
|
|
149
|
-
child.kill();
|
|
150
|
-
process.exit(2);
|
|
151
|
-
}
|
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
// D2 — trim oversized tool results at `tools/post-execute`.
|
|
2
|
-
//
|
|
3
|
-
// Every case runs against a fake host: a fake `exec`, a fake tool `result`, a
|
|
4
|
-
// fake waterfall `next()` and an injected archive writer where the failure
|
|
5
|
-
// path matters. No host package is imported; the seam is a plain async
|
|
6
|
-
// function, which is what the Cordis waterfall calls.
|
|
7
|
-
//
|
|
8
|
-
// Acceptance cases (the five D2 asks):
|
|
9
|
-
// - a small output is untouched;
|
|
10
|
-
// - an oversized output is replaced by head + tail + one notice line;
|
|
11
|
-
// - the archived file is byte-identical to the original text;
|
|
12
|
-
// - a privacy hit writes nothing and leaves a placeholder;
|
|
13
|
-
// - a write failure keeps the original content (fail-open).
|
|
14
|
-
//
|
|
15
|
-
// Extras that make the seam safe: default off, skip read/error/PTC children,
|
|
16
|
-
// a `value` decision is never rewritten, the turn comes from the agents
|
|
17
|
-
// projection or the session log, and the 70 KB acceptance probe holds the
|
|
18
|
-
// context delta under 5 K while the path still reads the full text back.
|
|
19
|
-
|
|
20
|
-
import fs from 'node:fs';
|
|
21
|
-
import os from 'node:os';
|
|
22
|
-
import path from 'node:path';
|
|
23
|
-
import assert from 'node:assert/strict';
|
|
24
|
-
import test, { after } from 'node:test';
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
D2_DEFAULTS,
|
|
28
|
-
createTrimHandler,
|
|
29
|
-
currentTurnOf,
|
|
30
|
-
decideToolOutputTrim,
|
|
31
|
-
formatD2Notice,
|
|
32
|
-
normalizeD2Config,
|
|
33
|
-
registerToolOutputTrim,
|
|
34
|
-
replaceTextParts,
|
|
35
|
-
splitHeadTail,
|
|
36
|
-
toolOutputDir,
|
|
37
|
-
toolOutputRoot,
|
|
38
|
-
} from '../src/tool-output.js';
|
|
39
|
-
import { archiveFileName } from '../src/original.js';
|
|
40
|
-
|
|
41
|
-
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'longlive-d2-'));
|
|
42
|
-
|
|
43
|
-
after(() => {
|
|
44
|
-
fs.rmSync(TMP, { recursive: true, force: true });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
function makeRoot(label) {
|
|
48
|
-
const root = path.join(TMP, label);
|
|
49
|
-
fs.mkdirSync(root, { recursive: true });
|
|
50
|
-
return root;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** A fake tool execution: only the fields D2 reads, no host types. */
|
|
54
|
-
function makeExec({ name = 'demo_tool', callId = 'call-1', turn = 3, log = null } = {}) {
|
|
55
|
-
const events = log ?? [
|
|
56
|
-
{ type: 'turn/start', data: { turn } },
|
|
57
|
-
{ type: 'user/message', data: {} },
|
|
58
|
-
];
|
|
59
|
-
return {
|
|
60
|
-
name,
|
|
61
|
-
callId,
|
|
62
|
-
agent: {
|
|
63
|
-
session: {
|
|
64
|
-
seq: events.length,
|
|
65
|
-
eventAt: (seq) => events[seq],
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function makeResult(text, { isError = false, extra = [] } = {}) {
|
|
72
|
-
return { content: [{ type: 'text', text }, ...extra], isError };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Run the real handler; the turn is resolved from the fake session log. */
|
|
76
|
-
function handlerFor(options = {}) {
|
|
77
|
-
return createTrimHandler({
|
|
78
|
-
settings: normalizeD2Config({ enabled: true, ...(options.settings ?? {}) }),
|
|
79
|
-
memoryRoot: options.memoryRoot,
|
|
80
|
-
currentTurn: options.currentTurn ?? ((exec) => currentTurnOf(null, exec)),
|
|
81
|
-
...(options.overrides ?? {}),
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function call(handler, exec, result) {
|
|
86
|
-
return handler(exec, result, async () => ({ kind: 'accept' }));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
test('D2 defaults: disabled, 4096 chars, read skipped, errors kept whole', () => {
|
|
90
|
-
const settings = normalizeD2Config(undefined);
|
|
91
|
-
assert.equal(settings.enabled, false);
|
|
92
|
-
assert.equal(settings.maxInlineChars, D2_DEFAULTS.maxInlineChars);
|
|
93
|
-
assert.deepEqual(settings.skipTools, ['read']);
|
|
94
|
-
assert.equal(settings.trimErrors, false);
|
|
95
|
-
|
|
96
|
-
// A typo must not create a nonsense budget.
|
|
97
|
-
const clamped = normalizeD2Config({ enabled: true, maxInlineChars: 5, headRatio: 9 });
|
|
98
|
-
assert.equal(clamped.maxInlineChars, 64);
|
|
99
|
-
assert.equal(clamped.headRatio, 0.8);
|
|
100
|
-
assert.equal(normalizeD2Config({ enabled: 'yes' }).enabled, false);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('D2 off by default: registration mounts nothing', () => {
|
|
104
|
-
const hooked = [];
|
|
105
|
-
const ctx = {
|
|
106
|
-
on: (event, handler) => { hooked.push({ event, handler }); return () => {}; },
|
|
107
|
-
get: () => null,
|
|
108
|
-
logger: { warn() {}, info() {} },
|
|
109
|
-
};
|
|
110
|
-
const status = registerToolOutputTrim(ctx, {}, { memoryRoot: makeRoot('off') });
|
|
111
|
-
assert.equal(status.registered, false);
|
|
112
|
-
assert.equal(hooked.length, 0);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('D2 small output is untouched (identity, no archive)', async () => {
|
|
116
|
-
const root = makeRoot('small');
|
|
117
|
-
const decision = { kind: 'accept' };
|
|
118
|
-
const handler = handlerFor({ memoryRoot: root });
|
|
119
|
-
const out = await handler(makeExec({ turn: 1 }), makeResult('a short tool answer'), async () => decision);
|
|
120
|
-
assert.equal(out, decision);
|
|
121
|
-
assert.equal(fs.existsSync(path.join(root, '.longlive')), false);
|
|
122
|
-
|
|
123
|
-
// Exactly at the budget: still inline, still the same decision object.
|
|
124
|
-
const tight = await handlerFor({ memoryRoot: root, settings: { maxInlineChars: 64 } })(
|
|
125
|
-
makeExec({ turn: 1 }),
|
|
126
|
-
makeResult('x'.repeat(64)),
|
|
127
|
-
async () => decision,
|
|
128
|
-
);
|
|
129
|
-
assert.equal(tight, decision);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('D2 oversized output: head + tail + one notice line, archive written', async () => {
|
|
133
|
-
const root = makeRoot('trim');
|
|
134
|
-
const text = `HEAD-MARKER-${'A'.repeat(20000)}-TAIL-MARKER`;
|
|
135
|
-
const out = await call(handlerFor({ memoryRoot: root }), makeExec({ turn: 3 }), makeResult(text));
|
|
136
|
-
|
|
137
|
-
assert.equal(out.kind, 'accept');
|
|
138
|
-
assert.notEqual(out, undefined);
|
|
139
|
-
const inline = out.content[0].text;
|
|
140
|
-
assert.ok(inline.length <= 4096 + 500, `inline ${inline.length} should stay near the budget`);
|
|
141
|
-
assert.ok(inline.length < text.length, 'the inline text is much smaller than the original');
|
|
142
|
-
assert.ok(inline.startsWith('HEAD-MARKER-'), 'head is kept first');
|
|
143
|
-
assert.ok(inline.endsWith('-TAIL-MARKER'), 'tail is kept last');
|
|
144
|
-
assert.match(inline, new RegExp(`\\[D2\\] tool output \\(demo_tool\\) trimmed: ${text.length} chars → \\d+ inline\\.`));
|
|
145
|
-
assert.ok(inline.includes(`Full output (${Buffer.byteLength(text, 'utf8')} bytes) saved at `));
|
|
146
|
-
|
|
147
|
-
const file = path.join(toolOutputDir(root, 3), 'call-1.txt');
|
|
148
|
-
assert.ok(fs.existsSync(file), 'archive path is the D3 convention');
|
|
149
|
-
assert.ok(inline.includes(file), 'the notice prints the path');
|
|
150
|
-
assert.ok(inline.includes('session_original(turn=3, toolCallId="call-1")'));
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test('D2 archive is byte-identical to the original text', async () => {
|
|
154
|
-
const root = makeRoot('bytes');
|
|
155
|
-
const text = '日本語テキスト\n'.repeat(4000) + '尾部';
|
|
156
|
-
await call(handlerFor({ memoryRoot: root }), makeExec({ name: 'grep', callId: 'call-9', turn: 2 }), makeResult(text));
|
|
157
|
-
const file = path.join(toolOutputDir(root, 2), archiveFileName({ kind: 'tool', toolCallId: 'call-9' }));
|
|
158
|
-
assert.equal(fs.readFileSync(file, 'utf8'), text);
|
|
159
|
-
assert.equal(fs.statSync(file).size, Buffer.byteLength(text, 'utf8'));
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test('D2 privacy hit writes nothing and leaves a placeholder', async () => {
|
|
163
|
-
const root = makeRoot('privacy');
|
|
164
|
-
const text = 'customer record: phone 13800138000\n' + 'x'.repeat(9000);
|
|
165
|
-
const out = await call(
|
|
166
|
-
handlerFor({
|
|
167
|
-
memoryRoot: root,
|
|
168
|
-
overrides: { scan: () => ({ verdict: 'local_only', reasons: ['phone'], spans: [] }) },
|
|
169
|
-
}),
|
|
170
|
-
makeExec({ turn: 4 }),
|
|
171
|
-
makeResult(text),
|
|
172
|
-
);
|
|
173
|
-
const inline = out.content[0].text;
|
|
174
|
-
assert.match(inline, new RegExp(`withheld: ${text.length} chars matched the privacy gate \\(phone\\)`));
|
|
175
|
-
assert.ok(!inline.includes('13800138000'), 'a flagged value is never echoed');
|
|
176
|
-
assert.equal(fs.existsSync(path.join(root, '.longlive')), false, 'nothing is written');
|
|
177
|
-
assert.ok(!inline.includes('.longlive'), 'no path is advertised');
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test('D2 write failure is fail-open: original decision survives', async () => {
|
|
181
|
-
const root = makeRoot('eacces');
|
|
182
|
-
const text = 'y'.repeat(9000);
|
|
183
|
-
const decision = { kind: 'accept' };
|
|
184
|
-
const handler = handlerFor({
|
|
185
|
-
memoryRoot: root,
|
|
186
|
-
overrides: {
|
|
187
|
-
writeArchive: () => {
|
|
188
|
-
const error = new Error('EACCES: permission denied, open archive');
|
|
189
|
-
error.code = 'EACCES';
|
|
190
|
-
throw error;
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
const out = await handler(makeExec({ turn: 5 }), makeResult(text), async () => decision);
|
|
195
|
-
assert.equal(out, decision, 'the untouched decision is returned');
|
|
196
|
-
assert.equal(fs.readdirSync(root).filter((name) => name.includes('tmp-')).length, 0, 'no tmp file is left behind');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test('D2 next() failure propagates untouched', async () => {
|
|
200
|
-
const handler = handlerFor({ memoryRoot: makeRoot('next-fail') });
|
|
201
|
-
const boom = new Error('below-seam failure');
|
|
202
|
-
await assert.rejects(
|
|
203
|
-
handler(makeExec({ turn: 1 }), makeResult('z'.repeat(9000)), async () => { throw boom; }),
|
|
204
|
-
boom,
|
|
205
|
-
);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test('D2 skips read, errors, PTC children and value decisions', async () => {
|
|
209
|
-
const root = makeRoot('skips');
|
|
210
|
-
const big = () => makeResult('q'.repeat(9000));
|
|
211
|
-
const cases = [
|
|
212
|
-
['read', makeExec({ name: 'read', turn: 1 }), big(), { kind: 'accept' }],
|
|
213
|
-
['error result', makeExec({ name: 'demo', turn: 1 }), makeResult('e'.repeat(9000), { isError: true }), { kind: 'accept' }],
|
|
214
|
-
['ptc child', { ...makeExec({ turn: 1 }), parent: {} }, big(), { kind: 'accept' }],
|
|
215
|
-
['value decision', makeExec({ name: 'demo', turn: 1 }), big(), { kind: 'accept', value: { ok: true } }],
|
|
216
|
-
['blocked decision', makeExec({ name: 'demo', turn: 1 }), big(), { kind: 'block', feedback: 'nope' }],
|
|
217
|
-
];
|
|
218
|
-
for (const [label, exec, result, decision] of cases) {
|
|
219
|
-
const out = await handlerFor({ memoryRoot: root })(exec, result, async () => decision);
|
|
220
|
-
assert.equal(out, decision, `${label} must be untouched`);
|
|
221
|
-
}
|
|
222
|
-
assert.equal(fs.existsSync(path.join(root, '.longlive')), false);
|
|
223
|
-
|
|
224
|
-
// trimErrors is the documented opt-out from the error exemption.
|
|
225
|
-
const out = await call(
|
|
226
|
-
handlerFor({ memoryRoot: root, settings: { skipTools: [], trimErrors: true, maxInlineChars: 64 } }),
|
|
227
|
-
makeExec({ name: 'demo', turn: 1 }),
|
|
228
|
-
makeResult('e'.repeat(5000), { isError: true }),
|
|
229
|
-
);
|
|
230
|
-
assert.match(out.content[0].text, /\[D2\] tool output/);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test('D2 turn resolution: agents projection first, session log as fallback, null keeps content', () => {
|
|
234
|
-
const events = [
|
|
235
|
-
{ type: 'turn/start', data: { turn: 7 } },
|
|
236
|
-
{ type: 'tool/result', data: {} },
|
|
237
|
-
{ type: 'turn/start', data: { turn: 8 } },
|
|
238
|
-
];
|
|
239
|
-
const exec = makeExec({ log: events });
|
|
240
|
-
|
|
241
|
-
// 1. the agents service's turnBoundary projection wins;
|
|
242
|
-
const projectionCtx = { get: (name) => (name === 'agents' ? { turnBoundaryOf: () => ({ lastTurn: 42 }) } : null) };
|
|
243
|
-
assert.equal(currentTurnOf(projectionCtx, exec), 42);
|
|
244
|
-
|
|
245
|
-
// 2. without it, the newest turn/start in the session log answers;
|
|
246
|
-
assert.equal(currentTurnOf(null, exec), 8);
|
|
247
|
-
|
|
248
|
-
// 3. a session with no turn/start yields null, and null keeps the content inline.
|
|
249
|
-
const orphan = makeExec({ log: [{ type: 'user/message', data: {} }] });
|
|
250
|
-
assert.equal(currentTurnOf(null, orphan), null);
|
|
251
|
-
const decision = { kind: 'accept' };
|
|
252
|
-
const outcome = decideToolOutputTrim({
|
|
253
|
-
exec: orphan,
|
|
254
|
-
result: makeResult('w'.repeat(9000)),
|
|
255
|
-
decision,
|
|
256
|
-
settings: normalizeD2Config({ enabled: true }),
|
|
257
|
-
memoryRoot: makeRoot('turn'),
|
|
258
|
-
turn: currentTurnOf(null, orphan),
|
|
259
|
-
});
|
|
260
|
-
assert.equal(outcome.action, 'keep');
|
|
261
|
-
assert.equal(outcome.decision, decision);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test('D2 keeps non-text parts and never splits a surrogate pair', async () => {
|
|
265
|
-
const root = makeRoot('parts');
|
|
266
|
-
const image = { type: 'image', mediaType: 'image/png', data: 'AAAA' };
|
|
267
|
-
const out = await call(
|
|
268
|
-
handlerFor({ memoryRoot: root }),
|
|
269
|
-
makeExec({ name: 'screenshot', callId: 'call-img', turn: 6 }),
|
|
270
|
-
makeResult('s'.repeat(9000), { extra: [image] }),
|
|
271
|
-
);
|
|
272
|
-
assert.equal(out.content.length, 2);
|
|
273
|
-
assert.equal(out.content[1], image);
|
|
274
|
-
assert.match(out.content[0].text, /\[D2\]/);
|
|
275
|
-
|
|
276
|
-
const split = splitHeadTail('😀'.repeat(5000), 100, 0.5);
|
|
277
|
-
assert.equal(split.trimmed, true);
|
|
278
|
-
assert.ok(split.omittedChars > 0);
|
|
279
|
-
assert.doesNotThrow(() => Buffer.from(split.head + split.tail, 'utf8'));
|
|
280
|
-
assert.ok(!/[\uD800-\uDBFF]$/.test(split.head), 'head does not end on a lone high surrogate');
|
|
281
|
-
assert.ok(!/^[\uDC00-\uDFFF]/.test(split.tail), 'tail does not start on a lone low surrogate');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test('D2 acceptance probe: 70 KB JSON output costs ≤5 K inline and reads back whole', async () => {
|
|
285
|
-
const root = makeRoot('probe-70k');
|
|
286
|
-
const payload = JSON.stringify({
|
|
287
|
-
items: Array.from({ length: 1800 }, (_, index) => ({ id: index, name: `row-${index}`, tags: ['alpha', 'beta'] })),
|
|
288
|
-
});
|
|
289
|
-
assert.ok(payload.length >= 70000, `probe payload is ${payload.length} chars`);
|
|
290
|
-
|
|
291
|
-
const decision = { kind: 'accept' };
|
|
292
|
-
const out = await handlerFor({ memoryRoot: root })(
|
|
293
|
-
makeExec({ name: 'query_db', callId: 'call-70k', turn: 11 }),
|
|
294
|
-
makeResult(payload),
|
|
295
|
-
async () => decision,
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
const inline = out.content[0].text;
|
|
299
|
-
assert.ok(inline.length <= 5120, `context delta ${inline.length} must be ≤ 5120 chars`);
|
|
300
|
-
assert.ok(inline.length < payload.length / 10, 'the context grows by an order of magnitude less');
|
|
301
|
-
|
|
302
|
-
const file = path.join(toolOutputDir(root, 11), 'call-70k.txt');
|
|
303
|
-
assert.equal(fs.readFileSync(file, 'utf8'), payload, 'the whole 70 KB reads back from the path');
|
|
304
|
-
assert.ok(inline.includes(file));
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test('D2 registration wires the seam and disposes with the plugin scope', () => {
|
|
308
|
-
const hooked = [];
|
|
309
|
-
const disposed = [];
|
|
310
|
-
const ctx = {
|
|
311
|
-
on: (event, handler) => {
|
|
312
|
-
hooked.push({ event, handler });
|
|
313
|
-
return () => disposed.push(`dispose:${event}`);
|
|
314
|
-
},
|
|
315
|
-
get: () => null,
|
|
316
|
-
effect: (factory) => {
|
|
317
|
-
const disposer = factory();
|
|
318
|
-
disposed.push(`effect:${typeof disposer}`);
|
|
319
|
-
},
|
|
320
|
-
logger: { warn() {}, info() {} },
|
|
321
|
-
};
|
|
322
|
-
const root = makeRoot('register');
|
|
323
|
-
const status = registerToolOutputTrim(ctx, { d2: { enabled: true, maxInlineChars: 512 } }, { memoryRoot: root });
|
|
324
|
-
assert.equal(status.registered, true);
|
|
325
|
-
assert.equal(status.settings.maxInlineChars, 512);
|
|
326
|
-
assert.equal(hooked.length, 1);
|
|
327
|
-
assert.equal(hooked[0].event, 'tools/post-execute');
|
|
328
|
-
assert.equal(status.archiveRoot, toolOutputRoot(root));
|
|
329
|
-
assert.deepEqual(disposed, ['effect:function']);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test('D2 applies through the plugin entry: d2.enabled mounts on tools/post-execute', async () => {
|
|
333
|
-
const home = makeRoot('apply-home');
|
|
334
|
-
const memoryRoot = path.join(home, 'memory');
|
|
335
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
336
|
-
const hooked = [];
|
|
337
|
-
const ctx = {
|
|
338
|
-
tools: { register: () => () => {} },
|
|
339
|
-
on: (event, handler) => {
|
|
340
|
-
hooked.push({ event, handler });
|
|
341
|
-
return () => {};
|
|
342
|
-
},
|
|
343
|
-
get: () => null,
|
|
344
|
-
effect: () => {},
|
|
345
|
-
logger: { warn() {}, info() {} },
|
|
346
|
-
};
|
|
347
|
-
const { apply } = await import('../src/index.js');
|
|
348
|
-
await apply(ctx, {
|
|
349
|
-
home,
|
|
350
|
-
memoryRoot,
|
|
351
|
-
env: {},
|
|
352
|
-
heartbeatMs: 0,
|
|
353
|
-
recoverOnStart: false,
|
|
354
|
-
defineTool: (definition) => definition,
|
|
355
|
-
routerRuntime: 'off',
|
|
356
|
-
d2: { enabled: true, maxInlineChars: 256 },
|
|
357
|
-
});
|
|
358
|
-
const seam = hooked.find((entry) => entry.event === 'tools/post-execute');
|
|
359
|
-
assert.ok(seam, 'd2.enabled must register a tools/post-execute listener');
|
|
360
|
-
|
|
361
|
-
const events = [{ type: 'turn/start', data: { turn: 4 } }];
|
|
362
|
-
const exec = {
|
|
363
|
-
name: 'query_db',
|
|
364
|
-
callId: 'call-apply',
|
|
365
|
-
agent: { session: { seq: 1, eventAt: (seq) => events[seq] } },
|
|
366
|
-
};
|
|
367
|
-
const payload = 'P'.repeat(9000);
|
|
368
|
-
const out = await seam.handler(exec, makeResult(payload), async () => ({ kind: 'accept' }));
|
|
369
|
-
const inline = out.content[0].text;
|
|
370
|
-
assert.ok(inline.length <= 256 + 500, `inline ${inline.length} near the configured budget`);
|
|
371
|
-
assert.ok(fs.existsSync(path.join(toolOutputDir(memoryRoot, 4), 'call-apply.txt')));
|
|
372
|
-
assert.equal(fs.readFileSync(path.join(toolOutputDir(memoryRoot, 4), 'call-apply.txt'), 'utf8'), payload);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
test('D2 notice and text replacement keep the no-call-id shape readable', () => {
|
|
376
|
-
const notice = formatD2Notice({
|
|
377
|
-
chars: 9000,
|
|
378
|
-
inlineChars: 4096,
|
|
379
|
-
bytes: 9000,
|
|
380
|
-
file: 'C:\\m\\.longlive\\tool-output\\2\\c.txt',
|
|
381
|
-
turn: null,
|
|
382
|
-
callId: '',
|
|
383
|
-
toolName: 'demo',
|
|
384
|
-
});
|
|
385
|
-
assert.ok(notice.startsWith('[D2] tool output (demo) trimmed: 9000 chars → 4096 inline.'));
|
|
386
|
-
assert.ok(!notice.includes('session_original'), 'no call id, no read-back hint');
|
|
387
|
-
|
|
388
|
-
const content = replaceTextParts(
|
|
389
|
-
[{ type: 'text', text: 'a' }, { type: 'image' }, { type: 'text', text: 'b' }],
|
|
390
|
-
'merged',
|
|
391
|
-
);
|
|
392
|
-
assert.deepEqual(content, [{ type: 'text', text: 'merged' }, { type: 'image' }]);
|
|
393
|
-
});
|