pi-ui-extend 1.0.39 → 1.0.41
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 +1 -1
- package/dist/app/commands/command-registry.js +2 -2
- package/dist/app/commands/command-session-actions.d.ts +0 -1
- package/dist/app/commands/command-session-actions.js +22 -13
- package/dist/app/icons.d.ts +14 -0
- package/dist/app/icons.js +33 -0
- package/dist/app/rendering/conversation-tool-renderer.js +2 -2
- package/dist/app/rendering/dcp-stats.d.ts +6 -1
- package/dist/app/rendering/dcp-stats.js +214 -46
- package/dist/app/rendering/editor-panels.js +8 -5
- package/dist/app/session/lazy-session-manager.js +12 -1
- package/dist/app/session/tabs-controller.d.ts +2 -5
- package/dist/app/session/tabs-controller.js +12 -21
- package/dist/app/subagents/subagents-model.d.ts +14 -1
- package/dist/app/subagents/subagents-model.js +34 -15
- package/dist/app/types.d.ts +2 -0
- package/dist/bundled-extensions/session-title/config.js +1 -1
- package/dist/markdown-format.js +27 -9
- package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
- package/dist/schemas/pi-tools-suite-schema.js +46 -31
- package/external/pi-tools-suite/README.md +392 -52
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
- package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
- package/external/pi-tools-suite/docs/evals.md +684 -0
- package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
- package/external/pi-tools-suite/package.json +10 -3
- package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
- package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
- package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
- package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
- package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
- package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
- package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
- package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
- package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
- package/external/pi-tools-suite/src/config.ts +1 -22
- package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
- package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
- package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
- package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
- package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
- package/external/pi-tools-suite/src/dcp/config.ts +36 -61
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
- package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
- package/external/pi-tools-suite/src/dcp/index.ts +617 -203
- package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
- package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
- package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
- package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
- package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
- package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
- package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
- package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
- package/external/pi-tools-suite/src/dcp/state.ts +158 -580
- package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
- package/external/pi-tools-suite/src/index.ts +9 -0
- package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
- package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
- package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
- package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
- package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
- package/package.json +6 -6
- package/schemas/pi-tools-suite.json +159 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
export type TestOutputClassification = "recognised" | "partial" | "unrecognised";
|
|
2
|
+
export type TestOutputFormat = "bun-test" | "tap" | "typescript" | "mixed" | "unknown";
|
|
3
|
+
export type TestOutputHostOutcome = "success" | "error";
|
|
4
|
+
|
|
5
|
+
export interface TestOutputDiagnostic {
|
|
6
|
+
severity: "error" | "warning";
|
|
7
|
+
/** Exact line from the parser's ANSI/CR-normalized text view. */
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TestOutputSummary {
|
|
12
|
+
passed?: number;
|
|
13
|
+
failed?: number;
|
|
14
|
+
tests?: number;
|
|
15
|
+
files?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ParsedTestBuildOutput {
|
|
19
|
+
version: 1;
|
|
20
|
+
classification: TestOutputClassification;
|
|
21
|
+
format: TestOutputFormat;
|
|
22
|
+
hostOutcome: TestOutputHostOutcome;
|
|
23
|
+
upstreamTruncated: boolean;
|
|
24
|
+
scanLimited: boolean;
|
|
25
|
+
terminalSummarySeen: boolean;
|
|
26
|
+
termination: "normal" | "timeout" | "abort";
|
|
27
|
+
summary?: TestOutputSummary;
|
|
28
|
+
diagnostics: TestOutputDiagnostic[];
|
|
29
|
+
parserWarnings: Array<
|
|
30
|
+
| "upstream-truncated"
|
|
31
|
+
| "mixed-formats"
|
|
32
|
+
| "host-outcome-conflict"
|
|
33
|
+
| "failed-summary-without-diagnostic"
|
|
34
|
+
| "nonterminal-summary"
|
|
35
|
+
| "scan-limited"
|
|
36
|
+
>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ProspectiveTestOutputDelivery {
|
|
40
|
+
version: 1;
|
|
41
|
+
decision: "compact-candidate" | "passthrough";
|
|
42
|
+
reason:
|
|
43
|
+
| "recognised-complete"
|
|
44
|
+
| "parser-not-complete"
|
|
45
|
+
| "upstream-truncated"
|
|
46
|
+
| "termination-not-normal"
|
|
47
|
+
| "compound-command"
|
|
48
|
+
| "command-scope-unknown"
|
|
49
|
+
| "compact-budget-exceeded";
|
|
50
|
+
bytes: number;
|
|
51
|
+
text?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ANSI_PATTERN = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
55
|
+
const DEFAULT_MAX_SCAN_CHARS = 1024 * 1024;
|
|
56
|
+
|
|
57
|
+
function boundedScanInput(text: string, maxScanChars: number): { text: string; limited: boolean } {
|
|
58
|
+
if (text.length <= maxScanChars) return { text, limited: false };
|
|
59
|
+
const half = Math.max(1, Math.floor(maxScanChars / 2));
|
|
60
|
+
return {
|
|
61
|
+
text: `${text.slice(0, half)}\n[context-gateway parser scan gap]\n${text.slice(-half)}`,
|
|
62
|
+
limited: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizedTerminalLines(text: string): string[] {
|
|
67
|
+
return text
|
|
68
|
+
.replace(/\r\n/g, "\n")
|
|
69
|
+
.split("\n")
|
|
70
|
+
.map((line) => {
|
|
71
|
+
// A bare CR is normally a terminal overwrite. Parse the final visible
|
|
72
|
+
// segment instead of concatenating stale progress text with the result.
|
|
73
|
+
const visible = line.includes("\r") ? line.slice(line.lastIndexOf("\r") + 1) : line;
|
|
74
|
+
return visible.replace(ANSI_PATTERN, "");
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function nonBlank(lines: readonly string[]): string[] {
|
|
79
|
+
return lines.filter((line) => line.trim().length > 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseCount(lines: readonly string[], pattern: RegExp): number | undefined {
|
|
83
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
84
|
+
const match = lines[index]!.match(pattern);
|
|
85
|
+
if (!match) continue;
|
|
86
|
+
const value = Number(match[1]);
|
|
87
|
+
if (Number.isSafeInteger(value) && value >= 0) return value;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function lastIndexWhere(lines: readonly string[], predicate: (line: string) => boolean): number {
|
|
93
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
94
|
+
if (predicate(lines[index]!)) return index;
|
|
95
|
+
}
|
|
96
|
+
return -1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function diagnosticLines(lines: readonly string[], patterns: Array<{ severity: "error" | "warning"; pattern: RegExp }>): TestOutputDiagnostic[] {
|
|
100
|
+
const result: TestOutputDiagnostic[] = [];
|
|
101
|
+
const seen = new Set<string>();
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
for (const entry of patterns) {
|
|
104
|
+
if (!entry.pattern.test(line)) continue;
|
|
105
|
+
const key = `${entry.severity}\u0000${line}`;
|
|
106
|
+
if (!seen.has(key)) {
|
|
107
|
+
seen.add(key);
|
|
108
|
+
result.push({ severity: entry.severity, text: line });
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function detectTermination(lines: readonly string[]): "normal" | "timeout" | "abort" {
|
|
117
|
+
const tail = lines.slice(-8).join("\n");
|
|
118
|
+
if (/Command timed out after \d+(?:\.\d+)? seconds/i.test(tail)) return "timeout";
|
|
119
|
+
if (/Command aborted/i.test(tail)) return "abort";
|
|
120
|
+
return "normal";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isHostStatusLine(line: string): boolean {
|
|
124
|
+
return /^Command exited with code \d+$/i.test(line.trim());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function nonHostTail(lines: readonly string[]): string[] {
|
|
128
|
+
return nonBlank(lines).filter((line) => !isHostStatusLine(line) && !/^npm notice\b/.test(line.trim()));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function outcomeConflict(hostOutcome: TestOutputHostOutcome, failed: number | undefined): boolean {
|
|
132
|
+
if (failed === undefined) return false;
|
|
133
|
+
if (hostOutcome === "success") return failed > 0;
|
|
134
|
+
return failed === 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseBun(lines: readonly string[], hostOutcome: TestOutputHostOutcome) {
|
|
138
|
+
const hasBanner = lines.some((line) => /^bun test v\d+/i.test(line.trim()));
|
|
139
|
+
const ranLineIndex = lastIndexWhere(lines, (line) => /^Ran \d+ tests? across \d+ files?\./.test(line.trim()));
|
|
140
|
+
if (!hasBanner && ranLineIndex < 0) return undefined;
|
|
141
|
+
const passed = parseCount(lines, /^\s*(\d+) pass\s*$/);
|
|
142
|
+
const failed = parseCount(lines, /^\s*(\d+) fail\s*$/);
|
|
143
|
+
const ran = ranLineIndex >= 0 ? lines[ranLineIndex]!.trim().match(/^Ran (\d+) tests? across (\d+) files?\./) : null;
|
|
144
|
+
const tests = ran ? Number(ran[1]) : undefined;
|
|
145
|
+
const files = ran ? Number(ran[2]) : undefined;
|
|
146
|
+
const diagnostics = diagnosticLines(lines, [
|
|
147
|
+
{ severity: "error", pattern: /^\s*\(fail\)\s+/ },
|
|
148
|
+
{ severity: "error", pattern: /^\s*(?:error|Error):\s+/ },
|
|
149
|
+
{ severity: "warning", pattern: /^\s*(?:warning|Warning):\s+/ },
|
|
150
|
+
]);
|
|
151
|
+
const tailAfterSummary = ranLineIndex >= 0 ? nonHostTail(lines.slice(ranLineIndex + 1)) : [];
|
|
152
|
+
return {
|
|
153
|
+
format: "bun-test" as const,
|
|
154
|
+
terminalSummarySeen: passed !== undefined && failed !== undefined && tests !== undefined && files !== undefined,
|
|
155
|
+
nonterminalSummary: tailAfterSummary.length > 0,
|
|
156
|
+
summary: { passed, failed, tests, files },
|
|
157
|
+
diagnostics,
|
|
158
|
+
outcomeConflict: outcomeConflict(hostOutcome, failed),
|
|
159
|
+
failedWithoutDiagnostic: (failed ?? 0) > 0 && diagnostics.length === 0,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseTap(lines: readonly string[], hostOutcome: TestOutputHostOutcome) {
|
|
164
|
+
const hasTap = lines.some((line) => /^TAP version \d+/.test(line.trim()))
|
|
165
|
+
|| lines.some((line) => /^\s*(?:ok|not ok)\s+\d+\b/.test(line));
|
|
166
|
+
if (!hasTap) return undefined;
|
|
167
|
+
const tests = parseCount(lines, /^\s*#\s*tests\s+(\d+)\s*$/i);
|
|
168
|
+
const passed = parseCount(lines, /^\s*#\s*pass\s+(\d+)\s*$/i);
|
|
169
|
+
const failed = parseCount(lines, /^\s*#\s*fail\s+(\d+)\s*$/i);
|
|
170
|
+
const planIndex = lastIndexWhere(lines, (line) => /^\s*1\.\.\d+\s*$/.test(line));
|
|
171
|
+
const diagnostics = diagnosticLines(lines, [
|
|
172
|
+
{ severity: "error", pattern: /^\s*not ok\s+\d+\b/ },
|
|
173
|
+
{ severity: "error", pattern: /^\s*(?:error|message|operator):\s+/i },
|
|
174
|
+
{ severity: "warning", pattern: /^\s*#\s*warning\b/i },
|
|
175
|
+
]);
|
|
176
|
+
const lastSummaryIndex = Math.max(
|
|
177
|
+
planIndex,
|
|
178
|
+
lastIndexWhere(lines, (line) => /^\s*#\s*(?:tests|pass|fail)\s+\d+\s*$/i.test(line)),
|
|
179
|
+
);
|
|
180
|
+
const tailAfterSummary = lastSummaryIndex >= 0 ? nonHostTail(lines.slice(lastSummaryIndex + 1)) : [];
|
|
181
|
+
return {
|
|
182
|
+
format: "tap" as const,
|
|
183
|
+
terminalSummarySeen: planIndex >= 0 && tests !== undefined && passed !== undefined && failed !== undefined,
|
|
184
|
+
nonterminalSummary: tailAfterSummary.length > 0,
|
|
185
|
+
summary: { passed, failed, tests },
|
|
186
|
+
diagnostics,
|
|
187
|
+
outcomeConflict: outcomeConflict(hostOutcome, failed),
|
|
188
|
+
failedWithoutDiagnostic: (failed ?? 0) > 0 && diagnostics.length === 0,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const TSC_LOCATION = "(?:.+?\\(\\d+,\\d+\\):?|.+?:\\d+:\\d+\\s+-)";
|
|
193
|
+
const TSC_DIAGNOSTIC = new RegExp(`^${TSC_LOCATION}\\s*(error|warning)\\s+TS\\d+:\\s+.+$`, "i");
|
|
194
|
+
|
|
195
|
+
function parseTypescript(lines: readonly string[], hostOutcome: TestOutputHostOutcome) {
|
|
196
|
+
const meaningful = nonBlank(lines);
|
|
197
|
+
const diagnostics = diagnosticLines(meaningful, [
|
|
198
|
+
{ severity: "error", pattern: new RegExp(`^${TSC_LOCATION}\\s*error\\s+TS\\d+:`, "i") },
|
|
199
|
+
{ severity: "warning", pattern: new RegExp(`^${TSC_LOCATION}\\s*warning\\s+TS\\d+:`, "i") },
|
|
200
|
+
]);
|
|
201
|
+
const summaryPattern = /^Found \d+ errors?(?: in \d+ files?)?.*$/i;
|
|
202
|
+
const summaryIndex = lastIndexWhere(meaningful, (line) => summaryPattern.test(line.trim()));
|
|
203
|
+
if (diagnostics.length === 0 && summaryIndex < 0) return undefined;
|
|
204
|
+
const summaryMatch = summaryIndex >= 0 ? meaningful[summaryIndex]!.trim().match(/^Found (\d+) errors?(?: in (\d+) files?)?/i) : null;
|
|
205
|
+
const failed = summaryMatch ? Number(summaryMatch[1]) : diagnostics.filter((entry) => entry.severity === "error").length;
|
|
206
|
+
const files = summaryMatch?.[2] ? Number(summaryMatch[2]) : undefined;
|
|
207
|
+
const recognizedLines = meaningful.filter((line) => TSC_DIAGNOSTIC.test(line) || summaryPattern.test(line.trim()) || isHostStatusLine(line));
|
|
208
|
+
const significantAfterSummary = summaryIndex >= 0 ? nonHostTail(meaningful.slice(summaryIndex + 1)) : [];
|
|
209
|
+
return {
|
|
210
|
+
format: "typescript" as const,
|
|
211
|
+
terminalSummarySeen: summaryIndex >= 0 && significantAfterSummary.length === 0,
|
|
212
|
+
nonterminalSummary: summaryIndex >= 0 && significantAfterSummary.length > 0,
|
|
213
|
+
summary: { failed, files },
|
|
214
|
+
diagnostics,
|
|
215
|
+
outcomeConflict: outcomeConflict(hostOutcome, failed),
|
|
216
|
+
failedWithoutDiagnostic: failed > 0 && diagnostics.length === 0,
|
|
217
|
+
unparsedLines: meaningful.length - recognizedLines.length,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function parseTestBuildOutput(input: {
|
|
222
|
+
text: string;
|
|
223
|
+
hostOutcome: TestOutputHostOutcome;
|
|
224
|
+
upstreamTruncated?: boolean;
|
|
225
|
+
maxScanChars?: number;
|
|
226
|
+
}): ParsedTestBuildOutput {
|
|
227
|
+
const maxScanChars = Number.isSafeInteger(input.maxScanChars) && (input.maxScanChars ?? 0) > 0
|
|
228
|
+
? input.maxScanChars!
|
|
229
|
+
: DEFAULT_MAX_SCAN_CHARS;
|
|
230
|
+
const bounded = boundedScanInput(input.text, maxScanChars);
|
|
231
|
+
const lines = normalizedTerminalLines(bounded.text);
|
|
232
|
+
const termination = detectTermination(lines);
|
|
233
|
+
const candidates = [
|
|
234
|
+
parseBun(lines, input.hostOutcome),
|
|
235
|
+
parseTap(lines, input.hostOutcome),
|
|
236
|
+
parseTypescript(lines, input.hostOutcome),
|
|
237
|
+
].filter((value): value is NonNullable<typeof value> => value !== undefined);
|
|
238
|
+
|
|
239
|
+
const warnings: ParsedTestBuildOutput["parserWarnings"] = [];
|
|
240
|
+
if (input.upstreamTruncated === true) warnings.push("upstream-truncated");
|
|
241
|
+
if (bounded.limited) warnings.push("scan-limited");
|
|
242
|
+
if (candidates.length > 1) warnings.push("mixed-formats");
|
|
243
|
+
if (candidates.length === 0) {
|
|
244
|
+
return {
|
|
245
|
+
version: 1,
|
|
246
|
+
classification: "unrecognised",
|
|
247
|
+
format: "unknown",
|
|
248
|
+
hostOutcome: input.hostOutcome,
|
|
249
|
+
upstreamTruncated: input.upstreamTruncated === true,
|
|
250
|
+
scanLimited: bounded.limited,
|
|
251
|
+
terminalSummarySeen: false,
|
|
252
|
+
termination,
|
|
253
|
+
diagnostics: [],
|
|
254
|
+
parserWarnings: warnings,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const candidate = candidates[0]!;
|
|
259
|
+
if (candidate.outcomeConflict) warnings.push("host-outcome-conflict");
|
|
260
|
+
if (candidate.failedWithoutDiagnostic) warnings.push("failed-summary-without-diagnostic");
|
|
261
|
+
if (candidate.nonterminalSummary) warnings.push("nonterminal-summary");
|
|
262
|
+
const typescriptUnparsed = candidate.format === "typescript" ? candidate.unparsedLines : 0;
|
|
263
|
+
const complete = candidates.length === 1
|
|
264
|
+
&& input.upstreamTruncated !== true
|
|
265
|
+
&& !bounded.limited
|
|
266
|
+
&& termination === "normal"
|
|
267
|
+
&& candidate.terminalSummarySeen
|
|
268
|
+
&& !candidate.outcomeConflict
|
|
269
|
+
&& !candidate.failedWithoutDiagnostic
|
|
270
|
+
&& !candidate.nonterminalSummary
|
|
271
|
+
&& typescriptUnparsed === 0;
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
version: 1,
|
|
275
|
+
classification: complete ? "recognised" : "partial",
|
|
276
|
+
format: candidates.length > 1 ? "mixed" : candidate.format,
|
|
277
|
+
hostOutcome: input.hostOutcome,
|
|
278
|
+
upstreamTruncated: input.upstreamTruncated === true,
|
|
279
|
+
scanLimited: bounded.limited,
|
|
280
|
+
terminalSummarySeen: candidate.terminalSummarySeen,
|
|
281
|
+
termination,
|
|
282
|
+
summary: candidate.summary,
|
|
283
|
+
diagnostics: candidate.diagnostics,
|
|
284
|
+
parserWarnings: warnings,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function summaryLine(parsed: ParsedTestBuildOutput): string | undefined {
|
|
289
|
+
if (!parsed.summary) return undefined;
|
|
290
|
+
const fields: string[] = [];
|
|
291
|
+
if (parsed.summary.passed !== undefined) fields.push(`${parsed.summary.passed} passed`);
|
|
292
|
+
if (parsed.summary.failed !== undefined) fields.push(`${parsed.summary.failed} failed`);
|
|
293
|
+
if (parsed.summary.tests !== undefined) fields.push(`${parsed.summary.tests} tests`);
|
|
294
|
+
if (parsed.summary.files !== undefined) fields.push(`${parsed.summary.files} files`);
|
|
295
|
+
return fields.length > 0 ? fields.join(", ") : undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function planProspectiveTestOutputDelivery(
|
|
299
|
+
parsed: ParsedTestBuildOutput,
|
|
300
|
+
maxBytes: number,
|
|
301
|
+
options: { commandScope?: "simple" | "compound" | "unknown" } = {},
|
|
302
|
+
): ProspectiveTestOutputDelivery {
|
|
303
|
+
if (parsed.upstreamTruncated) return { version: 1, decision: "passthrough", reason: "upstream-truncated", bytes: 0 };
|
|
304
|
+
if (parsed.termination !== "normal") return { version: 1, decision: "passthrough", reason: "termination-not-normal", bytes: 0 };
|
|
305
|
+
if (parsed.classification !== "recognised") return { version: 1, decision: "passthrough", reason: "parser-not-complete", bytes: 0 };
|
|
306
|
+
const commandScope = options.commandScope ?? "unknown";
|
|
307
|
+
if (commandScope === "compound") return { version: 1, decision: "passthrough", reason: "compound-command", bytes: 0 };
|
|
308
|
+
if (commandScope !== "simple") return { version: 1, decision: "passthrough", reason: "command-scope-unknown", bytes: 0 };
|
|
309
|
+
|
|
310
|
+
const lines = [
|
|
311
|
+
`Execution outcome: ${parsed.hostOutcome.toUpperCase()}`,
|
|
312
|
+
`Recognised format: ${parsed.format}`,
|
|
313
|
+
];
|
|
314
|
+
const summary = summaryLine(parsed);
|
|
315
|
+
if (summary) lines.push(`Terminal summary: ${summary}`);
|
|
316
|
+
if (parsed.diagnostics.length > 0) {
|
|
317
|
+
lines.push("Diagnostics:");
|
|
318
|
+
for (const diagnostic of parsed.diagnostics) lines.push(diagnostic.text);
|
|
319
|
+
}
|
|
320
|
+
const text = lines.join("\n");
|
|
321
|
+
const bytes = new TextEncoder().encode(text).byteLength;
|
|
322
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || bytes > maxBytes) {
|
|
323
|
+
return { version: 1, decision: "passthrough", reason: "compact-budget-exceeded", bytes };
|
|
324
|
+
}
|
|
325
|
+
return { version: 1, decision: "compact-candidate", reason: "recognised-complete", bytes, text };
|
|
326
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export type ContextGatewayMode = "off" | "observe" | "enforce";
|
|
2
|
+
|
|
3
|
+
export type ContextGatewayEffectiveMode = Exclude<ContextGatewayMode, "enforce">;
|
|
4
|
+
|
|
5
|
+
export interface ContextGatewayBudgets {
|
|
6
|
+
maxInlineBytes: number;
|
|
7
|
+
maxResultBytes: number;
|
|
8
|
+
maxExactReadBytes: number;
|
|
9
|
+
maxSearchBytes: number;
|
|
10
|
+
maxSearchMatches: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Parsed layered config. `enforce` remains a valid requested value so doctor can refuse it explicitly. */
|
|
14
|
+
export interface ContextGatewayResolvedConfig {
|
|
15
|
+
mode: ContextGatewayMode;
|
|
16
|
+
budgets: ContextGatewayBudgets;
|
|
17
|
+
issues: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ContextGatewayToolClass =
|
|
21
|
+
| "code-read"
|
|
22
|
+
| "shell"
|
|
23
|
+
| "repo-search"
|
|
24
|
+
| "repo-ast"
|
|
25
|
+
| "repo-structure"
|
|
26
|
+
| "ast-grep"
|
|
27
|
+
| "mutation"
|
|
28
|
+
| "web"
|
|
29
|
+
| "subagent"
|
|
30
|
+
| "other";
|
|
31
|
+
|
|
32
|
+
export type ContextGatewayExecutionOutcome = "success" | "error";
|
|
33
|
+
|
|
34
|
+
export type ContextGatewayObservedCompleteness = "upstream-truncated" | "unknown";
|
|
35
|
+
|
|
36
|
+
export interface ContextGatewayObservedSource {
|
|
37
|
+
kind: "tool-result-boundary";
|
|
38
|
+
completeness: ContextGatewayObservedCompleteness;
|
|
39
|
+
contentBytes: number;
|
|
40
|
+
textBytes: number;
|
|
41
|
+
imageBytes: number;
|
|
42
|
+
detailsBytes: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ContextGatewayObservedView {
|
|
46
|
+
kind: "text-content";
|
|
47
|
+
textBytes: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ContextGatewayObservedDelivery {
|
|
51
|
+
representation: "passthrough";
|
|
52
|
+
contentBytes: number;
|
|
53
|
+
textBytes: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ContextGatewayTestOutputClassification = "recognised" | "partial" | "unrecognised";
|
|
57
|
+
export type ContextGatewayTestOutputFormat = "bun-test" | "tap" | "typescript" | "mixed" | "unknown";
|
|
58
|
+
|
|
59
|
+
export interface ContextGatewayTestOutputObservation {
|
|
60
|
+
parserVersion: 1;
|
|
61
|
+
classification: ContextGatewayTestOutputClassification;
|
|
62
|
+
format: ContextGatewayTestOutputFormat;
|
|
63
|
+
scanLimited: boolean;
|
|
64
|
+
terminalSummarySeen: boolean;
|
|
65
|
+
diagnosticCount: number;
|
|
66
|
+
warningCount: number;
|
|
67
|
+
commandScope: "simple" | "compound" | "unknown";
|
|
68
|
+
prospectiveDecision: "compact-candidate" | "passthrough";
|
|
69
|
+
prospectiveReason:
|
|
70
|
+
| "recognised-complete"
|
|
71
|
+
| "parser-not-complete"
|
|
72
|
+
| "upstream-truncated"
|
|
73
|
+
| "termination-not-normal"
|
|
74
|
+
| "compound-command"
|
|
75
|
+
| "command-scope-unknown"
|
|
76
|
+
| "compact-budget-exceeded";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** P01 keeps only metadata/sizes. No body, arguments, paths, or secret-derived hashes are exposed. */
|
|
80
|
+
export interface ContextGatewayObservation {
|
|
81
|
+
version: 1;
|
|
82
|
+
toolClass: ContextGatewayToolClass;
|
|
83
|
+
outcome: ContextGatewayExecutionOutcome;
|
|
84
|
+
source: ContextGatewayObservedSource;
|
|
85
|
+
view: ContextGatewayObservedView;
|
|
86
|
+
delivery: ContextGatewayObservedDelivery;
|
|
87
|
+
overBudget: boolean;
|
|
88
|
+
potentialBytesOverBudget: number;
|
|
89
|
+
testOutput?: ContextGatewayTestOutputObservation;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ContextGatewayClassTelemetry {
|
|
93
|
+
results: number;
|
|
94
|
+
errors: number;
|
|
95
|
+
contentBytes: number;
|
|
96
|
+
textBytes: number;
|
|
97
|
+
imageBytes: number;
|
|
98
|
+
detailsBytes: number;
|
|
99
|
+
upstreamTruncatedResults: number;
|
|
100
|
+
overBudgetResults: number;
|
|
101
|
+
potentialBytesOverBudget: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type ContextGatewayNativePolicyReason =
|
|
105
|
+
| "invalid-wrapper-budget"
|
|
106
|
+
| "unknown-flag"
|
|
107
|
+
| "missing-flag-value"
|
|
108
|
+
| "duplicate-flag"
|
|
109
|
+
| "invalid-flag-value"
|
|
110
|
+
| "conflicting-flags"
|
|
111
|
+
| "compact-limit-exceeded"
|
|
112
|
+
| "full-limit-exceeded"
|
|
113
|
+
| "unknown";
|
|
114
|
+
|
|
115
|
+
export interface ContextGatewayNativePolicyTelemetry {
|
|
116
|
+
results: number;
|
|
117
|
+
refusals: number;
|
|
118
|
+
fullOverrides: number;
|
|
119
|
+
byReason: Partial<Record<ContextGatewayNativePolicyReason, number>>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ContextGatewayTestOutputTelemetry {
|
|
123
|
+
parserVersion: 1;
|
|
124
|
+
results: number;
|
|
125
|
+
scanLimitedResults: number;
|
|
126
|
+
compactCandidates: number;
|
|
127
|
+
passthroughRecommended: number;
|
|
128
|
+
byClassification: Record<ContextGatewayTestOutputClassification, number>;
|
|
129
|
+
byFormat: Record<ContextGatewayTestOutputFormat, number>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface ContextGatewayTelemetrySnapshot extends ContextGatewayClassTelemetry {
|
|
133
|
+
version: 1;
|
|
134
|
+
pendingCalls: number;
|
|
135
|
+
unboundResults: number;
|
|
136
|
+
repeatCandidateCount: number;
|
|
137
|
+
retrievalCalls: number;
|
|
138
|
+
nativePolicy: ContextGatewayNativePolicyTelemetry;
|
|
139
|
+
testOutput: ContextGatewayTestOutputTelemetry;
|
|
140
|
+
byClass: Partial<Record<ContextGatewayToolClass, ContextGatewayClassTelemetry>>;
|
|
141
|
+
lastObservation?: ContextGatewayObservation;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ContextGatewayRuntimeState {
|
|
145
|
+
requestedMode: ContextGatewayMode;
|
|
146
|
+
effectiveMode: ContextGatewayEffectiveMode;
|
|
147
|
+
readonly config: ContextGatewayResolvedConfig;
|
|
148
|
+
readonly telemetry: {
|
|
149
|
+
snapshot(): ContextGatewayTelemetrySnapshot;
|
|
150
|
+
reset(): void;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
AutoCompressionBlockedError,
|
|
4
|
+
createAutoCompressionBlock,
|
|
5
|
+
type AutoCompressionResult,
|
|
6
|
+
type CreateAutoCompressionBlockOptions,
|
|
7
|
+
} from "./auto-compress.js";
|
|
8
|
+
import { canonicalMessageHash } from "./conversation-index.js";
|
|
9
|
+
import { stableMessageKeys } from "./pruner-message-ids.js";
|
|
10
|
+
import type { CompressionCandidate } from "./pruner-types.js";
|
|
11
|
+
import type { DcpState } from "./state.js";
|
|
12
|
+
|
|
13
|
+
const rejectedSources = new WeakMap<DcpState, { key: string; error: AutoCompressionBlockedError }>();
|
|
14
|
+
const FINALIZATION_GRACE_MS = 1_000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A source-size estimate is not a net-savings estimate. Try the economical
|
|
18
|
+
* prefix first, then the largest prefix which passed the SAME retention and
|
|
19
|
+
* provider-evidence policy. Both attempts share one deadline. Never relax
|
|
20
|
+
* protection/evidence or claim insufficient gain as a successful compression.
|
|
21
|
+
*/
|
|
22
|
+
export async function createBudgetedAutoCompressionBlock(
|
|
23
|
+
options: CreateAutoCompressionBlockOptions,
|
|
24
|
+
largestSafeCandidate: CompressionCandidate | null,
|
|
25
|
+
): Promise<AutoCompressionResult> {
|
|
26
|
+
options.signal?.throwIfAborted();
|
|
27
|
+
const epoch = options.state.sessionEpoch;
|
|
28
|
+
const key = createHash("sha256").update(JSON.stringify({
|
|
29
|
+
epoch,
|
|
30
|
+
source: options.messages.map(canonicalMessageHash),
|
|
31
|
+
ids: stableMessageKeys(options.messages),
|
|
32
|
+
config: options.config,
|
|
33
|
+
budget: options.requiredGainTokens ?? 0,
|
|
34
|
+
allowPartialGain: options.allowPartialGain === true,
|
|
35
|
+
largest: largestSafeCandidate,
|
|
36
|
+
})).digest("hex");
|
|
37
|
+
const rejected = rejectedSources.get(options.state);
|
|
38
|
+
if (rejected?.key === key) throw rejected.error;
|
|
39
|
+
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const onAbort = () => controller.abort(options.signal?.reason);
|
|
42
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
43
|
+
const timeout = Math.max(1, options.config.compress.autoCompress.timeoutMs);
|
|
44
|
+
// generateModelSummary owns the configured summarizer deadline and can
|
|
45
|
+
// deliberately recover from it with the deterministic extractive fallback.
|
|
46
|
+
// Keep a small bounded tail for that fallback and durable publication;
|
|
47
|
+
// aborting at exactly `timeout` races the fallback and turns every slow model
|
|
48
|
+
// into a generic preparation failure instead of a successful compression.
|
|
49
|
+
const operationDeadline = timeout + FINALIZATION_GRACE_MS;
|
|
50
|
+
let internalDeadlineExceeded = false;
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
internalDeadlineExceeded = true;
|
|
53
|
+
controller.abort(new Error("DCP compression operation deadline exceeded"));
|
|
54
|
+
}, operationDeadline);
|
|
55
|
+
// A parent cancellation is passed through untouched and never becomes a
|
|
56
|
+
// blocked reason. Only the internal whole-operation deadline is classified:
|
|
57
|
+
// otherwise callers surface a generic preparation failure and keep retrying
|
|
58
|
+
// an operation that can no longer make progress within its budget.
|
|
59
|
+
const operationAbortError = (): unknown => {
|
|
60
|
+
if (options.signal?.aborted) {
|
|
61
|
+
return options.signal.reason ?? controller.signal.reason ?? new Error("DCP compression operation aborted");
|
|
62
|
+
}
|
|
63
|
+
if (internalDeadlineExceeded) {
|
|
64
|
+
return new AutoCompressionBlockedError(
|
|
65
|
+
"summarizer-unavailable",
|
|
66
|
+
`Auto-compression preparation exceeded its whole-operation deadline of ${operationDeadline}ms ` +
|
|
67
|
+
"(DCP compression operation deadline exceeded) and was stopped without publishing state",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return controller.signal.reason ?? new Error("DCP compression operation aborted");
|
|
71
|
+
};
|
|
72
|
+
const candidates = [options.candidate];
|
|
73
|
+
if (largestSafeCandidate && (
|
|
74
|
+
largestSafeCandidate.startId !== options.candidate.startId ||
|
|
75
|
+
largestSafeCandidate.endId !== options.candidate.endId
|
|
76
|
+
)) candidates.push(largestSafeCandidate);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
for (let attempt = 0; attempt < candidates.length; attempt++) {
|
|
80
|
+
if (controller.signal.aborted) throw operationAbortError();
|
|
81
|
+
if (options.state.sessionEpoch !== epoch) throw new Error("stale_plan: DCP owner changed before budget replan");
|
|
82
|
+
try {
|
|
83
|
+
const result = await createAutoCompressionBlock({
|
|
84
|
+
...options,
|
|
85
|
+
candidate: candidates[attempt]!,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
});
|
|
88
|
+
rejectedSources.delete(options.state);
|
|
89
|
+
return result;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (controller.signal.aborted) throw operationAbortError();
|
|
92
|
+
const insufficient = error instanceof AutoCompressionBlockedError &&
|
|
93
|
+
(error.blockedReason === "budget-exhausted" || error.blockedReason === "non-positive-gain");
|
|
94
|
+
if (!insufficient) throw error;
|
|
95
|
+
if (attempt + 1 === candidates.length) {
|
|
96
|
+
rejectedSources.set(options.state, { key, error });
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
throw new AutoCompressionBlockedError("budget-exhausted", "No budget-safe compression candidate");
|
|
102
|
+
} finally {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
105
|
+
}
|
|
106
|
+
}
|