@wangzhizhi/remi 0.1.225 → 0.1.244
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 +206 -2
- package/dist/help.js +1 -1
- package/dist/initPrompt.js +1 -1
- package/dist/setup.js +5 -4
- package/dist/statusCard.js +1 -1
- package/dist/statusline.js +1 -1
- package/dist/tui/RemiApp.js +55 -7
- package/dist/tui/hooksPanel.js +2 -2
- package/dist/tui/renderers/MessageList.js +10 -7
- package/dist/version.js +1 -1
- package/node_modules/@remi/compact/dist/index.js +13 -5
- package/node_modules/@remi/compact/package.json +1 -1
- package/node_modules/@remi/config/dist/index.js +816 -38
- package/node_modules/@remi/config/package.json +1 -1
- package/node_modules/@remi/core/dist/cacheBenchmark.js +217 -0
- package/node_modules/@remi/core/dist/cacheDiagnostics.js +300 -0
- package/node_modules/@remi/core/dist/contextBuilder.js +13 -11
- package/node_modules/@remi/core/dist/index.js +630 -33
- package/node_modules/@remi/core/dist/stableHash.js +30 -0
- package/node_modules/@remi/core/package.json +1 -1
- package/node_modules/@remi/hooks/package.json +1 -1
- package/node_modules/@remi/llm/package.json +1 -1
- package/node_modules/@remi/mcp/dist/index.js +32 -18
- package/node_modules/@remi/mcp/package.json +1 -1
- package/node_modules/@remi/memory/package.json +1 -1
- package/node_modules/@remi/permissions/package.json +1 -1
- package/node_modules/@remi/sessions/dist/index.js +38 -0
- package/node_modules/@remi/sessions/package.json +1 -1
- package/node_modules/@remi/skills/dist/index.js +2 -2
- package/node_modules/@remi/skills/package.json +1 -1
- package/node_modules/@remi/terminal-markdown/dist/index.js +159 -22
- package/node_modules/@remi/terminal-markdown/package.json +1 -1
- package/node_modules/@remi/tools/dist/index.js +1 -1
- package/node_modules/@remi/tools/package.json +1 -1
- package/package.json +13 -13
- package/prompt/tool-use-system.md +19 -3
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { createHeuristicTokenEstimator } from '@remi/llm';
|
|
2
|
+
import { buildProviderContext } from './contextBuilder.js';
|
|
3
|
+
import { stableHash, stableStringify } from './stableHash.js';
|
|
4
|
+
const defaultTokenEstimator = createHeuristicTokenEstimator();
|
|
5
|
+
export function buildCacheShapeBenchmark(requests, options = {}) {
|
|
6
|
+
const tokenEstimator = options.tokenEstimator ?? defaultTokenEstimator;
|
|
7
|
+
const tokenEstimateProfile = options.tokenEstimateProfile ?? 'default';
|
|
8
|
+
const shapedRequests = requests.map(request => buildShapedRequest(request, tokenEstimator, tokenEstimateProfile));
|
|
9
|
+
const pairs = [];
|
|
10
|
+
for (let index = 1; index < shapedRequests.length; index += 1) {
|
|
11
|
+
const before = shapedRequests[index - 1];
|
|
12
|
+
const after = shapedRequests[index];
|
|
13
|
+
if (before && after) {
|
|
14
|
+
pairs.push(compareShapedRequests(before, after));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
requestCount: shapedRequests.length,
|
|
19
|
+
requests: shapedRequests.map(request => request.summary),
|
|
20
|
+
pairs,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function buildRemiCacheShapeBenchmarkFixture() {
|
|
24
|
+
return buildCacheShapeBenchmark([
|
|
25
|
+
buildRemiCacheShapeBenchmarkRequest({
|
|
26
|
+
label: 'turn-1',
|
|
27
|
+
runtimePrompt: 'cwd=/Users/example/project\nlanguage=zh-Hans\nturn=1',
|
|
28
|
+
currentUserMessage: '优化缓存命中率',
|
|
29
|
+
}),
|
|
30
|
+
buildRemiCacheShapeBenchmarkRequest({
|
|
31
|
+
label: 'turn-2',
|
|
32
|
+
runtimePrompt: 'cwd=/Users/example/project\nlanguage=zh-Hans\nturn=2',
|
|
33
|
+
currentUserMessage: '继续验证 benchmark',
|
|
34
|
+
}),
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
export function buildRemiCacheShapeBenchmarkRequest(options) {
|
|
38
|
+
const context = buildProviderContext({
|
|
39
|
+
systemPrompt: 'Remi stable core behavior.',
|
|
40
|
+
toolPrompt: 'Remi stable tool-use policy.',
|
|
41
|
+
projectInstructions: 'Follow PLAN.md and update checklist after each step.',
|
|
42
|
+
projectInstructionsSource: 'AGENTS.md + PLAN.md',
|
|
43
|
+
memories: [
|
|
44
|
+
{
|
|
45
|
+
id: 'cache-memory',
|
|
46
|
+
type: 'project',
|
|
47
|
+
scope: 'project',
|
|
48
|
+
description: 'Cache optimization memory',
|
|
49
|
+
createdAt: '2026-06-04T00:00:00.000Z',
|
|
50
|
+
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
51
|
+
useCount: 1,
|
|
52
|
+
body: 'Keep stable context layers before dynamic runtime facts.',
|
|
53
|
+
reason: 'benchmark fixture',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
sessionMemorySummary: 'Stable session memory summary for cache benchmark.',
|
|
57
|
+
compactSummary: 'Stable compact summary for cache benchmark.',
|
|
58
|
+
recentMessages: [
|
|
59
|
+
{ role: 'user', content: '上一轮请求' },
|
|
60
|
+
{ role: 'assistant', content: '上一轮已完成观测。' },
|
|
61
|
+
],
|
|
62
|
+
runtimePrompt: options.runtimePrompt,
|
|
63
|
+
currentTurnPrompt: 'Current turn reminder stays near the active user message.',
|
|
64
|
+
currentUserMessage: options.currentUserMessage,
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
label: options.label,
|
|
68
|
+
messages: context.messages,
|
|
69
|
+
trace: context.trace,
|
|
70
|
+
toolDefinitions: options.toolDefinitions ?? benchmarkToolDefinitions,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function buildShapedRequest(request, tokenEstimator, tokenEstimateProfile) {
|
|
74
|
+
const toolSchema = stableStringify(canonicalToolDefinitions(request.toolDefinitions));
|
|
75
|
+
const toolSchemaUnit = unitForText('tool-schema', toolSchema, tokenEstimator, tokenEstimateProfile);
|
|
76
|
+
const layerIds = messageLayerIds(request.trace);
|
|
77
|
+
const messageUnits = request.messages.map((message, index) => {
|
|
78
|
+
const serialized = stableStringify({ role: message.role, content: message.content });
|
|
79
|
+
return unitForText('message', serialized, tokenEstimator, tokenEstimateProfile, layerIds[index]);
|
|
80
|
+
});
|
|
81
|
+
const units = [toolSchemaUnit, ...messageUnits];
|
|
82
|
+
return {
|
|
83
|
+
request,
|
|
84
|
+
units,
|
|
85
|
+
summary: {
|
|
86
|
+
label: request.label,
|
|
87
|
+
messageCount: request.messages.length,
|
|
88
|
+
toolCount: request.toolDefinitions.length,
|
|
89
|
+
toolSchemaHash: toolSchemaUnit.hash,
|
|
90
|
+
toolSchemaTokens: toolSchemaUnit.tokenCount,
|
|
91
|
+
layerOrder: request.trace.layers.map(layer => layer.id),
|
|
92
|
+
currentUserMessageLast: request.messages.at(-1)?.role === 'user',
|
|
93
|
+
totalBytes: units.reduce((sum, unit) => sum + unit.byteCount, 0),
|
|
94
|
+
totalTokens: units.reduce((sum, unit) => sum + unit.tokenCount, 0),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function compareShapedRequests(before, after) {
|
|
99
|
+
let sharedUnitCount = 0;
|
|
100
|
+
let sharedByteCount = 0;
|
|
101
|
+
let sharedTokenCount = 0;
|
|
102
|
+
const sharedLayerIds = [];
|
|
103
|
+
const maxSharedUnits = Math.min(before.units.length, after.units.length);
|
|
104
|
+
for (let index = 0; index < maxSharedUnits; index += 1) {
|
|
105
|
+
const beforeUnit = before.units[index];
|
|
106
|
+
const afterUnit = after.units[index];
|
|
107
|
+
if (!beforeUnit || !afterUnit || beforeUnit.kind !== afterUnit.kind || beforeUnit.layerId !== afterUnit.layerId || beforeUnit.hash !== afterUnit.hash) {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
sharedUnitCount += 1;
|
|
111
|
+
sharedByteCount += afterUnit.byteCount;
|
|
112
|
+
sharedTokenCount += afterUnit.tokenCount;
|
|
113
|
+
if (afterUnit.layerId && !sharedLayerIds.includes(afterUnit.layerId)) {
|
|
114
|
+
sharedLayerIds.push(afterUnit.layerId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const firstChanged = after.units[sharedUnitCount];
|
|
118
|
+
const firstChangedLayer = firstChanged?.layerId ? layerIndexAndId(after.request.trace, firstChanged.layerId) : undefined;
|
|
119
|
+
const changedLayers = changedLayerReports(before.request.trace, after.request.trace);
|
|
120
|
+
const stableLayers = after.request.trace.layers
|
|
121
|
+
.filter(layer => layer.included && !changedLayers.some(changed => changed.id === layer.id))
|
|
122
|
+
.map(layer => ({
|
|
123
|
+
id: layer.id,
|
|
124
|
+
tokenCount: layer.tokenCount,
|
|
125
|
+
...(layer.hash ? { hash: layer.hash } : {}),
|
|
126
|
+
}));
|
|
127
|
+
return {
|
|
128
|
+
from: before.request.label,
|
|
129
|
+
to: after.request.label,
|
|
130
|
+
toolSchemaChanged: before.summary.toolSchemaHash !== after.summary.toolSchemaHash,
|
|
131
|
+
toolSchemaHashBefore: before.summary.toolSchemaHash,
|
|
132
|
+
toolSchemaHashAfter: after.summary.toolSchemaHash,
|
|
133
|
+
toolSchemaTokensBefore: before.summary.toolSchemaTokens,
|
|
134
|
+
toolSchemaTokensAfter: after.summary.toolSchemaTokens,
|
|
135
|
+
firstChangedUnit: firstChanged ? firstChanged.kind : 'none',
|
|
136
|
+
...(firstChanged && firstChanged.kind === 'message' ? { firstChangedMessageIndex: sharedUnitCount - 1 } : {}),
|
|
137
|
+
...(firstChangedLayer ? { firstChangedLayerIndex: firstChangedLayer.index, firstChangedLayerId: firstChangedLayer.id } : {}),
|
|
138
|
+
sharedPrefix: {
|
|
139
|
+
unitCount: sharedUnitCount,
|
|
140
|
+
byteCount: sharedByteCount,
|
|
141
|
+
tokenCount: sharedTokenCount,
|
|
142
|
+
percentOfNextBytes: after.summary.totalBytes > 0 ? sharedByteCount / after.summary.totalBytes : 0,
|
|
143
|
+
layerIds: sharedLayerIds,
|
|
144
|
+
},
|
|
145
|
+
stableLayers,
|
|
146
|
+
changedLayers,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function canonicalToolDefinitions(tools) {
|
|
150
|
+
return [...tools]
|
|
151
|
+
.map(tool => ({
|
|
152
|
+
name: tool.name,
|
|
153
|
+
description: tool.description,
|
|
154
|
+
parameters: tool.parameters,
|
|
155
|
+
}))
|
|
156
|
+
.sort((left, right) => left.name.localeCompare(right.name) || left.description.localeCompare(right.description));
|
|
157
|
+
}
|
|
158
|
+
function messageLayerIds(trace) {
|
|
159
|
+
return trace.layers.flatMap(layer => Array.from({ length: layer.messageCount }, () => layer.id));
|
|
160
|
+
}
|
|
161
|
+
function unitForText(kind, text, tokenEstimator, tokenEstimateProfile, layerId) {
|
|
162
|
+
return {
|
|
163
|
+
kind,
|
|
164
|
+
hash: stableHash(text),
|
|
165
|
+
byteCount: Buffer.byteLength(text, 'utf8'),
|
|
166
|
+
tokenCount: tokenEstimator.estimateText({ text, profile: tokenEstimateProfile }).tokens,
|
|
167
|
+
...(layerId ? { layerId } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function layerIndexAndId(trace, layerId) {
|
|
171
|
+
const index = trace.layers.findIndex(layer => layer.id === layerId);
|
|
172
|
+
return index >= 0 ? { index, id: layerId } : undefined;
|
|
173
|
+
}
|
|
174
|
+
function changedLayerReports(before, after) {
|
|
175
|
+
const beforeById = new Map(before.layers.map(layer => [layer.id, layer]));
|
|
176
|
+
return after.layers
|
|
177
|
+
.filter(layer => {
|
|
178
|
+
const previous = beforeById.get(layer.id);
|
|
179
|
+
return !previous || previous.included !== layer.included || previous.hash !== layer.hash;
|
|
180
|
+
})
|
|
181
|
+
.map(layer => {
|
|
182
|
+
const previous = beforeById.get(layer.id);
|
|
183
|
+
return {
|
|
184
|
+
id: layer.id,
|
|
185
|
+
...(previous?.hash ? { beforeHash: previous.hash } : {}),
|
|
186
|
+
...(layer.hash ? { afterHash: layer.hash } : {}),
|
|
187
|
+
includedBefore: previous?.included ?? false,
|
|
188
|
+
includedAfter: layer.included,
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const benchmarkToolDefinitions = [
|
|
193
|
+
{
|
|
194
|
+
name: 'read_file',
|
|
195
|
+
description: 'Read a local file.',
|
|
196
|
+
parameters: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
path: { type: 'string' },
|
|
200
|
+
},
|
|
201
|
+
required: ['path'],
|
|
202
|
+
additionalProperties: false,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'search_text',
|
|
207
|
+
description: 'Search text in local files.',
|
|
208
|
+
parameters: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
query: { type: 'string' },
|
|
212
|
+
},
|
|
213
|
+
required: ['query'],
|
|
214
|
+
additionalProperties: false,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
];
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { stableHash, stableStringify } from './stableHash.js';
|
|
2
|
+
export function latestCacheDiagnostic(events) {
|
|
3
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
4
|
+
const event = events[index];
|
|
5
|
+
if (event?.type === 'cache_diagnostic') {
|
|
6
|
+
return event;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
export function buildCacheDiagnosticReport(events) {
|
|
12
|
+
const diagnostics = events.filter((event) => event.type === 'cache_diagnostic');
|
|
13
|
+
if (diagnostics.length === 0) {
|
|
14
|
+
return emptyCacheDiagnosticReport();
|
|
15
|
+
}
|
|
16
|
+
const missReasons = new Map();
|
|
17
|
+
const changedLayers = new Map();
|
|
18
|
+
let inputTokens = 0;
|
|
19
|
+
let cachedInputTokens = 0;
|
|
20
|
+
let outputTokens = 0;
|
|
21
|
+
let totalTokens = 0;
|
|
22
|
+
let toolSchemaChanges = 0;
|
|
23
|
+
let prefixChanges = 0;
|
|
24
|
+
for (const diagnostic of diagnostics) {
|
|
25
|
+
inputTokens += diagnostic.usage.inputTokens;
|
|
26
|
+
cachedInputTokens += diagnostic.usage.cachedInputTokens;
|
|
27
|
+
outputTokens += diagnostic.usage.outputTokens;
|
|
28
|
+
totalTokens += diagnostic.usage.totalTokens;
|
|
29
|
+
missReasons.set(diagnostic.inference.missReason, (missReasons.get(diagnostic.inference.missReason) ?? 0) + 1);
|
|
30
|
+
if (diagnostic.inference.toolSchemaChanged) {
|
|
31
|
+
toolSchemaChanges += 1;
|
|
32
|
+
}
|
|
33
|
+
if (diagnostic.inference.prefixChanged) {
|
|
34
|
+
prefixChanges += 1;
|
|
35
|
+
}
|
|
36
|
+
for (const layer of diagnostic.inference.changedLayers) {
|
|
37
|
+
changedLayers.set(layer, (changedLayers.get(layer) ?? 0) + 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const latest = diagnostics.at(-1);
|
|
41
|
+
const totalInputTokens = inputTokens + cachedInputTokens;
|
|
42
|
+
return {
|
|
43
|
+
diagnosticCount: diagnostics.length,
|
|
44
|
+
inputTokens,
|
|
45
|
+
cachedInputTokens,
|
|
46
|
+
outputTokens,
|
|
47
|
+
totalTokens,
|
|
48
|
+
totalInputTokens,
|
|
49
|
+
cacheHitRate: totalInputTokens > 0 ? cachedInputTokens / totalInputTokens : 0,
|
|
50
|
+
missReasons: reasonCountEntries(missReasons),
|
|
51
|
+
changedLayers: idCountEntries(changedLayers),
|
|
52
|
+
toolSchemaChanges,
|
|
53
|
+
prefixChanges,
|
|
54
|
+
...(latest
|
|
55
|
+
? {
|
|
56
|
+
latest: {
|
|
57
|
+
turn: latest.request.turn,
|
|
58
|
+
model: latest.model.displayName ?? latest.model.alias,
|
|
59
|
+
missReason: latest.inference.missReason,
|
|
60
|
+
changedLayers: latest.inference.changedLayers,
|
|
61
|
+
toolSchemaChanged: latest.inference.toolSchemaChanged,
|
|
62
|
+
prefixChanged: latest.inference.prefixChanged,
|
|
63
|
+
prefixHash: latest.request.prefixHash,
|
|
64
|
+
toolSchemaHash: latest.request.toolSchemaHash,
|
|
65
|
+
toolSchemaTokens: latest.request.toolSchemaTokens,
|
|
66
|
+
toolCount: latest.request.toolCount,
|
|
67
|
+
toolNames: latest.request.toolNames,
|
|
68
|
+
cacheHitRate: latest.usage.cacheHitRate,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function formatCacheDiagnosticStatus(report, language = 'en') {
|
|
75
|
+
if (report.diagnosticCount === 0 || !report.latest) {
|
|
76
|
+
return language === 'zh-Hans' ? '暂无诊断' : 'no diagnostics yet';
|
|
77
|
+
}
|
|
78
|
+
const latest = report.latest;
|
|
79
|
+
const parts = language === 'zh-Hans'
|
|
80
|
+
? [
|
|
81
|
+
`命中 ${formatPercent(report.cacheHitRate)}`,
|
|
82
|
+
`cached ${formatTokenCount(report.cachedInputTokens)} / input ${formatTokenCount(report.totalInputTokens)}`,
|
|
83
|
+
`最近 ${formatMissReason(latest.missReason, language)}`,
|
|
84
|
+
]
|
|
85
|
+
: [
|
|
86
|
+
`${formatPercent(report.cacheHitRate)} hit`,
|
|
87
|
+
`cached ${formatTokenCount(report.cachedInputTokens)} / input ${formatTokenCount(report.totalInputTokens)}`,
|
|
88
|
+
`last ${formatMissReason(latest.missReason, language)}`,
|
|
89
|
+
];
|
|
90
|
+
if (latest.changedLayers.length > 0) {
|
|
91
|
+
parts.push(language === 'zh-Hans' ? `层 ${latest.changedLayers.join(',')}` : `changed ${latest.changedLayers.join(',')}`);
|
|
92
|
+
}
|
|
93
|
+
if (latest.toolSchemaChanged) {
|
|
94
|
+
parts.push(language === 'zh-Hans' ? '工具 schema 变更' : 'tools changed');
|
|
95
|
+
}
|
|
96
|
+
if (latest.prefixChanged) {
|
|
97
|
+
parts.push(language === 'zh-Hans' ? 'prefix 变更' : 'prefix changed');
|
|
98
|
+
}
|
|
99
|
+
return parts.join(' · ');
|
|
100
|
+
}
|
|
101
|
+
export function buildCacheDiagnostic(options) {
|
|
102
|
+
const canonicalTools = canonicalToolDefinitions(options.toolDefinitions);
|
|
103
|
+
const toolSchemaText = stableStringify(canonicalTools);
|
|
104
|
+
const toolSchemaHash = stableHash(canonicalTools);
|
|
105
|
+
const messageShape = options.messages.map(cacheMessageShape);
|
|
106
|
+
const messageHash = stableHash(messageShape);
|
|
107
|
+
const prefixHash = stableHash({ messages: messageShape, tools: canonicalTools });
|
|
108
|
+
const cachedInputTokens = options.usage.cachedInputTokens ?? 0;
|
|
109
|
+
const inputTokens = options.usage.inputTokens;
|
|
110
|
+
const inputDenominator = inputTokens + cachedInputTokens;
|
|
111
|
+
const toolNames = canonicalTools.map(tool => tool.name);
|
|
112
|
+
const layers = options.contextTrace.layers.map(layer => ({
|
|
113
|
+
id: layer.id,
|
|
114
|
+
included: layer.included,
|
|
115
|
+
...(layer.hash ? { hash: layer.hash } : {}),
|
|
116
|
+
tokenCount: layer.tokenCount,
|
|
117
|
+
messageCount: layer.messageCount,
|
|
118
|
+
}));
|
|
119
|
+
const changedLayers = changedLayerIds(options.previous, layers);
|
|
120
|
+
const toolSchemaChanged = options.previous ? options.previous.request.toolSchemaHash !== toolSchemaHash : false;
|
|
121
|
+
const prefixChanged = options.previous ? options.previous.request.prefixHash !== prefixHash : false;
|
|
122
|
+
return {
|
|
123
|
+
type: 'cache_diagnostic',
|
|
124
|
+
model: {
|
|
125
|
+
profile: options.model.profile,
|
|
126
|
+
role: options.model.role,
|
|
127
|
+
alias: options.model.alias,
|
|
128
|
+
displayName: options.model.displayName,
|
|
129
|
+
provider: options.model.providerAlias,
|
|
130
|
+
model: options.model.model,
|
|
131
|
+
...(options.model.effort ? { effort: options.model.effort } : {}),
|
|
132
|
+
},
|
|
133
|
+
request: {
|
|
134
|
+
turn: options.turn,
|
|
135
|
+
messageCount: options.messages.length,
|
|
136
|
+
prefixHash,
|
|
137
|
+
messageHash,
|
|
138
|
+
toolSchemaHash,
|
|
139
|
+
toolSchemaTokens: options.tokenEstimator.estimateText({ text: toolSchemaText, profile: options.tokenEstimateProfile }).tokens,
|
|
140
|
+
toolCount: canonicalTools.length,
|
|
141
|
+
toolNames,
|
|
142
|
+
},
|
|
143
|
+
layers,
|
|
144
|
+
usage: {
|
|
145
|
+
inputTokens,
|
|
146
|
+
cachedInputTokens,
|
|
147
|
+
outputTokens: options.usage.outputTokens,
|
|
148
|
+
totalTokens: Math.max(options.usage.totalTokens, inputTokens + cachedInputTokens + options.usage.outputTokens),
|
|
149
|
+
cacheHitRate: inputDenominator > 0 ? cachedInputTokens / inputDenominator : 0,
|
|
150
|
+
},
|
|
151
|
+
inference: {
|
|
152
|
+
inferred: true,
|
|
153
|
+
missReason: inferMissReason({
|
|
154
|
+
missTokens: inputTokens,
|
|
155
|
+
...(options.previous ? { previous: options.previous } : {}),
|
|
156
|
+
changedLayers,
|
|
157
|
+
toolSchemaChanged,
|
|
158
|
+
prefixChanged,
|
|
159
|
+
}),
|
|
160
|
+
changedLayers,
|
|
161
|
+
toolSchemaChanged,
|
|
162
|
+
prefixChanged,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function canonicalToolDefinitions(tools) {
|
|
167
|
+
return [...tools]
|
|
168
|
+
.map(tool => ({
|
|
169
|
+
name: tool.name,
|
|
170
|
+
description: tool.description,
|
|
171
|
+
parameters: tool.parameters,
|
|
172
|
+
}))
|
|
173
|
+
.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
174
|
+
}
|
|
175
|
+
function cacheMessageShape(message) {
|
|
176
|
+
if (message.role === 'tool') {
|
|
177
|
+
return {
|
|
178
|
+
role: 'tool',
|
|
179
|
+
toolCallId: message.toolCallId,
|
|
180
|
+
contentHash: stableHash(message.content),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
|
|
184
|
+
return {
|
|
185
|
+
role: 'assistant',
|
|
186
|
+
contentHash: stableHash(message.content),
|
|
187
|
+
...(message.reasoningContent ? { reasoningHash: stableHash(message.reasoningContent) } : {}),
|
|
188
|
+
toolCallsHash: stableHash(message.toolCalls),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
role: message.role,
|
|
193
|
+
contentHash: stableHash(message.content),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function changedLayerIds(previous, currentLayers) {
|
|
197
|
+
if (!previous) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
const previousById = new Map(previous.layers.map(layer => [layer.id, layer]));
|
|
201
|
+
return currentLayers
|
|
202
|
+
.filter(layer => {
|
|
203
|
+
const before = previousById.get(layer.id);
|
|
204
|
+
return !before || before.hash !== layer.hash || before.included !== layer.included;
|
|
205
|
+
})
|
|
206
|
+
.map(layer => layer.id);
|
|
207
|
+
}
|
|
208
|
+
function inferMissReason(options) {
|
|
209
|
+
if (options.missTokens <= 0) {
|
|
210
|
+
return 'no-miss';
|
|
211
|
+
}
|
|
212
|
+
if (!options.previous) {
|
|
213
|
+
return 'cold-start';
|
|
214
|
+
}
|
|
215
|
+
if (options.toolSchemaChanged) {
|
|
216
|
+
return 'tool-schema-changed';
|
|
217
|
+
}
|
|
218
|
+
if (options.changedLayers.length > 0) {
|
|
219
|
+
return 'layer-changed';
|
|
220
|
+
}
|
|
221
|
+
if (!options.prefixChanged) {
|
|
222
|
+
return 'prefix-stable-provider-side';
|
|
223
|
+
}
|
|
224
|
+
return 'unknown';
|
|
225
|
+
}
|
|
226
|
+
function emptyCacheDiagnosticReport() {
|
|
227
|
+
return {
|
|
228
|
+
diagnosticCount: 0,
|
|
229
|
+
inputTokens: 0,
|
|
230
|
+
cachedInputTokens: 0,
|
|
231
|
+
outputTokens: 0,
|
|
232
|
+
totalTokens: 0,
|
|
233
|
+
totalInputTokens: 0,
|
|
234
|
+
cacheHitRate: 0,
|
|
235
|
+
missReasons: [],
|
|
236
|
+
changedLayers: [],
|
|
237
|
+
toolSchemaChanges: 0,
|
|
238
|
+
prefixChanges: 0,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function reasonCountEntries(counts) {
|
|
242
|
+
const orderByValue = new Map(reasonOrder.map((value, index) => [value, index]));
|
|
243
|
+
const entries = [...counts.entries()].sort(([leftId, leftCount], [rightId, rightCount]) => {
|
|
244
|
+
if (leftCount !== rightCount) {
|
|
245
|
+
return rightCount - leftCount;
|
|
246
|
+
}
|
|
247
|
+
const leftOrder = orderByValue.get(leftId) ?? Number.MAX_SAFE_INTEGER;
|
|
248
|
+
const rightOrder = orderByValue.get(rightId) ?? Number.MAX_SAFE_INTEGER;
|
|
249
|
+
if (leftOrder !== rightOrder) {
|
|
250
|
+
return leftOrder - rightOrder;
|
|
251
|
+
}
|
|
252
|
+
return leftId < rightId ? -1 : leftId > rightId ? 1 : 0;
|
|
253
|
+
});
|
|
254
|
+
return entries.map(([reason, count]) => ({ reason, count }));
|
|
255
|
+
}
|
|
256
|
+
function idCountEntries(counts) {
|
|
257
|
+
const entries = [...counts.entries()].sort(([leftId, leftCount], [rightId, rightCount]) => {
|
|
258
|
+
if (leftCount !== rightCount) {
|
|
259
|
+
return rightCount - leftCount;
|
|
260
|
+
}
|
|
261
|
+
return leftId < rightId ? -1 : leftId > rightId ? 1 : 0;
|
|
262
|
+
});
|
|
263
|
+
return entries.map(([id, count]) => ({ id, count }));
|
|
264
|
+
}
|
|
265
|
+
const reasonOrder = [
|
|
266
|
+
'no-miss',
|
|
267
|
+
'cold-start',
|
|
268
|
+
'tool-schema-changed',
|
|
269
|
+
'layer-changed',
|
|
270
|
+
'prefix-stable-provider-side',
|
|
271
|
+
'unknown',
|
|
272
|
+
];
|
|
273
|
+
function formatMissReason(reason, language) {
|
|
274
|
+
const labels = {
|
|
275
|
+
'no-miss': { en: 'no miss', 'zh-Hans': '无 miss' },
|
|
276
|
+
'cold-start': { en: 'cold start', 'zh-Hans': '冷启动' },
|
|
277
|
+
'tool-schema-changed': { en: 'tool schema changed', 'zh-Hans': '工具 schema 变化' },
|
|
278
|
+
'layer-changed': { en: 'layer changed', 'zh-Hans': '上下文层变化' },
|
|
279
|
+
'prefix-stable-provider-side': { en: 'provider-side miss', 'zh-Hans': 'prefix 稳定但 provider miss' },
|
|
280
|
+
unknown: { en: 'unknown', 'zh-Hans': '未知' },
|
|
281
|
+
};
|
|
282
|
+
return labels[reason][language];
|
|
283
|
+
}
|
|
284
|
+
function formatPercent(value) {
|
|
285
|
+
const percent = value * 100;
|
|
286
|
+
const precision = percent >= 99 && percent < 100 ? 1 : 0;
|
|
287
|
+
return `${percent.toFixed(precision).replace(/\.0$/, '')}%`;
|
|
288
|
+
}
|
|
289
|
+
function formatTokenCount(value) {
|
|
290
|
+
if (value >= 1_000_000) {
|
|
291
|
+
return `${trimFixed(value / 1_000_000)}m`;
|
|
292
|
+
}
|
|
293
|
+
if (value >= 1_000) {
|
|
294
|
+
return `${trimFixed(value / 1_000)}k`;
|
|
295
|
+
}
|
|
296
|
+
return String(value);
|
|
297
|
+
}
|
|
298
|
+
function trimFixed(value) {
|
|
299
|
+
return value.toFixed(value >= 10 ? 0 : 1).replace(/\.0$/, '');
|
|
300
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHeuristicTokenEstimator } from '@remi/llm';
|
|
2
|
+
import { stableHash } from './stableHash.js';
|
|
2
3
|
const defaultTokenEstimator = createHeuristicTokenEstimator();
|
|
3
4
|
// Global "收口": per-layer budgets are allocated with floors, so for a small
|
|
4
5
|
// effective input budget their sum can exceed the budget. Shrink low-priority
|
|
@@ -221,6 +222,17 @@ export function buildProviderContext(options) {
|
|
|
221
222
|
tokenCount: 0,
|
|
222
223
|
}, charBudgets.compact, tokenBudgets.compact);
|
|
223
224
|
}
|
|
225
|
+
addMessageLayer({
|
|
226
|
+
id: 'recent',
|
|
227
|
+
source: '~/.remi sessions recent user/assistant transcript',
|
|
228
|
+
messages,
|
|
229
|
+
layers,
|
|
230
|
+
layerMessages: applyMessagesBudget(options.recentMessages ?? [], charBudgets.recent, tokenBudgets.recent, tokenEstimator, tokenEstimateProfile),
|
|
231
|
+
budgetChars: charBudgets.recent,
|
|
232
|
+
budgetTokens: tokenBudgets.recent,
|
|
233
|
+
tokenEstimator,
|
|
234
|
+
tokenEstimateProfile,
|
|
235
|
+
});
|
|
224
236
|
if (options.runtimePrompt && options.runtimePrompt.trim().length > 0) {
|
|
225
237
|
addMessageLayer({
|
|
226
238
|
id: 'runtime',
|
|
@@ -249,17 +261,6 @@ export function buildProviderContext(options) {
|
|
|
249
261
|
tokenCount: 0,
|
|
250
262
|
}, charBudgets.runtime, tokenBudgets.runtime);
|
|
251
263
|
}
|
|
252
|
-
addMessageLayer({
|
|
253
|
-
id: 'recent',
|
|
254
|
-
source: '~/.remi sessions recent user/assistant transcript',
|
|
255
|
-
messages,
|
|
256
|
-
layers,
|
|
257
|
-
layerMessages: applyMessagesBudget(options.recentMessages ?? [], charBudgets.recent, tokenBudgets.recent, tokenEstimator, tokenEstimateProfile),
|
|
258
|
-
budgetChars: charBudgets.recent,
|
|
259
|
-
budgetTokens: tokenBudgets.recent,
|
|
260
|
-
tokenEstimator,
|
|
261
|
-
tokenEstimateProfile,
|
|
262
|
-
});
|
|
263
264
|
const currentLayerMessages = [];
|
|
264
265
|
if (options.currentTurnPrompt && options.currentTurnPrompt.trim().length > 0) {
|
|
265
266
|
currentLayerMessages.push({
|
|
@@ -323,6 +324,7 @@ function addMessageLayer(options) {
|
|
|
323
324
|
messageCount: options.layerMessages.length,
|
|
324
325
|
charCount: content.length,
|
|
325
326
|
tokenCount: estimateTextTokens(content, options.tokenEstimator, options.tokenEstimateProfile),
|
|
327
|
+
...(included ? { hash: stableHash(layerMessages.map(message => ({ role: message.role, content: message.content }))) } : {}),
|
|
326
328
|
}, options.budgetChars, options.budgetTokens);
|
|
327
329
|
}
|
|
328
330
|
function addTrace(layers, trace, budgetChars, budgetTokens) {
|