Step 424: add context window optimizer
This commit is contained in:
@@ -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)
|
||||
|
||||
115
editor/src/ContextWindowOptimizer.h
Normal file
115
editor/src/ContextWindowOptimizer.h
Normal file
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
|
||||
#include "ModelProfileRegistry.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
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;
|
||||
}
|
||||
};
|
||||
157
editor/tests/step424_test.cpp
Normal file
157
editor/tests/step424_test.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
// Step 424: Context Window Optimization Tests (12 tests)
|
||||
|
||||
#include "ContextWindowOptimizer.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
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;
|
||||
}
|
||||
47
progress.md
47
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)
|
||||
|
||||
Reference in New Issue
Block a user