Skip to content

Commit e93519e

Browse files
KRATOSKRATOS
authored andcommitted
fix(output): preserve hook budgets and search paths
1 parent ea27648 commit e93519e

4 files changed

Lines changed: 60 additions & 8 deletions

File tree

‎internal/tools/output_boundary.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ func selfManagedOutputBudget(toolName string, args map[string]any) outputBudget
8585
func (registry *Registry) RebudgetAfterHook(toolName string, args map[string]any, result Result) Result {
8686
result = scrubResultSecrets(result)
8787
tool, _ := registry.Get(toolName)
88+
if _, ok := tool.(selfBudgeting); ok {
89+
// Match the primary registry boundary: self-managed tools keep their
90+
// call-specific capture/output budget instead of being tightened or
91+
// loosened to the generic registry ceiling after hook feedback.
92+
return applySelfManagedOutputBudget(tool, toolName, args, result)
93+
}
8894
result = applyRegistryOutputBudget(tool, toolName, args, result)
8995
return enforceOutputCeiling(toolName, result)
9096
}

‎internal/tools/output_boundary_test.go‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,26 @@ func TestRegistryAppliesSemanticBudgetToSelfManagedProcessOutput(t *testing.T) {
307307
}
308308
}
309309
}
310+
311+
func TestRebudgetAfterHookPreservesSelfManagedExecBudget(t *testing.T) {
312+
setTestTempDir(t)
313+
registry := NewRegistry()
314+
registry.Register(NewExecCommandTool(t.TempDir(), newExecSessionManager()))
315+
316+
t.Run("tight call budget remains authoritative", func(t *testing.T) {
317+
args := map[string]any{"cmd": "make build", "max_output_tokens": 10}
318+
result := registry.RebudgetAfterHook(ExecCommandToolName, args, Result{Status: StatusOK, Output: strings.Repeat("x", 1_000)})
319+
if !result.Truncated || len(result.Output) > 40 {
320+
t.Fatalf("post-hook output escaped call budget: truncated=%t bytes=%d meta=%#v", result.Truncated, len(result.Output), result.Meta)
321+
}
322+
})
323+
324+
t.Run("raised call budget is not replaced by generic ceiling", func(t *testing.T) {
325+
args := map[string]any{"cmd": "make build", "max_output_tokens": 20_000}
326+
output := strings.Repeat("x", 70_000)
327+
result := registry.RebudgetAfterHook(ExecCommandToolName, args, Result{Status: StatusOK, Output: output})
328+
if result.Truncated || result.Output != output {
329+
t.Fatalf("post-hook output was incorrectly limited by generic ceiling: truncated=%t bytes=%d", result.Truncated, len(result.Output))
330+
}
331+
})
332+
}

‎internal/tools/output_policies.go‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -398,21 +398,28 @@ func collapseConsecutiveDuplicateLines(lines []string) []string {
398398
}
399399

400400
func searchResultFile(line string) string {
401-
// Search output is path:line: text. Locate the numeric line field from
402-
// the right so a Windows drive prefix (C:) remains part of the path.
403-
for end := len(line); end > 0; {
404-
lineEnd := strings.LastIndex(line[:end], ":")
405-
if lineEnd < 0 {
401+
// Search output is path:line: text. Locate the first numeric field from
402+
// the left after an optional Windows drive prefix. Match text may itself
403+
// contain numeric colon-delimited fields, which are not record structure.
404+
searchFrom := 0
405+
if len(line) >= 2 && line[1] == ':' && ((line[0] >= 'A' && line[0] <= 'Z') || (line[0] >= 'a' && line[0] <= 'z')) {
406+
searchFrom = 2
407+
}
408+
for searchFrom < len(line) {
409+
lineStartOffset := strings.IndexByte(line[searchFrom:], ':')
410+
if lineStartOffset < 0 {
406411
return ""
407412
}
408-
lineStart := strings.LastIndex(line[:lineEnd], ":")
409-
if lineStart < 0 {
413+
lineStart := searchFrom + lineStartOffset
414+
lineEndOffset := strings.IndexByte(line[lineStart+1:], ':')
415+
if lineEndOffset < 0 {
410416
return ""
411417
}
418+
lineEnd := lineStart + 1 + lineEndOffset
412419
if _, err := strconv.Atoi(line[lineStart+1 : lineEnd]); err == nil {
413420
return strings.TrimSpace(line[:lineStart])
414421
}
415-
end = lineStart
422+
searchFrom = lineStart + 1
416423
}
417424
return ""
418425
}

‎internal/tools/output_policies_test.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ func TestSearchOutputPolicyPreservesWindowsPathCoverage(t *testing.T) {
5858
}
5959
}
6060

61+
func TestSearchResultFileIgnoresNumericMatchContent(t *testing.T) {
62+
tests := []struct {
63+
line string
64+
want string
65+
}{
66+
{line: `C:\workspace\a.go:12: error code:123: failed`, want: `C:\workspace\a.go`},
67+
{line: `/workspace/a.go:12: error code:123: failed`, want: `/workspace/a.go`},
68+
{line: `/workspace/name:with-colon.go:12: match`, want: `/workspace/name:with-colon.go`},
69+
}
70+
for _, test := range tests {
71+
if got := searchResultFile(test.line); got != test.want {
72+
t.Errorf("searchResultFile(%q) = %q, want %q", test.line, got, test.want)
73+
}
74+
}
75+
}
76+
6177
func TestProcessOutputPolicyCollapsesRepetitiveLogsAndKeepsDiagnostics(t *testing.T) {
6278
input := "starting server\n" + strings.Repeat("polling...\n", 300) + "WARNING: queue slow\nERROR: request failed\nshutdown complete"
6379
got := budgetSemanticOutput(input, outputCategoryProcess, semanticTestBudget())

0 commit comments

Comments
 (0)