diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index d46c393..dd83031 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2695,4 +2695,13 @@ target_link_libraries(step423_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step424_test tests/step424_test.cpp) +target_include_directories(step424_test PRIVATE src) +target_link_libraries(step424_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/ContextWindowOptimizer.h b/editor/src/ContextWindowOptimizer.h new file mode 100644 index 0000000..ba2a932 --- /dev/null +++ b/editor/src/ContextWindowOptimizer.h @@ -0,0 +1,115 @@ +#pragma once + +#include "ModelProfileRegistry.h" + +#include +#include +#include + +#include +using json = nlohmann::json; + +struct ContextPack { + std::string instructions; + std::string localContext; + std::string fileContext; + std::string projectContext; +}; + +struct OptimizedContext { + std::string scope; // local|file|project + std::string context; + int tokenEstimate = 0; + int budgetTokens = 0; + bool truncated = false; + + json toJson() const { + return { + {"scope", scope}, + {"context", context}, + {"tokenEstimate", tokenEstimate}, + {"budgetTokens", budgetTokens}, + {"truncated", truncated} + }; + } +}; + +class ContextWindowOptimizer { +public: + static int estimateTokens(const std::string& text) { + if (text.empty()) return 0; + int chars = (int)text.size(); + int estimate = chars / 4; + return estimate > 0 ? estimate : 1; + } + + static int budgetForContextWindow(int contextWindow) { + if (contextWindow <= 0) return 512; + // Reserve ~40% for output/system/tool wrappers. + int budget = (int)(contextWindow * 0.6); + if (budget < 512) budget = 512; + return budget; + } + + static OptimizedContext optimize(const ContextPack& pack, + const ModelProfile& profile) { + OptimizedContext out; + out.budgetTokens = budgetForContextWindow(profile.contextWindow); + + std::string chosenScope = chooseScope(profile.contextWindow); + std::string base = pack.instructions; + if (!base.empty()) base += "\n\n"; + + if (chosenScope == "project") { + base += "Project Context:\n" + pack.projectContext + "\n\n"; + base += "File Context:\n" + pack.fileContext + "\n\n"; + base += "Local Context:\n" + pack.localContext; + } else if (chosenScope == "file") { + base += "File Context:\n" + pack.fileContext + "\n\n"; + base += "Local Context:\n" + pack.localContext; + } else { + base += "Local Context:\n" + pack.localContext; + } + + out.scope = chosenScope; + out.context = trimToBudget(base, out.budgetTokens, out.truncated); + out.tokenEstimate = estimateTokens(out.context); + return out; + } + + static OptimizedContext optimizeForWorker(const ContextPack& pack, + const std::string& workerType, + const ModelProfileRegistry& registry) { + auto profile = registry.resolveForWorker(workerType); + if (profile.name.empty()) { + // Fall back to local-only conservative budget. + profile.name = "fallback"; + profile.contextWindow = 8192; + } + return optimize(pack, profile); + } + +private: + static std::string chooseScope(int contextWindow) { + if (contextWindow >= 120000) return "project"; + if (contextWindow >= 24000) return "file"; + return "local"; + } + + static std::string trimToBudget(const std::string& text, + int budgetTokens, + bool& truncated) { + int est = estimateTokens(text); + if (est <= budgetTokens) { + truncated = false; + return text; + } + truncated = true; + int maxChars = budgetTokens * 4; + if (maxChars <= 0) return ""; + if ((int)text.size() <= maxChars) return text; + std::string out = text.substr(0, (size_t)maxChars); + out += "\n\n[truncated to fit model context window]"; + return out; + } +}; diff --git a/editor/tests/step424_test.cpp b/editor/tests/step424_test.cpp new file mode 100644 index 0000000..0b47f87 --- /dev/null +++ b/editor/tests/step424_test.cpp @@ -0,0 +1,157 @@ +// Step 424: Context Window Optimization Tests (12 tests) + +#include "ContextWindowOptimizer.h" + +#include +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +static std::string repeat(const std::string& s, int n) { + std::string out; + for (int i = 0; i < n; ++i) out += s; + return out; +} + +static ContextPack makePack() { + ContextPack p; + p.instructions = "Implement task safely."; + p.localContext = "fn add(x,y) { return x+y; }"; + p.fileContext = repeat("file_line;\n", 400); + p.projectContext = repeat("project_line;\n", 3000); + return p; +} + +void test_estimate_tokens_non_empty() { + TEST(estimate_tokens_non_empty); + CHECK(ContextWindowOptimizer::estimateTokens("abcd") == 1, "4 chars => 1 token"); + CHECK(ContextWindowOptimizer::estimateTokens("abcdefgh") == 2, "8 chars => 2 tokens"); + PASS(); +} + +void test_estimate_tokens_empty_zero() { + TEST(estimate_tokens_empty_zero); + CHECK(ContextWindowOptimizer::estimateTokens("") == 0, "empty should be zero"); + PASS(); +} + +void test_budget_for_context_window_has_floor() { + TEST(budget_for_context_window_has_floor); + CHECK(ContextWindowOptimizer::budgetForContextWindow(100) == 512, "budget floor 512"); + PASS(); +} + +void test_small_context_uses_local_scope() { + TEST(small_context_uses_local_scope); + auto p = makePack(); + ModelProfile m{"local-slm", 8000, 0, 0, "fast", {}}; + auto out = ContextWindowOptimizer::optimize(p, m); + CHECK(out.scope == "local", "small context should select local scope"); + CHECK(out.context.find("Local Context:") != std::string::npos, "missing local context"); + PASS(); +} + +void test_medium_context_uses_file_scope() { + TEST(medium_context_uses_file_scope); + auto p = makePack(); + ModelProfile m{"mid", 32000, 0, 0, "medium", {}}; + auto out = ContextWindowOptimizer::optimize(p, m); + CHECK(out.scope == "file", "medium context should select file scope"); + CHECK(out.context.find("File Context:") != std::string::npos, "missing file context"); + PASS(); +} + +void test_large_context_uses_project_scope() { + TEST(large_context_uses_project_scope); + auto p = makePack(); + ModelProfile m{"large", 200000, 0, 0, "slow", {}}; + auto out = ContextWindowOptimizer::optimize(p, m); + CHECK(out.scope == "project", "large context should select project scope"); + CHECK(out.context.find("Project Context:") != std::string::npos, "missing project context"); + PASS(); +} + +void test_optimizer_preserves_instructions_prefix() { + TEST(optimizer_preserves_instructions_prefix); + auto p = makePack(); + ModelProfile m{"mid", 32000, 0, 0, "medium", {}}; + auto out = ContextWindowOptimizer::optimize(p, m); + CHECK(out.context.find("Implement task safely.") == 0, "instructions should remain prefix"); + PASS(); +} + +void test_optimizer_truncates_when_over_budget() { + TEST(optimizer_truncates_when_over_budget); + auto p = makePack(); + p.localContext = repeat("local_payload_", 5000); // force local-scope overflow + ModelProfile m{"tiny", 1, 0, 0, "fast", {}}; + auto out = ContextWindowOptimizer::optimize(p, m); + CHECK(out.truncated, "expected truncation"); + CHECK(out.context.find("[truncated to fit model context window]") != std::string::npos, + "missing truncation marker"); + PASS(); +} + +void test_optimizer_respects_budget_token_upper_bound() { + TEST(optimizer_respects_budget_token_upper_bound); + auto p = makePack(); + ModelProfile m{"tiny", 2000, 0, 0, "fast", {}}; + auto out = ContextWindowOptimizer::optimize(p, m); + CHECK(out.tokenEstimate <= out.budgetTokens + 20, "token estimate should stay near budget"); + PASS(); +} + +void test_optimize_for_worker_uses_registry_mapping() { + TEST(optimize_for_worker_uses_registry_mapping); + auto p = makePack(); + ModelProfileRegistry reg; + reg.setWorkerProfileMapping("slm", "local-slm"); + auto out = ContextWindowOptimizer::optimizeForWorker(p, "slm", reg); + CHECK(out.scope == "local", "slm should map to local profile scope"); + PASS(); +} + +void test_optimize_for_worker_unknown_uses_fallback() { + TEST(optimize_for_worker_unknown_uses_fallback); + auto p = makePack(); + ModelProfileRegistry reg; + auto out = ContextWindowOptimizer::optimizeForWorker(p, "mystery", reg); + CHECK(!out.context.empty(), "fallback should still produce context"); + CHECK(out.budgetTokens >= 512, "fallback should have sane budget"); + PASS(); +} + +void test_project_scope_contains_file_and_local_sections() { + TEST(project_scope_contains_file_and_local_sections); + auto p = makePack(); + ModelProfile m{"large", 200000, 0, 0, "slow", {}}; + auto out = ContextWindowOptimizer::optimize(p, m); + CHECK(out.context.find("File Context:") != std::string::npos, "project should include file section"); + CHECK(out.context.find("Local Context:") != std::string::npos, "project should include local section"); + PASS(); +} + +int main() { + std::cout << "Step 424: Context Window Optimization Tests\n"; + + test_estimate_tokens_non_empty(); // 1 + test_estimate_tokens_empty_zero(); // 2 + test_budget_for_context_window_has_floor(); // 3 + test_small_context_uses_local_scope(); // 4 + test_medium_context_uses_file_scope(); // 5 + test_large_context_uses_project_scope(); // 6 + test_optimizer_preserves_instructions_prefix(); // 7 + test_optimizer_truncates_when_over_budget(); // 8 + test_optimizer_respects_budget_token_upper_bound(); // 9 + test_optimize_for_worker_uses_registry_mapping(); // 10 + test_optimize_for_worker_unknown_uses_fallback(); // 11 + test_project_scope_contains_file_and_local_sections();// 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 906b294..2ad4ea7 100644 --- a/progress.md +++ b/progress.md @@ -4567,6 +4567,53 @@ metadata, including `.whetstone/config.json`-style override support. - `editor/src/MCPServer.h` (`1940` > `600`) - `editor/src/HeadlessAgentRPCHandler.h` (`2768` > `600`) +### Step 424: Context Window Optimization +**Status:** PASS (12/12 tests) + +Added context-window-aware bundle optimization so routing profiles can adapt +prompt/context scope to the selected model budget (`local`, `file`, `project`) +with deterministic truncation behavior. + +**Files created:** +- `editor/src/ContextWindowOptimizer.h` — context optimization support: + - token estimation heuristic + - usable-input budget derivation from model context window + - scope selection: + - small window -> `local` + - medium window -> `file` + - large window -> `project` + - worker-aware optimization via `ModelProfileRegistry` mapping + - truncation marker + budget clamp behavior +- `editor/tests/step424_test.cpp` — 12 tests covering: + 1. token estimation baseline + 2. empty-input token estimate edge case + 3. budget floor behavior + 4. small-model local scope + 5. medium-model file scope + 6. large-model project scope + 7. instruction-prefix preservation + 8. truncation path under hard budget pressure + 9. token estimate budget adherence + 10. worker-mapping optimization path + 11. unknown-worker fallback behavior + 12. project-scope composition includes file/local sections + +**Files modified:** +- `editor/CMakeLists.txt` — `step424_test` target + +**Verification run:** +- `step424_test` — PASS (12/12) new step coverage +- `step423_test` — PASS (12/12) regression coverage +- `step422_test` — PASS (8/8) regression coverage + +**Architecture gate check:** +- `editor/src/ContextWindowOptimizer.h` within header-size limit (`115` <= `600`) +- `editor/tests/step424_test.cpp` within test-file size guidance (`156` lines) +- Legacy oversized headers persist: + - `editor/src/ast/Serialization.h` (`1427` > `600`) + - `editor/src/MCPServer.h` (`1940` > `600`) + - `editor/src/HeadlessAgentRPCHandler.h` (`2768` > `600`) + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)