#pragma once // Step 248: Compact AST response format // // Token-efficient AST serialization for agent consumption. // Provides compact mode, subtree extraction, and AST diff support. #include "ast/ASTNode.h" #include "ast/Serialization.h" #include #include #include #include using json = nlohmann::json; // --- Extract a human-readable name from any AST node --- inline std::string getNodeName(const ASTNode* node) { if (!node) return ""; const auto& ct = node->conceptType; if (ct == "Module") return static_cast(node)->name; if (ct == "Function") return static_cast(node)->name; if (ct == "Variable") return static_cast(node)->name; if (ct == "Parameter") return static_cast(node)->name; if (ct == "FunctionCall") return static_cast(node)->functionName; if (ct == "VariableReference") return static_cast(node)->variableName; if (ct == "BinaryOperation") return static_cast(node)->op; if (ct == "UnaryOperation") return static_cast(node)->op; if (ct == "StringLiteral") return static_cast(node)->value; if (ct == "Import") return static_cast(node)->moduleName; if (ct == "ExternalModule") return static_cast(node)->name; if (ct == "PrimitiveType") return static_cast(node)->kind; if (ct == "CustomType") return static_cast(node)->typeName; if (ct == "MemberAccess") return static_cast(node)->memberName; if (ct == "TypeSignature") return static_cast(node)->name; if (ct == "ForLoop") return static_cast(node)->iteratorName; if (ct == "IntegerLiteral") return std::to_string( static_cast(node)->value); if (ct == "BooleanLiteral") return static_cast(node)->value ? "true" : "false"; return ""; } // --- Compact AST serialization --- // Returns: {id, type, name, line, childCount, children: [child_ids]} // Uses short keys and omits empty fields for minimal token usage. inline json toJsonCompact(const ASTNode* node) { if (!node) return json(); json j; j["id"] = node->id; j["type"] = node->conceptType; std::string name = getNodeName(node); if (!name.empty()) j["name"] = name; if (node->hasSpan()) j["line"] = node->spanStartLine; auto kids = node->allChildren(); if (!kids.empty()) { j["childCount"] = (int)kids.size(); json childIds = json::array(); for (const auto* child : kids) childIds.push_back(child->id); j["children"] = childIds; } return j; } // Compact summary: top-level nodes only (Module + direct children). // Deeper nodes omitted — use getASTSubtree for detail. inline json toJsonCompactSummary(const ASTNode* root) { if (!root) return json::array(); json nodes = json::array(); // Root node json rootJ; rootJ["id"] = root->id; rootJ["type"] = root->conceptType; std::string rname = getNodeName(root); if (!rname.empty()) rootJ["name"] = rname; if (root->hasSpan()) rootJ["line"] = root->spanStartLine; auto rootKids = root->allChildren(); if (!rootKids.empty()) rootJ["childCount"] = (int)rootKids.size(); nodes.push_back(rootJ); // Direct children (depth 1 only — functions, imports, etc.) for (const auto* child : rootKids) { json cj; cj["id"] = child->id; cj["type"] = child->conceptType; std::string cname = getNodeName(child); if (!cname.empty()) cj["name"] = cname; if (child->hasSpan()) cj["line"] = child->spanStartLine; auto grandkids = child->allChildren(); if (!grandkids.empty()) cj["childCount"] = (int)grandkids.size(); nodes.push_back(cj); } return nodes; } // Collect all nodes in compact format (flat list) inline json toJsonCompactTree(const ASTNode* node) { if (!node) return json::array(); json nodes = json::array(); nodes.push_back(toJsonCompact(node)); for (const auto* child : node->allChildren()) { json childNodes = toJsonCompactTree(child); for (auto& cn : childNodes) nodes.push_back(std::move(cn)); } return nodes; } // --- Subtree extraction --- // Returns full JSON for the subtree rooted at nodeId inline json toJsonSubtree(ASTNode* root, const std::string& nodeId) { ASTNode* target = findNodeById(root, nodeId); if (!target) return json(); return toJson(target); } // --- Token estimate --- // Rough estimate: characters / 4 (approximates LLM tokens) inline int tokenEstimate(const json& j) { std::string s = j.dump(); return (int)s.size() / 4; } // --- AST version tracking --- // Stored in HeadlessBufferState, records which node IDs changed per version. struct ASTVersionTracker { int version = 0; // version -> list of affected node IDs std::map> changes; void recordMutation(const std::vector& affectedIds) { ++version; changes[version] = affectedIds; } // Get all node IDs that changed since a given version std::vector changedSince(int sinceVersion) const { std::vector result; for (const auto& [v, ids] : changes) { if (v > sinceVersion) { for (const auto& id : ids) result.push_back(id); } } return result; } // Build a diff response: full JSON for changed nodes only json buildDiff(ASTNode* root, int sinceVersion) const { auto changedIds = changedSince(sinceVersion); json nodes = json::array(); for (const auto& id : changedIds) { ASTNode* node = findNodeById(root, id); if (node) nodes.push_back(toJson(node)); } return { {"sinceVersion", sinceVersion}, {"currentVersion", version}, {"changedCount", (int)nodes.size()}, {"nodes", nodes} }; } // Prune old entries to prevent unbounded growth void pruneOlderThan(int keepVersion) { auto it = changes.begin(); while (it != changes.end() && it->first < keepVersion) it = changes.erase(it); } };