@tea-agent/loop-agent 0.39.0-beta.12 → 0.39.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/application/dag/generate-task-dag.js +6 -2
- package/dist/application/task-lifecycle/advance.js +20 -4
- package/dist/application/task-lifecycle/observe.js +171 -17
- package/dist/application/task-lifecycle/plan-transitions.js +42 -7
- package/dist/build-stamp.json +3 -3
- package/dist/commands/client-recovery.js +8 -36
- package/dist/executors/dag-pi-executor.js +636 -109
- package/dist/executors/pi-executor.js +8 -4
- package/dist/executors/pi-sdk-executor.js +33 -5
- package/dist/executors/shell-executor.js +102 -32
- package/dist/governance/checks.js +1 -0
- package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
- package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
- package/dist/shared/pi-context-pressure/env.js +58 -0
- package/dist/shared/pi-context-pressure/extension.js +100 -0
- package/dist/shared/pi-context-pressure/index.js +7 -0
- package/dist/shared/pi-context-pressure/overflow.js +252 -0
- package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
- package/dist/shared/pi-context-pressure/telemetry.js +51 -0
- package/dist/task/frontend-project-capability.js +3 -1
- package/dist/task/source-prepare/fragment-inventory.js +4 -1
- package/dist/worker/console/chat/pi-runtime.js +146 -5
- package/dist/worker/console/chat/provider-error.js +2 -1
- package/dist/worker/console/chat/routes.js +3 -0
- package/dist/worker/console/chat/sift-bridge.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +20 -2
- package/dist/worker/console/operator-actions.js +4 -3
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CXj_GnSb.js → abnfDiagram-N423BO3Z-DC863mud.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BZp6JAp7.js → arc-CftC38G9.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DfpcEuYU.js → architectureDiagram-T3A2C74G-B3PAPQCw.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-_Gf0xadb.js → blockDiagram-VBNYF7ZC-C9UDJNRv.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct2QPCmv.js → c4Diagram-5PPSVZJV--BJ76fv_.js} +1 -1
- package/dist/worker/console/static/assets/channel-CUz-Bg86.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BfklkzKl.js → chunk-2GRJ4B5K--hiIqoGp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Dy25vZJV.js → chunk-2Q5K7J3B-DgTzpIa3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BFlCRZep.js → chunk-5RXB4S5H-DIwpJziP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-op3oVxIE.js → chunk-5VM5RSS4-BJzbdUi0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C0R7rzF2.js → chunk-6Q2QTUOP-BbGouI1Z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bt44TCGy.js → chunk-GF5L2VYU-B9247w69.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-_uEE_XFx.js → chunk-JWPE2WC7-CXob_wYy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-C3TOYGZ9.js → chunk-KBJHAD2P-C6FUel1g.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Cz60oBPV.js → chunk-RYQCIY6F-DGKRYKHu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-8Bik0qis.js → chunk-XXDRQBXY-kNBqNPfx.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlUacmD.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlUacmD.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C2QIOA4a.js → cose-bilkent-JH36ORCC--xmkDwfD.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CSH_yUUd.js → cynefin-VYW2F7L2-Cw5FMMuG.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BDLw2XFM.js → cynefinDiagram-MW4NZA55-CuLslt2t.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CcD9ZtF4.js → dagre-VZM6K2ZE-D9ngqrs2.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DqwtkBsI.js → diagram-7IWD3JNH-RDRSbHQp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BWVEcqsC.js → diagram-B4RE2ZJO-BgQ1P9aV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-M-WAbtQi.js → diagram-LBJQPF4R-D3WWyPtn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DKW7SixP.js → diagram-Q27KOJAE-L2k28wTR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-PHHPPtrj.js → diagram-UB23O5K3-DDNrA5Qw.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BQD-B4RX.js → ebnfDiagram-BXEA7PRR-BaRFCOdM.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BFvULb51.js → erDiagram-JOGREHBK-CM33DVVM.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Bw-aXTCb.js → flowDiagram-UKHOOZJN-CVBSjOxe.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BGKw04Qx.js → ganttDiagram-PKOTCBZU-BdscewE_.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-RqKaPR-I.js → gitGraphDiagram-DS77QQ5N-HRuSZb7g.js} +1 -1
- package/dist/worker/console/static/assets/{index-B_D8rbWc.js → index-B9JJQsVK.js} +102 -72
- package/dist/worker/console/static/assets/index-rWaGv4jz.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-YO5dnzrJ.js → infoDiagram-6WML65LV-cYfHvfAR.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js → ishikawaDiagram-WSZJBQD7-CZmaoOuy.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Bszls1DD.js → journeyDiagram-NVQOT4AX-ntYijh7g.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PhzeaZ09.js → kanban-definition-27J2QSJJ-Cz3b0DeD.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BsjbDoXi.js → linear-DvGonpsP.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-0B7NnWKk.js → mermaid.core-B-3vjfyW.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BJr4Fj-q.js → mindmap-definition-FAOFIHXS-5iKxlsX3.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-moC4fpGB.js → pegDiagram-VL7TDLO6-C8BUwapW.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BEw37-2c.js → pieDiagram-7S7Q4E2Y-B2rPoQQG.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq6LyasU.js → quadrantDiagram-CIZ2JOQS-jDeVUAy4.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DUCMcK0D.js → railroadDiagram-AXF67PYL-DV140Dwm.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3upTZm7.js → requirementDiagram-LRYGKXZP-CnYgHtYC.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BI_gMsCW.js → sankeyDiagram-W5VNT64P-SIK3CdWw.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-YFOIRzfN.js → sequenceDiagram-SI44F4Z6-DW_vZix7.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-dOnB7UDD.js → sizeCapture-X5ZJPWSS-NBAtkg0C.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BXUniaIh.js → stateDiagram-OKZ733FA-Bi1bQxpi.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DjWKYjQ1.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-2tA4wTNu.js → swimlanes-SLNWSIFB-8PT_uP_i.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B4cdm7bH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DO0HkJXC.js → timeline-definition-Z64GVDOM-CD0ZZPk0.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DvIOzixv.js → vennDiagram-T6HMQDX7-BbEgWdhK.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DXDZ0cTj.js → wardleyDiagram-T6FBY63Y-DCwPKdLl.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B32Ark0D.js → xychartDiagram-ELKLHX3M-sfC5PcNg.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/static/styles.css +9 -0
- package/dist/worker/observe/static/views/dag-inspector.js +40 -0
- package/dist/worker/observe/static/views/session-timeline.js +135 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +157 -7
- package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
- package/dist/workflows/dag/backend-test-result-contract.js +4 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +174 -31
- package/dist/workflows/dag/frontend-review-context.js +12 -1
- package/dist/workflows/dag/frontend-shadow-dual-write.js +59 -13
- package/dist/workflows/dag/frontend-writer-admission.js +13 -0
- package/dist/workflows/dag/init-hybrid.js +8 -3
- package/dist/workflows/dag/node-execution.js +277 -3
- package/dist/workflows/dag/rerun-feedback.js +135 -3
- package/dist/workflows/dag/retry-policy.js +13 -122
- package/dist/workflows/dag/types.js +11 -4
- package/docs/architecture/runtime-boundaries.md +2 -1
- package/docs/templates/backend-test-dag.json +4 -3
- package/harness.json +3 -3
- package/package.json +4 -3
- package/dist/worker/console/static/assets/channel-3TxJgYaH.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +0 -1
- package/dist/worker/console/static/assets/index-BdNx6fj0.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D-RJBbb0.js +0 -8
|
@@ -1023,15 +1023,19 @@ export function classifyPiFailure(input) {
|
|
|
1023
1023
|
return "context-overflow";
|
|
1024
1024
|
// Prefer explicit rate-limit / HTTP 429 before generic "limit" quota phrases.
|
|
1025
1025
|
// Chinese gateways often say "请求频率已达到限制" without English "rate limit".
|
|
1026
|
-
|
|
1026
|
+
// Patterns need error-shaped context: the captured stream also contains
|
|
1027
|
+
// serialized session events, where a bare "429" is an output-token counter,
|
|
1028
|
+
// bare "rpm"/"tpm" are base64 substrings of encrypted reasoning, and bare
|
|
1029
|
+
// "network" is a "no-network" UI state name.
|
|
1030
|
+
if (/rate.?limit|too many requests|(?:http|status)[^\d]{0,4}429\b|"code"\s*:\s*"?429\b|(?:^|\n)429[\s:]|请求频率|频率.*限制|\b(?:rpm|tpm)\s*(?:limit|quota|exceeded)/.test(combined))
|
|
1027
1031
|
return "rate-limit";
|
|
1028
|
-
if (/quota|usage limit|reached
|
|
1032
|
+
if (/quota|usage limit|reached[^\n.]{0,60}limit|5\s*小时|5小时|insufficient_quota|billing/.test(combined))
|
|
1029
1033
|
return "quota";
|
|
1030
1034
|
if (/\bunauthorized\b|\bhttp\s*40[13]\b|invalid api key|authentication (?:failed|required|error)|auth(?:entication)? failed/.test(combined))
|
|
1031
1035
|
return "auth";
|
|
1032
|
-
if (/unknown provider|unknown model|model
|
|
1036
|
+
if (/unknown provider|unknown model|(?:provider|model|service|gateway)\s+(?:is\s+)?unavailable|overloaded|at capacity|temporarily unavailable/.test(combined))
|
|
1033
1037
|
return "unavailable";
|
|
1034
|
-
if (/network|
|
|
1038
|
+
if (/network\s*(?:error|failure|issue|timeout|unreachable|reset|abort)|networkerror|connection (?:reset|error|refused|closed|timed\s*out)|request timed out|socket hang up|econn\w*|etimedout|fetch failed|dns (?:error|failure|lookup failed)/.test(combined))
|
|
1035
1039
|
return "network";
|
|
1036
1040
|
if (input.exitCode === 0 && !input.assistantText.trim())
|
|
1037
1041
|
return "empty-output";
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
3
5
|
import { computeExtensionToolAllowlistExtras } from "./pi-extension-resolver.js";
|
|
4
6
|
import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_ABORT_GRACE_MS, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, extractPiEventStopReason, extractAssistantTextFromPiJson, } from "./pi-executor.js";
|
|
5
7
|
import { serializeSessionEvent } from "./pi-event-serializer.js";
|
|
6
8
|
import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
|
|
9
|
+
import { createCompactionController, createContextPressureExtension, createSiftBridgeState, inMemoryCompactionSettings, isDagSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, SIFT_RETRIEVE_TOOL_NAME, } from "../shared/pi-context-pressure/index.js";
|
|
7
10
|
/** Same-session recovery after a transport stall. Exactly one continue per SDK attempt. */
|
|
8
11
|
export const PI_SDK_STALL_CONTINUE_MAX = 1;
|
|
9
12
|
/**
|
|
@@ -215,15 +218,37 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
215
218
|
// Inject rate-limit-tolerant auto-retry settings for the session lifecycle.
|
|
216
219
|
// Prefer SettingsManager.inMemory so harness runs do not depend on ~/.pi or
|
|
217
220
|
// project .pi/settings.json being present/trusted.
|
|
221
|
+
const policy = parseCompactionPolicy();
|
|
222
|
+
const compaction = {
|
|
223
|
+
...PI_RECOMMENDED_COMPACTION,
|
|
224
|
+
...inMemoryCompactionSettings(policy),
|
|
225
|
+
};
|
|
218
226
|
const settingsManager = typeof SettingsManager?.inMemory === "function"
|
|
219
227
|
? SettingsManager.inMemory({
|
|
220
228
|
retry: {
|
|
221
229
|
...PI_RECOMMENDED_RETRY,
|
|
222
230
|
provider: { ...PI_RECOMMENDED_RETRY.provider },
|
|
223
231
|
},
|
|
224
|
-
compaction
|
|
232
|
+
compaction,
|
|
225
233
|
}, { projectTrusted: true })
|
|
226
234
|
: undefined;
|
|
235
|
+
const nodeSessionId = `dag-${randomUUID()}`;
|
|
236
|
+
const controller = createCompactionController();
|
|
237
|
+
const siftEnabled = isDagSiftEnabled();
|
|
238
|
+
const sift = siftEnabled.enabled
|
|
239
|
+
? createSiftBridgeState(parseSiftBridgeConfig({
|
|
240
|
+
sessionId: nodeSessionId,
|
|
241
|
+
defaultStashRoot: path.join(os.tmpdir(), "loop-agent-dag-sift"),
|
|
242
|
+
}))
|
|
243
|
+
: undefined;
|
|
244
|
+
if (sift)
|
|
245
|
+
sift.counters = controller.counters;
|
|
246
|
+
const pressureExtension = createContextPressureExtension({
|
|
247
|
+
name: "dag-pi-context-pressure",
|
|
248
|
+
policy,
|
|
249
|
+
controller,
|
|
250
|
+
sift,
|
|
251
|
+
});
|
|
227
252
|
const loader = new DefaultResourceLoader({
|
|
228
253
|
cwd: input.cwd,
|
|
229
254
|
agentDir,
|
|
@@ -233,6 +258,7 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
233
258
|
...(input.additionalExtensionPaths && input.additionalExtensionPaths.length > 0
|
|
234
259
|
? { additionalExtensionPaths: input.additionalExtensionPaths }
|
|
235
260
|
: {}),
|
|
261
|
+
...(pressureExtension ? { extensionFactories: [pressureExtension] } : {}),
|
|
236
262
|
...(settingsManager ? { settingsManager } : {}),
|
|
237
263
|
appendSystemPromptOverride: (base) => [
|
|
238
264
|
...base,
|
|
@@ -247,10 +273,12 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
247
273
|
// extras into the create-time allowlist; protected builtins, the deny
|
|
248
274
|
// surface, and write-capable extension tools never join.
|
|
249
275
|
const extensionToolExtras = [];
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
extensionToolExtras.
|
|
276
|
+
const registered = collectLoaderExtensionToolNames(loader);
|
|
277
|
+
extensionToolExtras.push(...computeExtensionToolAllowlistExtras(registered.map((name) => ({ name })), input.toolNames));
|
|
278
|
+
if (siftEnabled.enabled &&
|
|
279
|
+
!extensionToolExtras.includes(SIFT_RETRIEVE_TOOL_NAME) &&
|
|
280
|
+
!input.toolNames.includes(SIFT_RETRIEVE_TOOL_NAME)) {
|
|
281
|
+
extensionToolExtras.push(...computeExtensionToolAllowlistExtras([{ name: SIFT_RETRIEVE_TOOL_NAME }], input.toolNames));
|
|
254
282
|
}
|
|
255
283
|
const sessionToolNames = [...input.toolNames, ...extensionToolExtras];
|
|
256
284
|
if (input.requireWriterCustomTools) {
|
|
@@ -42,11 +42,11 @@ import { materializeBackendTestExecutionContract } from "../workflows/dag/backen
|
|
|
42
42
|
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
43
43
|
import { frontendTestLayoutFromSpec, } from "../workflows/dag/frontend-test-layout.js";
|
|
44
44
|
import { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
|
|
45
|
-
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
45
|
+
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, classifyBackendTestCorrespondenceRepair, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
46
46
|
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
|
|
47
47
|
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, markdownFiles, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
48
48
|
import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
|
|
49
|
-
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
49
|
+
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, summarizeBackendTestExclusions, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
50
50
|
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
51
51
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
52
52
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
@@ -880,26 +880,49 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
880
880
|
const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
|
|
881
881
|
await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
|
|
882
882
|
outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
|
|
883
|
-
if (correspondence.facts.status === "FAIL"
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
.
|
|
887
|
-
.
|
|
888
|
-
|
|
889
|
-
facts.status = "REPAIRABLE";
|
|
890
|
-
facts.repairEligible = true;
|
|
891
|
-
facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
|
|
883
|
+
if (correspondence.facts.status === "FAIL") {
|
|
884
|
+
const repair = classifyBackendTestCorrespondenceRepair(correspondence.facts, backendLayout);
|
|
885
|
+
if (repair.markdownBlockingEntries.length > 0) {
|
|
886
|
+
facts.status = "BLOCKED";
|
|
887
|
+
facts.repairEligible = false;
|
|
888
|
+
facts.repairPaths = [];
|
|
892
889
|
facts.findings.push({
|
|
893
|
-
kind: "markdown-
|
|
890
|
+
kind: "markdown-correspondence-contract",
|
|
894
891
|
classification: "test-asset-defect",
|
|
895
|
-
repairability: "
|
|
896
|
-
detail:
|
|
897
|
-
.
|
|
898
|
-
.flatMap((entry) => entry.findings)
|
|
892
|
+
repairability: "blocked",
|
|
893
|
+
detail: repair.markdownBlockingEntries
|
|
894
|
+
.map((entry) => `${entry.caseId ?? "unknown Case"}: ${entry.reasonCodes.join(", ")}`)
|
|
899
895
|
.join("; ")
|
|
900
|
-
.slice(0, 12_000)
|
|
896
|
+
.slice(0, 12_000),
|
|
901
897
|
});
|
|
902
898
|
}
|
|
899
|
+
else if (repair.pytestRepairPaths.length > 0 &&
|
|
900
|
+
(facts.status === "PASS" || facts.status === "REPAIRABLE")) {
|
|
901
|
+
facts.status = "REPAIRABLE";
|
|
902
|
+
facts.repairEligible = true;
|
|
903
|
+
facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...repair.pytestRepairPaths])).sort();
|
|
904
|
+
const repairEntries = correspondence.facts.entries.filter((entry) => repair.pytestRepairEntries.some((candidate) => candidate.caseId === entry.caseId));
|
|
905
|
+
for (const entry of repairEntries.slice(0, 40)) {
|
|
906
|
+
const classified = repair.pytestRepairEntries.find((candidate) => candidate.caseId === entry.caseId);
|
|
907
|
+
facts.findings.push({
|
|
908
|
+
kind: "markdown-pytest-correspondence",
|
|
909
|
+
classification: "test-asset-defect",
|
|
910
|
+
repairability: "repairable",
|
|
911
|
+
detail: entry.findings.join("; ").slice(0, 2_000) || "Markdown-to-pytest correspondence is incomplete",
|
|
912
|
+
...(entry.caseId ? { caseId: entry.caseId } : {}),
|
|
913
|
+
reasonCodes: classified?.reasonCodes ?? [],
|
|
914
|
+
repairPaths: classified?.paths ?? [],
|
|
915
|
+
payloadExpectedPaths: Array.from(new Set([
|
|
916
|
+
...entry.payloadAssessment.requiredPaths,
|
|
917
|
+
...entry.payloadAssessment.allowedPaths,
|
|
918
|
+
])).sort(),
|
|
919
|
+
payloadObservedPaths: entry.payloadAssessment.observedPaths,
|
|
920
|
+
payloadMissingPaths: entry.payloadAssessment.missingPaths,
|
|
921
|
+
payloadUnexpectedPaths: entry.payloadAssessment.unexpectedPaths,
|
|
922
|
+
payloadEnumMismatches: entry.payloadAssessment.enumMismatches,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
903
926
|
}
|
|
904
927
|
}
|
|
905
928
|
catch (error) {
|
|
@@ -922,6 +945,19 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
922
945
|
collectedItemCount: facts.collectedItemCount,
|
|
923
946
|
missingMappedScripts: facts.missingMappedScripts,
|
|
924
947
|
repairPaths: facts.repairPaths,
|
|
948
|
+
repairFindings: facts.findings.map((finding) => ({
|
|
949
|
+
kind: finding.kind,
|
|
950
|
+
repairability: finding.repairability,
|
|
951
|
+
detail: finding.detail,
|
|
952
|
+
...(finding.caseId ? { caseId: finding.caseId } : {}),
|
|
953
|
+
...(finding.reasonCodes ? { reasonCodes: finding.reasonCodes } : {}),
|
|
954
|
+
...(finding.repairPaths ? { repairPaths: finding.repairPaths } : {}),
|
|
955
|
+
...(finding.payloadExpectedPaths ? { payloadExpectedPaths: finding.payloadExpectedPaths } : {}),
|
|
956
|
+
...(finding.payloadObservedPaths ? { payloadObservedPaths: finding.payloadObservedPaths } : {}),
|
|
957
|
+
...(finding.payloadMissingPaths ? { payloadMissingPaths: finding.payloadMissingPaths } : {}),
|
|
958
|
+
...(finding.payloadUnexpectedPaths ? { payloadUnexpectedPaths: finding.payloadUnexpectedPaths } : {}),
|
|
959
|
+
...(finding.payloadEnumMismatches ? { payloadEnumMismatches: finding.payloadEnumMismatches } : {}),
|
|
960
|
+
})),
|
|
925
961
|
factsPath: "contracts/backend-test-pytest-collection-initial.json",
|
|
926
962
|
}),
|
|
927
963
|
stderr: "",
|
|
@@ -1021,6 +1057,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1021
1057
|
})
|
|
1022
1058
|
: { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
|
|
1023
1059
|
const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
|
|
1060
|
+
const exclusionSummary = summarizeBackendTestExclusions(eligibility.excludedItems);
|
|
1024
1061
|
const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
|
|
1025
1062
|
await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
|
|
1026
1063
|
await writeFile(eligibilityFactsPath, `${JSON.stringify({
|
|
@@ -1031,6 +1068,9 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1031
1068
|
excludedItemCount: eligibility.excludedItems.length,
|
|
1032
1069
|
eligibleItemIds: eligibility.eligibleItemIds,
|
|
1033
1070
|
excludedItems: eligibility.excludedItems,
|
|
1071
|
+
exclusionSummary,
|
|
1072
|
+
pytestSpawned: false,
|
|
1073
|
+
businessTestBodyExecutedCount: 0,
|
|
1034
1074
|
inputHashes: eligibilityInputHashes,
|
|
1035
1075
|
}, null, 2)}\n`, "utf8");
|
|
1036
1076
|
const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
|
|
@@ -1043,6 +1083,10 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1043
1083
|
`- Collected items: ${effective.collectedItemIds.length}`,
|
|
1044
1084
|
`- Eligible items: ${eligibility.eligibleItemIds.length}`,
|
|
1045
1085
|
`- Excluded items: ${eligibility.excludedItems.length}`,
|
|
1086
|
+
`- Excluded Cases: ${exclusionSummary.excludedCaseCount}`,
|
|
1087
|
+
`- Exclusion categories: ${Object.entries(exclusionSummary.excludedByReason).map(([category, count]) => `${category}=${count}`).join("; ") || "none"}`,
|
|
1088
|
+
"- pytest spawned: false",
|
|
1089
|
+
"- Business test bodies executed: 0",
|
|
1046
1090
|
"",
|
|
1047
1091
|
"## Excluded Items",
|
|
1048
1092
|
"",
|
|
@@ -1071,6 +1115,9 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1071
1115
|
executionReadinessStatus: readiness.status,
|
|
1072
1116
|
eligibleItemCount: readiness.eligibleItemIds.length,
|
|
1073
1117
|
excludedItemCount: readiness.excludedItems.length,
|
|
1118
|
+
exclusionSummary: readiness.exclusionSummary,
|
|
1119
|
+
pytestSpawned: readiness.pytestSpawned,
|
|
1120
|
+
businessTestBodyExecutedCount: readiness.businessTestBodyExecutedCount,
|
|
1074
1121
|
factsPath: "contracts/backend-test-pytest-collection-effective.json",
|
|
1075
1122
|
readinessPath: "contracts/backend-test-execution-readiness.json",
|
|
1076
1123
|
reportPath: artifacts.reportPath,
|
|
@@ -2224,6 +2271,21 @@ async function collectReviewRestartPhase(runDir, reviewNodeId) {
|
|
|
2224
2271
|
}
|
|
2225
2272
|
/** A+B (AC-009): deterministic §4.10 restart phase for a committed
|
|
2226
2273
|
* `request_design_changes` fact. Mirrors the review collector. */
|
|
2274
|
+
/** Number of findings attached to the committed request_design_changes
|
|
2275
|
+
* terminal fact (0 when absent) — used for the admission advisory. */
|
|
2276
|
+
async function collectDesignReviewFindingCount(runDir, designNodeId) {
|
|
2277
|
+
try {
|
|
2278
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
|
|
2279
|
+
const request = records.find((record) => record.phase === "committed" &&
|
|
2280
|
+
record.fact.kind ===
|
|
2281
|
+
"request_design_changes");
|
|
2282
|
+
const findings = (request?.fact).findings;
|
|
2283
|
+
return Array.isArray(findings) ? findings.length : 0;
|
|
2284
|
+
}
|
|
2285
|
+
catch {
|
|
2286
|
+
return 0;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2227
2289
|
async function collectDesignRestartPhase(runDir, designNodeId) {
|
|
2228
2290
|
try {
|
|
2229
2291
|
const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
|
|
@@ -2540,21 +2602,30 @@ async function executeFrontendWriterAdmission(input, meta) {
|
|
|
2540
2602
|
const shell = input.task.shell;
|
|
2541
2603
|
const config = shell.frontendWriterAdmission;
|
|
2542
2604
|
try {
|
|
2543
|
-
// 1. Design verdict
|
|
2544
|
-
//
|
|
2545
|
-
// The verdict is read from the committed typed design terminal fact, not
|
|
2605
|
+
// 1. Design verdict handling (r12 policy change). The typed terminal
|
|
2606
|
+
// verdict is read from the committed typed design terminal fact, not
|
|
2546
2607
|
// from the legacy first-line VERDICT text protocol (AC-003/AC-004).
|
|
2608
|
+
// A request_design_changes verdict no longer blocks writer admission:
|
|
2609
|
+
// it is downgraded to an advisory that rides along in the admission
|
|
2610
|
+
// receipt (and reaches implement via the review node's upstream
|
|
2611
|
+
// context). Blocking is owned by the deterministic gates (prewrite,
|
|
2612
|
+
// write guard) and the final code review. A missing typed terminal
|
|
2613
|
+
// verdict still fails closed. In small topology the design review node
|
|
2614
|
+
// is absent and the check is vacuous.
|
|
2547
2615
|
let verdictPass = true;
|
|
2548
|
-
let
|
|
2616
|
+
let designAdvisory;
|
|
2549
2617
|
if (config.designReviewFromNodeId) {
|
|
2550
2618
|
const designKinds = await collectDesignTerminalKinds(meta.runDir, config.designReviewFromNodeId);
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2619
|
+
if (designKinds.length !== 1) {
|
|
2620
|
+
verdictPass = false;
|
|
2621
|
+
}
|
|
2622
|
+
else if (designKinds[0] !== "approve_design") {
|
|
2623
|
+
designAdvisory = {
|
|
2624
|
+
verdict: designKinds[0],
|
|
2625
|
+
restartPhase: await collectDesignRestartPhase(meta.runDir, config.designReviewFromNodeId),
|
|
2626
|
+
findingCount: await collectDesignReviewFindingCount(meta.runDir, config.designReviewFromNodeId),
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2558
2629
|
}
|
|
2559
2630
|
// 2. Read the canonical contract + design-policy result.
|
|
2560
2631
|
const contractRaw = await readRunDirJson(meta.runDir, "contracts/frontend-implementation-contract.json");
|
|
@@ -2630,10 +2701,8 @@ async function executeFrontendWriterAdmission(input, meta) {
|
|
|
2630
2701
|
: [
|
|
2631
2702
|
...admission.findings,
|
|
2632
2703
|
{
|
|
2633
|
-
code: "design-approval-
|
|
2634
|
-
message:
|
|
2635
|
-
? `design review typed terminal verdict is not approve_design (restartPhase=${designRestartPhase})`
|
|
2636
|
-
: "design review typed terminal verdict is not approve_design",
|
|
2704
|
+
code: "design-approval-missing",
|
|
2705
|
+
message: "design review typed terminal verdict missing or ambiguous (fail closed)",
|
|
2637
2706
|
},
|
|
2638
2707
|
];
|
|
2639
2708
|
const classification = findings.length === 0 && admission.classification === "accepted"
|
|
@@ -2654,6 +2723,7 @@ async function executeFrontendWriterAdmission(input, meta) {
|
|
|
2654
2723
|
? { baselineDebtPolicy: admission.baselineDebtPolicy }
|
|
2655
2724
|
: {}),
|
|
2656
2725
|
findings,
|
|
2726
|
+
...(designAdvisory ? { designReviewAdvisory: designAdvisory } : {}),
|
|
2657
2727
|
failureSource,
|
|
2658
2728
|
});
|
|
2659
2729
|
await writeDagRunJsonArtifact(meta.runDir, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, result);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const CHECKPOINT_MAX_BYTES = 12 * 1024;
|
|
2
|
+
function boundText(value, maxChars) {
|
|
3
|
+
if (typeof value !== "string" || !value.trim())
|
|
4
|
+
return "";
|
|
5
|
+
return value.length <= maxChars ? value : `${value.slice(0, maxChars - 1)}…`;
|
|
6
|
+
}
|
|
7
|
+
function asRecord(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
9
|
+
? value
|
|
10
|
+
: undefined;
|
|
11
|
+
}
|
|
12
|
+
function extractLastUserGoal(messages) {
|
|
13
|
+
if (!Array.isArray(messages))
|
|
14
|
+
return "";
|
|
15
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
16
|
+
const message = asRecord(messages[index]);
|
|
17
|
+
if (!message || message.role !== "user")
|
|
18
|
+
continue;
|
|
19
|
+
const content = message.content;
|
|
20
|
+
if (typeof content === "string")
|
|
21
|
+
return boundText(content, 800);
|
|
22
|
+
if (Array.isArray(content)) {
|
|
23
|
+
const texts = content
|
|
24
|
+
.map((block) => {
|
|
25
|
+
const rec = asRecord(block);
|
|
26
|
+
return typeof rec?.text === "string" ? rec.text : "";
|
|
27
|
+
})
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
if (texts.length > 0)
|
|
30
|
+
return boundText(texts.join("\n"), 800);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
function extractTodos(details) {
|
|
36
|
+
const record = asRecord(details);
|
|
37
|
+
const todos = record?.todos ?? record?.items;
|
|
38
|
+
if (!Array.isArray(todos))
|
|
39
|
+
return "";
|
|
40
|
+
const lines = todos
|
|
41
|
+
.slice(0, 24)
|
|
42
|
+
.map((item) => {
|
|
43
|
+
const rec = asRecord(item);
|
|
44
|
+
const status = typeof rec?.status === "string" ? rec.status : "pending";
|
|
45
|
+
const content = typeof rec?.content === "string"
|
|
46
|
+
? rec.content
|
|
47
|
+
: typeof rec?.title === "string"
|
|
48
|
+
? rec.title
|
|
49
|
+
: "";
|
|
50
|
+
return content ? `- [${status}] ${content}` : "";
|
|
51
|
+
})
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
return boundText(lines.join("\n"), 1200);
|
|
54
|
+
}
|
|
55
|
+
function extractFileOps(fileOps) {
|
|
56
|
+
const record = asRecord(fileOps);
|
|
57
|
+
if (!record)
|
|
58
|
+
return "";
|
|
59
|
+
const parts = [];
|
|
60
|
+
for (const key of ["edited", "written", "read", "deleted", "ops"]) {
|
|
61
|
+
const value = record[key];
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
parts.push(`${key}: ${value
|
|
64
|
+
.slice(0, 20)
|
|
65
|
+
.map((item) => (typeof item === "string" ? item : JSON.stringify(item)))
|
|
66
|
+
.join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
else if (typeof value === "string" && value) {
|
|
69
|
+
parts.push(`${key}: ${value}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (parts.length === 0) {
|
|
73
|
+
try {
|
|
74
|
+
return boundText(JSON.stringify(fileOps), 800);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return boundText(parts.join("\n"), 800);
|
|
81
|
+
}
|
|
82
|
+
export function buildOverflowCheckpoint(input) {
|
|
83
|
+
const preparation = input.preparation;
|
|
84
|
+
const firstKeptEntryId = typeof preparation?.firstKeptEntryId === "string" &&
|
|
85
|
+
preparation.firstKeptEntryId
|
|
86
|
+
? preparation.firstKeptEntryId
|
|
87
|
+
: undefined;
|
|
88
|
+
const tokensBefore = typeof preparation?.tokensBefore === "number" &&
|
|
89
|
+
Number.isFinite(preparation.tokensBefore)
|
|
90
|
+
? preparation.tokensBefore
|
|
91
|
+
: undefined;
|
|
92
|
+
if (!firstKeptEntryId || tokensBefore === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
const sections = [
|
|
95
|
+
"Deterministic overflow checkpoint (no extra summary request).",
|
|
96
|
+
boundText(preparation?.previousSummary, 2000)
|
|
97
|
+
? `Previous summary:\n${boundText(preparation?.previousSummary, 2000)}`
|
|
98
|
+
: "",
|
|
99
|
+
extractLastUserGoal(preparation?.messagesToSummarize)
|
|
100
|
+
? `Latest user goal:\n${extractLastUserGoal(preparation?.messagesToSummarize)}`
|
|
101
|
+
: "",
|
|
102
|
+
extractTodos(input.todos) ? `Open todos:\n${extractTodos(input.todos)}` : "",
|
|
103
|
+
extractFileOps(preparation?.fileOps)
|
|
104
|
+
? `File ops:\n${extractFileOps(preparation?.fileOps)}`
|
|
105
|
+
: "",
|
|
106
|
+
typeof input.lastError === "string" && input.lastError
|
|
107
|
+
? `Latest error:\n${boundText(input.lastError, 400)}`
|
|
108
|
+
: "",
|
|
109
|
+
"Next step: continue the interrupted turn from the kept recent messages. Do not repeat completed file writes.",
|
|
110
|
+
].filter(Boolean);
|
|
111
|
+
let summary = sections.join("\n\n");
|
|
112
|
+
if (Buffer.byteLength(summary, "utf8") > CHECKPOINT_MAX_BYTES) {
|
|
113
|
+
summary = `${summary.slice(0, CHECKPOINT_MAX_BYTES - 1)}…`;
|
|
114
|
+
}
|
|
115
|
+
return { summary, firstKeptEntryId, tokensBefore };
|
|
116
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { parseBoundedNumber, readContextPressureMode, } from "./env.js";
|
|
2
|
+
export const PI_DEFAULT_RESERVE_TOKENS = 16_384;
|
|
3
|
+
export const DEFAULT_TRAILING_BUDGET_TOKENS = 8_192;
|
|
4
|
+
export const DEFAULT_SAFETY_MARGIN_TOKENS = 4_096;
|
|
5
|
+
export const DEFAULT_MINIMUM_WORKING_SET = 32_768;
|
|
6
|
+
export const DEFAULT_KEEP_RECENT_TOKENS = 20_000;
|
|
7
|
+
export function parseCompactionPolicy(env = process.env) {
|
|
8
|
+
const diagnostics = [];
|
|
9
|
+
const modeResult = readContextPressureMode(env);
|
|
10
|
+
if (modeResult.diagnostic)
|
|
11
|
+
diagnostics.push(modeResult.diagnostic);
|
|
12
|
+
const k = parseBoundedNumber(env.HARNESS_CONTEXT_GATEWAY_K, 1, 1, 8);
|
|
13
|
+
if (k.diagnostic)
|
|
14
|
+
diagnostics.push(k.diagnostic);
|
|
15
|
+
const trailing = parseBoundedNumber(env.HARNESS_CONTEXT_TRAILING_BUDGET_TOKENS, DEFAULT_TRAILING_BUDGET_TOKENS, 256, 128_000);
|
|
16
|
+
if (trailing.diagnostic)
|
|
17
|
+
diagnostics.push(trailing.diagnostic);
|
|
18
|
+
const safety = parseBoundedNumber(env.HARNESS_CONTEXT_SAFETY_MARGIN_TOKENS, DEFAULT_SAFETY_MARGIN_TOKENS, 0, 64_000);
|
|
19
|
+
if (safety.diagnostic)
|
|
20
|
+
diagnostics.push(safety.diagnostic);
|
|
21
|
+
const explicit = env.HARNESS_COMPACTION_RESERVE_TOKENS;
|
|
22
|
+
const explicitReserve = explicit === undefined || explicit.trim() === ""
|
|
23
|
+
? undefined
|
|
24
|
+
: parseBoundedNumber(explicit, PI_DEFAULT_RESERVE_TOKENS, 1_024, 1_000_000);
|
|
25
|
+
if (explicitReserve?.diagnostic)
|
|
26
|
+
diagnostics.push(explicitReserve.diagnostic);
|
|
27
|
+
return {
|
|
28
|
+
mode: modeResult.mode,
|
|
29
|
+
k: k.value,
|
|
30
|
+
trailingBudgetTokens: trailing.value,
|
|
31
|
+
safetyMarginTokens: safety.value,
|
|
32
|
+
minimumWorkingSet: DEFAULT_MINIMUM_WORKING_SET,
|
|
33
|
+
...(explicitReserve ? { explicitReserveTokens: explicitReserve.value } : {}),
|
|
34
|
+
diagnostics,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function deriveEffectiveWindow(contextWindow, policy) {
|
|
38
|
+
const W = Math.floor(contextWindow);
|
|
39
|
+
if (!Number.isFinite(W) || W <= 0) {
|
|
40
|
+
return {
|
|
41
|
+
recommendedReserve: PI_DEFAULT_RESERVE_TOKENS,
|
|
42
|
+
triggerTokens: 0,
|
|
43
|
+
fallback: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (policy.explicitReserveTokens !== undefined) {
|
|
47
|
+
const recommendedReserve = Math.min(Math.max(policy.explicitReserveTokens, 1), Math.max(1, W - 1));
|
|
48
|
+
return {
|
|
49
|
+
recommendedReserve,
|
|
50
|
+
triggerTokens: Math.max(0, W - recommendedReserve),
|
|
51
|
+
fallback: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const rawTrigger = Math.floor(W / policy.k) -
|
|
55
|
+
policy.trailingBudgetTokens -
|
|
56
|
+
policy.safetyMarginTokens;
|
|
57
|
+
const rawReserve = W - rawTrigger;
|
|
58
|
+
const maxReserve = Math.max(1, W - policy.minimumWorkingSet);
|
|
59
|
+
if (!Number.isFinite(rawTrigger) ||
|
|
60
|
+
!Number.isFinite(rawReserve) ||
|
|
61
|
+
maxReserve < PI_DEFAULT_RESERVE_TOKENS) {
|
|
62
|
+
const recommendedReserve = Math.min(PI_DEFAULT_RESERVE_TOKENS, Math.max(1, W - 1));
|
|
63
|
+
return {
|
|
64
|
+
recommendedReserve,
|
|
65
|
+
triggerTokens: Math.max(0, W - recommendedReserve),
|
|
66
|
+
fallback: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const recommendedReserve = Math.min(Math.max(PI_DEFAULT_RESERVE_TOKENS, rawReserve), maxReserve);
|
|
70
|
+
return {
|
|
71
|
+
recommendedReserve,
|
|
72
|
+
triggerTokens: Math.max(0, W - recommendedReserve),
|
|
73
|
+
fallback: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function conservativeTokenEstimate(text, imageCount = 0) {
|
|
77
|
+
const chars = Array.from(text);
|
|
78
|
+
let cjk = 0;
|
|
79
|
+
let other = 0;
|
|
80
|
+
for (const char of chars) {
|
|
81
|
+
if (/\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}/u.test(char)) {
|
|
82
|
+
cjk += 1;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
other += 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const utf8 = Buffer.byteLength(text, "utf8");
|
|
89
|
+
const fromChars = cjk + Math.ceil(other / 2);
|
|
90
|
+
const fromBytes = Math.ceil(utf8 / 2);
|
|
91
|
+
return (Math.max(fromChars, fromBytes, Math.ceil(chars.length / 2)) +
|
|
92
|
+
Math.max(0, imageCount) * 1024);
|
|
93
|
+
}
|
|
94
|
+
export function projectPromptPressure(input) {
|
|
95
|
+
if (input.currentContextTokens === null ||
|
|
96
|
+
input.currentContextTokens === undefined ||
|
|
97
|
+
!Number.isFinite(input.currentContextTokens)) {
|
|
98
|
+
return { projected: 0, usageUnavailable: true };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
projected: Math.max(0, input.currentContextTokens) +
|
|
102
|
+
conservativeTokenEstimate(input.promptText, input.imageCount ?? 0),
|
|
103
|
+
usageUnavailable: false,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function isExplicitlyDisabled(manager) {
|
|
107
|
+
const project = manager.getProjectSettings?.()?.compaction?.enabled;
|
|
108
|
+
const global = manager.getGlobalSettings?.()?.compaction?.enabled;
|
|
109
|
+
return project === false || (project === undefined && global === false);
|
|
110
|
+
}
|
|
111
|
+
export function applyProcessCompactionOverride(manager, input) {
|
|
112
|
+
if (!manager?.applyOverrides || input.policy.mode === "off") {
|
|
113
|
+
return { applied: false };
|
|
114
|
+
}
|
|
115
|
+
const explicitOff = input.policy.mode === "safe" && isExplicitlyDisabled(manager);
|
|
116
|
+
const enabled = input.policy.mode === "strict" ? true : explicitOff ? false : true;
|
|
117
|
+
const window = typeof input.contextWindow === "number" && input.contextWindow > 0
|
|
118
|
+
? deriveEffectiveWindow(input.contextWindow, input.policy)
|
|
119
|
+
: undefined;
|
|
120
|
+
const compaction = { enabled };
|
|
121
|
+
if (window && enabled) {
|
|
122
|
+
compaction.reserveTokens = window.recommendedReserve;
|
|
123
|
+
compaction.keepRecentTokens = DEFAULT_KEEP_RECENT_TOKENS;
|
|
124
|
+
}
|
|
125
|
+
manager.applyOverrides({ compaction });
|
|
126
|
+
return {
|
|
127
|
+
applied: true,
|
|
128
|
+
enabled,
|
|
129
|
+
...(typeof compaction.reserveTokens === "number"
|
|
130
|
+
? { reserveTokens: compaction.reserveTokens }
|
|
131
|
+
: {}),
|
|
132
|
+
...(typeof compaction.keepRecentTokens === "number"
|
|
133
|
+
? { keepRecentTokens: compaction.keepRecentTokens }
|
|
134
|
+
: {}),
|
|
135
|
+
...(window?.fallback
|
|
136
|
+
? { diagnostic: "small-or-invalid-window-fell-back-to-pi-default-reserve" }
|
|
137
|
+
: {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function inMemoryCompactionSettings(policy) {
|
|
141
|
+
if (policy.mode === "off") {
|
|
142
|
+
return { enabled: true };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
enabled: true,
|
|
146
|
+
keepRecentTokens: DEFAULT_KEEP_RECENT_TOKENS,
|
|
147
|
+
...(policy.explicitReserveTokens
|
|
148
|
+
? { reserveTokens: policy.explicitReserveTokens }
|
|
149
|
+
: {}),
|
|
150
|
+
};
|
|
151
|
+
}
|