Step 385: add context bundle format
This commit is contained in:
@@ -2344,4 +2344,13 @@ target_link_libraries(step384_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step385_test tests/step385_test.cpp)
|
||||
target_include_directories(step385_test PRIVATE src)
|
||||
target_link_libraries(step385_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)
|
||||
|
||||
168
editor/src/ContextBundle.h
Normal file
168
editor/src/ContextBundle.h
Normal file
@@ -0,0 +1,168 @@
|
||||
#pragma once
|
||||
// Step 385: Standard context bundle format for external model invocation.
|
||||
|
||||
#include "RoutingEngine.h"
|
||||
#include "WorkerRegistry.h"
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct AttemptSummary {
|
||||
std::string workerType;
|
||||
std::string generatedCode;
|
||||
std::string feedback;
|
||||
|
||||
json toJson() const {
|
||||
return {
|
||||
{"workerType", workerType},
|
||||
{"generatedCode", generatedCode},
|
||||
{"feedback", feedback}
|
||||
};
|
||||
}
|
||||
|
||||
static AttemptSummary fromJson(const json& j) {
|
||||
AttemptSummary a;
|
||||
a.workerType = j.value("workerType", "");
|
||||
a.generatedCode = j.value("generatedCode", "");
|
||||
a.feedback = j.value("feedback", "");
|
||||
return a;
|
||||
}
|
||||
};
|
||||
|
||||
struct ContextBundle {
|
||||
std::string taskDescription;
|
||||
std::string skeletonCode;
|
||||
std::string intent;
|
||||
std::vector<std::string> constraints;
|
||||
std::string contextCode;
|
||||
std::string projectSummary;
|
||||
std::string existingTests;
|
||||
std::vector<AttemptSummary> previousAttempts;
|
||||
int tokenBudget = 0;
|
||||
std::string outputFormat; // "code-only" | "code-with-explanation"
|
||||
|
||||
json toJson() const {
|
||||
json attempts = json::array();
|
||||
for (const auto& a : previousAttempts) attempts.push_back(a.toJson());
|
||||
return {
|
||||
{"taskDescription", taskDescription},
|
||||
{"skeletonCode", skeletonCode},
|
||||
{"intent", intent},
|
||||
{"constraints", constraints},
|
||||
{"contextCode", contextCode},
|
||||
{"projectSummary", projectSummary},
|
||||
{"existingTests", existingTests},
|
||||
{"previousAttempts", attempts},
|
||||
{"tokenBudget", tokenBudget},
|
||||
{"outputFormat", outputFormat}
|
||||
};
|
||||
}
|
||||
|
||||
static ContextBundle fromJson(const json& j) {
|
||||
ContextBundle b;
|
||||
b.taskDescription = j.value("taskDescription", "");
|
||||
b.skeletonCode = j.value("skeletonCode", "");
|
||||
b.intent = j.value("intent", "");
|
||||
if (j.contains("constraints") && j["constraints"].is_array()) {
|
||||
b.constraints = j["constraints"].get<std::vector<std::string>>();
|
||||
}
|
||||
b.contextCode = j.value("contextCode", "");
|
||||
b.projectSummary = j.value("projectSummary", "");
|
||||
b.existingTests = j.value("existingTests", "");
|
||||
if (j.contains("previousAttempts") && j["previousAttempts"].is_array()) {
|
||||
for (const auto& a : j["previousAttempts"]) {
|
||||
b.previousAttempts.push_back(AttemptSummary::fromJson(a));
|
||||
}
|
||||
}
|
||||
b.tokenBudget = j.value("tokenBudget", 0);
|
||||
b.outputFormat = j.value("outputFormat", "code-with-explanation");
|
||||
return b;
|
||||
}
|
||||
};
|
||||
|
||||
inline std::vector<std::string> collectBundleConstraints(const WorkerContext& context,
|
||||
const WorkItem& item) {
|
||||
std::vector<std::string> out;
|
||||
for (const auto& anno : context.annotations) {
|
||||
std::string type = anno.value("type", "");
|
||||
std::string name = anno.value("name", "");
|
||||
std::string value = anno.value("value", "");
|
||||
if (!type.empty() || !name.empty() || !value.empty()) {
|
||||
std::string line = type;
|
||||
if (!name.empty()) line += ":" + name;
|
||||
if (!value.empty()) line += "=" + value;
|
||||
out.push_back(line);
|
||||
}
|
||||
}
|
||||
if (item.reviewRequired) out.push_back("requires-review");
|
||||
if (out.empty()) out.push_back("none");
|
||||
return out;
|
||||
}
|
||||
|
||||
inline ContextBundle buildBundle(const WorkItem& item,
|
||||
const WorkerContext& context,
|
||||
const RoutingDecision& routingDecision) {
|
||||
ContextBundle bundle;
|
||||
bundle.taskDescription = "Implement " + item.nodeType + " '" + item.nodeName + "'";
|
||||
bundle.skeletonCode = context.nodeAst.is_null() ? "" : context.nodeAst.dump(2);
|
||||
bundle.intent = context.skeletonIntent;
|
||||
if (bundle.intent.empty() && !item.rejectionFeedback.empty()) {
|
||||
bundle.intent = item.rejectionFeedback;
|
||||
}
|
||||
bundle.constraints = collectBundleConstraints(context, item);
|
||||
bundle.contextCode = context.bufferContent;
|
||||
bundle.projectSummary = context.projectSummary.is_null() ? "" :
|
||||
context.projectSummary.dump(2);
|
||||
bundle.existingTests = "";
|
||||
|
||||
for (const auto& attempt : item.rejectionHistory) {
|
||||
bundle.previousAttempts.push_back({
|
||||
attempt.workerType,
|
||||
attempt.result.generatedCode,
|
||||
attempt.feedback
|
||||
});
|
||||
}
|
||||
|
||||
bundle.tokenBudget = routingDecision.contextBudgetTokens > 0
|
||||
? routingDecision.contextBudgetTokens
|
||||
: estimateContextBudget(routingDecision.contextWidth);
|
||||
bundle.outputFormat = (routingDecision.workerType == "deterministic" ||
|
||||
routingDecision.workerType == "template")
|
||||
? "code-only"
|
||||
: "code-with-explanation";
|
||||
return bundle;
|
||||
}
|
||||
|
||||
inline std::string bundleToPrompt(const ContextBundle& bundle) {
|
||||
std::ostringstream oss;
|
||||
oss << "Task:\n" << bundle.taskDescription << "\n\n";
|
||||
oss << "Intent:\n" << (bundle.intent.empty() ? "(none)" : bundle.intent) << "\n\n";
|
||||
oss << "Constraints:\n";
|
||||
for (const auto& c : bundle.constraints) {
|
||||
oss << "- " << c << "\n";
|
||||
}
|
||||
oss << "\nSkeleton:\n" << (bundle.skeletonCode.empty() ? "(none)" : bundle.skeletonCode) << "\n\n";
|
||||
oss << "Context Code:\n" << (bundle.contextCode.empty() ? "(none)" : bundle.contextCode) << "\n\n";
|
||||
oss << "Project Summary:\n" << (bundle.projectSummary.empty() ? "(none)" : bundle.projectSummary) << "\n\n";
|
||||
oss << "Previous Attempts:\n";
|
||||
if (bundle.previousAttempts.empty()) {
|
||||
oss << "(none)\n";
|
||||
} else {
|
||||
for (const auto& a : bundle.previousAttempts) {
|
||||
oss << "- worker=" << a.workerType << "\n";
|
||||
if (!a.generatedCode.empty()) oss << " code:\n" << a.generatedCode << "\n";
|
||||
if (!a.feedback.empty()) oss << " feedback: " << a.feedback << "\n";
|
||||
}
|
||||
}
|
||||
oss << "\nToken Budget: " << bundle.tokenBudget << "\n";
|
||||
oss << "Output Format: " << bundle.outputFormat << "\n";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
inline json bundleToJson(const ContextBundle& bundle) {
|
||||
return bundle.toJson();
|
||||
}
|
||||
|
||||
inline int estimateBundleTokens(const ContextBundle& bundle) {
|
||||
return static_cast<int>(bundle.toJson().dump().size() / 4);
|
||||
}
|
||||
192
editor/tests/step385_test.cpp
Normal file
192
editor/tests/step385_test.cpp
Normal file
@@ -0,0 +1,192 @@
|
||||
// Step 385: Context bundle format (12 tests)
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include "ContextBundle.h"
|
||||
|
||||
static WorkItem makeItem(const std::string& id = "w1") {
|
||||
WorkItem item;
|
||||
item.id = id;
|
||||
item.nodeId = id + "_node";
|
||||
item.nodeName = "buildThing";
|
||||
item.nodeType = "Function";
|
||||
item.bufferId = "main.py";
|
||||
item.contextWidth = "project";
|
||||
item.workerType = "llm";
|
||||
item.reviewRequired = true;
|
||||
item.priority = "high";
|
||||
item.status = WI_READY;
|
||||
item.createdAt = workItemTimestamp();
|
||||
return item;
|
||||
}
|
||||
|
||||
static WorkerContext makeContext() {
|
||||
WorkerContext ctx;
|
||||
ctx.nodeAst = json{{"id", "w1_node"}, {"type", "Function"}, {"name", "buildThing"}};
|
||||
ctx.bufferContent = "def helper():\n return 1\n";
|
||||
ctx.projectSummary = json{{"files", 4}, {"keyModules", json::array({"core", "api"})}};
|
||||
ctx.annotations = {
|
||||
json{{"type", "Contract"}, {"name", "precondition"}, {"value", "input non-empty"}},
|
||||
json{{"type", "Risk"}, {"name", "level"}, {"value", "medium"}}
|
||||
};
|
||||
ctx.skeletonIntent = "Build normalized result map";
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static RoutingDecision makeDecision() {
|
||||
RoutingDecision d;
|
||||
d.workerType = "llm";
|
||||
d.contextWidth = "project";
|
||||
d.contextBudgetTokens = 8000;
|
||||
d.reviewRequired = true;
|
||||
d.confidence = 0.7f;
|
||||
d.reasoning = "Project context";
|
||||
d.agentRole = "generator";
|
||||
return d;
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
|
||||
// Test 1: bundle contains core fields
|
||||
{
|
||||
auto b = buildBundle(makeItem(), makeContext(), makeDecision());
|
||||
assert(!b.taskDescription.empty());
|
||||
assert(!b.skeletonCode.empty());
|
||||
assert(!b.contextCode.empty());
|
||||
assert(b.tokenBudget == 8000);
|
||||
std::cout << "Test 1 PASSED: bundle contains required fields\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 2: bundleToPrompt produces structured readable output
|
||||
{
|
||||
auto b = buildBundle(makeItem(), makeContext(), makeDecision());
|
||||
std::string prompt = bundleToPrompt(b);
|
||||
assert(prompt.find("Task:") != std::string::npos);
|
||||
assert(prompt.find("Constraints:") != std::string::npos);
|
||||
assert(prompt.find("Output Format:") != std::string::npos);
|
||||
std::cout << "Test 2 PASSED: prompt rendering structured\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 3: rejection history included in previous attempts
|
||||
{
|
||||
WorkItem item = makeItem();
|
||||
RejectionAttempt a;
|
||||
a.workerType = "slm";
|
||||
a.result.generatedCode = "return tmp";
|
||||
a.feedback = "missed edge case";
|
||||
item.rejectionHistory.push_back(a);
|
||||
auto b = buildBundle(item, makeContext(), makeDecision());
|
||||
assert(b.previousAttempts.size() == 1);
|
||||
assert(b.previousAttempts[0].feedback == "missed edge case");
|
||||
std::cout << "Test 3 PASSED: rejection history carried into bundle\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 4: token estimate is positive and size-correlated
|
||||
{
|
||||
auto b = buildBundle(makeItem(), makeContext(), makeDecision());
|
||||
int estimate = estimateBundleTokens(b);
|
||||
assert(estimate > 0);
|
||||
assert(std::abs(estimate - (int)(b.toJson().dump().size() / 4)) <= 1);
|
||||
std::cout << "Test 4 PASSED: token estimate reasonable\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 5: budget value preserved from routing decision
|
||||
{
|
||||
RoutingDecision d = makeDecision();
|
||||
d.contextBudgetTokens = 1234;
|
||||
auto b = buildBundle(makeItem(), makeContext(), d);
|
||||
assert(b.tokenBudget == 1234);
|
||||
std::cout << "Test 5 PASSED: budget respected from routing decision\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 6: annotation constraints included
|
||||
{
|
||||
auto b = buildBundle(makeItem(), makeContext(), makeDecision());
|
||||
assert(!b.constraints.empty());
|
||||
bool hasContract = false;
|
||||
for (const auto& c : b.constraints) {
|
||||
if (c.find("Contract") != std::string::npos) hasContract = true;
|
||||
}
|
||||
assert(hasContract);
|
||||
std::cout << "Test 6 PASSED: constraints include annotations\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 7: intent comes from worker context
|
||||
{
|
||||
auto b = buildBundle(makeItem(), makeContext(), makeDecision());
|
||||
assert(b.intent == "Build normalized result map");
|
||||
std::cout << "Test 7 PASSED: intent carried from context\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 8: empty context fields handled gracefully
|
||||
{
|
||||
WorkItem item = makeItem();
|
||||
WorkerContext ctx;
|
||||
RoutingDecision d = makeDecision();
|
||||
auto b = buildBundle(item, ctx, d);
|
||||
std::string prompt = bundleToPrompt(b);
|
||||
assert(!prompt.empty());
|
||||
assert(b.constraints.size() == 1); // default "none"
|
||||
std::cout << "Test 8 PASSED: empty fields handled gracefully\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 9: bundle JSON roundtrip
|
||||
{
|
||||
auto b = buildBundle(makeItem(), makeContext(), makeDecision());
|
||||
auto round = ContextBundle::fromJson(bundleToJson(b));
|
||||
assert(round.taskDescription == b.taskDescription);
|
||||
assert(round.tokenBudget == b.tokenBudget);
|
||||
assert(round.outputFormat == b.outputFormat);
|
||||
std::cout << "Test 9 PASSED: JSON roundtrip works\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 10: prompt format remains model-agnostic
|
||||
{
|
||||
auto b = buildBundle(makeItem(), makeContext(), makeDecision());
|
||||
std::string prompt = bundleToPrompt(b);
|
||||
assert(prompt.find("Claude") == std::string::npos);
|
||||
assert(prompt.find("OpenAI") == std::string::npos);
|
||||
std::cout << "Test 10 PASSED: prompt format model-agnostic\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 11: deterministic/template defaults to code-only output format
|
||||
{
|
||||
RoutingDecision d = makeDecision();
|
||||
d.workerType = "template";
|
||||
auto b = buildBundle(makeItem(), makeContext(), d);
|
||||
assert(b.outputFormat == "code-only");
|
||||
std::cout << "Test 11 PASSED: code-only format for deterministic/template\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 12: previous attempts preserve insertion order
|
||||
{
|
||||
WorkItem item = makeItem();
|
||||
RejectionAttempt a1; a1.workerType = "slm"; a1.feedback = "first";
|
||||
RejectionAttempt a2; a2.workerType = "llm"; a2.feedback = "second";
|
||||
item.rejectionHistory.push_back(a1);
|
||||
item.rejectionHistory.push_back(a2);
|
||||
auto b = buildBundle(item, makeContext(), makeDecision());
|
||||
assert(b.previousAttempts.size() == 2);
|
||||
assert(b.previousAttempts[0].feedback == "first");
|
||||
assert(b.previousAttempts[1].feedback == "second");
|
||||
std::cout << "Test 12 PASSED: previous-attempt order preserved\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
std::cout << "\nResults: " << passed << "/12\n";
|
||||
assert(passed == 12);
|
||||
return 0;
|
||||
}
|
||||
39
progress.md
39
progress.md
@@ -3063,6 +3063,45 @@ and next-action guidance for clients.
|
||||
- `editor/src/WorkflowProtocol.h` within header-size limit (`185` <= `600`)
|
||||
- `editor/tests/step384_test.cpp` within test-file size guidance (`177` lines)
|
||||
|
||||
### Step 385: Context Bundle Format
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Standardized the external-model context contract with a structured bundle,
|
||||
prompt rendering, JSON transport format, and token estimation utilities.
|
||||
|
||||
**Files created:**
|
||||
- `editor/src/ContextBundle.h` — context bundle model:
|
||||
- `ContextBundle` + `AttemptSummary`
|
||||
- `buildBundle(workItem, workerContext, routingDecision)`
|
||||
- `bundleToPrompt(bundle)` (model-agnostic prompt rendering)
|
||||
- `bundleToJson(bundle)` / `ContextBundle::fromJson(...)`
|
||||
- `estimateBundleTokens(bundle)`
|
||||
- `editor/tests/step385_test.cpp` — 12 tests covering:
|
||||
1. required bundle field population
|
||||
2. prompt structure readability
|
||||
3. rejection-history inclusion
|
||||
4. token-estimate reasonableness
|
||||
5. routing budget propagation
|
||||
6. annotation-constraint extraction
|
||||
7. intent propagation
|
||||
8. empty-field handling
|
||||
9. JSON roundtrip
|
||||
10. model-agnostic prompt format
|
||||
11. output-format policy (`code-only` vs `code-with-explanation`)
|
||||
12. previous-attempt ordering
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` — `step385_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `step385_test` — PASS (12/12) new step coverage
|
||||
- `step384_test` — PASS (12/12) regression coverage
|
||||
- `step383_test` — PASS (8/8) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/ContextBundle.h` within header-size limit (`168` <= `600`)
|
||||
- `editor/tests/step385_test.cpp` within test-file size guidance (`192` lines)
|
||||
|
||||
# Roadmap Planning — Sprints 12-25+
|
||||
|
||||
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)
|
||||
|
||||
Reference in New Issue
Block a user