pi-distill 1.2.0 → 1.3.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/README.md +6 -0
- package/README.zh-CN.md +6 -0
- package/locales/fallback-renderer.json +16 -0
- package/locales/index.json +20 -0
- package/package.json +1 -1
- package/src/fallback-renderer.ts +24 -4
- package/src/index.ts +443 -36
- package/src/output-limit.ts +0 -1
- package/src/summary-utils.ts +0 -6
package/README.md
CHANGED
|
@@ -189,6 +189,12 @@ Configuration-file fields take precedence over environment variables. Unspecifie
|
|
|
189
189
|
`/pi-distill` remains available as a compatibility alias.
|
|
190
190
|
| `render.*` | Controls the audit card, prompt preview, and result preview. |
|
|
191
191
|
|
|
192
|
+
## Session statistics
|
|
193
|
+
|
|
194
|
+
Use `/distill:stats` to view distillation statistics for the current Pi session. Statistics are kept in memory, reset when the session starts, and never store raw tool output.
|
|
195
|
+
|
|
196
|
+
The report includes tool-result counts, success/failure/fallback counts, model attempts, original and summary character totals, compression ratio, `chars/4` estimated original/summary tokens, estimated tokens saved, and model-reported input/output/cache/total tokens and cost. Counts use compact `k` and `m` units at 1,000 and 1,000,000; durations use `ms`, `s`, or `min` based on their value. Estimated context tokens are labeled as estimates; usage and cost fields are shown as unavailable when the provider does not return usage data.
|
|
197
|
+
|
|
192
198
|
The main environment variables are `PI_DISTILL_MODEL`, `PI_DISTILL_MIN_CHARS`, `PI_DISTILL_MAX_CHARS`, `PI_DISTILL_MAX_OUTPUT_CHARS`, `PI_DISTILL_TIMEOUT_SECONDS`, `PI_DISTILL_TIMEOUT_RETRY_COUNT`, `PI_DISTILL_ERROR_RETRY_COUNT`, `PI_DISTILL_MISSED_COMPRESSION_RATIO`, and `PI_DISTILL_SUMMARIZE_ERRORS`.
|
|
193
199
|
|
|
194
200
|
## Requirements
|
package/README.zh-CN.md
CHANGED
|
@@ -192,6 +192,12 @@ Agent 消费更适合当前决策的结果,并获得可审计的处理诊断
|
|
|
192
192
|
`/pi-distill` 仍作为兼容别名保留。
|
|
193
193
|
| `render.*` | 控制审计卡片、prompt 预览和结果预览。 |
|
|
194
194
|
|
|
195
|
+
## Session 统计
|
|
196
|
+
|
|
197
|
+
使用 `/distill:stats` 查看当前 Pi 会话的提炼统计。统计只保存在内存中,在会话开始时重置,不保存原始工具输出。
|
|
198
|
+
|
|
199
|
+
统计包括工具结果数量、成功/失败/回退次数、模型尝试次数、原始与摘要字符数、压缩比、按 `chars/4` 估算的原文/摘要 Token、预计节省 Token、提炼实际消耗的 input/output/cache/total Token 和成本。数量达到 1,000 或 1,000,000 时分别使用 `k` 或 `m` 紧凑显示;耗时会根据数值显示为 `ms`、`s` 或 `min`。原文/摘要 Token 是估算值;provider 未返回 usage 时,提炼消耗 Token 或成本字段显示为不可用。
|
|
200
|
+
|
|
195
201
|
主要环境变量包括 `PI_DISTILL_MODEL`、`PI_DISTILL_MIN_CHARS`、`PI_DISTILL_MAX_CHARS`、`PI_DISTILL_MAX_OUTPUT_CHARS`、`PI_DISTILL_TIMEOUT_SECONDS`、`PI_DISTILL_TIMEOUT_RETRY_COUNT`、`PI_DISTILL_ERROR_RETRY_COUNT`、`PI_DISTILL_MISSED_COMPRESSION_RATIO` 和 `PI_DISTILL_SUMMARIZE_ERRORS`。
|
|
196
202
|
|
|
197
203
|
## 要求
|
|
@@ -43,6 +43,22 @@
|
|
|
43
43
|
"zh-CN": "字符",
|
|
44
44
|
"en-US": "chars"
|
|
45
45
|
},
|
|
46
|
+
"tokens": {
|
|
47
|
+
"zh-CN": "Token",
|
|
48
|
+
"en-US": "tokens"
|
|
49
|
+
},
|
|
50
|
+
"compressionTokens": {
|
|
51
|
+
"zh-CN": "压缩消耗",
|
|
52
|
+
"en-US": "compression"
|
|
53
|
+
},
|
|
54
|
+
"tokensSaved": {
|
|
55
|
+
"zh-CN": "节省",
|
|
56
|
+
"en-US": "saved"
|
|
57
|
+
},
|
|
58
|
+
"savedPercent": {
|
|
59
|
+
"zh-CN": "节省",
|
|
60
|
+
"en-US": "saved"
|
|
61
|
+
},
|
|
46
62
|
"tool": {
|
|
47
63
|
"zh-CN": "工具",
|
|
48
64
|
"en-US": "Tool"
|
package/locales/index.json
CHANGED
|
@@ -163,6 +163,14 @@
|
|
|
163
163
|
"zh-CN": "非超时异常",
|
|
164
164
|
"en-US": "a non-timeout error"
|
|
165
165
|
},
|
|
166
|
+
"outputRequestUnavailable": {
|
|
167
|
+
"zh-CN": "无法扩展工具 {tool} 的参数结构;outputRequest 不可用。",
|
|
168
|
+
"en-US": "Could not extend the {tool} parameter schema; outputRequest is unavailable."
|
|
169
|
+
},
|
|
170
|
+
"extendOutputRequestFailed": {
|
|
171
|
+
"zh-CN": "扩展 outputRequest 参数失败:{error}",
|
|
172
|
+
"en-US": "Failed to extend the outputRequest parameter: {error}"
|
|
173
|
+
},
|
|
166
174
|
"thresholdTitle": {
|
|
167
175
|
"zh-CN": "长输出阈值",
|
|
168
176
|
"en-US": "Long-output threshold"
|
|
@@ -171,6 +179,18 @@
|
|
|
171
179
|
"zh-CN": "交互式配置工具输出提炼",
|
|
172
180
|
"en-US": "Configure tool-output distillation interactively"
|
|
173
181
|
},
|
|
182
|
+
"statsCommandDescription": {
|
|
183
|
+
"zh-CN": "查看本次会话的工具输出提炼统计",
|
|
184
|
+
"en-US": "Show tool-output distillation statistics for this session"
|
|
185
|
+
},
|
|
186
|
+
"statsUnavailable": {
|
|
187
|
+
"zh-CN": "不可用",
|
|
188
|
+
"en-US": "n/a"
|
|
189
|
+
},
|
|
190
|
+
"statsReport": {
|
|
191
|
+
"zh-CN": "Distill 会话统计\n工具结果 {toolResults}|成功 {summarizedResults}|回退 {fallbackResults}|失败 {failedResults}\nRAW {rawResults}|跳过 {skippedResults}|非文本 {nonTextResults}\n尝试 {summaryAttempts}|重试 {retryCount}|耗时 {summaryDuration}(平均 {summaryAverageDuration})\n字符:{originalOutputChars} → {summaryChars}({compressionRatio}x)\nToken(估算):{estimatedOriginalOutputTokens} → {estimatedSummaryTokens}(节省 {estimatedTokensSaved})\n模型总 Token:{summaryTotalTokens}(输入 {summaryInputTokens}|输出 {summaryOutputTokens}|推理 {summaryReasoningTokens})\n缓存 Token:读 {summaryCacheReadTokens}|写 {summaryCacheWriteTokens}\n模型成本:{summaryCost}",
|
|
192
|
+
"en-US": "Distill session statistics\nTool results {toolResults} | summarized {summarizedResults} | fallback {fallbackResults} | failed {failedResults}\nRAW {rawResults} | skipped {skippedResults} | non-text {nonTextResults}\nAttempts {summaryAttempts} | retries {retryCount} | time {summaryDuration} (avg {summaryAverageDuration})\nChars: {originalOutputChars} → {summaryChars} ({compressionRatio}x)\nTokens (estimated): {estimatedOriginalOutputTokens} → {estimatedSummaryTokens} (saved {estimatedTokensSaved})\nModel total tokens: {summaryTotalTokens} (input {summaryInputTokens} | output {summaryOutputTokens} | reasoning {summaryReasoningTokens})\nCache tokens: read {summaryCacheReadTokens} | write {summaryCacheWriteTokens}\nModel cost: {summaryCost}"
|
|
193
|
+
},
|
|
174
194
|
"outputRequest": {
|
|
175
195
|
"zh-CN": "outputRequest",
|
|
176
196
|
"en-US": "outputRequest"
|
package/package.json
CHANGED
package/src/fallback-renderer.ts
CHANGED
|
@@ -56,6 +56,13 @@ function formatCount(value: number): string {
|
|
|
56
56
|
return Math.round(value).toLocaleString("en-US");
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function formatCompactCount(value: number): string {
|
|
60
|
+
const rounded = Math.round(value);
|
|
61
|
+
if (rounded >= 1_000_000) return `${(rounded / 1_000_000).toFixed(1).replace(/\.0$/, "")}m`;
|
|
62
|
+
if (rounded >= 1_000) return `${(rounded / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
63
|
+
return String(rounded);
|
|
64
|
+
}
|
|
65
|
+
|
|
59
66
|
function renderDistillAuditLine(audit: DistillAuditView, line: string, index: number, theme: RenderTheme): string {
|
|
60
67
|
if (index === 0) {
|
|
61
68
|
const title = theme.fg("accent", theme.bold("◇ Distill"));
|
|
@@ -191,6 +198,10 @@ export function buildDistillAuditLines(
|
|
|
191
198
|
const error = getString(details.outputSummaryError);
|
|
192
199
|
const originalChars = getFiniteNumber(details.originalOutputChars);
|
|
193
200
|
const summaryChars = getFiniteNumber(details.summaryChars);
|
|
201
|
+
const estimatedOriginalTokens = getFiniteNumber(details.estimatedOriginalOutputTokens);
|
|
202
|
+
const estimatedSummaryTokens = getFiniteNumber(details.estimatedSummaryTokens);
|
|
203
|
+
const estimatedTokensSaved = getFiniteNumber(details.estimatedTokensSaved);
|
|
204
|
+
const summaryTotalTokens = getFiniteNumber(details.summaryTotalTokens);
|
|
194
205
|
const compressionRatio = getFiniteNumber(details.compressionRatio);
|
|
195
206
|
const compressionSavedPercent = getFiniteNumber(details.compressionSavedPercent);
|
|
196
207
|
const toolExecutionMs = getFiniteNumber(details.toolExecutionMs);
|
|
@@ -201,14 +212,23 @@ export function buildDistillAuditLines(
|
|
|
201
212
|
const statusView = statusViews[status] ?? { label: status, tone: "muted" as const };
|
|
202
213
|
const metrics: string[] = [];
|
|
203
214
|
|
|
204
|
-
if (
|
|
215
|
+
if (estimatedOriginalTokens !== undefined && estimatedSummaryTokens !== undefined) {
|
|
216
|
+
const tokenMetric = `≈${formatCompactCount(estimatedOriginalTokens)} → ${formatCompactCount(estimatedSummaryTokens)} ${i18n.t("tokens")}`;
|
|
217
|
+
const savedMetric = estimatedTokensSaved !== undefined && estimatedTokensSaved > 0
|
|
218
|
+
? `(${i18n.t("tokensSaved")} ≈${formatCompactCount(estimatedTokensSaved)}${compressionSavedPercent !== undefined ? `,${compressionSavedPercent.toFixed(1)}%` : ""})`
|
|
219
|
+
: "";
|
|
220
|
+
metrics.push(`${tokenMetric}${savedMetric}`);
|
|
221
|
+
} else if (originalChars !== undefined && summaryChars !== undefined) {
|
|
205
222
|
metrics.push(`${formatCount(originalChars)} → ${formatCount(summaryChars)} ${i18n.t("chars")}`);
|
|
223
|
+
if (compressionRatio !== undefined) metrics.push(`${compressionRatio.toFixed(2)}×`);
|
|
224
|
+
if (compressionSavedPercent !== undefined) {
|
|
225
|
+
metrics.push(`${compressionSavedPercent.toFixed(1)}% ${i18n.t("savedPercent")}`);
|
|
226
|
+
}
|
|
206
227
|
} else if (originalChars !== undefined) {
|
|
207
228
|
metrics.push(`${formatCount(originalChars)} ${i18n.t("chars")}`);
|
|
208
229
|
}
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
metrics.push(`${compressionSavedPercent.toFixed(1)}% saved`);
|
|
230
|
+
if (summaryTotalTokens !== undefined && summaryTotalTokens > 0) {
|
|
231
|
+
metrics.push(`${i18n.t("compressionTokens")} ${formatCompactCount(summaryTotalTokens)} ${i18n.t("tokens")}`);
|
|
212
232
|
}
|
|
213
233
|
if (toolExecutionMs !== undefined && toolExecutionMs >= 50) {
|
|
214
234
|
metrics.push(`${i18n.t("tool")} ${formatDuration(toolExecutionMs)}`);
|
package/src/index.ts
CHANGED
|
@@ -123,19 +123,69 @@ type SummaryDecision = {
|
|
|
123
123
|
reason: string;
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
+
type SummaryUsage = {
|
|
127
|
+
input?: number;
|
|
128
|
+
output?: number;
|
|
129
|
+
reasoning?: number;
|
|
130
|
+
cacheRead?: number;
|
|
131
|
+
cacheWrite?: number;
|
|
132
|
+
totalTokens?: number;
|
|
133
|
+
cost?: {
|
|
134
|
+
input?: number;
|
|
135
|
+
output?: number;
|
|
136
|
+
reasoning?: number;
|
|
137
|
+
cacheRead?: number;
|
|
138
|
+
cacheWrite?: number;
|
|
139
|
+
total?: number;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
126
143
|
type SummaryResult = {
|
|
127
144
|
text: string;
|
|
128
145
|
summaryChars: number;
|
|
129
146
|
summaryFilePath?: string;
|
|
130
147
|
summaryModel: string;
|
|
131
148
|
decision: SummaryDecision;
|
|
149
|
+
usage?: SummaryUsage;
|
|
150
|
+
attempts?: number;
|
|
132
151
|
};
|
|
133
152
|
|
|
134
153
|
type SummaryCompletion = (...args: Parameters<typeof complete>) => ReturnType<typeof complete>;
|
|
154
|
+
type DistillWarningReporter = (message: string) => void;
|
|
155
|
+
|
|
156
|
+
class SummaryAttemptError extends Error {
|
|
157
|
+
constructor(message: string, readonly usage?: SummaryUsage) {
|
|
158
|
+
super(message);
|
|
159
|
+
this.name = "SummaryAttemptError";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class SummaryRetryError extends Error {
|
|
164
|
+
constructor(
|
|
165
|
+
message: string,
|
|
166
|
+
readonly attempts: number,
|
|
167
|
+
readonly usage?: SummaryUsage,
|
|
168
|
+
) {
|
|
169
|
+
super(message);
|
|
170
|
+
this.name = "SummaryRetryError";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
135
173
|
|
|
136
174
|
type SummaryDiagnostics = {
|
|
137
175
|
toolExecutionMs?: number;
|
|
138
176
|
summaryDurationMs?: number;
|
|
177
|
+
summaryAttempts?: number;
|
|
178
|
+
summaryInputTokens?: number;
|
|
179
|
+
summaryOutputTokens?: number;
|
|
180
|
+
summaryReasoningTokens?: number;
|
|
181
|
+
summaryCacheReadTokens?: number;
|
|
182
|
+
summaryCacheWriteTokens?: number;
|
|
183
|
+
summaryTotalTokens?: number;
|
|
184
|
+
summaryCost?: number;
|
|
185
|
+
/** 原始输出与最终摘要的 Token 数是按 Pi 的 chars/4 规则估算的。 */
|
|
186
|
+
estimatedOriginalOutputTokens?: number;
|
|
187
|
+
estimatedSummaryTokens?: number;
|
|
188
|
+
estimatedTokensSaved?: number;
|
|
139
189
|
outputSummaryIntent?: string;
|
|
140
190
|
outputSummaryPrompt?: string;
|
|
141
191
|
outputSummaryRender?: DistillRenderConfig;
|
|
@@ -158,6 +208,278 @@ type SummaryDiagnostics = {
|
|
|
158
208
|
missedCompressionRatio?: number;
|
|
159
209
|
};
|
|
160
210
|
|
|
211
|
+
function toFiniteNumber(value: unknown): number | undefined {
|
|
212
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function normalizeSummaryUsage(value: unknown): SummaryUsage | undefined {
|
|
216
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
217
|
+
const record = value as Record<string, unknown>;
|
|
218
|
+
const usage: SummaryUsage = {
|
|
219
|
+
input: toFiniteNumber(record.input),
|
|
220
|
+
output: toFiniteNumber(record.output),
|
|
221
|
+
reasoning: toFiniteNumber(record.reasoning),
|
|
222
|
+
cacheRead: toFiniteNumber(record.cacheRead),
|
|
223
|
+
cacheWrite: toFiniteNumber(record.cacheWrite),
|
|
224
|
+
totalTokens: toFiniteNumber(record.totalTokens),
|
|
225
|
+
};
|
|
226
|
+
if (record.cost && typeof record.cost === "object" && !Array.isArray(record.cost)) {
|
|
227
|
+
const cost = record.cost as Record<string, unknown>;
|
|
228
|
+
usage.cost = {
|
|
229
|
+
input: toFiniteNumber(cost.input),
|
|
230
|
+
output: toFiniteNumber(cost.output),
|
|
231
|
+
reasoning: toFiniteNumber(cost.reasoning),
|
|
232
|
+
cacheRead: toFiniteNumber(cost.cacheRead),
|
|
233
|
+
cacheWrite: toFiniteNumber(cost.cacheWrite),
|
|
234
|
+
total: toFiniteNumber(cost.total),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return Object.values(usage).some((entry) => typeof entry === "number") || usage.cost !== undefined
|
|
238
|
+
? usage
|
|
239
|
+
: undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function mergeSummaryUsage(first: SummaryUsage | undefined, second: SummaryUsage | undefined): SummaryUsage | undefined {
|
|
243
|
+
if (!first && !second) return undefined;
|
|
244
|
+
const merged: SummaryUsage = {};
|
|
245
|
+
for (const key of ["input", "output", "reasoning", "cacheRead", "cacheWrite", "totalTokens"] as const) {
|
|
246
|
+
const value = (first?.[key] ?? 0) + (second?.[key] ?? 0);
|
|
247
|
+
if ((first?.[key] !== undefined || second?.[key] !== undefined) && Number.isFinite(value)) {
|
|
248
|
+
merged[key] = value;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (first?.cost || second?.cost) {
|
|
252
|
+
merged.cost = {};
|
|
253
|
+
for (const key of ["input", "output", "reasoning", "cacheRead", "cacheWrite", "total"] as const) {
|
|
254
|
+
const value = (first?.cost?.[key] ?? 0) + (second?.cost?.[key] ?? 0);
|
|
255
|
+
if ((first?.cost?.[key] !== undefined || second?.cost?.[key] !== undefined) && Number.isFinite(value)) {
|
|
256
|
+
merged.cost[key] = value;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return merged;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getSummaryUsageDiagnostics(usage: SummaryUsage | undefined): Pick<
|
|
264
|
+
SummaryDiagnostics,
|
|
265
|
+
"summaryInputTokens" | "summaryOutputTokens" | "summaryReasoningTokens" | "summaryCacheReadTokens" | "summaryCacheWriteTokens" | "summaryTotalTokens" | "summaryCost"
|
|
266
|
+
> {
|
|
267
|
+
return {
|
|
268
|
+
summaryInputTokens: usage?.input,
|
|
269
|
+
summaryOutputTokens: usage?.output,
|
|
270
|
+
summaryReasoningTokens: usage?.reasoning,
|
|
271
|
+
summaryCacheReadTokens: usage?.cacheRead,
|
|
272
|
+
summaryCacheWriteTokens: usage?.cacheWrite,
|
|
273
|
+
summaryTotalTokens: usage?.totalTokens,
|
|
274
|
+
summaryCost: usage?.cost?.total,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const ESTIMATED_CHARS_PER_TOKEN = 4;
|
|
279
|
+
|
|
280
|
+
function estimateTextTokens(text: string): number {
|
|
281
|
+
return Math.ceil(text.length / ESTIMATED_CHARS_PER_TOKEN);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function getTokenCompressionDiagnostics(
|
|
285
|
+
originalOutput: string,
|
|
286
|
+
finalOutput: string,
|
|
287
|
+
): Pick<SummaryDiagnostics, "estimatedOriginalOutputTokens" | "estimatedSummaryTokens" | "estimatedTokensSaved"> {
|
|
288
|
+
const estimatedOriginalOutputTokens = estimateTextTokens(originalOutput);
|
|
289
|
+
const estimatedSummaryTokens = estimateTextTokens(finalOutput);
|
|
290
|
+
return {
|
|
291
|
+
estimatedOriginalOutputTokens,
|
|
292
|
+
estimatedSummaryTokens,
|
|
293
|
+
estimatedTokensSaved: Math.max(0, estimatedOriginalOutputTokens - estimatedSummaryTokens),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
type DistillSessionStats = {
|
|
298
|
+
startedAt: number;
|
|
299
|
+
toolResults: number;
|
|
300
|
+
summarizedResults: number;
|
|
301
|
+
fallbackResults: number;
|
|
302
|
+
failedResults: number;
|
|
303
|
+
rawResults: number;
|
|
304
|
+
skippedResults: number;
|
|
305
|
+
nonTextResults: number;
|
|
306
|
+
summaryAttempts: number;
|
|
307
|
+
retryCount: number;
|
|
308
|
+
originalOutputChars: number;
|
|
309
|
+
summaryChars: number;
|
|
310
|
+
summaryDurationMs: number;
|
|
311
|
+
summaryInputTokens: number;
|
|
312
|
+
summaryOutputTokens: number;
|
|
313
|
+
summaryReasoningTokens: number;
|
|
314
|
+
summaryCacheReadTokens: number;
|
|
315
|
+
summaryCacheWriteTokens: number;
|
|
316
|
+
summaryTotalTokens: number;
|
|
317
|
+
hasSummaryTokenUsage: boolean;
|
|
318
|
+
estimatedOriginalOutputTokens: number;
|
|
319
|
+
estimatedSummaryTokens: number;
|
|
320
|
+
estimatedTokensSaved: number;
|
|
321
|
+
summaryCost: number;
|
|
322
|
+
hasSummaryCost: boolean;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
function createDistillSessionStats(): DistillSessionStats {
|
|
326
|
+
return {
|
|
327
|
+
startedAt: Date.now(),
|
|
328
|
+
toolResults: 0,
|
|
329
|
+
summarizedResults: 0,
|
|
330
|
+
fallbackResults: 0,
|
|
331
|
+
failedResults: 0,
|
|
332
|
+
rawResults: 0,
|
|
333
|
+
skippedResults: 0,
|
|
334
|
+
nonTextResults: 0,
|
|
335
|
+
summaryAttempts: 0,
|
|
336
|
+
retryCount: 0,
|
|
337
|
+
originalOutputChars: 0,
|
|
338
|
+
summaryChars: 0,
|
|
339
|
+
summaryDurationMs: 0,
|
|
340
|
+
summaryInputTokens: 0,
|
|
341
|
+
summaryOutputTokens: 0,
|
|
342
|
+
summaryReasoningTokens: 0,
|
|
343
|
+
summaryCacheReadTokens: 0,
|
|
344
|
+
summaryCacheWriteTokens: 0,
|
|
345
|
+
summaryTotalTokens: 0,
|
|
346
|
+
hasSummaryTokenUsage: false,
|
|
347
|
+
estimatedOriginalOutputTokens: 0,
|
|
348
|
+
estimatedSummaryTokens: 0,
|
|
349
|
+
estimatedTokensSaved: 0,
|
|
350
|
+
summaryCost: 0,
|
|
351
|
+
hasSummaryCost: false,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function getDetailNumber(details: Record<string, unknown> | undefined, key: string): number | undefined {
|
|
356
|
+
return toFiniteNumber(details?.[key]);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function recordDistillSessionResult(
|
|
360
|
+
stats: DistillSessionStats,
|
|
361
|
+
details: Record<string, unknown> | undefined,
|
|
362
|
+
): void {
|
|
363
|
+
stats.toolResults += 1;
|
|
364
|
+
const status = typeof details?.outputSummaryStatus === "string"
|
|
365
|
+
? details.outputSummaryStatus
|
|
366
|
+
: undefined;
|
|
367
|
+
if (status === "summarized") stats.summarizedResults += 1;
|
|
368
|
+
else if (status === "summary-fallback") stats.fallbackResults += 1;
|
|
369
|
+
else if (status === "summary-failed") stats.failedResults += 1;
|
|
370
|
+
else if (status === "full-output") stats.rawResults += 1;
|
|
371
|
+
else if (status === "non-text-output") stats.nonTextResults += 1;
|
|
372
|
+
else stats.skippedResults += 1;
|
|
373
|
+
|
|
374
|
+
stats.summaryAttempts += getDetailNumber(details, "summaryAttempts") ?? 0;
|
|
375
|
+
const attempts = getDetailNumber(details, "summaryAttempts");
|
|
376
|
+
if (attempts !== undefined) stats.retryCount += Math.max(0, attempts - 1);
|
|
377
|
+
stats.originalOutputChars += getDetailNumber(details, "originalOutputChars") ?? 0;
|
|
378
|
+
stats.summaryChars += getDetailNumber(details, "summaryChars") ?? 0;
|
|
379
|
+
stats.summaryDurationMs += getDetailNumber(details, "summaryDurationMs") ?? 0;
|
|
380
|
+
stats.summaryInputTokens += getDetailNumber(details, "summaryInputTokens") ?? 0;
|
|
381
|
+
stats.summaryOutputTokens += getDetailNumber(details, "summaryOutputTokens") ?? 0;
|
|
382
|
+
stats.summaryReasoningTokens += getDetailNumber(details, "summaryReasoningTokens") ?? 0;
|
|
383
|
+
stats.summaryCacheReadTokens += getDetailNumber(details, "summaryCacheReadTokens") ?? 0;
|
|
384
|
+
stats.summaryCacheWriteTokens += getDetailNumber(details, "summaryCacheWriteTokens") ?? 0;
|
|
385
|
+
const summaryTotalTokens = getDetailNumber(details, "summaryTotalTokens");
|
|
386
|
+
if (summaryTotalTokens !== undefined) {
|
|
387
|
+
stats.summaryTotalTokens += summaryTotalTokens;
|
|
388
|
+
}
|
|
389
|
+
if (
|
|
390
|
+
getDetailNumber(details, "summaryInputTokens") !== undefined
|
|
391
|
+
|| getDetailNumber(details, "summaryOutputTokens") !== undefined
|
|
392
|
+
|| getDetailNumber(details, "summaryTotalTokens") !== undefined
|
|
393
|
+
) {
|
|
394
|
+
stats.hasSummaryTokenUsage = true;
|
|
395
|
+
}
|
|
396
|
+
stats.estimatedOriginalOutputTokens += getDetailNumber(details, "estimatedOriginalOutputTokens") ?? 0;
|
|
397
|
+
stats.estimatedSummaryTokens += getDetailNumber(details, "estimatedSummaryTokens") ?? 0;
|
|
398
|
+
stats.estimatedTokensSaved += getDetailNumber(details, "estimatedTokensSaved") ?? 0;
|
|
399
|
+
const cost = getDetailNumber(details, "summaryCost");
|
|
400
|
+
if (cost !== undefined) {
|
|
401
|
+
stats.summaryCost += cost;
|
|
402
|
+
stats.hasSummaryCost = true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function formatCompactCount(value: number): string {
|
|
407
|
+
const absolute = Math.abs(value);
|
|
408
|
+
const sign = value < 0 ? "-" : "";
|
|
409
|
+
const formatScaled = (scaled: number): string => {
|
|
410
|
+
const rounded = Math.round(scaled * 10) / 10;
|
|
411
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
412
|
+
};
|
|
413
|
+
if (absolute >= 1_000_000) return `${sign}${formatScaled(absolute / 1_000_000)}m`;
|
|
414
|
+
if (absolute >= 1_000) return `${sign}${formatScaled(absolute / 1_000)}k`;
|
|
415
|
+
return String(Math.round(value));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function formatSessionDuration(milliseconds: number): string {
|
|
419
|
+
if (milliseconds < 1_000) return `${Math.max(0, Math.round(milliseconds))}ms`;
|
|
420
|
+
const seconds = milliseconds / 1_000;
|
|
421
|
+
if (seconds < 60) return `${seconds.toFixed(1).replace(/\.0$/, "")}s`;
|
|
422
|
+
const minutes = seconds / 60;
|
|
423
|
+
return `${minutes.toFixed(1).replace(/\.0$/, "")}min`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function formatDistillSessionStats(stats: DistillSessionStats): string {
|
|
427
|
+
const compressionRatio = stats.summaryChars > 0
|
|
428
|
+
? (stats.originalOutputChars / stats.summaryChars).toFixed(2)
|
|
429
|
+
: "-";
|
|
430
|
+
const cost = stats.hasSummaryCost ? stats.summaryCost.toFixed(6) : i18n.t("statsUnavailable");
|
|
431
|
+
const summaryOperations = stats.summarizedResults
|
|
432
|
+
+ stats.fallbackResults
|
|
433
|
+
+ stats.failedResults
|
|
434
|
+
+ stats.rawResults;
|
|
435
|
+
const averageDurationMs = summaryOperations > 0
|
|
436
|
+
? Math.round(stats.summaryDurationMs / summaryOperations)
|
|
437
|
+
: 0;
|
|
438
|
+
const tokenUsage = stats.hasSummaryTokenUsage
|
|
439
|
+
? {
|
|
440
|
+
input: formatCompactCount(stats.summaryInputTokens),
|
|
441
|
+
output: formatCompactCount(stats.summaryOutputTokens),
|
|
442
|
+
reasoning: formatCompactCount(stats.summaryReasoningTokens),
|
|
443
|
+
cacheRead: formatCompactCount(stats.summaryCacheReadTokens),
|
|
444
|
+
cacheWrite: formatCompactCount(stats.summaryCacheWriteTokens),
|
|
445
|
+
total: formatCompactCount(stats.summaryTotalTokens),
|
|
446
|
+
}
|
|
447
|
+
: {
|
|
448
|
+
input: i18n.t("statsUnavailable"),
|
|
449
|
+
output: i18n.t("statsUnavailable"),
|
|
450
|
+
reasoning: i18n.t("statsUnavailable"),
|
|
451
|
+
cacheRead: i18n.t("statsUnavailable"),
|
|
452
|
+
cacheWrite: i18n.t("statsUnavailable"),
|
|
453
|
+
total: i18n.t("statsUnavailable"),
|
|
454
|
+
};
|
|
455
|
+
return i18n.t("statsReport", {
|
|
456
|
+
toolResults: formatCompactCount(stats.toolResults),
|
|
457
|
+
summarizedResults: formatCompactCount(stats.summarizedResults),
|
|
458
|
+
fallbackResults: formatCompactCount(stats.fallbackResults),
|
|
459
|
+
failedResults: formatCompactCount(stats.failedResults),
|
|
460
|
+
rawResults: formatCompactCount(stats.rawResults),
|
|
461
|
+
skippedResults: formatCompactCount(stats.skippedResults),
|
|
462
|
+
nonTextResults: formatCompactCount(stats.nonTextResults),
|
|
463
|
+
summaryAttempts: formatCompactCount(stats.summaryAttempts),
|
|
464
|
+
retryCount: formatCompactCount(stats.retryCount),
|
|
465
|
+
originalOutputChars: formatCompactCount(stats.originalOutputChars),
|
|
466
|
+
summaryChars: formatCompactCount(stats.summaryChars),
|
|
467
|
+
compressionRatio,
|
|
468
|
+
estimatedOriginalOutputTokens: formatCompactCount(stats.estimatedOriginalOutputTokens),
|
|
469
|
+
estimatedSummaryTokens: formatCompactCount(stats.estimatedSummaryTokens),
|
|
470
|
+
estimatedTokensSaved: formatCompactCount(stats.estimatedTokensSaved),
|
|
471
|
+
summaryDuration: formatSessionDuration(stats.summaryDurationMs),
|
|
472
|
+
summaryAverageDuration: formatSessionDuration(averageDurationMs),
|
|
473
|
+
summaryInputTokens: tokenUsage.input,
|
|
474
|
+
summaryOutputTokens: tokenUsage.output,
|
|
475
|
+
summaryReasoningTokens: tokenUsage.reasoning,
|
|
476
|
+
summaryCacheReadTokens: tokenUsage.cacheRead,
|
|
477
|
+
summaryCacheWriteTokens: tokenUsage.cacheWrite,
|
|
478
|
+
summaryTotalTokens: tokenUsage.total,
|
|
479
|
+
summaryCost: cost,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
161
483
|
function attachDiagnostics(result: ToolResult, diagnostics: SummaryDiagnostics): ToolResult {
|
|
162
484
|
return {
|
|
163
485
|
...result,
|
|
@@ -394,8 +716,12 @@ async function summarizeOutput(
|
|
|
394
716
|
},
|
|
395
717
|
);
|
|
396
718
|
|
|
719
|
+
const usage = normalizeSummaryUsage(response.usage);
|
|
397
720
|
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
398
|
-
throw new
|
|
721
|
+
throw new SummaryAttemptError(
|
|
722
|
+
response.errorMessage ?? `Summarizer stopped with reason: ${response.stopReason}`,
|
|
723
|
+
usage,
|
|
724
|
+
);
|
|
399
725
|
}
|
|
400
726
|
|
|
401
727
|
const rawResponse = response.content
|
|
@@ -404,17 +730,23 @@ async function summarizeOutput(
|
|
|
404
730
|
.join("\n")
|
|
405
731
|
.trim();
|
|
406
732
|
|
|
407
|
-
if (!rawResponse) throw new
|
|
733
|
+
if (!rawResponse) throw new SummaryAttemptError("Summarizer returned no text", usage);
|
|
408
734
|
const summaryModel = `${model.provider}/${model.id}`;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
735
|
+
let parsed: SummaryResult;
|
|
736
|
+
try {
|
|
737
|
+
parsed = parseSummaryResponse(rawResponse, summaryModel);
|
|
738
|
+
} catch (error) {
|
|
739
|
+
throw new SummaryAttemptError(error instanceof Error ? error.message : String(error), usage);
|
|
740
|
+
}
|
|
741
|
+
if (parsed.decision.mode === "RAW") return { ...parsed, usage };
|
|
742
|
+
if (parsed.summaryChars <= config.maxChars) return { ...parsed, usage };
|
|
412
743
|
|
|
413
744
|
const summaryFilePath = await writeSummaryFile(parsed.text);
|
|
414
745
|
return {
|
|
415
746
|
...parsed,
|
|
416
747
|
text: `Summary exceeded ${config.maxChars} chars and was written to: ${summaryFilePath}`,
|
|
417
748
|
summaryFilePath,
|
|
749
|
+
usage,
|
|
418
750
|
};
|
|
419
751
|
}
|
|
420
752
|
|
|
@@ -427,10 +759,12 @@ async function summarizeOutputWithRetries(
|
|
|
427
759
|
): Promise<SummaryResult> {
|
|
428
760
|
let timeoutRetries = 0;
|
|
429
761
|
let errorRetries = 0;
|
|
762
|
+
let attempts = 0;
|
|
763
|
+
let totalUsage: SummaryUsage | undefined;
|
|
430
764
|
|
|
431
765
|
while (true) {
|
|
432
766
|
if (context.signal?.aborted) {
|
|
433
|
-
throw new
|
|
767
|
+
throw new SummaryRetryError("Summarization aborted", attempts, totalUsage);
|
|
434
768
|
}
|
|
435
769
|
|
|
436
770
|
const attemptController = new AbortController();
|
|
@@ -445,9 +779,10 @@ async function summarizeOutputWithRetries(
|
|
|
445
779
|
},
|
|
446
780
|
config.timeoutSeconds * 1000,
|
|
447
781
|
);
|
|
782
|
+
attempts += 1;
|
|
448
783
|
|
|
449
784
|
try {
|
|
450
|
-
|
|
785
|
+
const result = await summarizeOutput(
|
|
451
786
|
prompt,
|
|
452
787
|
output,
|
|
453
788
|
config,
|
|
@@ -455,19 +790,39 @@ async function summarizeOutputWithRetries(
|
|
|
455
790
|
attemptController.signal,
|
|
456
791
|
completion,
|
|
457
792
|
);
|
|
793
|
+
totalUsage = mergeSummaryUsage(totalUsage, result.usage);
|
|
794
|
+
return {
|
|
795
|
+
...result,
|
|
796
|
+
usage: totalUsage,
|
|
797
|
+
attempts,
|
|
798
|
+
};
|
|
458
799
|
} catch (error) {
|
|
459
|
-
|
|
800
|
+
totalUsage = mergeSummaryUsage(
|
|
801
|
+
totalUsage,
|
|
802
|
+
error instanceof SummaryAttemptError || error instanceof SummaryRetryError
|
|
803
|
+
? error.usage
|
|
804
|
+
: undefined,
|
|
805
|
+
);
|
|
806
|
+
if (context.signal?.aborted) {
|
|
807
|
+
throw new SummaryRetryError("Summarization aborted", attempts, totalUsage);
|
|
808
|
+
}
|
|
460
809
|
const retryLimit = timedOut ? config.timeoutRetryCount : config.errorRetryCount;
|
|
461
810
|
const retriesUsed = timedOut ? timeoutRetries : errorRetries;
|
|
462
|
-
if (retriesUsed >= retryLimit)
|
|
811
|
+
if (retriesUsed >= retryLimit) {
|
|
812
|
+
throw new SummaryRetryError(
|
|
813
|
+
error instanceof Error ? error.message : String(error),
|
|
814
|
+
attempts,
|
|
815
|
+
totalUsage,
|
|
816
|
+
);
|
|
817
|
+
}
|
|
463
818
|
if (timedOut) timeoutRetries += 1;
|
|
464
819
|
else errorRetries += 1;
|
|
465
|
-
|
|
820
|
+
context.ctx.ui.notify(i18n.t("retryingSummary", {
|
|
466
821
|
kind: i18n.t(timedOut ? "retryKindTimeout" : "retryKindError"),
|
|
467
822
|
retry: retriesUsed + 1,
|
|
468
823
|
limit: retryLimit,
|
|
469
824
|
error: error instanceof Error ? error.message : String(error),
|
|
470
|
-
}));
|
|
825
|
+
}), "warning");
|
|
471
826
|
} finally {
|
|
472
827
|
clearTimeout(timeout);
|
|
473
828
|
context.signal?.removeEventListener("abort", abortFromParent);
|
|
@@ -496,7 +851,9 @@ export async function processToolResult(
|
|
|
496
851
|
const maxReturnedChars = config?.maxOutputChars ?? 10_000;
|
|
497
852
|
const finish = (candidate: ToolResult) => limitReturnedToolResult(candidate, maxReturnedChars);
|
|
498
853
|
if (loaded.warnings.length > 0) {
|
|
499
|
-
|
|
854
|
+
context.ctx.ui.notify(i18n.t("configWarnings", {
|
|
855
|
+
warnings: loaded.warnings.join(" "),
|
|
856
|
+
}), "warning");
|
|
500
857
|
}
|
|
501
858
|
|
|
502
859
|
if (config && loaded.enabled && !isDistillToolEnabled(config, context.toolName)) return result;
|
|
@@ -535,9 +892,7 @@ export async function processToolResult(
|
|
|
535
892
|
try {
|
|
536
893
|
output = await getCompleteOutput(result);
|
|
537
894
|
} catch (error) {
|
|
538
|
-
|
|
539
|
-
`[tool-output-summary] ${context.toolName} could not read the full output; returning the original result: ${error instanceof Error ? error.message : String(error)}`,
|
|
540
|
-
);
|
|
895
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
541
896
|
return finish(attachDiagnostics(result, {
|
|
542
897
|
toolExecutionMs,
|
|
543
898
|
outputSummaryPrompt: prompt || undefined,
|
|
@@ -547,6 +902,7 @@ export async function processToolResult(
|
|
|
547
902
|
summaryTriggerMaxChars: null,
|
|
548
903
|
summaryResultMaxChars: config.maxChars,
|
|
549
904
|
missedCompressionRatio: config.missedCompressionRatio,
|
|
905
|
+
outputSummaryError: errorMessage,
|
|
550
906
|
}));
|
|
551
907
|
}
|
|
552
908
|
|
|
@@ -564,6 +920,7 @@ export async function processToolResult(
|
|
|
564
920
|
summaryTriggerMaxChars: null,
|
|
565
921
|
summaryResultMaxChars: config.maxChars,
|
|
566
922
|
missedCompressionRatio: config.missedCompressionRatio,
|
|
923
|
+
...getTokenCompressionDiagnostics(output, output),
|
|
567
924
|
...skippedDiagnostics,
|
|
568
925
|
};
|
|
569
926
|
const candidate = {
|
|
@@ -594,6 +951,9 @@ export async function processToolResult(
|
|
|
594
951
|
const diagnostics: SummaryDiagnostics = {
|
|
595
952
|
originalOutputChars: output.length,
|
|
596
953
|
summaryChars: output.length,
|
|
954
|
+
summaryAttempts: summarized.attempts,
|
|
955
|
+
...getSummaryUsageDiagnostics(summarized.usage),
|
|
956
|
+
...getTokenCompressionDiagnostics(output, output),
|
|
597
957
|
compressionRatio: 1,
|
|
598
958
|
compressionSavedPercent: 0,
|
|
599
959
|
...rawDiagnostics,
|
|
@@ -628,6 +988,9 @@ export async function processToolResult(
|
|
|
628
988
|
const summaryDiagnostics: SummaryDiagnostics = {
|
|
629
989
|
originalOutputChars: output.length,
|
|
630
990
|
summaryChars: summarized.summaryChars,
|
|
991
|
+
summaryAttempts: summarized.attempts,
|
|
992
|
+
...getSummaryUsageDiagnostics(summarized.usage),
|
|
993
|
+
...getTokenCompressionDiagnostics(output, summarized.text),
|
|
631
994
|
...compressionDiagnostics,
|
|
632
995
|
};
|
|
633
996
|
const agentDiagnostic = buildAgentDiagnosticText(summaryDiagnostics);
|
|
@@ -650,6 +1013,7 @@ export async function processToolResult(
|
|
|
650
1013
|
outputSummaryReasonCode: summarized.decision.reasonCode,
|
|
651
1014
|
outputSummaryReason: summarized.decision.reason,
|
|
652
1015
|
...summaryDiagnostics,
|
|
1016
|
+
...getTokenCompressionDiagnostics(output, output),
|
|
653
1017
|
}),
|
|
654
1018
|
content: [{ type: "text", text: output }],
|
|
655
1019
|
});
|
|
@@ -686,14 +1050,14 @@ export async function processToolResult(
|
|
|
686
1050
|
} catch (error) {
|
|
687
1051
|
const summaryDurationMs = Math.round(performance.now() - summaryStartedAt);
|
|
688
1052
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1053
|
+
const retryError = error instanceof SummaryRetryError ? error : undefined;
|
|
689
1054
|
// 总结链路任何异常都必须保留原始结果,不能把异常文本替换给 AI。
|
|
690
|
-
console.warn(
|
|
691
|
-
`[tool-output-summary] ${context.toolName} summarization failed; returning the original result: ${errorMessage}`,
|
|
692
|
-
);
|
|
693
1055
|
const diagnostics: SummaryDiagnostics = {
|
|
694
1056
|
toolExecutionMs,
|
|
695
1057
|
summaryDurationMs,
|
|
696
1058
|
originalOutputChars: output.length,
|
|
1059
|
+
summaryAttempts: retryError?.attempts,
|
|
1060
|
+
...getSummaryUsageDiagnostics(retryError?.usage),
|
|
697
1061
|
outputSummaryIntent: decision.intent,
|
|
698
1062
|
outputSummaryPrompt: prompt || undefined,
|
|
699
1063
|
outputSummaryRender,
|
|
@@ -702,6 +1066,7 @@ export async function processToolResult(
|
|
|
702
1066
|
summaryTriggerMaxChars: null,
|
|
703
1067
|
summaryResultMaxChars: config.maxChars,
|
|
704
1068
|
missedCompressionRatio: config.missedCompressionRatio,
|
|
1069
|
+
...getTokenCompressionDiagnostics(output, output),
|
|
705
1070
|
outputSummaryAnomalies: ["summary-failed"],
|
|
706
1071
|
outputSummaryAdvice: `Summarization failed; the original output was preserved. Check model configuration or authentication. Requests still running after ${config.timeoutSeconds}s are treated as timed out.`,
|
|
707
1072
|
outputSummaryError: errorMessage,
|
|
@@ -739,17 +1104,21 @@ function restoreOutputRequestParameter(parameters: Record<string, unknown>): boo
|
|
|
739
1104
|
return true;
|
|
740
1105
|
}
|
|
741
1106
|
|
|
742
|
-
function extendOutputRequestParameter(
|
|
1107
|
+
function extendOutputRequestParameter(
|
|
1108
|
+
tool: ToolInfo,
|
|
1109
|
+
enabled: boolean,
|
|
1110
|
+
reportWarning: DistillWarningReporter,
|
|
1111
|
+
): boolean {
|
|
743
1112
|
const parameters = tool.parameters as unknown as Record<string, unknown> | undefined;
|
|
744
1113
|
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
|
745
|
-
|
|
1114
|
+
reportWarning(i18n.t("outputRequestUnavailable", { tool: tool.name }));
|
|
746
1115
|
return false;
|
|
747
1116
|
}
|
|
748
1117
|
|
|
749
1118
|
if (!enabled) return restoreOutputRequestParameter(parameters);
|
|
750
1119
|
|
|
751
1120
|
if (parameters.type !== "object") {
|
|
752
|
-
|
|
1121
|
+
reportWarning(i18n.t("outputRequestUnavailable", { tool: tool.name }));
|
|
753
1122
|
return false;
|
|
754
1123
|
}
|
|
755
1124
|
|
|
@@ -758,7 +1127,7 @@ function extendOutputRequestParameter(tool: ToolInfo, enabled: boolean): boolean
|
|
|
758
1127
|
if (properties === undefined) {
|
|
759
1128
|
parameters.properties = {};
|
|
760
1129
|
} else if (typeof properties !== "object" || properties === null || Array.isArray(properties)) {
|
|
761
|
-
|
|
1130
|
+
reportWarning(i18n.t("outputRequestUnavailable", { tool: tool.name }));
|
|
762
1131
|
return false;
|
|
763
1132
|
}
|
|
764
1133
|
|
|
@@ -790,11 +1159,12 @@ function extendOutputRequestParameter(tool: ToolInfo, enabled: boolean): boolean
|
|
|
790
1159
|
export function extendDistillToolParameters(
|
|
791
1160
|
pi: Pick<ExtensionAPI, "getAllTools">,
|
|
792
1161
|
loaded = loadDistillConfig(),
|
|
1162
|
+
reportWarning: DistillWarningReporter = () => undefined,
|
|
793
1163
|
): number {
|
|
794
1164
|
let extended = 0;
|
|
795
1165
|
for (const tool of pi.getAllTools()) {
|
|
796
1166
|
const enabled = loaded.enabled && Boolean(loaded.config) && isDistillToolEnabled(loaded.config, tool.name);
|
|
797
|
-
if (extendOutputRequestParameter(tool, enabled) && enabled) extended += 1;
|
|
1167
|
+
if (extendOutputRequestParameter(tool, enabled, reportWarning) && enabled) extended += 1;
|
|
798
1168
|
}
|
|
799
1169
|
return extended;
|
|
800
1170
|
}
|
|
@@ -1042,7 +1412,10 @@ async function runDistillConfigUi(
|
|
|
1042
1412
|
}
|
|
1043
1413
|
}
|
|
1044
1414
|
|
|
1045
|
-
function registerDistillConfigCommand(
|
|
1415
|
+
function registerDistillConfigCommand(
|
|
1416
|
+
pi: ExtensionAPI,
|
|
1417
|
+
onSaved: (ctx: ExtensionCommandContext) => void,
|
|
1418
|
+
): void {
|
|
1046
1419
|
const command = {
|
|
1047
1420
|
description: i18n.t("commandDescription"),
|
|
1048
1421
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
@@ -1050,7 +1423,7 @@ function registerDistillConfigCommand(pi: ExtensionAPI, onSaved: () => void): vo
|
|
|
1050
1423
|
ctx.ui.notify(i18n.t("interactiveOnly"), "warning");
|
|
1051
1424
|
return;
|
|
1052
1425
|
}
|
|
1053
|
-
await runDistillConfigUi(ctx, pi, getDistillConfigPath(), onSaved);
|
|
1426
|
+
await runDistillConfigUi(ctx, pi, getDistillConfigPath(), () => onSaved(ctx));
|
|
1054
1427
|
},
|
|
1055
1428
|
};
|
|
1056
1429
|
for (const name of ["config:distill", "pi-distill"] as const) {
|
|
@@ -1058,23 +1431,51 @@ function registerDistillConfigCommand(pi: ExtensionAPI, onSaved: () => void): vo
|
|
|
1058
1431
|
}
|
|
1059
1432
|
}
|
|
1060
1433
|
|
|
1434
|
+
function registerDistillStatsCommand(
|
|
1435
|
+
pi: ExtensionAPI,
|
|
1436
|
+
getStats: () => DistillSessionStats,
|
|
1437
|
+
): void {
|
|
1438
|
+
pi.registerCommand("distill:stats", {
|
|
1439
|
+
description: i18n.t("statsCommandDescription"),
|
|
1440
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
1441
|
+
if (!ctx.hasUI) {
|
|
1442
|
+
ctx.ui.notify(i18n.t("interactiveOnly"), "warning");
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
ctx.ui.notify(formatDistillSessionStats(getStats()), "info");
|
|
1446
|
+
},
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1061
1450
|
export default function piDistillExtension(pi: ExtensionAPI) {
|
|
1062
1451
|
const pendingCalls = new Map<string, PendingDistillCall>();
|
|
1452
|
+
const reportedWarnings = new Set<string>();
|
|
1453
|
+
let sessionStats = createDistillSessionStats();
|
|
1063
1454
|
let originalUserPrompt = "";
|
|
1064
1455
|
const disposeToolDisplayMiddleware = registerDistillToolDisplayMiddleware();
|
|
1065
1456
|
registerDistillFallbackRenderer(pi);
|
|
1066
|
-
const extendParameters = () => {
|
|
1457
|
+
const extendParameters = (ctx: ExtensionContext) => {
|
|
1458
|
+
const reportWarning = (message: string) => {
|
|
1459
|
+
if (reportedWarnings.has(message)) return;
|
|
1460
|
+
reportedWarnings.add(message);
|
|
1461
|
+
ctx.ui.notify(message, "warning");
|
|
1462
|
+
};
|
|
1067
1463
|
try {
|
|
1068
|
-
extendDistillToolParameters(pi, loadDistillConfig());
|
|
1464
|
+
extendDistillToolParameters(pi, loadDistillConfig(), reportWarning);
|
|
1069
1465
|
} catch (error) {
|
|
1070
|
-
|
|
1466
|
+
reportWarning(i18n.t("extendOutputRequestFailed", {
|
|
1467
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1468
|
+
}));
|
|
1071
1469
|
}
|
|
1072
1470
|
};
|
|
1073
1471
|
|
|
1074
|
-
pi.on("session_start",
|
|
1075
|
-
|
|
1472
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1473
|
+
sessionStats = createDistillSessionStats();
|
|
1474
|
+
extendParameters(ctx);
|
|
1475
|
+
});
|
|
1476
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
1076
1477
|
originalUserPrompt = typeof event.prompt === "string" ? event.prompt : "";
|
|
1077
|
-
extendParameters();
|
|
1478
|
+
extendParameters(ctx);
|
|
1078
1479
|
return {
|
|
1079
1480
|
systemPrompt: [
|
|
1080
1481
|
typeof event.systemPrompt === "string" ? event.systemPrompt : "",
|
|
@@ -1099,11 +1500,15 @@ export default function piDistillExtension(pi: ExtensionAPI) {
|
|
|
1099
1500
|
pi.on("tool_result", async (event: ToolResultEvent, ctx) => {
|
|
1100
1501
|
const pending = pendingCalls.get(event.toolCallId);
|
|
1101
1502
|
pendingCalls.delete(event.toolCallId);
|
|
1102
|
-
if (pending && !pending.enabled)
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1503
|
+
if (pending && !pending.enabled) {
|
|
1504
|
+
const untouchedResult: ToolResult = {
|
|
1505
|
+
content: event.content,
|
|
1506
|
+
details: event.details as Record<string, unknown> | undefined,
|
|
1507
|
+
isError: event.isError,
|
|
1508
|
+
};
|
|
1509
|
+
recordDistillSessionResult(sessionStats, untouchedResult.details);
|
|
1510
|
+
return toToolResultEventResult(untouchedResult);
|
|
1511
|
+
}
|
|
1107
1512
|
const outputRequest = pending?.outputRequest ?? getOutputRequest(event.input);
|
|
1108
1513
|
const result = await processToolResult(
|
|
1109
1514
|
{
|
|
@@ -1120,6 +1525,7 @@ export default function piDistillExtension(pi: ExtensionAPI) {
|
|
|
1120
1525
|
},
|
|
1121
1526
|
pending ? Math.round(performance.now() - pending.startedAt) : 0,
|
|
1122
1527
|
);
|
|
1528
|
+
recordDistillSessionResult(sessionStats, result.details);
|
|
1123
1529
|
if (!isDistillToolDisplayMiddlewareActive(event.toolName)) {
|
|
1124
1530
|
appendDistillFallbackAudit(pi, event.toolName, result.details, loadDistillConfig().render);
|
|
1125
1531
|
}
|
|
@@ -1127,5 +1533,6 @@ export default function piDistillExtension(pi: ExtensionAPI) {
|
|
|
1127
1533
|
});
|
|
1128
1534
|
pi.on("agent_end", () => pendingCalls.clear());
|
|
1129
1535
|
pi.on("session_shutdown", () => disposeToolDisplayMiddleware());
|
|
1536
|
+
registerDistillStatsCommand(pi, () => sessionStats);
|
|
1130
1537
|
registerDistillConfigCommand(pi, extendParameters);
|
|
1131
1538
|
}
|
package/src/output-limit.ts
CHANGED
|
@@ -59,7 +59,6 @@ export async function limitReturnedToolResult(
|
|
|
59
59
|
};
|
|
60
60
|
} catch (error) {
|
|
61
61
|
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
-
console.warn(i18n.t("outputLimitWriteFailed", { error: message }));
|
|
63
62
|
return {
|
|
64
63
|
...result,
|
|
65
64
|
content: [{ type: "text", text: text.slice(0, maxChars) }],
|
package/src/summary-utils.ts
CHANGED
|
@@ -160,9 +160,6 @@ export function parseBashSummaryConfig(
|
|
|
160
160
|
missedCompressionRatio === undefined ||
|
|
161
161
|
summarizeErrors === undefined
|
|
162
162
|
) {
|
|
163
|
-
console.warn(
|
|
164
|
-
"[pi-distill] Invalid distillation config; distillation disabled. Check PI_DISTILL_MIN_CHARS, PI_DISTILL_MAX_CHARS, PI_DISTILL_MAX_OUTPUT_CHARS, PI_DISTILL_TIMEOUT_SECONDS, PI_DISTILL_TIMEOUT_RETRY_COUNT, PI_DISTILL_ERROR_RETRY_COUNT, PI_DISTILL_MISSED_COMPRESSION_RATIO, and PI_DISTILL_SUMMARIZE_ERRORS (legacy PI_BASH_SUMMARY_* variables remain supported).",
|
|
165
|
-
);
|
|
166
163
|
return undefined;
|
|
167
164
|
}
|
|
168
165
|
|
|
@@ -181,9 +178,6 @@ export function parseBashSummaryConfig(
|
|
|
181
178
|
|
|
182
179
|
const separator = modelRef.indexOf("/");
|
|
183
180
|
if (separator <= 0 || separator === modelRef.length - 1) {
|
|
184
|
-
console.warn(
|
|
185
|
-
`[pi-distill] Invalid PI_DISTILL_MODEL; expected provider/model, got: ${modelRef}`,
|
|
186
|
-
);
|
|
187
181
|
return undefined;
|
|
188
182
|
}
|
|
189
183
|
|