@causyne/agent 0.0.0-stage → 1.10.1
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/LICENSE +102 -0
- package/README.md +31 -2
- package/SECURITY.md +24 -0
- package/bin/causyne.js +13 -0
- package/docs/ARCHITECTURE.md +63 -0
- package/docs/PROVENANCE.md +29 -0
- package/docs/SECURITY_REVIEW.md +89 -0
- package/docs/THIRD_PARTY_NOTICES.md +49 -0
- package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
- package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
- package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
- package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
- package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
- package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
- package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
- package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
- package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
- package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
- package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
- package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
- package/native-skills/android/play-store-release/SKILL.md +43 -0
- package/native-skills/android/room-and-persistence/SKILL.md +40 -0
- package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
- package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
- package/native-skills/backend/caching-strategy/SKILL.md +44 -0
- package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
- package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
- package/native-skills/backend/rest-api-design/SKILL.md +40 -0
- package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
- package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
- package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
- package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
- package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
- package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
- package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
- package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
- package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
- package/native-skills/data/metric-definition/SKILL.md +42 -0
- package/native-skills/data/notebook-to-production/SKILL.md +43 -0
- package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
- package/native-skills/database/connection-pooling/SKILL.md +42 -0
- package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
- package/native-skills/database/query-optimisation/SKILL.md +38 -0
- package/native-skills/database/safe-migrations/SKILL.md +47 -0
- package/native-skills/database/schema-design/SKILL.md +38 -0
- package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
- package/native-skills/design/colour-system-construction/SKILL.md +38 -0
- package/native-skills/design/design-direction-brief/SKILL.md +41 -0
- package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
- package/native-skills/design/responsive-strategy/SKILL.md +39 -0
- package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
- package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
- package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
- package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
- package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
- package/native-skills/devops/incident-triage/SKILL.md +38 -0
- package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
- package/native-skills/devops/observability-baseline/SKILL.md +38 -0
- package/native-skills/devops/secret-management/SKILL.md +40 -0
- package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
- package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
- package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
- package/native-skills/git/branch-strategy/SKILL.md +39 -0
- package/native-skills/git/commit-hygiene/SKILL.md +41 -0
- package/native-skills/git/history-recovery/SKILL.md +44 -0
- package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
- package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
- package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
- package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
- package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
- package/native-skills/mobile/deep-linking/SKILL.md +39 -0
- package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
- package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
- package/native-skills/mobile/push-notifications/SKILL.md +38 -0
- package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
- package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
- package/native-skills/performance/load-testing/SKILL.md +42 -0
- package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
- package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
- package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
- package/native-skills/python/async-correctness/SKILL.md +40 -0
- package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
- package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
- package/native-skills/python/environment-isolation/SKILL.md +41 -0
- package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
- package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
- package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
- package/native-skills/security/authorization-model/SKILL.md +42 -0
- package/native-skills/security/crypto-usage/SKILL.md +48 -0
- package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
- package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
- package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
- package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
- package/native-skills/security/threat-modelling/SKILL.md +46 -0
- package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
- package/native-skills/testing/property-based-testing/SKILL.md +46 -0
- package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
- package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
- package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
- package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
- package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
- package/native-skills/web/accessible-components/SKILL.md +45 -0
- package/native-skills/web/api-client-resilience/SKILL.md +39 -0
- package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
- package/native-skills/web/core-web-vitals/SKILL.md +40 -0
- package/native-skills/web/css-layout-systems/SKILL.md +41 -0
- package/native-skills/web/form-validation-ux/SKILL.md +39 -0
- package/native-skills/web/react-state-architecture/SKILL.md +41 -0
- package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
- package/package.json +68 -4
- package/src/agentLoop.js +2458 -0
- package/src/appChannel.js +109 -0
- package/src/approvalGrants.js +66 -0
- package/src/auditLog.js +52 -0
- package/src/autoUpdate.js +12 -0
- package/src/capabilities.js +36 -0
- package/src/checkpoints.js +1199 -0
- package/src/cliMode.js +638 -0
- package/src/commands.js +805 -0
- package/src/compiledSkills.js +540 -0
- package/src/config.js +130 -0
- package/src/contextManager.js +498 -0
- package/src/cortex/autoCondenser.js +192 -0
- package/src/cortex/provisioner.js +27 -0
- package/src/cortex/vectorDb.js +447 -0
- package/src/designPalettes.js +99 -0
- package/src/devMode.js +6 -0
- package/src/embodimentSession.js +180 -0
- package/src/entrypoint.js +194 -0
- package/src/execSafe.js +21 -0
- package/src/gitSnapshot.js +378 -0
- package/src/mcp/jsonRpc.js +165 -0
- package/src/mcp/mcpClient.js +136 -0
- package/src/mcp/mcpRegistry.js +207 -0
- package/src/memory.js +62 -0
- package/src/memoryManager.js +124 -0
- package/src/modelCatalogue.js +132 -0
- package/src/modelIds.js +42 -0
- package/src/modelRouter.js +145 -0
- package/src/outputStore.js +246 -0
- package/src/perception.js +124 -0
- package/src/planCache.js +437 -0
- package/src/procSignal.js +32 -0
- package/src/providers/anthropic.js +408 -0
- package/src/providers/base.js +328 -0
- package/src/providers/dashscope-byok.js +86 -0
- package/src/providers/deepseek.js +80 -0
- package/src/providers/factory.js +37 -0
- package/src/providers/gemini.js +346 -0
- package/src/providers/moonshot.js +73 -0
- package/src/providers/muse.js +169 -0
- package/src/providers/ollama.js +104 -0
- package/src/providers/openai-compatible.js +84 -0
- package/src/providers/openai.js +20 -0
- package/src/providers/qwen-byok.js +76 -0
- package/src/providers/xai.js +73 -0
- package/src/provisioning/capabilityConsent.js +24 -0
- package/src/provisioning/capabilityProvisioner.js +271 -0
- package/src/provisioning/capabilityTools.js +29 -0
- package/src/reporting.js +373 -0
- package/src/sandbox.js +529 -0
- package/src/sessionStore.js +279 -0
- package/src/skillsManager.js +288 -0
- package/src/slopDetector.js +307 -0
- package/src/taskBuffer.js +45 -0
- package/src/taskShape.js +241 -0
- package/src/textDiff.js +150 -0
- package/src/tokenTracker.js +560 -0
- package/src/tool-scripts/ax_snapshot.applescript +39 -0
- package/src/tool-scripts/ax_snapshot.ps1 +10 -0
- package/src/tool-scripts/desktop_act.applescript +58 -0
- package/src/tool-scripts/resolve_automator.py +119 -0
- package/src/toolPacks.js +524 -0
- package/src/tools/archive.js +63 -0
- package/src/tools/astryxOracle.js +78 -0
- package/src/tools/axSnapshot.js +165 -0
- package/src/tools/blenderAutomator.js +349 -0
- package/src/tools/browserAutomator.js +252 -0
- package/src/tools/checkSystemSpecs.js +64 -0
- package/src/tools/clipboard.js +36 -0
- package/src/tools/codeSearchAst.js +101 -0
- package/src/tools/codeSearcher.js +230 -0
- package/src/tools/codeTools.js +262 -0
- package/src/tools/codebaseSearch.js +75 -0
- package/src/tools/createSkill.js +129 -0
- package/src/tools/deferredMcpSearch.js +246 -0
- package/src/tools/desktopAct.js +208 -0
- package/src/tools/desktopOpen.js +34 -0
- package/src/tools/dynamicLoader.js +44 -0
- package/src/tools/executeProceduralMedia.js +93 -0
- package/src/tools/fsDelete.js +51 -0
- package/src/tools/fsEdit.js +105 -0
- package/src/tools/fsFind.js +128 -0
- package/src/tools/fsList.js +77 -0
- package/src/tools/fsMove.js +57 -0
- package/src/tools/fsRead.js +87 -0
- package/src/tools/fsSearch.js +198 -0
- package/src/tools/fsWrite.js +41 -0
- package/src/tools/gameAiBehaviorTree.js +208 -0
- package/src/tools/gameAnimationRig.js +194 -0
- package/src/tools/gameAssetFetcher.js +372 -0
- package/src/tools/gameAssetHarvester.js +210 -0
- package/src/tools/gameAudioSynth.js +267 -0
- package/src/tools/gameBuilder.js +886 -0
- package/src/tools/gameCanvasAudit.js +101 -0
- package/src/tools/gameCinematicSequencer.js +151 -0
- package/src/tools/gameDataForge.js +124 -0
- package/src/tools/gameDialogueTreeGen.js +166 -0
- package/src/tools/gameEconomyBalancer.js +161 -0
- package/src/tools/gameEntitySpawner.js +207 -0
- package/src/tools/gameLoreForge.js +153 -0
- package/src/tools/gameQuestArcBuilder.js +152 -0
- package/src/tools/gameSceneBuilder.js +158 -0
- package/src/tools/gameShaderForge.js +181 -0
- package/src/tools/gameTilemapGenerator.js +164 -0
- package/src/tools/gameUiHudBuilder.js +292 -0
- package/src/tools/gameVfxEmitterGen.js +108 -0
- package/src/tools/gameWebRuntimeInspector.js +91 -0
- package/src/tools/generateMediaViaApi.js +358 -0
- package/src/tools/gitExec.js +54 -0
- package/src/tools/godotAutomator.js +437 -0
- package/src/tools/manageLocalAiDependencies.js +220 -0
- package/src/tools/memoryWrite.js +99 -0
- package/src/tools/multiEdit.js +107 -0
- package/src/tools/palmierAutomator.js +104 -0
- package/src/tools/pdfReader.js +112 -0
- package/src/tools/persistentShell.js +148 -0
- package/src/tools/printer.js +316 -0
- package/src/tools/procTerm.js +91 -0
- package/src/tools/processList.js +52 -0
- package/src/tools/readNativeSkill.js +39 -0
- package/src/tools/registry.js +277 -0
- package/src/tools/resolveAutomator.js +102 -0
- package/src/tools/screenshot.js +75 -0
- package/src/tools/subagentOrchestrator.js +305 -0
- package/src/tools/sysRun.js +125 -0
- package/src/tools/todoWrite.js +75 -0
- package/src/tools/toolDispatch.js +127 -0
- package/src/tools/toolForge.js +124 -0
- package/src/tools/updatePlan.js +88 -0
- package/src/tools/viewImage.js +82 -0
- package/src/tools/visionInspector.js +154 -0
- package/src/tools/webFetch.js +74 -0
- package/src/tools/webSearch.js +82 -0
- package/src/tui/ask.js +164 -0
- package/src/tui/banner.js +158 -0
- package/src/tui/block.js +152 -0
- package/src/tui/chatRenderer.js +953 -0
- package/src/tui/completion.js +424 -0
- package/src/tui/dialogs.js +606 -0
- package/src/tui/diff.js +296 -0
- package/src/tui/history.js +135 -0
- package/src/tui/home.js +217 -0
- package/src/tui/invocation.js +105 -0
- package/src/tui/keys.js +443 -0
- package/src/tui/lineEditor.js +256 -0
- package/src/tui/overlay.js +256 -0
- package/src/tui/prompt.js +1258 -0
- package/src/tui/selectList.js +335 -0
- package/src/tui/stage.js +330 -0
- package/src/tui/stageSurface.js +609 -0
- package/src/tui/statusLine.js +254 -0
- package/src/tui/text.js +345 -0
- package/src/tui/theme.js +393 -0
- package/src/tui/toolDisplay.js +176 -0
- package/src/tui/transcript.js +235 -0
- package/src/tui/viewport.js +337 -0
- package/src/uncertainty.js +271 -0
- package/src/userSkills.js +265 -0
- package/src/utils/cli_ui.js +0 -0
- package/src/utils/inquirer.js +12 -0
- package/src/utils/paths.js +37 -0
- package/src/version.js +38 -0
- package/src/vfeTracker.js +220 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { canonicalModelId } from './modelIds.js';
|
|
6
|
+
import { DEFAULT_MODEL_BY_PROVIDER } from './modelCatalogue.js';
|
|
7
|
+
import { causyneHome } from './utils/paths.js';
|
|
8
|
+
|
|
9
|
+
const CONFIG_DIR = causyneHome();
|
|
10
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
11
|
+
const KEY_FILE = path.join(CONFIG_DIR, '.vault_key');
|
|
12
|
+
|
|
13
|
+
function ensureConfigDir() {
|
|
14
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
15
|
+
// SECURITY: Directory must only be accessible by the owner
|
|
16
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getVaultKey() {
|
|
21
|
+
ensureConfigDir();
|
|
22
|
+
if (!fs.existsSync(KEY_FILE)) {
|
|
23
|
+
const key = crypto.randomBytes(32);
|
|
24
|
+
fs.writeFileSync(KEY_FILE, key, { mode: 0o600 });
|
|
25
|
+
return key;
|
|
26
|
+
}
|
|
27
|
+
return fs.readFileSync(KEY_FILE);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function encryptString(text) {
|
|
31
|
+
if (!text) return text;
|
|
32
|
+
if (text.startsWith('ENC:')) return text; // Already encrypted
|
|
33
|
+
const key = getVaultKey();
|
|
34
|
+
const iv = crypto.randomBytes(16);
|
|
35
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
36
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
37
|
+
encrypted += cipher.final('hex');
|
|
38
|
+
const authTag = cipher.getAuthTag().toString('hex');
|
|
39
|
+
return `ENC:${iv.toString('hex')}:${authTag}:${encrypted}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function decryptString(encryptedText) {
|
|
43
|
+
if (!encryptedText || !encryptedText.startsWith('ENC:')) return encryptedText;
|
|
44
|
+
try {
|
|
45
|
+
const parts = encryptedText.split(':');
|
|
46
|
+
if (parts.length !== 4) return '';
|
|
47
|
+
const iv = Buffer.from(parts[1], 'hex');
|
|
48
|
+
const authTag = Buffer.from(parts[2], 'hex');
|
|
49
|
+
const encrypted = parts[3];
|
|
50
|
+
const key = getVaultKey();
|
|
51
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
52
|
+
decipher.setAuthTag(authTag);
|
|
53
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
54
|
+
decrypted += decipher.final('utf8');
|
|
55
|
+
return decrypted;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error('Failed to decrypt API key:', e);
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function loadConfig() {
|
|
63
|
+
ensureConfigDir();
|
|
64
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
67
|
+
|
|
68
|
+
// Decrypt API key into memory
|
|
69
|
+
if (parsed.apiKey) {
|
|
70
|
+
if (!parsed.apiKey.startsWith('ENC:')) {
|
|
71
|
+
// Found legacy plaintext key. We must encrypt it and re-save immediately.
|
|
72
|
+
const plaintext = parsed.apiKey;
|
|
73
|
+
parsed.apiKey = encryptString(plaintext);
|
|
74
|
+
saveConfig(parsed); // Re-saves safely encrypted
|
|
75
|
+
parsed.apiKey = plaintext; // Keep plaintext in memory for this session
|
|
76
|
+
} else {
|
|
77
|
+
parsed.apiKey = decryptString(parsed.apiKey);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Self-heal a model id written before src/modelIds.js existed. The
|
|
82
|
+
// provider normalises too, but tokenTracker.getPricing() and the /model
|
|
83
|
+
// display both read config.model directly —
|
|
84
|
+
// they must agree with what is actually sent, not with what a stale
|
|
85
|
+
// Android build once wrote here.
|
|
86
|
+
const canonicalModel = canonicalModelId(parsed.model);
|
|
87
|
+
if (canonicalModel !== parsed.model) {
|
|
88
|
+
parsed.model = canonicalModel;
|
|
89
|
+
saveConfig(parsed);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return parsed;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error('Error reading config.json:', e);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Default config
|
|
99
|
+
const defaultConfig = {
|
|
100
|
+
provider: 'dashscope',
|
|
101
|
+
// From the catalogue, not repeated here — this had been left at the
|
|
102
|
+
// long-retired `qwen-plus` while the picker had moved to qwen3.7.
|
|
103
|
+
model: DEFAULT_MODEL_BY_PROVIDER.dashscope,
|
|
104
|
+
apiKey: '',
|
|
105
|
+
autonomyMode: 'B',
|
|
106
|
+
tokenBudget: {
|
|
107
|
+
enabled: false,
|
|
108
|
+
maxTokensPerTask: 500000,
|
|
109
|
+
maxTokensPerDay: 2000000,
|
|
110
|
+
maxCostUsd: 5.00, // Hard stop if cost exceeds this in BYOK mode
|
|
111
|
+
warningThreshold: 0.8,
|
|
112
|
+
hardStop: true
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
saveConfig(defaultConfig);
|
|
116
|
+
return defaultConfig;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function saveConfig(config) {
|
|
120
|
+
ensureConfigDir();
|
|
121
|
+
// SECURITY: Deep copy to avoid mutating the in-memory config
|
|
122
|
+
const configToSave = JSON.parse(JSON.stringify(config));
|
|
123
|
+
if (configToSave.apiKey) {
|
|
124
|
+
configToSave.apiKey = encryptString(configToSave.apiKey);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// SECURITY: Config file contains encrypted keys, must be 0600 (owner read/write only)
|
|
128
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(configToSave, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
129
|
+
fs.chmodSync(CONFIG_FILE, 0o600); // Enforce on existing files
|
|
130
|
+
}
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
// Sovereign Context Manager
|
|
2
|
+
//
|
|
3
|
+
// Previously the agent loop computed a sliding window as a per-iteration *view*
|
|
4
|
+
// while `this.messages` grew forever: across a long task the array, the
|
|
5
|
+
// chat_history.json on disk, and the JSON.stringify cost of every save all grew
|
|
6
|
+
// without bound. This module owns the whole context lifecycle instead:
|
|
7
|
+
//
|
|
8
|
+
// 1. Tool-result decay — old tool outputs collapse into one-line stubs, the
|
|
9
|
+
// client-side equivalent of Anthropic's clear_tool_uses context editing.
|
|
10
|
+
// 2. Window construction — character-pool budget, newest first.
|
|
11
|
+
// 3. Real compaction — amputated messages are physically replaced by a single
|
|
12
|
+
// summary message, so the array stops growing.
|
|
13
|
+
// 4. Sequence sanitising — guarantees valid tool_call / tool_result pairing.
|
|
14
|
+
|
|
15
|
+
import { buildDecayPointer } from './outputStore.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Characters per token, for display only.
|
|
19
|
+
*
|
|
20
|
+
* The pools below are already annotated with their token equivalents at this
|
|
21
|
+
* ratio (120000 chars = "~30k tokens"), so anything that shows a token figure
|
|
22
|
+
* derived from a character budget must use the SAME divisor or the two numbers
|
|
23
|
+
* on screen will disagree with each other. Never used for billing — providers
|
|
24
|
+
* report real counts and those are what tokenTracker records.
|
|
25
|
+
*/
|
|
26
|
+
export const CHARS_PER_TOKEN = 4;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* How many characters of a tool's result may enter the context, per tool.
|
|
30
|
+
*
|
|
31
|
+
* Every tool shared ONE 32,000-character allowance, which treats a source file
|
|
32
|
+
* and a build log as equally worth reading. They are not: `fs_read` returns
|
|
33
|
+
* what the task asked for, while a failing compile can emit thirty thousand
|
|
34
|
+
* characters of the same warning. The dense tool and the chatty one paid the
|
|
35
|
+
* same, so the chatty one crowded out the window.
|
|
36
|
+
*
|
|
37
|
+
* NOTHING IS LOST BY LOWERING A CAP. `outputStore.spillToolResult()` writes the
|
|
38
|
+
* COMPLETE result to disk first and the notice that replaces it names the path,
|
|
39
|
+
* so a tighter cap moves bytes out of the window and behind a bounded read — it
|
|
40
|
+
* never destroys them. That is what makes these numbers safe to tune.
|
|
41
|
+
*
|
|
42
|
+
* @type {Record<string, number>}
|
|
43
|
+
*/
|
|
44
|
+
export const RESULT_CAPS = {
|
|
45
|
+
// Content the task actually asked for. Unchanged.
|
|
46
|
+
fs_read: 32000,
|
|
47
|
+
code_search_ast: 32000,
|
|
48
|
+
MultiEdit: 32000,
|
|
49
|
+
|
|
50
|
+
// Command output. Low information density, high volume, and the conclusion is
|
|
51
|
+
// almost always in the first and last few lines.
|
|
52
|
+
sysRun: 12000,
|
|
53
|
+
sysRunBg: 12000,
|
|
54
|
+
terminal_persistent_shell: 12000,
|
|
55
|
+
|
|
56
|
+
// Fetched pages are mostly navigation, boilerplate and scripts.
|
|
57
|
+
web_fetch: 12000,
|
|
58
|
+
web_search: 12000,
|
|
59
|
+
|
|
60
|
+
// A listing this long is a signal to narrow the query, not to read further.
|
|
61
|
+
fs_list: 8000,
|
|
62
|
+
fs_find: 8000,
|
|
63
|
+
fs_search: 12000
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** The allowance for a tool with no entry of its own. */
|
|
67
|
+
export const DEFAULT_RESULT_CAP = 32000;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} tool
|
|
71
|
+
* @returns {number} characters of this tool's result allowed into the window
|
|
72
|
+
*/
|
|
73
|
+
export function resultCapFor(tool) {
|
|
74
|
+
const cap = RESULT_CAPS[String(tool || '')];
|
|
75
|
+
return Number.isFinite(cap) && cap > 0 ? cap : DEFAULT_RESULT_CAP;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
export const CHAR_POOLS = {
|
|
80
|
+
super_max: 400000, // ~100k tokens
|
|
81
|
+
max: 240000, // ~60k tokens
|
|
82
|
+
premium: 180000, // ~45k tokens — the middle rung now buys a real context
|
|
83
|
+
// bump over fast, not just the right to run (monetization
|
|
84
|
+
// Move 3 / Gap #2 in docs/AGENT_MONETIZATION.md).
|
|
85
|
+
default: 120000 // ~30k tokens (fast / starter / free)
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export function resolveCharPool(tier, override) {
|
|
89
|
+
if (override && Number.isFinite(override) && override > 0) return override;
|
|
90
|
+
if (tier === 'super_max') return CHAR_POOLS.super_max;
|
|
91
|
+
if (tier === 'max') return CHAR_POOLS.max;
|
|
92
|
+
if (tier === 'premium' || tier === 'premium_annual') return CHAR_POOLS.premium;
|
|
93
|
+
return CHAR_POOLS.default;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function contentLength(content) {
|
|
97
|
+
if (typeof content === 'string') return content.length;
|
|
98
|
+
if (content == null) return 0;
|
|
99
|
+
return JSON.stringify(content).length;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function measureMessages(messages) {
|
|
103
|
+
let total = 0;
|
|
104
|
+
for (const msg of messages) total += contentLength(msg.content);
|
|
105
|
+
return total;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Tool-result decay
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
const DECAY_MARKER = '[decayed]';
|
|
113
|
+
|
|
114
|
+
function summariseToolResult(msg) {
|
|
115
|
+
const raw = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
116
|
+
const sizeKb = (raw.length / 1024).toFixed(1);
|
|
117
|
+
|
|
118
|
+
// Keep the success/error verdict — that is the part the model still reasons
|
|
119
|
+
// about ten steps later. The payload is what it never re-reads.
|
|
120
|
+
let verdict = '';
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(raw);
|
|
123
|
+
if (parsed && typeof parsed === 'object') {
|
|
124
|
+
verdict = parsed.success === false
|
|
125
|
+
? ` failed: ${String(parsed.error || '').slice(0, 160)}`
|
|
126
|
+
: ' succeeded';
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
verdict = '';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// When the agent loop spilled this result to disk, send the model to the file
|
|
133
|
+
// instead of telling it to re-run. A re-run costs a full tool execution and
|
|
134
|
+
// puts another full-size payload back in the window; a targeted read of the
|
|
135
|
+
// spill costs a few hundred tokens. See src/outputStore.js.
|
|
136
|
+
const pointer = buildDecayPointer({ path: msg.spillPath });
|
|
137
|
+
const fallback = ' Re-run the tool or read the specific file/lines again if you still need it.';
|
|
138
|
+
|
|
139
|
+
return `${DECAY_MARKER} ${msg.name || 'tool'}${verdict}. Output (${sizeKb} KB) was cleared to protect the context window.${pointer || fallback}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
export function decayToolResults(messages, options = {}) {
|
|
144
|
+
const keepRecent = Number.isInteger(options.keepRecent) ? options.keepRecent : 8;
|
|
145
|
+
const minChars = Number.isInteger(options.minChars) ? options.minChars : 600;
|
|
146
|
+
const keepMediaRecent = Number.isInteger(options.keepMediaRecent) ? options.keepMediaRecent : 2;
|
|
147
|
+
|
|
148
|
+
let reclaimed = 0;
|
|
149
|
+
let toolsSeen = 0;
|
|
150
|
+
let mediaSeen = 0;
|
|
151
|
+
|
|
152
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
153
|
+
const msg = messages[i];
|
|
154
|
+
|
|
155
|
+
if (msg.role === 'tool') {
|
|
156
|
+
toolsSeen++;
|
|
157
|
+
if (toolsSeen <= keepRecent) continue;
|
|
158
|
+
const original = contentLength(msg.content);
|
|
159
|
+
if (original < minChars) continue;
|
|
160
|
+
if (typeof msg.content === 'string' && msg.content.startsWith(DECAY_MARKER)) continue;
|
|
161
|
+
|
|
162
|
+
msg.content = summariseToolResult(msg);
|
|
163
|
+
reclaimed += original - msg.content.length;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if (Array.isArray(msg.content)) {
|
|
169
|
+
const hasMedia = msg.content.some(
|
|
170
|
+
block => block && (block.type === 'image' || block.type === 'image_url' || block.type === 'input_audio')
|
|
171
|
+
);
|
|
172
|
+
if (!hasMedia) continue;
|
|
173
|
+
|
|
174
|
+
mediaSeen++;
|
|
175
|
+
if (mediaSeen <= keepMediaRecent) continue;
|
|
176
|
+
|
|
177
|
+
const original = contentLength(msg.content);
|
|
178
|
+
const captions = msg.content
|
|
179
|
+
.filter(block => block && block.type === 'text' && block.text)
|
|
180
|
+
.map(block => block.text)
|
|
181
|
+
.join(' ');
|
|
182
|
+
|
|
183
|
+
msg.content = `${DECAY_MARKER} media attachment cleared to protect the context window. ${captions}`.trim();
|
|
184
|
+
reclaimed += original - msg.content.length;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return reclaimed;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Window construction
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Walk backward from the newest message accumulating until the character pool
|
|
197
|
+
* is exhausted.
|
|
198
|
+
*
|
|
199
|
+
* @returns {{ window: Array, splitIndex: number }} splitIndex is the index of
|
|
200
|
+
* the newest message that did NOT fit, or -1 when everything fit.
|
|
201
|
+
*/
|
|
202
|
+
export function buildWindow(messages, maxCharPool) {
|
|
203
|
+
const window = [];
|
|
204
|
+
let characterCount = 0;
|
|
205
|
+
let splitIndex = -1;
|
|
206
|
+
|
|
207
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
208
|
+
const length = contentLength(messages[i].content);
|
|
209
|
+
if (characterCount + length > maxCharPool) {
|
|
210
|
+
splitIndex = i;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
characterCount += length;
|
|
214
|
+
window.unshift(messages[i]);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// NEVER RETURN AN EMPTY WINDOW.
|
|
218
|
+
//
|
|
219
|
+
// The loop above breaks on the FIRST message that does not fit, so a single
|
|
220
|
+
// message larger than the whole pool produced a window of length zero. The
|
|
221
|
+
// agent loop then skipped its root anchor (it is guarded on
|
|
222
|
+
// `slidingWindow.length > 0`) and sent the provider nothing but system
|
|
223
|
+
// blocks — which is the 400 "`messages` must contain at least one message
|
|
224
|
+
// with role `user` or `tool`" that killed a 340,000-token Blender run at
|
|
225
|
+
// step 13. A long traceback in a big conversation is all it takes.
|
|
226
|
+
//
|
|
227
|
+
// Keeping the last message truncated is strictly better than sending none:
|
|
228
|
+
// the model loses detail, where before it lost the entire turn.
|
|
229
|
+
if (window.length === 0 && messages.length > 0) {
|
|
230
|
+
const last = messages[messages.length - 1];
|
|
231
|
+
const text = typeof last.content === 'string' ? last.content : JSON.stringify(last.content ?? '');
|
|
232
|
+
window.push({
|
|
233
|
+
...last,
|
|
234
|
+
content: text.length > maxCharPool
|
|
235
|
+
? text.slice(0, Math.max(0, maxCharPool - 80)) + '\n[truncated: this single message exceeded the whole context budget]'
|
|
236
|
+
: text
|
|
237
|
+
});
|
|
238
|
+
splitIndex = messages.length - 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// A window that opens on tool results is broken: the assistant message that
|
|
242
|
+
// called them was amputated.
|
|
243
|
+
while (window.length > 1 && window[0].role === 'tool') {
|
|
244
|
+
window.shift();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const last = window[window.length - 1];
|
|
248
|
+
if (last && last.role === 'assistant' && last.tool_calls && last.tool_calls.length > 0) {
|
|
249
|
+
delete last.tool_calls;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { window, splitIndex };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Physically replace the amputated range with a single summary message so the
|
|
257
|
+
* live array stops growing. Index 0 (the root anchor / original user intent) is
|
|
258
|
+
* never compacted away.
|
|
259
|
+
*
|
|
260
|
+
* @returns {number} number of messages removed
|
|
261
|
+
*/
|
|
262
|
+
export function compactInPlace(messages, splitIndex, summaryText) {
|
|
263
|
+
const start = 1;
|
|
264
|
+
const end = splitIndex;
|
|
265
|
+
if (end < start) return 0;
|
|
266
|
+
|
|
267
|
+
const removed = end - start + 1;
|
|
268
|
+
const summary = {
|
|
269
|
+
role: 'user',
|
|
270
|
+
content: `[COMPACTED CONTEXT SUMMARY — ${removed} earlier messages were condensed to protect the context window]\n${summaryText}`
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
messages.splice(start, removed, summary);
|
|
274
|
+
return removed - 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Cheap deterministic summary used when no LLM condenser is configured. */
|
|
278
|
+
export function describeDroppedMessages(dropped) {
|
|
279
|
+
const toolCalls = new Map();
|
|
280
|
+
let userTurns = 0;
|
|
281
|
+
|
|
282
|
+
for (const msg of dropped) {
|
|
283
|
+
if (msg.role === 'tool' && msg.name) {
|
|
284
|
+
toolCalls.set(msg.name, (toolCalls.get(msg.name) || 0) + 1);
|
|
285
|
+
} else if (msg.role === 'user') {
|
|
286
|
+
userTurns++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const tools = [...toolCalls.entries()]
|
|
291
|
+
.sort((a, b) => b[1] - a[1])
|
|
292
|
+
.slice(0, 12)
|
|
293
|
+
.map(([name, count]) => `${name}x${count}`)
|
|
294
|
+
.join(', ');
|
|
295
|
+
|
|
296
|
+
const parts = [];
|
|
297
|
+
if (userTurns > 0) parts.push(`${userTurns} earlier user turn(s)`);
|
|
298
|
+
if (tools) parts.push(`tools used: ${tools}`);
|
|
299
|
+
return parts.length > 0
|
|
300
|
+
? parts.join('. ') + '. Re-read files or re-run tools if you need those details again.'
|
|
301
|
+
: 'Earlier exchange omitted.';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Sequence sanitising
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Guarantee 100% valid tool-call sequences: every assistant message carrying
|
|
310
|
+
* tool_calls must be immediately followed by matching tool results, and no
|
|
311
|
+
* orphan tool message may survive.
|
|
312
|
+
*/
|
|
313
|
+
export function sanitizeSequences(messages) {
|
|
314
|
+
for (let i = 0; i < messages.length; i++) {
|
|
315
|
+
const msg = messages[i];
|
|
316
|
+
if (msg.role === 'assistant' && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
317
|
+
const foundToolIds = new Set();
|
|
318
|
+
let followingResults = 0;
|
|
319
|
+
let j = i + 1;
|
|
320
|
+
while (j < messages.length && messages[j].role === 'tool') {
|
|
321
|
+
if (messages[j].tool_call_id) foundToolIds.add(messages[j].tool_call_id);
|
|
322
|
+
followingResults++;
|
|
323
|
+
j++;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Id matching is the correct pairing, but a provider that returns tool
|
|
327
|
+
// calls without ids would match nothing and have its entire turn erased.
|
|
328
|
+
// When no result carries an id and the counts line up, the results can
|
|
329
|
+
// only be for these calls — pair them positionally instead of discarding
|
|
330
|
+
// a turn the model genuinely took.
|
|
331
|
+
const canPairPositionally =
|
|
332
|
+
foundToolIds.size === 0 && followingResults === msg.tool_calls.length;
|
|
333
|
+
|
|
334
|
+
if (!canPairPositionally) {
|
|
335
|
+
msg.tool_calls = msg.tool_calls.filter(tc => foundToolIds.has(tc.id));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (msg.tool_calls.length === 0) {
|
|
339
|
+
delete msg.tool_calls;
|
|
340
|
+
if (!msg.content || typeof msg.content !== 'string' || msg.content.trim() === '') {
|
|
341
|
+
msg.content = '[Action initiated and completed]';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let validToolBlock = false;
|
|
348
|
+
const clean = [];
|
|
349
|
+
for (const msg of messages) {
|
|
350
|
+
if (msg.role === 'assistant' && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
351
|
+
validToolBlock = true;
|
|
352
|
+
clean.push(msg);
|
|
353
|
+
} else if (msg.role === 'tool') {
|
|
354
|
+
if (validToolBlock) clean.push(msg);
|
|
355
|
+
// otherwise drop the orphan rather than let the provider 400
|
|
356
|
+
} else {
|
|
357
|
+
validToolBlock = false;
|
|
358
|
+
clean.push(msg);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return clean;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// 5-Stage Progressive Compaction (Claude Code Architecture / Research Section E)
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
/** Stage 1: Budget Reduction triggered when raw context exceeds 70% capacity */
|
|
370
|
+
export const STAGE1_BUDGET_THRESHOLD = 0.70;
|
|
371
|
+
|
|
372
|
+
/** Stage 2: Snip triggered when any single tool response exceeds 15,000 tokens ≈ 60,000 chars */
|
|
373
|
+
export const STAGE2_SNIP_CHARS = 60000;
|
|
374
|
+
export const STAGE2_SNIP_HEAD = 20000;
|
|
375
|
+
export const STAGE2_SNIP_TAIL = 10000;
|
|
376
|
+
|
|
377
|
+
/** Stage 4: Context Collapse triggered when active context exceeds 85% capacity */
|
|
378
|
+
export const STAGE4_COLLAPSE_THRESHOLD = 0.85;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Stage 2: Snip oversized tool responses.
|
|
382
|
+
*
|
|
383
|
+
* Walks conversation history and truncates any single tool output exceeding
|
|
384
|
+
* 15,000 tokens (60,000 chars) down to head + tail with an informative marker.
|
|
385
|
+
* This prevents massive build logs, package listings, or grep dumps from
|
|
386
|
+
* crowding out the active context window.
|
|
387
|
+
*
|
|
388
|
+
* @param {Array} messages
|
|
389
|
+
* @param {Object} [options]
|
|
390
|
+
* @returns {{ snippedCount: number, reclaimedChars: number }}
|
|
391
|
+
*/
|
|
392
|
+
export function snipOversizedToolOutputs(messages, options = {}) {
|
|
393
|
+
const threshold = options.threshold || STAGE2_SNIP_CHARS;
|
|
394
|
+
const headChars = options.headChars || STAGE2_SNIP_HEAD;
|
|
395
|
+
const tailChars = options.tailChars || STAGE2_SNIP_TAIL;
|
|
396
|
+
|
|
397
|
+
let snippedCount = 0;
|
|
398
|
+
let reclaimedChars = 0;
|
|
399
|
+
|
|
400
|
+
for (const msg of messages) {
|
|
401
|
+
if (msg.role !== 'tool' && !msg.tool_call_id) continue;
|
|
402
|
+
if (typeof msg.content !== 'string') continue;
|
|
403
|
+
|
|
404
|
+
const len = msg.content.length;
|
|
405
|
+
if (len > threshold) {
|
|
406
|
+
const removed = len - (headChars + tailChars);
|
|
407
|
+
const head = msg.content.slice(0, headChars);
|
|
408
|
+
const tail = msg.content.slice(-tailChars);
|
|
409
|
+
const pointer = msg.spillPath ? ` Full output preserved at: ${msg.spillPath}.` : '';
|
|
410
|
+
|
|
411
|
+
msg.content = `${head}\n\n[... ✂️ SNIPPED ${removed.toLocaleString()} CHARS (${Math.round(removed / CHARS_PER_TOKEN).toLocaleString()} TOKENS) TO PREVENT CONTEXT OVERFLOW.${pointer} Use targeted file reads to inspect specific sections ...] \n\n${tail}`;
|
|
412
|
+
|
|
413
|
+
snippedCount++;
|
|
414
|
+
reclaimedChars += (len - msg.content.length);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return { snippedCount, reclaimedChars };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Stage 3: Microcompact text.
|
|
423
|
+
* Strips redundant whitespace and collapses multiple blank lines to keep prompts
|
|
424
|
+
* tight without altering semantic content.
|
|
425
|
+
*
|
|
426
|
+
* @param {string} text
|
|
427
|
+
* @returns {string}
|
|
428
|
+
*/
|
|
429
|
+
export function microcompactText(text) {
|
|
430
|
+
if (typeof text !== 'string') return text;
|
|
431
|
+
return text.replace(/[ \t]+$/gm, '').replace(/\n{3,}/g, '\n\n');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Progressive Compaction Pipeline (Stages 1 through 4).
|
|
436
|
+
* Evaluated before model calls to guarantee memory stability.
|
|
437
|
+
*
|
|
438
|
+
* @param {Array} messages
|
|
439
|
+
* @param {number} maxCharPool
|
|
440
|
+
* @param {Object} [options]
|
|
441
|
+
* @returns {{
|
|
442
|
+
* totalReclaimed: number,
|
|
443
|
+
* stagesFired: string[],
|
|
444
|
+
* finalChars: number,
|
|
445
|
+
* finalRatio: number
|
|
446
|
+
* }}
|
|
447
|
+
*/
|
|
448
|
+
export function applyProgressiveCompaction(messages, maxCharPool, options = {}) {
|
|
449
|
+
let totalReclaimed = 0;
|
|
450
|
+
const stagesFired = [];
|
|
451
|
+
|
|
452
|
+
// Stage 2: Snip oversized tool outputs (>15K tokens / 60K chars)
|
|
453
|
+
const snipRes = snipOversizedToolOutputs(messages, options.snipOptions);
|
|
454
|
+
if (snipRes.snippedCount > 0) {
|
|
455
|
+
totalReclaimed += snipRes.reclaimedChars;
|
|
456
|
+
stagesFired.push(`snip:${snipRes.snippedCount}_tools`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
let currentChars = measureMessages(messages);
|
|
460
|
+
const ratio = maxCharPool > 0 ? currentChars / maxCharPool : 0;
|
|
461
|
+
|
|
462
|
+
// Stage 1: Budget Reduction (>70% of character pool)
|
|
463
|
+
if (ratio >= STAGE1_BUDGET_THRESHOLD) {
|
|
464
|
+
stagesFired.push(`budget_reduction:${(ratio * 100).toFixed(0)}%`);
|
|
465
|
+
for (const msg of messages) {
|
|
466
|
+
if (msg.role === 'tool' && typeof msg.content === 'string') {
|
|
467
|
+
const origLen = msg.content.length;
|
|
468
|
+
msg.content = microcompactText(msg.content);
|
|
469
|
+
totalReclaimed += (origLen - msg.content.length);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const decayReclaimed = decayToolResults(messages, {
|
|
473
|
+
keepRecent: options.budgetKeepRecent ?? 4,
|
|
474
|
+
minChars: 300
|
|
475
|
+
});
|
|
476
|
+
totalReclaimed += decayReclaimed;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Stage 4: Context Collapse (>85% of character pool)
|
|
480
|
+
currentChars = measureMessages(messages);
|
|
481
|
+
const updatedRatio = maxCharPool > 0 ? currentChars / maxCharPool : 0;
|
|
482
|
+
if (updatedRatio >= STAGE4_COLLAPSE_THRESHOLD) {
|
|
483
|
+
stagesFired.push(`context_collapse:${(updatedRatio * 100).toFixed(0)}%`);
|
|
484
|
+
const collapseReclaimed = decayToolResults(messages, {
|
|
485
|
+
keepRecent: options.collapseKeepRecent ?? 2,
|
|
486
|
+
minChars: 200,
|
|
487
|
+
keepMediaRecent: 0
|
|
488
|
+
});
|
|
489
|
+
totalReclaimed += collapseReclaimed;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
totalReclaimed,
|
|
494
|
+
stagesFired,
|
|
495
|
+
finalChars: measureMessages(messages),
|
|
496
|
+
finalRatio: maxCharPool > 0 ? measureMessages(messages) / maxCharPool : 0
|
|
497
|
+
};
|
|
498
|
+
}
|