ctxloom-pro 1.2.6 → 1.3.0
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.
|
@@ -4610,86 +4610,349 @@ var ToolRegistry = class {
|
|
|
4610
4610
|
|
|
4611
4611
|
// packages/core/src/tools/search.ts
|
|
4612
4612
|
import { z as z3 } from "zod";
|
|
4613
|
+
|
|
4614
|
+
// packages/core/src/budget/budget.ts
|
|
4615
|
+
var defaultTokenEstimator = (text) => Math.ceil(text.length / 4);
|
|
4616
|
+
function hasBudgetArgs(args) {
|
|
4617
|
+
if (!args || typeof args !== "object") return false;
|
|
4618
|
+
const a = args;
|
|
4619
|
+
return a.max_response_tokens !== void 0 || a.on_budget_exceeded !== void 0 || a.response_format !== void 0;
|
|
4620
|
+
}
|
|
4621
|
+
function readBudgetArgs(args) {
|
|
4622
|
+
if (!args || typeof args !== "object") return {};
|
|
4623
|
+
const a = args;
|
|
4624
|
+
const out = {};
|
|
4625
|
+
if (typeof a.max_response_tokens === "number") out.max_response_tokens = a.max_response_tokens;
|
|
4626
|
+
if (a.on_budget_exceeded === "skeleton" || a.on_budget_exceeded === "truncate" || a.on_budget_exceeded === "error") {
|
|
4627
|
+
out.on_budget_exceeded = a.on_budget_exceeded;
|
|
4628
|
+
}
|
|
4629
|
+
if (a.response_format === "full" || a.response_format === "skeleton" || a.response_format === "auto") {
|
|
4630
|
+
out.response_format = a.response_format;
|
|
4631
|
+
}
|
|
4632
|
+
return out;
|
|
4633
|
+
}
|
|
4634
|
+
function isBudgetDisabled() {
|
|
4635
|
+
return process.env.CTXLOOM_DISABLE_BUDGET === "1";
|
|
4636
|
+
}
|
|
4637
|
+
function emitTelemetry(event) {
|
|
4638
|
+
if (process.env.CTXLOOM_TELEMETRY_LEVEL !== "full") return;
|
|
4639
|
+
logger.info(event.event, event);
|
|
4640
|
+
}
|
|
4641
|
+
async function enforceBudget(opts) {
|
|
4642
|
+
const { full, args, toolName, defaultMaxTokens, skeletonProducer } = opts;
|
|
4643
|
+
const estimate = opts.estimator ?? defaultTokenEstimator;
|
|
4644
|
+
const originalTokens = estimate(full);
|
|
4645
|
+
if (isBudgetDisabled()) {
|
|
4646
|
+
return {
|
|
4647
|
+
text: full,
|
|
4648
|
+
meta: {
|
|
4649
|
+
format: "full",
|
|
4650
|
+
original_tokens_est: originalTokens,
|
|
4651
|
+
returned_tokens_est: originalTokens,
|
|
4652
|
+
fallback_reason: null
|
|
4653
|
+
}
|
|
4654
|
+
};
|
|
4655
|
+
}
|
|
4656
|
+
if (args.response_format === "skeleton" && skeletonProducer) {
|
|
4657
|
+
const skeleton2 = await safeSkeleton(skeletonProducer, toolName);
|
|
4658
|
+
if (skeleton2 !== null) {
|
|
4659
|
+
const skTokens = estimate(skeleton2);
|
|
4660
|
+
return {
|
|
4661
|
+
text: skeleton2,
|
|
4662
|
+
meta: {
|
|
4663
|
+
format: "skeleton",
|
|
4664
|
+
original_tokens_est: originalTokens,
|
|
4665
|
+
returned_tokens_est: skTokens,
|
|
4666
|
+
fallback_reason: null
|
|
4667
|
+
}
|
|
4668
|
+
};
|
|
4669
|
+
}
|
|
4670
|
+
return {
|
|
4671
|
+
text: full,
|
|
4672
|
+
meta: {
|
|
4673
|
+
format: "full",
|
|
4674
|
+
original_tokens_est: originalTokens,
|
|
4675
|
+
returned_tokens_est: originalTokens,
|
|
4676
|
+
fallback_reason: "skeleton_failed"
|
|
4677
|
+
}
|
|
4678
|
+
};
|
|
4679
|
+
}
|
|
4680
|
+
const budget = args.max_response_tokens ?? defaultMaxTokens;
|
|
4681
|
+
if (budget === void 0) {
|
|
4682
|
+
return {
|
|
4683
|
+
text: full,
|
|
4684
|
+
meta: {
|
|
4685
|
+
format: "full",
|
|
4686
|
+
original_tokens_est: originalTokens,
|
|
4687
|
+
returned_tokens_est: originalTokens,
|
|
4688
|
+
fallback_reason: null
|
|
4689
|
+
}
|
|
4690
|
+
};
|
|
4691
|
+
}
|
|
4692
|
+
if (originalTokens <= budget) {
|
|
4693
|
+
return {
|
|
4694
|
+
text: full,
|
|
4695
|
+
meta: {
|
|
4696
|
+
format: "full",
|
|
4697
|
+
original_tokens_est: originalTokens,
|
|
4698
|
+
returned_tokens_est: originalTokens,
|
|
4699
|
+
fallback_reason: null
|
|
4700
|
+
}
|
|
4701
|
+
};
|
|
4702
|
+
}
|
|
4703
|
+
emitTelemetry({
|
|
4704
|
+
event: "mcp.budget.exceeded",
|
|
4705
|
+
tool: toolName,
|
|
4706
|
+
original_tokens: originalTokens,
|
|
4707
|
+
budget,
|
|
4708
|
+
ratio: originalTokens / budget
|
|
4709
|
+
});
|
|
4710
|
+
const mode = args.on_budget_exceeded ?? "skeleton";
|
|
4711
|
+
if (mode === "error") {
|
|
4712
|
+
const err = new Error(
|
|
4713
|
+
`Response of ~${originalTokens} tokens exceeds max_response_tokens=${budget} for tool '${toolName}'. Re-ask with response_format: 'skeleton' or a larger budget.`
|
|
4714
|
+
);
|
|
4715
|
+
err.tokensOriginal = originalTokens;
|
|
4716
|
+
err.budget = budget;
|
|
4717
|
+
err.tool = toolName;
|
|
4718
|
+
throw err;
|
|
4719
|
+
}
|
|
4720
|
+
if (mode === "truncate") {
|
|
4721
|
+
const sliced2 = full.slice(0, budget * 4);
|
|
4722
|
+
const slicedTokens = estimate(sliced2);
|
|
4723
|
+
emitTelemetry({
|
|
4724
|
+
event: "mcp.fallback.used",
|
|
4725
|
+
tool: toolName,
|
|
4726
|
+
fallback_reason: "budget_exceeded",
|
|
4727
|
+
mode: "truncate"
|
|
4728
|
+
});
|
|
4729
|
+
return {
|
|
4730
|
+
text: sliced2,
|
|
4731
|
+
meta: {
|
|
4732
|
+
format: "truncated",
|
|
4733
|
+
original_tokens_est: originalTokens,
|
|
4734
|
+
returned_tokens_est: slicedTokens,
|
|
4735
|
+
fallback_reason: "budget_exceeded"
|
|
4736
|
+
}
|
|
4737
|
+
};
|
|
4738
|
+
}
|
|
4739
|
+
const skeleton = skeletonProducer ? await safeSkeleton(skeletonProducer, toolName) : null;
|
|
4740
|
+
if (skeleton !== null) {
|
|
4741
|
+
const skTokens = estimate(skeleton);
|
|
4742
|
+
if (skTokens <= budget) {
|
|
4743
|
+
emitTelemetry({
|
|
4744
|
+
event: "mcp.fallback.used",
|
|
4745
|
+
tool: toolName,
|
|
4746
|
+
fallback_reason: "budget_exceeded",
|
|
4747
|
+
mode: "skeleton"
|
|
4748
|
+
});
|
|
4749
|
+
return {
|
|
4750
|
+
text: skeleton,
|
|
4751
|
+
meta: {
|
|
4752
|
+
format: "skeleton",
|
|
4753
|
+
original_tokens_est: originalTokens,
|
|
4754
|
+
returned_tokens_est: skTokens,
|
|
4755
|
+
fallback_reason: "budget_exceeded"
|
|
4756
|
+
}
|
|
4757
|
+
};
|
|
4758
|
+
}
|
|
4759
|
+
const slicedSk = skeleton.slice(0, budget * 4);
|
|
4760
|
+
emitTelemetry({
|
|
4761
|
+
event: "mcp.fallback.used",
|
|
4762
|
+
tool: toolName,
|
|
4763
|
+
fallback_reason: "budget_exceeded",
|
|
4764
|
+
mode: "skeleton+truncate"
|
|
4765
|
+
});
|
|
4766
|
+
return {
|
|
4767
|
+
text: slicedSk,
|
|
4768
|
+
meta: {
|
|
4769
|
+
format: "truncated",
|
|
4770
|
+
original_tokens_est: originalTokens,
|
|
4771
|
+
returned_tokens_est: estimate(slicedSk),
|
|
4772
|
+
fallback_reason: "budget_exceeded"
|
|
4773
|
+
}
|
|
4774
|
+
};
|
|
4775
|
+
}
|
|
4776
|
+
const sliced = full.slice(0, budget * 4);
|
|
4777
|
+
emitTelemetry({
|
|
4778
|
+
event: "mcp.fallback.used",
|
|
4779
|
+
tool: toolName,
|
|
4780
|
+
fallback_reason: "skeleton_failed",
|
|
4781
|
+
mode: "truncate-fallback"
|
|
4782
|
+
});
|
|
4783
|
+
return {
|
|
4784
|
+
text: sliced,
|
|
4785
|
+
meta: {
|
|
4786
|
+
format: "truncated",
|
|
4787
|
+
original_tokens_est: originalTokens,
|
|
4788
|
+
returned_tokens_est: estimate(sliced),
|
|
4789
|
+
fallback_reason: "skeleton_failed"
|
|
4790
|
+
}
|
|
4791
|
+
};
|
|
4792
|
+
}
|
|
4793
|
+
async function safeSkeleton(producer, toolName) {
|
|
4794
|
+
try {
|
|
4795
|
+
return await producer();
|
|
4796
|
+
} catch (err) {
|
|
4797
|
+
logger.warn("Skeleton fallback failed", {
|
|
4798
|
+
tool: toolName,
|
|
4799
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
4800
|
+
});
|
|
4801
|
+
return null;
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
function wrapResponse(result) {
|
|
4805
|
+
const envelope = {
|
|
4806
|
+
data: result.text,
|
|
4807
|
+
meta: result.meta
|
|
4808
|
+
};
|
|
4809
|
+
return JSON.stringify(envelope);
|
|
4810
|
+
}
|
|
4811
|
+
|
|
4812
|
+
// packages/core/src/tools/search.ts
|
|
4813
|
+
var DEFAULT_MAX_RESPONSE_TOKENS = 4e3;
|
|
4613
4814
|
var Schema = z3.object({
|
|
4614
4815
|
query: z3.string().describe("Search query \u2014 natural language or code fragment"),
|
|
4615
4816
|
limit: z3.number().max(100).optional().default(10).describe("Maximum results to return"),
|
|
4616
|
-
project_root: ProjectRootField
|
|
4817
|
+
project_root: ProjectRootField,
|
|
4818
|
+
// ─── Phase B2 budget surface ──
|
|
4819
|
+
max_response_tokens: z3.number().int().positive().optional().describe("Soft response budget. Default: 4000 (when budget surface is opted into). Over-budget rebuilds the result list without the content snippets (paths + scores only)."),
|
|
4820
|
+
on_budget_exceeded: z3.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) drops snippets; 'truncate' slices the raw XML; 'error' throws."),
|
|
4821
|
+
response_format: z3.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces the path-and-score-only view; 'full'/'auto' lets the budget decide.")
|
|
4617
4822
|
});
|
|
4618
4823
|
function escapeXML3(text) {
|
|
4619
4824
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4620
4825
|
}
|
|
4826
|
+
function renderResults(query, ranked, includeContent) {
|
|
4827
|
+
const lines = [`<search_results query="${escapeXML3(query)}" count="${ranked.length}">`];
|
|
4828
|
+
for (const result of ranked) {
|
|
4829
|
+
lines.push(` <result file="${escapeXML3(result.filePath)}" score="${result.score.toFixed(4)}">`);
|
|
4830
|
+
if (includeContent && result.content) {
|
|
4831
|
+
lines.push(` ${result.content.slice(0, 200).replace(/&/g, "&").replace(/</g, "<")}`);
|
|
4832
|
+
}
|
|
4833
|
+
lines.push(" </result>");
|
|
4834
|
+
}
|
|
4835
|
+
lines.push("</search_results>");
|
|
4836
|
+
return lines.join("\n");
|
|
4837
|
+
}
|
|
4621
4838
|
function registerSearchTool(registry, ctx) {
|
|
4622
4839
|
registry.register(
|
|
4623
4840
|
"ctx_search",
|
|
4624
4841
|
{
|
|
4625
4842
|
name: "ctx_search",
|
|
4626
|
-
description: "Hybrid semantic + graph search over the codebase. Uses vector embeddings for semantic similarity and the dependency graph for structural expansion. Returns ranked file results.",
|
|
4843
|
+
description: "Hybrid semantic + graph search over the codebase. Uses vector embeddings for semantic similarity and the dependency graph for structural expansion. Returns ranked file results. When callers opt into the budget surface, over-budget responses drop the content snippets and return paths + scores only.",
|
|
4627
4844
|
inputSchema: {
|
|
4628
4845
|
type: "object",
|
|
4629
4846
|
properties: {
|
|
4630
4847
|
query: { type: "string", description: "Search query \u2014 natural language or code fragment" },
|
|
4631
4848
|
limit: { type: "number", description: "Maximum results to return (default: 10)" },
|
|
4632
|
-
project_root: PROJECT_ROOT_JSON_SCHEMA
|
|
4849
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
4850
|
+
max_response_tokens: {
|
|
4851
|
+
type: "number",
|
|
4852
|
+
description: "Soft response budget in tokens. Default: 4000 (when opted into)."
|
|
4853
|
+
},
|
|
4854
|
+
on_budget_exceeded: {
|
|
4855
|
+
type: "string",
|
|
4856
|
+
enum: ["skeleton", "truncate", "error"],
|
|
4857
|
+
description: "Behavior when over budget. 'skeleton' (default) drops snippets; 'truncate' slices; 'error' throws."
|
|
4858
|
+
},
|
|
4859
|
+
response_format: {
|
|
4860
|
+
type: "string",
|
|
4861
|
+
enum: ["full", "skeleton", "auto"],
|
|
4862
|
+
description: "'skeleton' forces path+score-only view; 'full'/'auto' lets the budget decide."
|
|
4863
|
+
}
|
|
4633
4864
|
},
|
|
4634
4865
|
required: ["query"]
|
|
4635
4866
|
}
|
|
4636
4867
|
},
|
|
4637
4868
|
async (args) => {
|
|
4638
|
-
const
|
|
4639
|
-
const [store, graph] = await Promise.all([ctx.getStore(project_root), ctx.getGraph(project_root)]);
|
|
4640
|
-
const queryEmbedding = await generateEmbedding(query);
|
|
4641
|
-
const vectorResults = await store.search(queryEmbedding, limit);
|
|
4869
|
+
const parsed = Schema.parse(args);
|
|
4870
|
+
const [store, graph] = await Promise.all([ctx.getStore(parsed.project_root), ctx.getGraph(parsed.project_root)]);
|
|
4871
|
+
const queryEmbedding = await generateEmbedding(parsed.query);
|
|
4872
|
+
const vectorResults = await store.search(queryEmbedding, parsed.limit);
|
|
4642
4873
|
const expandedResults = /* @__PURE__ */ new Map();
|
|
4643
|
-
for (const
|
|
4644
|
-
const existingScore = expandedResults.get(
|
|
4645
|
-
if (
|
|
4646
|
-
expandedResults.set(
|
|
4874
|
+
for (const result2 of vectorResults) {
|
|
4875
|
+
const existingScore = expandedResults.get(result2.filePath)?.score ?? Infinity;
|
|
4876
|
+
if (result2.score < existingScore) {
|
|
4877
|
+
expandedResults.set(result2.filePath, { score: result2.score, content: result2.content });
|
|
4647
4878
|
}
|
|
4648
|
-
for (const related of [...graph.getImports(
|
|
4879
|
+
for (const related of [...graph.getImports(result2.filePath), ...graph.getImporters(result2.filePath)]) {
|
|
4649
4880
|
if (!expandedResults.has(related)) {
|
|
4650
|
-
expandedResults.set(related, { score:
|
|
4881
|
+
expandedResults.set(related, { score: result2.score + 0.1, content: "" });
|
|
4651
4882
|
}
|
|
4652
4883
|
}
|
|
4653
4884
|
}
|
|
4654
|
-
const ranked = Array.from(expandedResults.entries()).map(([filePath, data]) => ({ filePath, score: data.score, content: data.content })).sort((a, b) => a.score - b.score).slice(0, limit);
|
|
4655
|
-
const
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4885
|
+
const ranked = Array.from(expandedResults.entries()).map(([filePath, data]) => ({ filePath, score: data.score, content: data.content })).sort((a, b) => a.score - b.score).slice(0, parsed.limit);
|
|
4886
|
+
const full = renderResults(parsed.query, ranked, true);
|
|
4887
|
+
if (!hasBudgetArgs(args)) return full;
|
|
4888
|
+
const skeletonProducer = async () => renderResults(parsed.query, ranked, false);
|
|
4889
|
+
const result = await enforceBudget({
|
|
4890
|
+
full,
|
|
4891
|
+
args: readBudgetArgs(args),
|
|
4892
|
+
toolName: "ctx_search",
|
|
4893
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS,
|
|
4894
|
+
skeletonProducer
|
|
4895
|
+
});
|
|
4896
|
+
return wrapResponse(result);
|
|
4665
4897
|
}
|
|
4666
4898
|
);
|
|
4667
4899
|
}
|
|
4668
4900
|
|
|
4669
4901
|
// packages/core/src/tools/file.ts
|
|
4670
4902
|
import { z as z4 } from "zod";
|
|
4903
|
+
var DEFAULT_MAX_RESPONSE_TOKENS2 = 8e3;
|
|
4671
4904
|
var Schema2 = z4.object({
|
|
4672
4905
|
path: z4.string().describe("Relative path to the file"),
|
|
4673
|
-
project_root: ProjectRootField
|
|
4906
|
+
project_root: ProjectRootField,
|
|
4907
|
+
// ─── Phase B2 budget surface (all optional; back-compat preserved) ──
|
|
4908
|
+
max_response_tokens: z4.number().int().positive().optional().describe("Soft response budget in tokens. Falls back to a skeleton when exceeded. Default: 8000 (when budget surface is opted into)."),
|
|
4909
|
+
on_budget_exceeded: z4.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when the response would exceed max_response_tokens. 'skeleton' (default) substitutes a Skeletonizer signature view; 'truncate' slices the raw text; 'error' throws a structured error with token counts so the caller can re-ask."),
|
|
4910
|
+
response_format: z4.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces a Skeletonizer view regardless of budget; 'full'/'auto' lets the budget decide.")
|
|
4674
4911
|
});
|
|
4675
4912
|
function registerFileTool(registry, ctx) {
|
|
4676
4913
|
registry.register(
|
|
4677
4914
|
"ctx_get_file",
|
|
4678
4915
|
{
|
|
4679
4916
|
name: "ctx_get_file",
|
|
4680
|
-
description: "Read a file from the project. Path is validated to prevent traversal outside the project root. Returns the full file content.",
|
|
4917
|
+
description: "Read a file from the project. Path is validated to prevent traversal outside the project root. Returns the full file content; when callers opt into the budget surface (max_response_tokens / on_budget_exceeded / response_format), the response is wrapped in a {data, meta} envelope and oversize content is auto-substituted with a Skeletonizer signature view.",
|
|
4681
4918
|
inputSchema: {
|
|
4682
4919
|
type: "object",
|
|
4683
4920
|
properties: {
|
|
4684
4921
|
path: { type: "string", description: "Relative path to the file" },
|
|
4685
|
-
project_root: PROJECT_ROOT_JSON_SCHEMA
|
|
4922
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
4923
|
+
max_response_tokens: {
|
|
4924
|
+
type: "number",
|
|
4925
|
+
description: "Soft response budget in tokens. Falls back to a skeleton when exceeded. Default: 8000 (when budget surface is opted into)."
|
|
4926
|
+
},
|
|
4927
|
+
on_budget_exceeded: {
|
|
4928
|
+
type: "string",
|
|
4929
|
+
enum: ["skeleton", "truncate", "error"],
|
|
4930
|
+
description: "Behavior when over budget. 'skeleton' (default) substitutes a signature view; 'truncate' slices the raw text; 'error' throws."
|
|
4931
|
+
},
|
|
4932
|
+
response_format: {
|
|
4933
|
+
type: "string",
|
|
4934
|
+
enum: ["full", "skeleton", "auto"],
|
|
4935
|
+
description: "'skeleton' forces a Skeletonizer view regardless of budget; 'full'/'auto' lets the budget decide."
|
|
4936
|
+
}
|
|
4686
4937
|
},
|
|
4687
4938
|
required: ["path"]
|
|
4688
4939
|
}
|
|
4689
4940
|
},
|
|
4690
4941
|
async (args) => {
|
|
4691
|
-
const
|
|
4692
|
-
|
|
4942
|
+
const parsed = Schema2.parse(args);
|
|
4943
|
+
const validator = ctx.getPathValidator(parsed.project_root);
|
|
4944
|
+
const full = validator.readFile(parsed.path);
|
|
4945
|
+
if (!hasBudgetArgs(args)) return full;
|
|
4946
|
+
const absPath = validator.validate(parsed.path);
|
|
4947
|
+
const skeletonizer = await ctx.getSkeletonizer(parsed.project_root);
|
|
4948
|
+
const result = await enforceBudget({
|
|
4949
|
+
full,
|
|
4950
|
+
args: readBudgetArgs(args),
|
|
4951
|
+
toolName: "ctx_get_file",
|
|
4952
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS2,
|
|
4953
|
+
skeletonProducer: () => skeletonizer.skeletonize(absPath)
|
|
4954
|
+
});
|
|
4955
|
+
return wrapResponse(result);
|
|
4693
4956
|
}
|
|
4694
4957
|
);
|
|
4695
4958
|
}
|
|
@@ -4697,34 +4960,71 @@ function registerFileTool(registry, ctx) {
|
|
|
4697
4960
|
// packages/core/src/tools/context-packet.ts
|
|
4698
4961
|
import { z as z5 } from "zod";
|
|
4699
4962
|
import path14 from "path";
|
|
4963
|
+
var DEFAULT_MAX_RESPONSE_TOKENS3 = 6e3;
|
|
4700
4964
|
var Schema3 = z5.object({
|
|
4701
4965
|
target_file: z5.string().describe("Relative path to the primary file"),
|
|
4702
4966
|
mode: z5.enum(["edit", "read"]).optional().default("edit").describe("Context mode"),
|
|
4703
|
-
project_root: ProjectRootField
|
|
4967
|
+
project_root: ProjectRootField,
|
|
4968
|
+
// ─── Phase B2 budget surface ──
|
|
4969
|
+
max_response_tokens: z5.number().int().positive().optional().describe("Soft response budget. Default: 6000 (when budget surface is opted into). Over-budget rebuilds the packet with the primary file replaced by its skeleton."),
|
|
4970
|
+
on_budget_exceeded: z5.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) re-renders the packet with the primary file skeletonized; 'truncate' slices the raw envelope; 'error' throws."),
|
|
4971
|
+
response_format: z5.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces the skeletonized-primary packet; 'full'/'auto' lets the budget decide.")
|
|
4704
4972
|
});
|
|
4973
|
+
function renderPacket(parts) {
|
|
4974
|
+
return [
|
|
4975
|
+
`<context_packet target="${parts.target_file}" mode="${parts.mode}">`,
|
|
4976
|
+
` <primary_context file="${parts.target_file}">`,
|
|
4977
|
+
` ${parts.primaryContent.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")}`,
|
|
4978
|
+
" </primary_context>",
|
|
4979
|
+
` <dependency_skeletons count="${parts.imports.length}">`,
|
|
4980
|
+
...parts.skeletons.map((s) => ` ${s}`),
|
|
4981
|
+
" </dependency_skeletons>",
|
|
4982
|
+
` <imported_by count="${parts.importers.length}">`,
|
|
4983
|
+
...parts.importers.map((imp) => ` <importer file="${imp}" />`),
|
|
4984
|
+
" </imported_by>",
|
|
4985
|
+
"</context_packet>"
|
|
4986
|
+
].join("\n");
|
|
4987
|
+
}
|
|
4705
4988
|
function registerContextPacketTool(registry, ctx) {
|
|
4706
4989
|
registry.register(
|
|
4707
4990
|
"ctx_get_context_packet",
|
|
4708
4991
|
{
|
|
4709
4992
|
name: "ctx_get_context_packet",
|
|
4710
|
-
description: "Returns a smart multi-file context packet: the full target file, skeletons of its imports, and the list of files that import it. Reduces token usage by ~80% vs. sending full dependencies.",
|
|
4993
|
+
description: "Returns a smart multi-file context packet: the full target file, skeletons of its imports, and the list of files that import it. Reduces token usage by ~80% vs. sending full dependencies. When callers opt into the budget surface, over-budget responses re-render the packet with the primary file ALSO replaced by its Skeletonizer view.",
|
|
4711
4994
|
inputSchema: {
|
|
4712
4995
|
type: "object",
|
|
4713
4996
|
properties: {
|
|
4714
4997
|
target_file: { type: "string", description: "Relative path to the primary file" },
|
|
4715
4998
|
mode: { type: "string", enum: ["edit", "read"], description: "Context mode (default: edit)" },
|
|
4716
|
-
project_root: PROJECT_ROOT_JSON_SCHEMA
|
|
4999
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
5000
|
+
max_response_tokens: {
|
|
5001
|
+
type: "number",
|
|
5002
|
+
description: "Soft response budget in tokens. Default: 6000 (when opted into)."
|
|
5003
|
+
},
|
|
5004
|
+
on_budget_exceeded: {
|
|
5005
|
+
type: "string",
|
|
5006
|
+
enum: ["skeleton", "truncate", "error"],
|
|
5007
|
+
description: "Behavior when over budget. 'skeleton' (default) skeletonizes the primary; 'truncate' slices; 'error' throws."
|
|
5008
|
+
},
|
|
5009
|
+
response_format: {
|
|
5010
|
+
type: "string",
|
|
5011
|
+
enum: ["full", "skeleton", "auto"],
|
|
5012
|
+
description: "'skeleton' forces the skeletonized-primary packet; 'full'/'auto' lets the budget decide."
|
|
5013
|
+
}
|
|
4717
5014
|
},
|
|
4718
5015
|
required: ["target_file"]
|
|
4719
5016
|
}
|
|
4720
5017
|
},
|
|
4721
5018
|
async (args) => {
|
|
4722
|
-
const
|
|
4723
|
-
const [skeletonizer, graph] = await Promise.all([
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
const
|
|
5019
|
+
const parsed = Schema3.parse(args);
|
|
5020
|
+
const [skeletonizer, graph] = await Promise.all([
|
|
5021
|
+
ctx.getSkeletonizer(parsed.project_root),
|
|
5022
|
+
ctx.getGraph(parsed.project_root)
|
|
5023
|
+
]);
|
|
5024
|
+
const pathValidator = ctx.getPathValidator(parsed.project_root);
|
|
5025
|
+
const primaryContent = pathValidator.readFile(parsed.target_file);
|
|
5026
|
+
const imports = graph.getImports(parsed.target_file);
|
|
5027
|
+
const importers = graph.getImporters(parsed.target_file);
|
|
4728
5028
|
const skeletons = await Promise.all(
|
|
4729
5029
|
imports.map(async (dep) => {
|
|
4730
5030
|
try {
|
|
@@ -4738,19 +5038,29 @@ ${sk}`;
|
|
|
4738
5038
|
}
|
|
4739
5039
|
})
|
|
4740
5040
|
);
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
5041
|
+
const parts = {
|
|
5042
|
+
target_file: parsed.target_file,
|
|
5043
|
+
mode: parsed.mode,
|
|
5044
|
+
primaryContent,
|
|
5045
|
+
skeletons,
|
|
5046
|
+
imports,
|
|
5047
|
+
importers
|
|
5048
|
+
};
|
|
5049
|
+
const full = renderPacket(parts);
|
|
5050
|
+
if (!hasBudgetArgs(args)) return full;
|
|
5051
|
+
const absPrimary = pathValidator.validate(parsed.target_file);
|
|
5052
|
+
const skeletonProducer = async () => {
|
|
5053
|
+
const primarySkeleton = await skeletonizer.skeletonize(absPrimary);
|
|
5054
|
+
return renderPacket({ ...parts, primaryContent: primarySkeleton });
|
|
5055
|
+
};
|
|
5056
|
+
const result = await enforceBudget({
|
|
5057
|
+
full,
|
|
5058
|
+
args: readBudgetArgs(args),
|
|
5059
|
+
toolName: "ctx_get_context_packet",
|
|
5060
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS3,
|
|
5061
|
+
skeletonProducer
|
|
5062
|
+
});
|
|
5063
|
+
return wrapResponse(result);
|
|
4754
5064
|
}
|
|
4755
5065
|
);
|
|
4756
5066
|
}
|
|
@@ -4845,9 +5155,14 @@ function registerCallGraphTool(registry, ctx) {
|
|
|
4845
5155
|
|
|
4846
5156
|
// packages/core/src/tools/definition.ts
|
|
4847
5157
|
import { z as z7 } from "zod";
|
|
5158
|
+
var DEFAULT_MAX_RESPONSE_TOKENS4 = 2e3;
|
|
4848
5159
|
var Schema5 = z7.object({
|
|
4849
5160
|
symbol: z7.string().describe("Symbol name to look up"),
|
|
4850
|
-
project_root: ProjectRootField
|
|
5161
|
+
project_root: ProjectRootField,
|
|
5162
|
+
// ─── Phase B2 budget surface (all optional; back-compat preserved) ──
|
|
5163
|
+
max_response_tokens: z7.number().int().positive().optional().describe("Soft response budget in tokens. Default: 2000 (when budget surface is opted into). No skeleton fallback for this tool \u2014 the response is structural metadata; over-budget falls back to truncation."),
|
|
5164
|
+
on_budget_exceeded: z7.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton'/'truncate' both slice the XML (no file context to skeletonize from); 'error' throws."),
|
|
5165
|
+
response_format: z7.enum(["full", "skeleton", "auto"]).optional().describe("'full'/'auto' default. 'skeleton' is accepted for consistency but produces the same output as 'full' here \u2014 the response is already a compact symbol list.")
|
|
4851
5166
|
});
|
|
4852
5167
|
function escapeXML5(text) {
|
|
4853
5168
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -4857,33 +5172,57 @@ function registerDefinitionTool(registry, ctx) {
|
|
|
4857
5172
|
"ctx_get_definition",
|
|
4858
5173
|
{
|
|
4859
5174
|
name: "ctx_get_definition",
|
|
4860
|
-
description: "Look up the definition of a symbol by name. Returns file path, type, and signature for all definitions matching the symbol name.",
|
|
5175
|
+
description: "Look up the definition of a symbol by name. Returns file path, type, and signature for all definitions matching the symbol name. When callers opt into the budget surface (max_response_tokens / on_budget_exceeded / response_format), the response is wrapped in a {data, meta} envelope and over-budget responses are truncated (no skeleton fallback \u2014 the response is already structural).",
|
|
4861
5176
|
inputSchema: {
|
|
4862
5177
|
type: "object",
|
|
4863
5178
|
properties: {
|
|
4864
5179
|
symbol: { type: "string", description: "Symbol name to look up" },
|
|
4865
|
-
project_root: PROJECT_ROOT_JSON_SCHEMA
|
|
5180
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
5181
|
+
max_response_tokens: {
|
|
5182
|
+
type: "number",
|
|
5183
|
+
description: "Soft response budget in tokens. Default: 2000 (when budget surface is opted into)."
|
|
5184
|
+
},
|
|
5185
|
+
on_budget_exceeded: {
|
|
5186
|
+
type: "string",
|
|
5187
|
+
enum: ["skeleton", "truncate", "error"],
|
|
5188
|
+
description: "Behavior when over budget. 'skeleton'/'truncate' both slice the XML; 'error' throws."
|
|
5189
|
+
},
|
|
5190
|
+
response_format: {
|
|
5191
|
+
type: "string",
|
|
5192
|
+
enum: ["full", "skeleton", "auto"],
|
|
5193
|
+
description: "'full'/'auto' default; 'skeleton' produces the same output (response is already compact)."
|
|
5194
|
+
}
|
|
4866
5195
|
},
|
|
4867
5196
|
required: ["symbol"]
|
|
4868
5197
|
}
|
|
4869
5198
|
},
|
|
4870
5199
|
async (args) => {
|
|
4871
|
-
const
|
|
4872
|
-
const graph = await ctx.getGraph(project_root);
|
|
4873
|
-
const definitions = graph.lookupSymbol(symbol);
|
|
5200
|
+
const parsed = Schema5.parse(args);
|
|
5201
|
+
const graph = await ctx.getGraph(parsed.project_root);
|
|
5202
|
+
const definitions = graph.lookupSymbol(parsed.symbol);
|
|
5203
|
+
let full;
|
|
4874
5204
|
if (definitions.length === 0) {
|
|
4875
|
-
|
|
5205
|
+
full = `<definitions symbol="${escapeXML5(parsed.symbol)}" count="0">
|
|
4876
5206
|
<!-- Symbol not found -->
|
|
4877
5207
|
</definitions>`;
|
|
4878
|
-
}
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
5208
|
+
} else {
|
|
5209
|
+
const lines = [`<definitions symbol="${escapeXML5(parsed.symbol)}" count="${definitions.length}">`];
|
|
5210
|
+
for (const def of definitions) {
|
|
5211
|
+
lines.push(` <definition file="${def.filePath}" type="${def.type}">`);
|
|
5212
|
+
lines.push(` ${def.signature.replace(/&/g, "&").replace(/</g, "<")}`);
|
|
5213
|
+
lines.push(" </definition>");
|
|
5214
|
+
}
|
|
5215
|
+
lines.push("</definitions>");
|
|
5216
|
+
full = lines.join("\n");
|
|
5217
|
+
}
|
|
5218
|
+
if (!hasBudgetArgs(args)) return full;
|
|
5219
|
+
const result = await enforceBudget({
|
|
5220
|
+
full,
|
|
5221
|
+
args: readBudgetArgs(args),
|
|
5222
|
+
toolName: "ctx_get_definition",
|
|
5223
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS4
|
|
5224
|
+
});
|
|
5225
|
+
return wrapResponse(result);
|
|
4887
5226
|
}
|
|
4888
5227
|
);
|
|
4889
5228
|
}
|
|
@@ -5848,6 +6187,7 @@ function registerSurprisingConnectionsTool(registry, ctx) {
|
|
|
5848
6187
|
// packages/core/src/tools/wiki-generate.ts
|
|
5849
6188
|
import { z as z17 } from "zod";
|
|
5850
6189
|
import fs14 from "fs";
|
|
6190
|
+
var DEFAULT_MAX_RESPONSE_TOKENS5 = 12e3;
|
|
5851
6191
|
var Schema15 = z17.object({
|
|
5852
6192
|
force: z17.boolean().optional().default(false).describe(
|
|
5853
6193
|
"Regenerate all pages even if content unchanged (default: false)"
|
|
@@ -5855,7 +6195,11 @@ var Schema15 = z17.object({
|
|
|
5855
6195
|
detail_level: z17.enum(["standard", "minimal"]).default("standard").describe(
|
|
5856
6196
|
'"standard" (default) lists each written page with size. "minimal" returns counts only.'
|
|
5857
6197
|
),
|
|
5858
|
-
project_root: ProjectRootField
|
|
6198
|
+
project_root: ProjectRootField,
|
|
6199
|
+
// ─── Phase B2 budget surface ──
|
|
6200
|
+
max_response_tokens: z17.number().int().positive().optional().describe("Soft response budget. Default: 12000 (when opted in). Over-budget re-renders at detail_level=minimal (counts only) \u2014 the wiki files themselves are unaffected."),
|
|
6201
|
+
on_budget_exceeded: z17.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) downgrades to minimal output; 'truncate' slices; 'error' throws."),
|
|
6202
|
+
response_format: z17.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces minimal output regardless of budget; 'full'/'auto' lets the budget decide.")
|
|
5859
6203
|
});
|
|
5860
6204
|
function escapeXML14(text) {
|
|
5861
6205
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -5885,7 +6229,10 @@ function registerWikiGenerateTool(registry, ctx) {
|
|
|
5885
6229
|
enum: ["standard", "minimal"],
|
|
5886
6230
|
description: '"standard" lists written pages with size. "minimal" returns counts only.'
|
|
5887
6231
|
},
|
|
5888
|
-
project_root: PROJECT_ROOT_JSON_SCHEMA
|
|
6232
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
6233
|
+
max_response_tokens: { type: "number", description: "Soft response budget. Default: 12000 (when opted in)." },
|
|
6234
|
+
on_budget_exceeded: { type: "string", enum: ["skeleton", "truncate", "error"], description: "Behavior over budget. 'skeleton' (default) downgrades to minimal; 'truncate' slices; 'error' throws." },
|
|
6235
|
+
response_format: { type: "string", enum: ["full", "skeleton", "auto"], description: "'skeleton' forces minimal output; 'full'/'auto' lets the budget decide." }
|
|
5889
6236
|
}
|
|
5890
6237
|
}
|
|
5891
6238
|
},
|
|
@@ -5894,20 +6241,30 @@ function registerWikiGenerateTool(registry, ctx) {
|
|
|
5894
6241
|
const [graph, skeletonizer] = await Promise.all([ctx.getGraph(project_root), ctx.getSkeletonizer(project_root)]);
|
|
5895
6242
|
const generator = new WikiGenerator(graph, ctx.projectRoot, skeletonizer);
|
|
5896
6243
|
const result = await generator.generate(force);
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
6244
|
+
const renderMinimal = () => `<wiki_generate detail_level="minimal" wiki_dir="${escapeXML14(result.wikiDir)}" written="${result.written.length}" skipped="${result.skipped.length}" />`;
|
|
6245
|
+
const renderStandard = () => {
|
|
6246
|
+
const lines = [
|
|
6247
|
+
`<wiki_generate wiki_dir="${escapeXML14(result.wikiDir)}" written="${result.written.length}" skipped="${result.skipped.length}">`
|
|
6248
|
+
];
|
|
6249
|
+
for (const p of result.written) {
|
|
6250
|
+
const size = safeFileSize(p.filePath);
|
|
6251
|
+
lines.push(
|
|
6252
|
+
` <page community="${escapeXML14(p.communityName)}" file="${escapeXML14(p.filePath)}" size="${size}" status="written" />`
|
|
6253
|
+
);
|
|
6254
|
+
}
|
|
6255
|
+
lines.push("</wiki_generate>");
|
|
6256
|
+
return lines.join("\n");
|
|
6257
|
+
};
|
|
6258
|
+
const full = detail_level === "minimal" ? renderMinimal() : renderStandard();
|
|
6259
|
+
if (!hasBudgetArgs(args)) return full;
|
|
6260
|
+
const budgetResult = await enforceBudget({
|
|
6261
|
+
full,
|
|
6262
|
+
args: readBudgetArgs(args),
|
|
6263
|
+
toolName: "ctx_wiki_generate",
|
|
6264
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS5,
|
|
6265
|
+
skeletonProducer: detail_level === "standard" ? async () => renderMinimal() : void 0
|
|
6266
|
+
});
|
|
6267
|
+
return wrapResponse(budgetResult);
|
|
5911
6268
|
}
|
|
5912
6269
|
);
|
|
5913
6270
|
}
|
|
@@ -5956,6 +6313,7 @@ function registerGraphExportTool(registry, ctx) {
|
|
|
5956
6313
|
import { z as z19 } from "zod";
|
|
5957
6314
|
import { execFile } from "child_process";
|
|
5958
6315
|
import { promisify as promisify2 } from "util";
|
|
6316
|
+
var DEFAULT_MAX_RESPONSE_TOKENS6 = 8e3;
|
|
5959
6317
|
var execFileAsync = promisify2(execFile);
|
|
5960
6318
|
var Schema17 = z19.object({
|
|
5961
6319
|
changed_files: z19.array(z19.string()).optional().describe(
|
|
@@ -5969,7 +6327,11 @@ var Schema17 = z19.object({
|
|
|
5969
6327
|
max_diff_lines: z19.number().min(10).max(2e3).optional().default(300).describe(
|
|
5970
6328
|
"Max diff lines to include per file (default: 300)"
|
|
5971
6329
|
),
|
|
5972
|
-
project_root: ProjectRootField
|
|
6330
|
+
project_root: ProjectRootField,
|
|
6331
|
+
// ─── Phase B2 budget surface ──
|
|
6332
|
+
max_response_tokens: z19.number().int().positive().optional().describe("Soft response budget. Default: 8000 (when opted in). Over-budget re-renders without <skeleton> blocks and without the transitive_importers section \u2014 keeps diffs, direct importers, and call sites."),
|
|
6333
|
+
on_budget_exceeded: z19.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) drops skeletons + transitive importers; 'truncate' slices; 'error' throws."),
|
|
6334
|
+
response_format: z19.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces the lighter view; 'full'/'auto' lets the budget decide.")
|
|
5973
6335
|
});
|
|
5974
6336
|
function escapeXML16(text) {
|
|
5975
6337
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -6017,7 +6379,10 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
6017
6379
|
description: "Include API skeletons for changed and importer files (default: true)"
|
|
6018
6380
|
},
|
|
6019
6381
|
max_diff_lines: { type: "number", description: "Max diff lines per file (default: 300)" },
|
|
6020
|
-
project_root: PROJECT_ROOT_JSON_SCHEMA
|
|
6382
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
6383
|
+
max_response_tokens: { type: "number", description: "Soft response budget. Default: 8000 (when opted in)." },
|
|
6384
|
+
on_budget_exceeded: { type: "string", enum: ["skeleton", "truncate", "error"], description: "Behavior over budget. 'skeleton' (default) drops <skeleton> blocks + transitive importers; 'truncate' slices; 'error' throws." },
|
|
6385
|
+
response_format: { type: "string", enum: ["full", "skeleton", "auto"], description: "'skeleton' forces lighter view; 'full'/'auto' lets the budget decide." }
|
|
6021
6386
|
}
|
|
6022
6387
|
}
|
|
6023
6388
|
},
|
|
@@ -6037,10 +6402,21 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
6037
6402
|
logger.warn("git diff failed \u2014 no changed files detected");
|
|
6038
6403
|
}
|
|
6039
6404
|
}
|
|
6405
|
+
const maybeBudget = async (full2, skeletonProducer) => {
|
|
6406
|
+
if (!hasBudgetArgs(args)) return full2;
|
|
6407
|
+
const result = await enforceBudget({
|
|
6408
|
+
full: full2,
|
|
6409
|
+
args: readBudgetArgs(args),
|
|
6410
|
+
toolName: "ctx_git_diff_review",
|
|
6411
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS6,
|
|
6412
|
+
skeletonProducer
|
|
6413
|
+
});
|
|
6414
|
+
return wrapResponse(result);
|
|
6415
|
+
};
|
|
6040
6416
|
if (files.length === 0) {
|
|
6041
|
-
return `<git_diff_review changed_files="0">
|
|
6417
|
+
return maybeBudget(`<git_diff_review changed_files="0">
|
|
6042
6418
|
<!-- No changed files detected -->
|
|
6043
|
-
</git_diff_review
|
|
6419
|
+
</git_diff_review>`);
|
|
6044
6420
|
}
|
|
6045
6421
|
const graph = await ctx.getGraph(project_root);
|
|
6046
6422
|
const blast = await computeBlastRadius({
|
|
@@ -6049,62 +6425,69 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
6049
6425
|
projectRoot: ctx.projectRoot,
|
|
6050
6426
|
graph
|
|
6051
6427
|
});
|
|
6052
|
-
const
|
|
6053
|
-
`<git_diff_review changed_files="${files.length}" depth="${depth}">`
|
|
6054
|
-
];
|
|
6055
|
-
lines.push(` <changed_files count="${files.length}">`);
|
|
6056
|
-
for (const file of files) {
|
|
6057
|
-
lines.push(` <file path="${escapeXML16(file)}">`);
|
|
6428
|
+
const changedFileData = await Promise.all(files.map(async (file) => {
|
|
6058
6429
|
const rawDiff = use_git ? await getFileDiff(ctx.projectRoot, file) : "";
|
|
6059
6430
|
const diffLines = rawDiff ? rawDiff.split("\n") : [];
|
|
6060
6431
|
const truncated = diffLines.length > max_diff_lines;
|
|
6061
6432
|
const diffContent = truncated ? [...diffLines.slice(0, max_diff_lines), `... (${diffLines.length - max_diff_lines} more lines)`].join("\n") : rawDiff;
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6433
|
+
const skeleton = include_skeletons ? await trySkeletonize(ctx, file, project_root) : "";
|
|
6434
|
+
return { file, diffLines, truncated, diffContent, skeleton };
|
|
6435
|
+
}));
|
|
6436
|
+
const skeletonLimit = 5;
|
|
6437
|
+
const directImporterSkeletons = await Promise.all(
|
|
6438
|
+
blast.directImporters.map(async (file, i) => ({
|
|
6439
|
+
file,
|
|
6440
|
+
skeleton: include_skeletons && i < skeletonLimit ? await trySkeletonize(ctx, file, project_root) : ""
|
|
6441
|
+
}))
|
|
6442
|
+
);
|
|
6443
|
+
const render = (withSkeletons, withTransitive) => {
|
|
6444
|
+
const out = [`<git_diff_review changed_files="${files.length}" depth="${depth}">`];
|
|
6445
|
+
out.push(` <changed_files count="${files.length}">`);
|
|
6446
|
+
for (const cd of changedFileData) {
|
|
6447
|
+
out.push(` <file path="${escapeXML16(cd.file)}">`);
|
|
6448
|
+
out.push(` <diff lines="${cd.diffLines.length}" truncated="${cd.truncated}">`);
|
|
6449
|
+
if (cd.diffContent) out.push(escapeXML16(cd.diffContent));
|
|
6450
|
+
out.push(" </diff>");
|
|
6451
|
+
if (withSkeletons && cd.skeleton) {
|
|
6452
|
+
out.push(" <skeleton>");
|
|
6453
|
+
out.push(escapeXML16(cd.skeleton));
|
|
6454
|
+
out.push(" </skeleton>");
|
|
6073
6455
|
}
|
|
6456
|
+
out.push(" </file>");
|
|
6457
|
+
}
|
|
6458
|
+
out.push(" </changed_files>");
|
|
6459
|
+
out.push(` <direct_importers count="${blast.directImporters.length}">`);
|
|
6460
|
+
for (const di of directImporterSkeletons) {
|
|
6461
|
+
out.push(` <file path="${escapeXML16(di.file)}">`);
|
|
6462
|
+
if (withSkeletons && di.skeleton) {
|
|
6463
|
+
out.push(" <skeleton>");
|
|
6464
|
+
out.push(escapeXML16(di.skeleton));
|
|
6465
|
+
out.push(" </skeleton>");
|
|
6466
|
+
}
|
|
6467
|
+
out.push(" </file>");
|
|
6074
6468
|
}
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
for (let i = 0; i < blast.directImporters.length; i++) {
|
|
6081
|
-
const file = blast.directImporters[i];
|
|
6082
|
-
lines.push(` <file path="${escapeXML16(file)}">`);
|
|
6083
|
-
if (include_skeletons && i < skeletonLimit) {
|
|
6084
|
-
const skeleton = await trySkeletonize(ctx, file, project_root);
|
|
6085
|
-
if (skeleton) {
|
|
6086
|
-
lines.push(" <skeleton>");
|
|
6087
|
-
lines.push(escapeXML16(skeleton));
|
|
6088
|
-
lines.push(" </skeleton>");
|
|
6469
|
+
out.push(" </direct_importers>");
|
|
6470
|
+
if (withTransitive) {
|
|
6471
|
+
out.push(` <transitive_importers count="${blast.transitiveImporters.length}">`);
|
|
6472
|
+
for (const file of blast.transitiveImporters) {
|
|
6473
|
+
out.push(` <file path="${escapeXML16(file)}" />`);
|
|
6089
6474
|
}
|
|
6475
|
+
out.push(" </transitive_importers>");
|
|
6476
|
+
} else {
|
|
6477
|
+
out.push(` <transitive_importers count="${blast.transitiveImporters.length}" omitted="budget"/>`);
|
|
6090
6478
|
}
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
);
|
|
6104
|
-
}
|
|
6105
|
-
lines.push(" </call_sites>");
|
|
6106
|
-
lines.push("</git_diff_review>");
|
|
6107
|
-
return lines.join("\n");
|
|
6479
|
+
out.push(` <call_sites count="${blast.callSites.length}">`);
|
|
6480
|
+
for (const cs of blast.callSites) {
|
|
6481
|
+
out.push(
|
|
6482
|
+
` <call_site file="${escapeXML16(cs.file)}" caller="${escapeXML16(cs.callerSymbol)}" callee="${escapeXML16(cs.calleeSymbol)}" />`
|
|
6483
|
+
);
|
|
6484
|
+
}
|
|
6485
|
+
out.push(" </call_sites>");
|
|
6486
|
+
out.push("</git_diff_review>");
|
|
6487
|
+
return out.join("\n");
|
|
6488
|
+
};
|
|
6489
|
+
const full = render(include_skeletons, true);
|
|
6490
|
+
return maybeBudget(full, async () => render(false, false));
|
|
6108
6491
|
}
|
|
6109
6492
|
);
|
|
6110
6493
|
}
|
|
@@ -6113,13 +6496,18 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
6113
6496
|
import { z as z20 } from "zod";
|
|
6114
6497
|
import fs15 from "fs";
|
|
6115
6498
|
import path17 from "path";
|
|
6499
|
+
var DEFAULT_MAX_RESPONSE_TOKENS7 = 4e3;
|
|
6116
6500
|
var Schema18 = z20.object({
|
|
6117
6501
|
symbol: z20.string().min(1).describe("Symbol name to rename (exact match, case-sensitive)"),
|
|
6118
6502
|
new_name: z20.string().min(1).describe("New name for the symbol"),
|
|
6119
6503
|
max_files: z20.number().min(1).max(200).optional().default(50).describe(
|
|
6120
6504
|
"Maximum number of files to scan for occurrences (default: 50)"
|
|
6121
6505
|
),
|
|
6122
|
-
project_root: ProjectRootField
|
|
6506
|
+
project_root: ProjectRootField,
|
|
6507
|
+
// ─── Phase B2 budget surface ──
|
|
6508
|
+
max_response_tokens: z20.number().int().positive().optional().describe("Soft response budget. Default: 4000 (when opted in). Over-budget drops the per-change before/after lines; keeps the file+occurrence summary so callers can decide which files to drill into."),
|
|
6509
|
+
on_budget_exceeded: z20.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) drops change details; 'truncate' slices; 'error' throws."),
|
|
6510
|
+
response_format: z20.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces the summary-only view; 'full'/'auto' lets the budget decide.")
|
|
6123
6511
|
});
|
|
6124
6512
|
function escapeXML17(text) {
|
|
6125
6513
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -6167,7 +6555,10 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
6167
6555
|
type: "number",
|
|
6168
6556
|
description: "Maximum number of candidate files to scan (default: 50)"
|
|
6169
6557
|
},
|
|
6170
|
-
project_root: PROJECT_ROOT_JSON_SCHEMA
|
|
6558
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
6559
|
+
max_response_tokens: { type: "number", description: "Soft response budget. Default: 4000 (when opted in)." },
|
|
6560
|
+
on_budget_exceeded: { type: "string", enum: ["skeleton", "truncate", "error"], description: "Behavior over budget. 'skeleton' (default) drops change details; 'truncate' slices; 'error' throws." },
|
|
6561
|
+
response_format: { type: "string", enum: ["full", "skeleton", "auto"], description: "'skeleton' forces summary-only view; 'full'/'auto' lets the budget decide." }
|
|
6171
6562
|
},
|
|
6172
6563
|
required: ["symbol", "new_name"]
|
|
6173
6564
|
}
|
|
@@ -6198,36 +6589,53 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
6198
6589
|
totalOccurrences += occurrences.length;
|
|
6199
6590
|
}
|
|
6200
6591
|
}
|
|
6201
|
-
const
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
xmlLines.push(" </definitions>");
|
|
6211
|
-
xmlLines.push(` <changes count="${fileChanges.length}">`);
|
|
6212
|
-
for (const fc of fileChanges) {
|
|
6213
|
-
xmlLines.push(` <file path="${escapeXML17(fc.filePath)}" occurrences="${fc.occurrences.length}">`);
|
|
6214
|
-
for (const occ of fc.occurrences) {
|
|
6215
|
-
xmlLines.push(` <change line="${occ.line}">`);
|
|
6216
|
-
xmlLines.push(` <before>${escapeXML17(occ.before)}</before>`);
|
|
6217
|
-
xmlLines.push(` <after>${escapeXML17(occ.after)}</after>`);
|
|
6218
|
-
xmlLines.push(" </change>");
|
|
6592
|
+
const render = (includeChanges) => {
|
|
6593
|
+
const xmlLines = [
|
|
6594
|
+
`<refactor_preview symbol="${escapeXML17(symbol)}" new_name="${escapeXML17(new_name)}" total_files="${fileChanges.length}" total_occurrences="${totalOccurrences}">`
|
|
6595
|
+
];
|
|
6596
|
+
xmlLines.push(` <definitions count="${definitions.length}">`);
|
|
6597
|
+
for (const def of definitions) {
|
|
6598
|
+
xmlLines.push(
|
|
6599
|
+
` <definition file="${escapeXML17(def.filePath)}" type="${escapeXML17(def.type)}" signature="${escapeXML17(def.signature)}" />`
|
|
6600
|
+
);
|
|
6219
6601
|
}
|
|
6220
|
-
xmlLines.push("
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6602
|
+
xmlLines.push(" </definitions>");
|
|
6603
|
+
xmlLines.push(` <changes count="${fileChanges.length}">`);
|
|
6604
|
+
for (const fc of fileChanges) {
|
|
6605
|
+
if (includeChanges) {
|
|
6606
|
+
xmlLines.push(` <file path="${escapeXML17(fc.filePath)}" occurrences="${fc.occurrences.length}">`);
|
|
6607
|
+
for (const occ of fc.occurrences) {
|
|
6608
|
+
xmlLines.push(` <change line="${occ.line}">`);
|
|
6609
|
+
xmlLines.push(` <before>${escapeXML17(occ.before)}</before>`);
|
|
6610
|
+
xmlLines.push(` <after>${escapeXML17(occ.after)}</after>`);
|
|
6611
|
+
xmlLines.push(" </change>");
|
|
6612
|
+
}
|
|
6613
|
+
xmlLines.push(" </file>");
|
|
6614
|
+
} else {
|
|
6615
|
+
xmlLines.push(` <file path="${escapeXML17(fc.filePath)}" occurrences="${fc.occurrences.length}"/>`);
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
6618
|
+
xmlLines.push(" </changes>");
|
|
6619
|
+
xmlLines.push("</refactor_preview>");
|
|
6620
|
+
return xmlLines.join("\n");
|
|
6621
|
+
};
|
|
6622
|
+
const full = render(true);
|
|
6623
|
+
if (!hasBudgetArgs(args)) return full;
|
|
6624
|
+
const result = await enforceBudget({
|
|
6625
|
+
full,
|
|
6626
|
+
args: readBudgetArgs(args),
|
|
6627
|
+
toolName: "ctx_refactor_preview",
|
|
6628
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS7,
|
|
6629
|
+
skeletonProducer: async () => render(false)
|
|
6630
|
+
});
|
|
6631
|
+
return wrapResponse(result);
|
|
6225
6632
|
}
|
|
6226
6633
|
);
|
|
6227
6634
|
}
|
|
6228
6635
|
|
|
6229
6636
|
// packages/core/src/tools/execution-flow.ts
|
|
6230
6637
|
import { z as z21 } from "zod";
|
|
6638
|
+
var DEFAULT_MAX_RESPONSE_TOKENS8 = 4e3;
|
|
6231
6639
|
var Schema19 = z21.object({
|
|
6232
6640
|
entry_point: z21.string().min(1).describe("Symbol name to start the execution flow from"),
|
|
6233
6641
|
entry_file: z21.string().optional().describe(
|
|
@@ -6237,7 +6645,11 @@ var Schema19 = z21.object({
|
|
|
6237
6645
|
max_nodes: z21.number().min(1).max(200).optional().default(50).describe(
|
|
6238
6646
|
"Max total steps to include in output (default: 50)"
|
|
6239
6647
|
),
|
|
6240
|
-
project_root: ProjectRootField
|
|
6648
|
+
project_root: ProjectRootField,
|
|
6649
|
+
// ─── Phase B2 budget surface ──
|
|
6650
|
+
max_response_tokens: z21.number().int().positive().optional().describe("Soft response budget. Default: 4000 (when opted in). No skeleton fallback \u2014 response is already a bounded step list; over-budget falls through to truncation."),
|
|
6651
|
+
on_budget_exceeded: z21.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton'/'truncate' both slice; 'error' throws."),
|
|
6652
|
+
response_format: z21.enum(["full", "skeleton", "auto"]).optional().describe("'full'/'auto' default; 'skeleton' same output.")
|
|
6241
6653
|
});
|
|
6242
6654
|
function escapeXML18(text) {
|
|
6243
6655
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -6299,7 +6711,10 @@ function registerExecutionFlowTool(registry, ctx) {
|
|
|
6299
6711
|
type: "number",
|
|
6300
6712
|
description: "Max total steps to return (default: 50)"
|
|
6301
6713
|
},
|
|
6302
|
-
project_root: PROJECT_ROOT_JSON_SCHEMA
|
|
6714
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
6715
|
+
max_response_tokens: { type: "number", description: "Soft response budget. Default: 4000 (when opted in)." },
|
|
6716
|
+
on_budget_exceeded: { type: "string", enum: ["skeleton", "truncate", "error"], description: "Behavior over budget." },
|
|
6717
|
+
response_format: { type: "string", enum: ["full", "skeleton", "auto"], description: "'full'/'auto' default; 'skeleton' same output." }
|
|
6303
6718
|
},
|
|
6304
6719
|
required: ["entry_point"]
|
|
6305
6720
|
}
|
|
@@ -6325,10 +6740,20 @@ function registerExecutionFlowTool(registry, ctx) {
|
|
|
6325
6740
|
}
|
|
6326
6741
|
}
|
|
6327
6742
|
}
|
|
6743
|
+
const maybeBudget = async (full) => {
|
|
6744
|
+
if (!hasBudgetArgs(args)) return full;
|
|
6745
|
+
const result = await enforceBudget({
|
|
6746
|
+
full,
|
|
6747
|
+
args: readBudgetArgs(args),
|
|
6748
|
+
toolName: "ctx_execution_flow",
|
|
6749
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS8
|
|
6750
|
+
});
|
|
6751
|
+
return wrapResponse(result);
|
|
6752
|
+
};
|
|
6328
6753
|
if (!resolvedFile) {
|
|
6329
|
-
return `<execution_flow entry="${escapeXML18(entry_point)}" total_steps="0" has_cycles="false">
|
|
6754
|
+
return maybeBudget(`<execution_flow entry="${escapeXML18(entry_point)}" total_steps="0" has_cycles="false">
|
|
6330
6755
|
<!-- No call graph entries found for symbol -->
|
|
6331
|
-
</execution_flow
|
|
6756
|
+
</execution_flow>`);
|
|
6332
6757
|
}
|
|
6333
6758
|
const { steps, hasCycles } = buildFlowSteps(
|
|
6334
6759
|
entry_point,
|
|
@@ -6353,7 +6778,7 @@ function registerExecutionFlowTool(registry, ctx) {
|
|
|
6353
6778
|
}
|
|
6354
6779
|
}
|
|
6355
6780
|
xmlLines.push("</execution_flow>");
|
|
6356
|
-
return xmlLines.join("\n");
|
|
6781
|
+
return maybeBudget(xmlLines.join("\n"));
|
|
6357
6782
|
}
|
|
6358
6783
|
);
|
|
6359
6784
|
}
|
|
@@ -6362,6 +6787,7 @@ function registerExecutionFlowTool(registry, ctx) {
|
|
|
6362
6787
|
import { z as z22 } from "zod";
|
|
6363
6788
|
import fs16 from "fs";
|
|
6364
6789
|
import path18 from "path";
|
|
6790
|
+
var DEFAULT_MAX_RESPONSE_TOKENS9 = 4e3;
|
|
6365
6791
|
var ALIAS_REGEX = /^[a-z0-9-]{1,40}$/;
|
|
6366
6792
|
var RESERVED_ALIASES = /* @__PURE__ */ new Set([
|
|
6367
6793
|
"register",
|
|
@@ -6464,7 +6890,11 @@ var Schema20 = z22.object({
|
|
|
6464
6890
|
repos: z22.array(z22.string()).optional().describe(
|
|
6465
6891
|
"Specific repo root paths to search. Omit to search all registered repos."
|
|
6466
6892
|
),
|
|
6467
|
-
project_root: ProjectRootField
|
|
6893
|
+
project_root: ProjectRootField,
|
|
6894
|
+
// ─── Phase B2 budget surface ──
|
|
6895
|
+
max_response_tokens: z22.number().int().positive().optional().describe("Soft response budget. Default: 4000 (when opted in). Over-budget drops content snippets (repo + path + score only)."),
|
|
6896
|
+
on_budget_exceeded: z22.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) drops content snippets; 'truncate' slices; 'error' throws."),
|
|
6897
|
+
response_format: z22.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces the path-and-score-only view; 'full'/'auto' lets the budget decide.")
|
|
6468
6898
|
});
|
|
6469
6899
|
function escapeXML19(text) {
|
|
6470
6900
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -6486,7 +6916,10 @@ function registerCrossRepoSearchTool(registry, _ctx, registryFilePath) {
|
|
|
6486
6916
|
items: { type: "string" },
|
|
6487
6917
|
description: "Specific repo root paths to search. Omit to search all registered repos."
|
|
6488
6918
|
},
|
|
6489
|
-
project_root: PROJECT_ROOT_JSON_SCHEMA
|
|
6919
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
6920
|
+
max_response_tokens: { type: "number", description: "Soft response budget. Default: 4000 (when opted in)." },
|
|
6921
|
+
on_budget_exceeded: { type: "string", enum: ["skeleton", "truncate", "error"], description: "Behavior over budget. 'skeleton' (default) drops content snippets; 'truncate' slices; 'error' throws." },
|
|
6922
|
+
response_format: { type: "string", enum: ["full", "skeleton", "auto"], description: "'skeleton' forces path+score-only view; 'full'/'auto' lets the budget decide." }
|
|
6490
6923
|
},
|
|
6491
6924
|
required: ["query"]
|
|
6492
6925
|
}
|
|
@@ -6555,19 +6988,32 @@ function registerCrossRepoSearchTool(registry, _ctx, registryFilePath) {
|
|
|
6555
6988
|
);
|
|
6556
6989
|
}
|
|
6557
6990
|
xmlLines.push(" </repos>");
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6991
|
+
const render = (includeContent) => {
|
|
6992
|
+
const out = [...xmlLines];
|
|
6993
|
+
out.push(` <results count="${topResults.length}">`);
|
|
6994
|
+
for (const r of topResults) {
|
|
6995
|
+
if (includeContent && r.content) {
|
|
6996
|
+
out.push(` <result repo="${escapeXML19(r.repoName)}" file="${escapeXML19(r.filePath)}" score="${r.score.toFixed(4)}">`);
|
|
6997
|
+
out.push(` ${escapeXML19(r.content.slice(0, 200))}`);
|
|
6998
|
+
out.push(" </result>");
|
|
6999
|
+
} else {
|
|
7000
|
+
out.push(` <result repo="${escapeXML19(r.repoName)}" file="${escapeXML19(r.filePath)}" score="${r.score.toFixed(4)}"/>`);
|
|
7001
|
+
}
|
|
6565
7002
|
}
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
7003
|
+
out.push(" </results>");
|
|
7004
|
+
out.push("</cross_repo_search>");
|
|
7005
|
+
return out.join("\n");
|
|
7006
|
+
};
|
|
7007
|
+
const full = render(true);
|
|
7008
|
+
if (!hasBudgetArgs(args)) return full;
|
|
7009
|
+
const result = await enforceBudget({
|
|
7010
|
+
full,
|
|
7011
|
+
args: readBudgetArgs(args),
|
|
7012
|
+
toolName: "ctx_cross_repo_search",
|
|
7013
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS9,
|
|
7014
|
+
skeletonProducer: async () => render(false)
|
|
7015
|
+
});
|
|
7016
|
+
return wrapResponse(result);
|
|
6571
7017
|
}
|
|
6572
7018
|
);
|
|
6573
7019
|
}
|
|
@@ -6576,6 +7022,7 @@ function registerCrossRepoSearchTool(registry, _ctx, registryFilePath) {
|
|
|
6576
7022
|
import { z as z23 } from "zod";
|
|
6577
7023
|
import fs17 from "fs";
|
|
6578
7024
|
import path19 from "path";
|
|
7025
|
+
var DEFAULT_MAX_RESPONSE_TOKENS10 = 2e3;
|
|
6579
7026
|
var Schema21 = z23.object({
|
|
6580
7027
|
symbol: z23.string().min(1).describe("Symbol name to rename (exact, case-sensitive)"),
|
|
6581
7028
|
new_name: z23.string().min(1).describe("New name for the symbol"),
|
|
@@ -6585,7 +7032,11 @@ var Schema21 = z23.object({
|
|
|
6585
7032
|
max_files: z23.number().min(1).max(200).optional().default(50).describe(
|
|
6586
7033
|
"Maximum candidate files to process (default: 50)"
|
|
6587
7034
|
),
|
|
6588
|
-
project_root: ProjectRootField
|
|
7035
|
+
project_root: ProjectRootField,
|
|
7036
|
+
// ─── Phase B2 budget surface ──
|
|
7037
|
+
max_response_tokens: z23.number().int().positive().optional().describe("Soft response budget. Default: 2000 (when opted in). No skeleton fallback \u2014 response is already compact; over-budget falls through to truncation."),
|
|
7038
|
+
on_budget_exceeded: z23.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton'/'truncate' both slice the XML; 'error' throws."),
|
|
7039
|
+
response_format: z23.enum(["full", "skeleton", "auto"]).optional().describe("'full'/'auto' default; 'skeleton' produces the same output.")
|
|
6589
7040
|
});
|
|
6590
7041
|
function escapeXML20(text) {
|
|
6591
7042
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -6619,7 +7070,10 @@ function registerApplyRefactorTool(registry, ctx) {
|
|
|
6619
7070
|
new_name: { type: "string", description: "New name" },
|
|
6620
7071
|
dry_run: { type: "boolean", description: "Preview only, no writes (default: false)" },
|
|
6621
7072
|
max_files: { type: "number", description: "Max candidate files (default: 50)" },
|
|
6622
|
-
project_root: PROJECT_ROOT_JSON_SCHEMA
|
|
7073
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
7074
|
+
max_response_tokens: { type: "number", description: "Soft response budget. Default: 2000 (when opted in)." },
|
|
7075
|
+
on_budget_exceeded: { type: "string", enum: ["skeleton", "truncate", "error"], description: "Behavior over budget." },
|
|
7076
|
+
response_format: { type: "string", enum: ["full", "skeleton", "auto"], description: "'full'/'auto' default; 'skeleton' same output." }
|
|
6623
7077
|
},
|
|
6624
7078
|
required: ["symbol", "new_name"]
|
|
6625
7079
|
}
|
|
@@ -6659,7 +7113,15 @@ function registerApplyRefactorTool(registry, ctx) {
|
|
|
6659
7113
|
);
|
|
6660
7114
|
}
|
|
6661
7115
|
xml.push("</apply_refactor>");
|
|
6662
|
-
|
|
7116
|
+
const full = xml.join("\n");
|
|
7117
|
+
if (!hasBudgetArgs(args)) return full;
|
|
7118
|
+
const result = await enforceBudget({
|
|
7119
|
+
full,
|
|
7120
|
+
args: readBudgetArgs(args),
|
|
7121
|
+
toolName: "ctx_apply_refactor",
|
|
7122
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS10
|
|
7123
|
+
});
|
|
7124
|
+
return wrapResponse(result);
|
|
6663
7125
|
}
|
|
6664
7126
|
);
|
|
6665
7127
|
}
|
|
@@ -6780,13 +7242,18 @@ function registerDetectChangesTool(registry, ctx) {
|
|
|
6780
7242
|
import { z as z25 } from "zod";
|
|
6781
7243
|
import fs18 from "fs";
|
|
6782
7244
|
import path20 from "path";
|
|
7245
|
+
var DEFAULT_MAX_RESPONSE_TOKENS11 = 4e3;
|
|
6783
7246
|
var Schema23 = z25.object({
|
|
6784
7247
|
query: z25.string().min(1).describe("Search term \u2014 literal or /regex/"),
|
|
6785
7248
|
mode: z25.enum(["hybrid", "keyword", "semantic"]).optional().default("hybrid"),
|
|
6786
7249
|
case_sensitive: z25.boolean().optional().default(false),
|
|
6787
7250
|
limit: z25.number().min(1).max(100).optional().default(20),
|
|
6788
7251
|
context_lines: z25.number().min(0).max(5).optional().default(1),
|
|
6789
|
-
project_root: ProjectRootField
|
|
7252
|
+
project_root: ProjectRootField,
|
|
7253
|
+
// ─── Phase B2 budget surface ──
|
|
7254
|
+
max_response_tokens: z25.number().int().positive().optional().describe("Soft response budget. Default: 4000 (when budget surface is opted into). Over-budget rebuilds the result list without match snippets (paths + match counts only)."),
|
|
7255
|
+
on_budget_exceeded: z25.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) drops snippets; 'truncate' slices the raw XML; 'error' throws."),
|
|
7256
|
+
response_format: z25.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces the path-and-count-only view; 'full'/'auto' lets the budget decide.")
|
|
6790
7257
|
});
|
|
6791
7258
|
function escapeXML22(text) {
|
|
6792
7259
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -6841,27 +7308,53 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
6841
7308
|
case_sensitive: { type: "boolean", description: "Case-sensitive match (default: false)" },
|
|
6842
7309
|
limit: { type: "number", description: "Max results (default: 20)" },
|
|
6843
7310
|
context_lines: { type: "number", description: "Context lines around each match (default: 1)" },
|
|
6844
|
-
project_root: PROJECT_ROOT_JSON_SCHEMA
|
|
7311
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
7312
|
+
max_response_tokens: {
|
|
7313
|
+
type: "number",
|
|
7314
|
+
description: "Soft response budget in tokens. Default: 4000 (when opted into)."
|
|
7315
|
+
},
|
|
7316
|
+
on_budget_exceeded: {
|
|
7317
|
+
type: "string",
|
|
7318
|
+
enum: ["skeleton", "truncate", "error"],
|
|
7319
|
+
description: "Behavior when over budget. 'skeleton' (default) drops match snippets; 'truncate' slices; 'error' throws."
|
|
7320
|
+
},
|
|
7321
|
+
response_format: {
|
|
7322
|
+
type: "string",
|
|
7323
|
+
enum: ["full", "skeleton", "auto"],
|
|
7324
|
+
description: "'skeleton' forces path+count-only view; 'full'/'auto' lets the budget decide."
|
|
7325
|
+
}
|
|
6845
7326
|
},
|
|
6846
7327
|
required: ["query"]
|
|
6847
7328
|
}
|
|
6848
7329
|
},
|
|
6849
7330
|
async (args) => {
|
|
6850
|
-
const
|
|
7331
|
+
const parsed = Schema23.parse(args);
|
|
7332
|
+
const { query, mode, case_sensitive, limit, context_lines, project_root } = parsed;
|
|
7333
|
+
const maybeBudget = async (full, skeletonProducer) => {
|
|
7334
|
+
if (!hasBudgetArgs(args)) return full;
|
|
7335
|
+
const result = await enforceBudget({
|
|
7336
|
+
full,
|
|
7337
|
+
args: readBudgetArgs(args),
|
|
7338
|
+
toolName: "ctx_full_text_search",
|
|
7339
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS11,
|
|
7340
|
+
skeletonProducer
|
|
7341
|
+
});
|
|
7342
|
+
return wrapResponse(result);
|
|
7343
|
+
};
|
|
6851
7344
|
if (mode === "semantic") {
|
|
6852
7345
|
try {
|
|
6853
7346
|
const { generateEmbedding: generateEmbedding2 } = await import("./embedder-R4KCXSGO.js");
|
|
6854
7347
|
const store = await ctx.getStore(project_root);
|
|
6855
7348
|
const embedding = await generateEmbedding2(query);
|
|
6856
7349
|
const results = await store.search(embedding, limit);
|
|
6857
|
-
const
|
|
7350
|
+
const xml = [`<full_text_search query="${escapeXML22(query)}" mode="semantic" count="${results.length}">`];
|
|
6858
7351
|
for (const r of results) {
|
|
6859
|
-
|
|
7352
|
+
xml.push(` <result file="${escapeXML22(r.filePath)}" matches="0"/>`);
|
|
6860
7353
|
}
|
|
6861
|
-
|
|
6862
|
-
return
|
|
7354
|
+
xml.push("</full_text_search>");
|
|
7355
|
+
return maybeBudget(xml.join("\n"));
|
|
6863
7356
|
} catch {
|
|
6864
|
-
return `<full_text_search query="${escapeXML22(query)}" mode="semantic" count="0"
|
|
7357
|
+
return maybeBudget(`<full_text_search query="${escapeXML22(query)}" mode="semantic" count="0"/>`);
|
|
6865
7358
|
}
|
|
6866
7359
|
}
|
|
6867
7360
|
const pattern = buildPattern(query, case_sensitive);
|
|
@@ -6903,18 +7396,28 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
6903
7396
|
} catch {
|
|
6904
7397
|
}
|
|
6905
7398
|
}
|
|
6906
|
-
const
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
7399
|
+
const render = (includeSnippets) => {
|
|
7400
|
+
const xml = [
|
|
7401
|
+
`<full_text_search query="${escapeXML22(query)}" mode="${mode}" case_sensitive="${case_sensitive}" count="${merged.length}">`
|
|
7402
|
+
];
|
|
7403
|
+
for (const r of merged) {
|
|
7404
|
+
if (includeSnippets && r.snippets.length > 0) {
|
|
7405
|
+
xml.push(` <result file="${escapeXML22(r.filePath)}" matches="${r.matchCount}">`);
|
|
7406
|
+
for (const snippet of r.snippets) {
|
|
7407
|
+
xml.push(` <match><![CDATA[${snippet}]]></match>`);
|
|
7408
|
+
}
|
|
7409
|
+
xml.push(" </result>");
|
|
7410
|
+
} else {
|
|
7411
|
+
xml.push(` <result file="${escapeXML22(r.filePath)}" matches="${r.matchCount}"/>`);
|
|
7412
|
+
}
|
|
6913
7413
|
}
|
|
6914
|
-
xml.push("
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
return
|
|
7414
|
+
xml.push("</full_text_search>");
|
|
7415
|
+
return xml.join("\n");
|
|
7416
|
+
};
|
|
7417
|
+
return maybeBudget(
|
|
7418
|
+
render(true),
|
|
7419
|
+
async () => render(false)
|
|
7420
|
+
);
|
|
6918
7421
|
}
|
|
6919
7422
|
);
|
|
6920
7423
|
}
|
|
@@ -7301,6 +7804,7 @@ function registerGraphDiffTool(registry, ctx) {
|
|
|
7301
7804
|
|
|
7302
7805
|
// packages/core/src/tools/find-large-functions.ts
|
|
7303
7806
|
import { z as z30 } from "zod";
|
|
7807
|
+
var DEFAULT_MAX_RESPONSE_TOKENS12 = 2e3;
|
|
7304
7808
|
var schema3 = z30.object({
|
|
7305
7809
|
threshold: z30.number().int().min(1).default(50).describe(
|
|
7306
7810
|
"Minimum line count to include (default: 50). Functions/classes shorter than this are excluded."
|
|
@@ -7311,7 +7815,11 @@ var schema3 = z30.object({
|
|
|
7311
7815
|
limit: z30.number().int().min(1).max(200).default(30).describe(
|
|
7312
7816
|
"Maximum results to return (default: 30)."
|
|
7313
7817
|
),
|
|
7314
|
-
project_root: ProjectRootField
|
|
7818
|
+
project_root: ProjectRootField,
|
|
7819
|
+
// ─── Phase B2 budget surface ──
|
|
7820
|
+
max_response_tokens: z30.number().int().positive().optional().describe("Soft response budget. Default: 2000 (when opted in). No skeleton fallback \u2014 response is already structural; over-budget falls through to truncation."),
|
|
7821
|
+
on_budget_exceeded: z30.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton'/'truncate' both slice the XML; 'error' throws."),
|
|
7822
|
+
response_format: z30.enum(["full", "skeleton", "auto"]).optional().describe("'full'/'auto' default; 'skeleton' produces the same output (response is already compact).")
|
|
7315
7823
|
});
|
|
7316
7824
|
function findLargeFunctions(graph, threshold, fileFilter) {
|
|
7317
7825
|
const results = [];
|
|
@@ -7361,7 +7869,10 @@ function registerFindLargeFunctionsTool(registry, ctx) {
|
|
|
7361
7869
|
type: "number",
|
|
7362
7870
|
description: "Maximum results to return (default: 30, max: 200)."
|
|
7363
7871
|
},
|
|
7364
|
-
project_root: PROJECT_ROOT_JSON_SCHEMA
|
|
7872
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
7873
|
+
max_response_tokens: { type: "number", description: "Soft response budget. Default: 2000 (when opted in)." },
|
|
7874
|
+
on_budget_exceeded: { type: "string", enum: ["skeleton", "truncate", "error"], description: "Behavior over budget. 'skeleton'/'truncate' slice; 'error' throws." },
|
|
7875
|
+
response_format: { type: "string", enum: ["full", "skeleton", "auto"], description: "'full'/'auto' default; 'skeleton' same output (already compact)." }
|
|
7365
7876
|
}
|
|
7366
7877
|
}
|
|
7367
7878
|
},
|
|
@@ -7369,19 +7880,29 @@ function registerFindLargeFunctionsTool(registry, ctx) {
|
|
|
7369
7880
|
const { threshold, file_filter, limit, project_root } = schema3.parse(args);
|
|
7370
7881
|
const graph = await ctx.getGraph(project_root);
|
|
7371
7882
|
const results = findLargeFunctions(graph, threshold, file_filter).slice(0, limit);
|
|
7883
|
+
let full;
|
|
7372
7884
|
if (results.length === 0) {
|
|
7373
|
-
|
|
7885
|
+
full = `<ctx_find_large_functions threshold="${threshold}" count="0">
|
|
7374
7886
|
<message>No functions or classes exceed ${threshold} lines.</message>
|
|
7375
7887
|
</ctx_find_large_functions>`;
|
|
7376
|
-
}
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7888
|
+
} else {
|
|
7889
|
+
const lines = [
|
|
7890
|
+
`<ctx_find_large_functions threshold="${threshold}" count="${results.length}">`,
|
|
7891
|
+
...results.map(
|
|
7892
|
+
(r) => ` <symbol name="${escapeXML24(r.name)}" type="${r.type}" file="${escapeXML24(r.filePath)}" start="${r.startLine}" end="${r.endLine}" lines="${r.lineCount}" />`
|
|
7893
|
+
),
|
|
7894
|
+
`</ctx_find_large_functions>`
|
|
7895
|
+
];
|
|
7896
|
+
full = lines.join("\n");
|
|
7897
|
+
}
|
|
7898
|
+
if (!hasBudgetArgs(args)) return full;
|
|
7899
|
+
const result = await enforceBudget({
|
|
7900
|
+
full,
|
|
7901
|
+
args: readBudgetArgs(args),
|
|
7902
|
+
toolName: "ctx_find_large_functions",
|
|
7903
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS12
|
|
7904
|
+
});
|
|
7905
|
+
return wrapResponse(result);
|
|
7385
7906
|
}
|
|
7386
7907
|
);
|
|
7387
7908
|
}
|
|
@@ -8821,7 +9342,7 @@ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
|
|
|
8821
9342
|
function getTelemetryLevel() {
|
|
8822
9343
|
return TELEMETRY_LEVEL;
|
|
8823
9344
|
}
|
|
8824
|
-
var CTXLOOM_VERSION = "1.
|
|
9345
|
+
var CTXLOOM_VERSION = "1.3.0".length > 0 ? "1.3.0" : "dev";
|
|
8825
9346
|
var POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
8826
9347
|
var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
|
|
8827
9348
|
var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
|
|
@@ -9551,4 +10072,4 @@ export {
|
|
|
9551
10072
|
FirstTouchTracker,
|
|
9552
10073
|
EmittedOnceTracker
|
|
9553
10074
|
};
|
|
9554
|
-
//# sourceMappingURL=chunk-
|
|
10075
|
+
//# sourceMappingURL=chunk-Q2KTZNNU.js.map
|