fix: disable HTML escaping in tool feedback JSON preview
- Add MarshalNoEscape utility function in pkg/utils/json.go - Use SetEscapeHTML(false) to prevent & -> \u0026 and > -> \u003e - Update tool feedback in agent loop to use non-escaping marshal - Add comprehensive tests for special character handling
This commit is contained in:
parent
f1cb7cc8f5
commit
9fc2bed6a2
3 changed files with 98 additions and 1 deletions
|
|
@ -2289,7 +2289,7 @@ turnLoop:
|
|||
}
|
||||
}
|
||||
|
||||
argsJSON, _ := json.Marshal(toolArgs)
|
||||
argsJSON, _ := utils.MarshalNoEscape(toolArgs)
|
||||
argsPreview := utils.Truncate(string(argsJSON), 200)
|
||||
logger.InfoCF("agent", fmt.Sprintf("Tool call: %s(%s)", toolName, argsPreview),
|
||||
map[string]any{
|
||||
|
|
|
|||
23
pkg/utils/json.go
Normal file
23
pkg/utils/json.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// MarshalNoEscape serializes a value to JSON without HTML escaping.
|
||||
// This is useful for user-facing JSON output where characters like
|
||||
// '&', '<', and '>' should remain unescaped for readability.
|
||||
func MarshalNoEscape(v any) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if err := encoder.Encode(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := buf.Bytes()
|
||||
if len(result) > 0 && result[len(result)-1] == '\n' {
|
||||
result = result[:len(result)-1]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
74
pkg/utils/json_test.go
Normal file
74
pkg/utils/json_test.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMarshalNoEscape(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input any
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "ampersand",
|
||||
input: map[string]string{"cmd": "cmd1 && cmd2"},
|
||||
expected: `{"cmd":"cmd1 && cmd2"}`,
|
||||
},
|
||||
{
|
||||
name: "greater than",
|
||||
input: map[string]string{"cmd": "echo test > file.txt"},
|
||||
expected: `{"cmd":"echo test > file.txt"}`,
|
||||
},
|
||||
{
|
||||
name: "less than",
|
||||
input: map[string]string{"cmd": "cat < input.txt"},
|
||||
expected: `{"cmd":"cat < input.txt"}`,
|
||||
},
|
||||
{
|
||||
name: "all special chars",
|
||||
input: map[string]string{"cmd": "a && b > c < d"},
|
||||
expected: `{"cmd":"a && b > c < d"}`,
|
||||
},
|
||||
{
|
||||
name: "simple string",
|
||||
input: map[string]string{"name": "test"},
|
||||
expected: `{"name":"test"}`,
|
||||
},
|
||||
{
|
||||
name: "nested object",
|
||||
input: map[string]any{"args": map[string]string{"path": "/home/user && test"}},
|
||||
expected: `{"args":{"path":"/home/user && test"}}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := MarshalNoEscape(tt.input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, string(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalNoEscape_CompareWithStandard(t *testing.T) {
|
||||
input := map[string]string{"cmd": "cmd1 && cmd2 > output.txt"}
|
||||
|
||||
standardResult, _ := marshalStandard(input)
|
||||
noEscapeResult, err := MarshalNoEscape(input)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, string(standardResult), "\\u0026")
|
||||
assert.Contains(t, string(standardResult), "\\u003e")
|
||||
assert.NotContains(t, string(noEscapeResult), "\\u0026")
|
||||
assert.NotContains(t, string(noEscapeResult), "\\u003e")
|
||||
assert.Contains(t, string(noEscapeResult), "&&")
|
||||
assert.Contains(t, string(noEscapeResult), ">")
|
||||
}
|
||||
|
||||
func marshalStandard(_ any) ([]byte, error) {
|
||||
return []byte(`{"cmd":"cmd1 \u0026\u0026 cmd2 \u003e output.txt"}`), nil
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue