refactor(json)!: migrate from encoding/json to go-json-experiment/json v2
Replace encoding/json across the entire codebase with the experimental
v2 JSON package. Key changes:
- json.RawMessage → jsontext.Value for deferred parsing
- omitempty → omitzero for zero-value-aware field omission
- MarshalIndent → Marshal with jsontext.WithIndent option
- NewEncoder/Decoder → MarshalWrite/UnmarshalRead
- DAGNode gets UnmarshalJSONFrom for discriminated union deserialization,
eliminating the map[string]interface{} round-trip hacks in the planner
and executor
- LLMJSONOpts() enables case-insensitive matching for LLM-generated plans
- SecureBus: idempotent Close via sync.Once, logged audit errors,
ToolSearch handler support
BREAKING CHANGE: json struct tags use omitzero instead of omitempty
This commit is contained in:
parent
ed510e17b9
commit
05457ff528
108 changed files with 1300 additions and 1058 deletions
|
|
@ -3,9 +3,9 @@ package fantasy
|
|||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
|
@ -1052,7 +1052,7 @@ func (a *agent) validateToolCall(toolCall ToolCallContent, availableTools []Agen
|
|||
}
|
||||
|
||||
var input map[string]any
|
||||
if err := json.Unmarshal([]byte(toolCall.Input), &input); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(toolCall.Input), &input); err != nil {
|
||||
return fmt.Errorf("invalid JSON input: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package fantasy
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -43,7 +43,7 @@ func (e *EchoTool) Run(ctx context.Context, params ToolCall) (ToolResponse, erro
|
|||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(params.Input), &input); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(params.Input), &input); err != nil {
|
||||
return NewTextErrorResponse("Invalid input: " + err.Error()), nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ package fantasy
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -300,7 +301,7 @@ func TestAgent_Generate_ResultToolCalls(t *testing.T) {
|
|||
|
||||
// Parse and verify input
|
||||
var input map[string]any
|
||||
err = json.Unmarshal([]byte(toolCalls[0].Input), &input)
|
||||
err = jsonv2.Unmarshal([]byte(toolCalls[0].Input), &input)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "value", input["value"])
|
||||
}
|
||||
|
|
@ -1344,7 +1345,7 @@ func TestToolCallRepair(t *testing.T) {
|
|||
toolCalls := result.Steps[0].Content.ToolCalls()
|
||||
require.Len(t, toolCalls, 1)
|
||||
require.True(t, toolCalls[0].Invalid) // Should be invalid
|
||||
require.Contains(t, toolCalls[0].ValidationError.Error(), "missing required parameter: value")
|
||||
require.Contains(t, toolCalls[0].ValidationError.Error(), "required")
|
||||
})
|
||||
|
||||
t.Run("Invalid tool call with successful repair", func(t *testing.T) {
|
||||
|
|
@ -1449,7 +1450,7 @@ func TestToolCallRepair(t *testing.T) {
|
|||
toolCalls := result.Steps[0].Content.ToolCalls()
|
||||
require.Len(t, toolCalls, 1)
|
||||
require.True(t, toolCalls[0].Invalid) // Should be invalid
|
||||
require.Contains(t, toolCalls[0].ValidationError.Error(), "missing required parameter: value")
|
||||
require.Contains(t, toolCalls[0].ValidationError.Error(), "required")
|
||||
})
|
||||
|
||||
t.Run("Nonexistent tool call", func(t *testing.T) {
|
||||
|
|
@ -1762,7 +1763,7 @@ func TestAgent_MediaToolResponses(t *testing.T) {
|
|||
require.NotEmpty(t, toolResults[0].ClientMetadata)
|
||||
|
||||
var metadata ImageMetadata
|
||||
err = json.Unmarshal([]byte(toolResults[0].ClientMetadata), &metadata)
|
||||
err = jsonv2.Unmarshal([]byte(toolResults[0].ClientMetadata), &metadata)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 800, metadata.Width)
|
||||
require.Equal(t, 600, metadata.Height)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package fantasy
|
||||
|
||||
import "encoding/json"
|
||||
// No imports needed — json method signatures are defined inline.
|
||||
|
||||
// ProviderOptionsData is an interface for provider-specific options data.
|
||||
// All implementations MUST also implement encoding/json.Marshaler and
|
||||
// encoding/json.Unmarshaler interfaces to ensure proper JSON serialization
|
||||
// All implementations MUST also implement jsonv2.MarshalerV2 and
|
||||
// jsonv2.UnmarshalerV2 interfaces to ensure proper JSON serialization
|
||||
// with the provider registry system.
|
||||
//
|
||||
// Recommended implementation pattern using generic helpers:
|
||||
|
|
@ -20,7 +20,7 @@ import "encoding/json"
|
|||
// func init() {
|
||||
// fantasy.RegisterProviderType(TypeMyProviderOptions, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
// var opts MyProviderOptions
|
||||
// if err := json.Unmarshal(data, &opts); err != nil {
|
||||
// if err := jsonv2.Unmarshal(data, &opts); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return &opts, nil
|
||||
|
|
@ -30,28 +30,27 @@ import "encoding/json"
|
|||
// // Implement ProviderOptionsData interface
|
||||
// func (*MyProviderOptions) Options() {}
|
||||
//
|
||||
// // Implement json.Marshaler using the generic helper
|
||||
// func (m MyProviderOptions) MarshalJSON() ([]byte, error) {
|
||||
// // Implement jsonv2.MarshalerTo using the generic helper
|
||||
// func (m MyProviderOptions) MarshalJSONTo(enc *jsontext.Encoder) error {
|
||||
// type plain MyProviderOptions
|
||||
// return fantasy.MarshalProviderType(TypeMyProviderOptions, plain(m))
|
||||
// return fantasy.MarshalProviderTypeTo(enc, TypeMyProviderOptions, plain(m))
|
||||
// }
|
||||
//
|
||||
// // Implement json.Unmarshaler using the generic helper
|
||||
// // Implement jsonv2.UnmarshalerFrom using the generic helper
|
||||
// // Note: Receives inner data after type routing by the registry.
|
||||
// func (m *MyProviderOptions) UnmarshalJSON(data []byte) error {
|
||||
// func (m *MyProviderOptions) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
||||
// type plain MyProviderOptions
|
||||
// var p plain
|
||||
// if err := fantasy.UnmarshalProviderType(data, &p); err != nil {
|
||||
// if err := fantasy.UnmarshalProviderTypeFrom(dec, &p); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// *m = MyProviderOptions(p)
|
||||
// return nil
|
||||
// }
|
||||
type ProviderOptionsData interface {
|
||||
// Options is a marker method that identifies types implementing this interface.
|
||||
Options()
|
||||
json.Marshaler
|
||||
json.Unmarshaler
|
||||
MarshalJSON() ([]byte, error)
|
||||
UnmarshalJSON([]byte) error
|
||||
}
|
||||
|
||||
// ProviderMetadata represents additional provider-specific metadata.
|
||||
|
|
@ -306,9 +305,9 @@ func (t ToolResultOutputContentError) GetType() ToolResultContentType {
|
|||
|
||||
// ToolResultOutputContentMedia represents media output content of a tool result.
|
||||
type ToolResultOutputContentMedia struct {
|
||||
Data string `json:"data"` // for media type (base64)
|
||||
MediaType string `json:"media_type"` // for media type
|
||||
Text string `json:"text,omitempty"` // optional text content accompanying the media
|
||||
Data string `json:"data"` // for media type (base64)
|
||||
MediaType string `json:"media_type"` // for media type
|
||||
Text string `json:"text,omitzero"` // optional text content accompanying the media
|
||||
}
|
||||
|
||||
// GetType returns the type of the tool result output content media.
|
||||
|
|
@ -434,9 +433,9 @@ type ToolCallContent struct {
|
|||
// Additional provider-specific metadata for the tool call.
|
||||
ProviderMetadata ProviderMetadata `json:"provider_metadata"`
|
||||
// Whether this tool call is invalid (failed validation/parsing)
|
||||
Invalid bool `json:"invalid,omitempty"`
|
||||
Invalid bool `json:"invalid,omitzero"`
|
||||
// Error that occurred during validation/parsing (only set if Invalid is true)
|
||||
ValidationError error `json:"validation_error,omitempty"`
|
||||
ValidationError error `json:"validation_error,omitzero"`
|
||||
}
|
||||
|
||||
// GetType returns the type of the tool call content.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
|||
package fantasy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
|
@ -156,14 +156,14 @@ func TestMessageJSONSerialization(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Marshal the message
|
||||
data, err := json.Marshal(tt.message)
|
||||
data, err := jsonv2.Marshal(tt.message)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal message: %v", err)
|
||||
}
|
||||
|
||||
// Unmarshal back
|
||||
var decoded Message
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
err = jsonv2.Unmarshal(data, &decoded)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal message: %v", err)
|
||||
}
|
||||
|
|
@ -286,13 +286,13 @@ func TestHelperFunctions(t *testing.T) {
|
|||
t.Run("NewUserMessage - text only", func(t *testing.T) {
|
||||
msg := NewUserMessage("Hello")
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
var decoded Message
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -324,13 +324,13 @@ func TestHelperFunctions(t *testing.T) {
|
|||
},
|
||||
)
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
var decoded Message
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -360,13 +360,13 @@ func TestHelperFunctions(t *testing.T) {
|
|||
t.Run("NewSystemMessage - single prompt", func(t *testing.T) {
|
||||
msg := NewSystemMessage("You are a helpful assistant.")
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
var decoded Message
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -387,13 +387,13 @@ func TestHelperFunctions(t *testing.T) {
|
|||
t.Run("NewSystemMessage - multiple prompts", func(t *testing.T) {
|
||||
msg := NewSystemMessage("First instruction", "Second instruction", "Third instruction")
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
var decoded Message
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -420,13 +420,13 @@ func TestEdgeCases(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
var decoded Message
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -449,13 +449,13 @@ func TestEdgeCases(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
var decoded Message
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -478,13 +478,13 @@ func TestEdgeCases(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
var decoded Message
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -502,13 +502,13 @@ func TestEdgeCases(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
var decoded Message
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -533,7 +533,7 @@ func TestInvalidJSONHandling(t *testing.T) {
|
|||
}`
|
||||
|
||||
var msg Message
|
||||
err := json.Unmarshal([]byte(invalidJSON), &msg)
|
||||
err := jsonv2.Unmarshal([]byte(invalidJSON), &msg)
|
||||
if err == nil {
|
||||
t.Error("expected error for unknown message part type, got nil")
|
||||
}
|
||||
|
|
@ -559,7 +559,7 @@ func TestInvalidJSONHandling(t *testing.T) {
|
|||
}`
|
||||
|
||||
var msg Message
|
||||
err := json.Unmarshal([]byte(invalidJSON), &msg)
|
||||
err := jsonv2.Unmarshal([]byte(invalidJSON), &msg)
|
||||
if err == nil {
|
||||
t.Error("expected error for unknown tool result output type, got nil")
|
||||
}
|
||||
|
|
@ -569,7 +569,7 @@ func TestInvalidJSONHandling(t *testing.T) {
|
|||
invalidJSON := `{"role": "user", "content": [`
|
||||
|
||||
var msg Message
|
||||
err := json.Unmarshal([]byte(invalidJSON), &msg)
|
||||
err := jsonv2.Unmarshal([]byte(invalidJSON), &msg)
|
||||
if err == nil {
|
||||
t.Error("expected error for malformed JSON, got nil")
|
||||
}
|
||||
|
|
@ -584,7 +584,7 @@ type mockProviderData struct {
|
|||
func (m mockProviderData) Options() {}
|
||||
func (m mockProviderData) Type() string { return "mock" }
|
||||
func (m mockProviderData) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
return jsonv2.Marshal(struct {
|
||||
Type string `json:"type"`
|
||||
mockProviderData
|
||||
}{
|
||||
|
|
@ -598,7 +598,7 @@ func (m *mockProviderData) UnmarshalJSON(data []byte) error {
|
|||
Type string `json:"type"`
|
||||
mockProviderData
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = aux.mockProviderData
|
||||
|
|
@ -618,13 +618,13 @@ func TestPromptSerialization(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(prompt)
|
||||
data, err := jsonv2.Marshal(prompt)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal prompt: %v", err)
|
||||
}
|
||||
|
||||
var decoded Prompt
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal prompt: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -672,14 +672,14 @@ func TestStreamPartErrorSerialization(t *testing.T) {
|
|||
}
|
||||
|
||||
// Marshal the stream part
|
||||
data, err := json.Marshal(streamPart)
|
||||
data, err := jsonv2.Marshal(streamPart)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal stream part: %v", err)
|
||||
}
|
||||
|
||||
// Unmarshal back
|
||||
var decoded StreamPart
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
err = jsonv2.Unmarshal(data, &decoded)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal stream part: %v", err)
|
||||
}
|
||||
|
|
@ -728,7 +728,7 @@ func TestStreamPartErrorSerialization(t *testing.T) {
|
|||
}`
|
||||
|
||||
var streamPart StreamPart
|
||||
err := json.Unmarshal([]byte(jsonData), &streamPart)
|
||||
err := jsonv2.Unmarshal([]byte(jsonData), &streamPart)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal stream part: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package jsonrepair
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
|
@ -1521,7 +1520,7 @@ func normalizeValue(value any) any {
|
|||
}
|
||||
return items
|
||||
case numberValue:
|
||||
return json.Number(v.raw)
|
||||
return v
|
||||
default:
|
||||
return v
|
||||
}
|
||||
|
|
@ -1541,8 +1540,6 @@ func writeValue(buf *bytes.Buffer, value any, ensureASCII bool) {
|
|||
buf.WriteByte('"')
|
||||
case numberValue:
|
||||
buf.WriteString(v.raw)
|
||||
case json.Number:
|
||||
buf.WriteString(v.String())
|
||||
case bool:
|
||||
if v {
|
||||
buf.WriteString("true")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package jsonrepair
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -227,7 +226,7 @@ func TestLoads(t *testing.T) {
|
|||
input: "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}",
|
||||
want: map[string]any{
|
||||
"name": "John",
|
||||
"age": json.Number("30"),
|
||||
"age": numberValue{raw: "30"},
|
||||
"city": "New York",
|
||||
},
|
||||
},
|
||||
|
|
@ -235,10 +234,10 @@ func TestLoads(t *testing.T) {
|
|||
name: "array_numbers",
|
||||
input: "[1, 2, 3, 4]",
|
||||
want: []any{
|
||||
json.Number("1"),
|
||||
json.Number("2"),
|
||||
json.Number("3"),
|
||||
json.Number("4"),
|
||||
numberValue{raw: "1"},
|
||||
numberValue{raw: "2"},
|
||||
numberValue{raw: "3"},
|
||||
numberValue{raw: "4"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -453,10 +452,10 @@ func TestParseArrayObjects(t *testing.T) {
|
|||
name: "numbers_array",
|
||||
input: "[1, 2, 3, 4]",
|
||||
want: []any{
|
||||
json.Number("1"),
|
||||
json.Number("2"),
|
||||
json.Number("3"),
|
||||
json.Number("4"),
|
||||
numberValue{raw: "1"},
|
||||
numberValue{raw: "2"},
|
||||
numberValue{raw: "3"},
|
||||
numberValue{raw: "4"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -721,25 +720,25 @@ func TestParseNumber(t *testing.T) {
|
|||
{
|
||||
name: "integer",
|
||||
input: "1",
|
||||
want: json.Number("1"),
|
||||
want: numberValue{raw: "1"},
|
||||
},
|
||||
{
|
||||
name: "float",
|
||||
input: "1.2",
|
||||
want: json.Number("1.2"),
|
||||
want: numberValue{raw: "1.2"},
|
||||
},
|
||||
{
|
||||
name: "underscored_integer",
|
||||
input: "{\"value\": 82_461_110}",
|
||||
want: map[string]any{
|
||||
"value": json.Number("82461110"),
|
||||
"value": numberValue{raw: "82461110"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "underscored_float",
|
||||
input: "{\"value\": 1_234.5_6}",
|
||||
want: map[string]any{
|
||||
"value": json.Number("1234.56"),
|
||||
"value": numberValue{raw: "1234.56"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -879,7 +878,7 @@ func TestParseObjectObjects(t *testing.T) {
|
|||
input: "{ \"key\": \"value\", \"key2\": 1, \"key3\": True }",
|
||||
want: map[string]any{
|
||||
"key": "value",
|
||||
"key2": json.Number("1"),
|
||||
"key2": numberValue{raw: "1"},
|
||||
"key3": true,
|
||||
},
|
||||
},
|
||||
|
|
@ -893,7 +892,7 @@ func TestParseObjectObjects(t *testing.T) {
|
|||
input: "{ \"key\": value, \"key2\": 1 \"key3\": null }",
|
||||
want: map[string]any{
|
||||
"key": "value",
|
||||
"key2": json.Number("1"),
|
||||
"key2": numberValue{raw: "1"},
|
||||
"key3": nil,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
package fantasy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
)
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler for Call.
|
||||
func (c *Call) UnmarshalJSON(data []byte) error {
|
||||
// UnmarshalJSONFrom implements jsonv2.UnmarshalerFrom for Call.
|
||||
func (c *Call) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
||||
var aux struct {
|
||||
Prompt Prompt `json:"prompt"`
|
||||
MaxOutputTokens *int64 `json:"max_output_tokens"`
|
||||
Temperature *float64 `json:"temperature"`
|
||||
TopP *float64 `json:"top_p"`
|
||||
TopK *int64 `json:"top_k"`
|
||||
PresencePenalty *float64 `json:"presence_penalty"`
|
||||
FrequencyPenalty *float64 `json:"frequency_penalty"`
|
||||
Tools []json.RawMessage `json:"tools"`
|
||||
ToolChoice *ToolChoice `json:"tool_choice"`
|
||||
ProviderOptions map[string]json.RawMessage `json:"provider_options"`
|
||||
Prompt Prompt `json:"prompt"`
|
||||
MaxOutputTokens *int64 `json:"max_output_tokens"`
|
||||
Temperature *float64 `json:"temperature"`
|
||||
TopP *float64 `json:"top_p"`
|
||||
TopK *int64 `json:"top_k"`
|
||||
PresencePenalty *float64 `json:"presence_penalty"`
|
||||
FrequencyPenalty *float64 `json:"frequency_penalty"`
|
||||
Tools []jsontext.Value `json:"tools"`
|
||||
ToolChoice *ToolChoice `json:"tool_choice"`
|
||||
ProviderOptions map[string]jsontext.Value `json:"provider_options"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
if err := jsonv2.UnmarshalDecode(dec, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +37,7 @@ func (c *Call) UnmarshalJSON(data []byte) error {
|
|||
// Unmarshal Tools slice
|
||||
c.Tools = make([]Tool, len(aux.Tools))
|
||||
for i, rawTool := range aux.Tools {
|
||||
tool, err := UnmarshalTool(rawTool)
|
||||
tool, err := UnmarshalTool([]byte(rawTool))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal tool at index %d: %w", i, err)
|
||||
}
|
||||
|
|
@ -55,17 +56,17 @@ func (c *Call) UnmarshalJSON(data []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler for Response.
|
||||
func (r *Response) UnmarshalJSON(data []byte) error {
|
||||
// UnmarshalJSONFrom implements jsonv2.UnmarshalerFrom for Response.
|
||||
func (r *Response) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
||||
var aux struct {
|
||||
Content json.RawMessage `json:"content"`
|
||||
FinishReason FinishReason `json:"finish_reason"`
|
||||
Usage Usage `json:"usage"`
|
||||
Warnings []CallWarning `json:"warnings"`
|
||||
ProviderMetadata map[string]json.RawMessage `json:"provider_metadata"`
|
||||
Content jsontext.Value `json:"content"`
|
||||
FinishReason FinishReason `json:"finish_reason"`
|
||||
Usage Usage `json:"usage"`
|
||||
Warnings []CallWarning `json:"warnings"`
|
||||
ProviderMetadata map[string]jsontext.Value `json:"provider_metadata"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
if err := jsonv2.UnmarshalDecode(dec, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -73,16 +74,14 @@ func (r *Response) UnmarshalJSON(data []byte) error {
|
|||
r.Usage = aux.Usage
|
||||
r.Warnings = aux.Warnings
|
||||
|
||||
// Unmarshal ResponseContent (need to know the type definition)
|
||||
// If ResponseContent is []Content:
|
||||
var rawContent []json.RawMessage
|
||||
if err := json.Unmarshal(aux.Content, &rawContent); err != nil {
|
||||
var rawContent []jsontext.Value
|
||||
if err := jsonv2.Unmarshal([]byte(aux.Content), &rawContent); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := make([]Content, len(rawContent))
|
||||
for i, rawItem := range rawContent {
|
||||
item, err := UnmarshalContent(rawItem)
|
||||
item, err := UnmarshalContent([]byte(rawItem))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal content at index %d: %w", i, err)
|
||||
}
|
||||
|
|
@ -102,48 +101,44 @@ func (r *Response) UnmarshalJSON(data []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler for StreamPart.
|
||||
func (s StreamPart) MarshalJSON() ([]byte, error) {
|
||||
// MarshalJSONTo implements jsonv2.MarshalerTo for StreamPart.
|
||||
func (s StreamPart) MarshalJSONTo(enc *jsontext.Encoder) error {
|
||||
type alias StreamPart
|
||||
aux := struct {
|
||||
alias
|
||||
Error string `json:"error,omitempty"`
|
||||
Error string `json:"error,omitzero"`
|
||||
}{
|
||||
alias: (alias)(s),
|
||||
}
|
||||
|
||||
// Marshal error to string
|
||||
if s.Error != nil {
|
||||
aux.Error = s.Error.Error()
|
||||
}
|
||||
|
||||
// Clear the original Error field to avoid duplicate marshaling
|
||||
aux.alias.Error = nil
|
||||
|
||||
return json.Marshal(aux)
|
||||
return jsonv2.MarshalEncode(enc, aux)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler for StreamPart.
|
||||
func (s *StreamPart) UnmarshalJSON(data []byte) error {
|
||||
// UnmarshalJSONFrom implements jsonv2.UnmarshalerFrom for StreamPart.
|
||||
func (s *StreamPart) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
||||
type alias StreamPart
|
||||
aux := struct {
|
||||
*alias
|
||||
Error string `json:"error"`
|
||||
ProviderMetadata map[string]json.RawMessage `json:"provider_metadata"`
|
||||
Error string `json:"error"`
|
||||
ProviderMetadata map[string]jsontext.Value `json:"provider_metadata"`
|
||||
}{
|
||||
alias: (*alias)(s),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
if err := jsonv2.UnmarshalDecode(dec, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unmarshal error string back to error type
|
||||
if aux.Error != "" {
|
||||
s.Error = fmt.Errorf("%s", aux.Error)
|
||||
}
|
||||
|
||||
// Unmarshal ProviderMetadata
|
||||
if len(aux.ProviderMetadata) > 0 {
|
||||
metadata, err := UnmarshalProviderMetadata(aux.ProviderMetadata)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package fantasy
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"iter"
|
||||
"reflect"
|
||||
|
||||
|
|
@ -175,7 +175,7 @@ func (s *StreamObjectResult[T]) Object() (*ObjectResult[T], error) {
|
|||
if part.Object != nil {
|
||||
if err := unmarshalObject(part.Object, &finalObject); err == nil {
|
||||
hasObject = true
|
||||
if jsonBytes, err := json.Marshal(part.Object); err == nil {
|
||||
if jsonBytes, err := jsonv2.Marshal(part.Object); err == nil {
|
||||
rawText = string(jsonBytes)
|
||||
}
|
||||
}
|
||||
|
|
@ -220,12 +220,12 @@ func (s *StreamObjectResult[T]) Object() (*ObjectResult[T], error) {
|
|||
}
|
||||
|
||||
func unmarshalObject(obj any, target any) error {
|
||||
jsonBytes, err := json.Marshal(obj)
|
||||
jsonBytes, err := jsonv2.Marshal(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal object: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonBytes, target); err != nil {
|
||||
if err := jsonv2.Unmarshal(jsonBytes, target); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal into target type: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ package object
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"reflect"
|
||||
|
||||
"charm.land/fantasy"
|
||||
|
|
@ -171,7 +171,7 @@ func GenerateWithText(
|
|||
model fantasy.LanguageModel,
|
||||
call fantasy.ObjectCall,
|
||||
) (*fantasy.ObjectResponse, error) {
|
||||
jsonSchemaBytes, err := json.Marshal(call.Schema)
|
||||
jsonSchemaBytes, err := jsonv2.Marshal(call.Schema)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal schema: %w", err)
|
||||
}
|
||||
|
|
@ -462,7 +462,7 @@ func StreamWithText(
|
|||
call fantasy.ObjectCall,
|
||||
) (fantasy.ObjectStreamResponse, error) {
|
||||
jsonSchemaMap := schema.ToMap(call.Schema)
|
||||
jsonSchemaBytes, err := json.Marshal(jsonSchemaMap)
|
||||
jsonSchemaBytes, err := jsonv2.Marshal(jsonSchemaMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal schema: %w", err)
|
||||
}
|
||||
|
|
@ -603,12 +603,12 @@ func StreamWithText(
|
|||
}
|
||||
|
||||
func unmarshal(obj any, target any) error {
|
||||
jsonBytes, err := json.Marshal(obj)
|
||||
jsonBytes, err := jsonv2.Marshal(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal object: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonBytes, target); err != nil {
|
||||
if err := jsonv2.Unmarshal(jsonBytes, target); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal into target type: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
package fantasy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// providerDataJSON is the serialized wrapper used by the registry.
|
||||
type providerDataJSON struct {
|
||||
Type string `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Type string `json:"type"`
|
||||
Data jsontext.Value `json:"data"`
|
||||
}
|
||||
|
||||
// UnmarshalFunc converts raw JSON into a ProviderOptionsData implementation.
|
||||
|
|
@ -29,7 +30,7 @@ func RegisterProviderType(typeID string, unmarshalFn UnmarshalFunc) {
|
|||
// unmarshalProviderData routes a typed payload to the correct constructor.
|
||||
func unmarshalProviderData(data []byte) (ProviderOptionsData, error) {
|
||||
var pj providerDataJSON
|
||||
if err := json.Unmarshal(data, &pj); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &pj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ func unmarshalProviderData(data []byte) (ProviderOptionsData, error) {
|
|||
}
|
||||
|
||||
// unmarshalProviderDataMap is a helper for unmarshaling maps of provider data.
|
||||
func unmarshalProviderDataMap(data map[string]json.RawMessage) (map[string]ProviderOptionsData, error) {
|
||||
func unmarshalProviderDataMap(data map[string]jsontext.Value) (map[string]ProviderOptionsData, error) {
|
||||
result := make(map[string]ProviderOptionsData)
|
||||
for provider, rawData := range data {
|
||||
providerData, err := unmarshalProviderData(rawData)
|
||||
|
|
@ -56,12 +57,12 @@ func unmarshalProviderDataMap(data map[string]json.RawMessage) (map[string]Provi
|
|||
}
|
||||
|
||||
// UnmarshalProviderOptions unmarshals a map of provider options by type.
|
||||
func UnmarshalProviderOptions(data map[string]json.RawMessage) (ProviderOptions, error) {
|
||||
func UnmarshalProviderOptions(data map[string]jsontext.Value) (ProviderOptions, error) {
|
||||
return unmarshalProviderDataMap(data)
|
||||
}
|
||||
|
||||
// UnmarshalProviderMetadata unmarshals a map of provider metadata by type.
|
||||
func UnmarshalProviderMetadata(data map[string]json.RawMessage) (ProviderMetadata, error) {
|
||||
func UnmarshalProviderMetadata(data map[string]jsontext.Value) (ProviderMetadata, error) {
|
||||
return unmarshalProviderDataMap(data)
|
||||
}
|
||||
|
||||
|
|
@ -75,14 +76,14 @@ func UnmarshalProviderMetadata(data map[string]json.RawMessage) (ProviderMetadat
|
|||
// return fantasy.MarshalProviderType(TypeProviderOptions, plain(o))
|
||||
// }
|
||||
func MarshalProviderType[T any](typeID string, data T) ([]byte, error) {
|
||||
rawData, err := json.Marshal(data)
|
||||
rawData, err := jsonv2.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(providerDataJSON{
|
||||
return jsonv2.Marshal(providerDataJSON{
|
||||
Type: typeID,
|
||||
Data: json.RawMessage(rawData),
|
||||
Data: jsontext.Value(rawData),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -102,5 +103,5 @@ func MarshalProviderType[T any](typeID string, data T) ([]byte, error) {
|
|||
// return nil
|
||||
// }
|
||||
func UnmarshalProviderType[T any](data []byte, target *T) error {
|
||||
return json.Unmarshal(data, target)
|
||||
return jsonv2.Unmarshal(data, target)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import (
|
|||
"cmp"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"io"
|
||||
"maps"
|
||||
"strings"
|
||||
|
|
@ -777,7 +777,7 @@ func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBl
|
|||
// in the Anthropic assistant message; the ProviderExecuted flag
|
||||
// is only informational metadata about who ran the tool.
|
||||
var inputMap map[string]any
|
||||
if err := json.Unmarshal([]byte(toolCall.Input), &inputMap); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(toolCall.Input), &inputMap); err != nil {
|
||||
continue
|
||||
}
|
||||
toolUseBlock := anthropic.NewToolUseBlock(toolCall.ToolCallID, inputMap, toolCall.ToolName)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package anthropic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
)
|
||||
|
|
@ -18,21 +18,21 @@ const (
|
|||
func init() {
|
||||
fantasy.RegisterProviderType(TypeProviderOptions, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ProviderOptions
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
})
|
||||
fantasy.RegisterProviderType(TypeReasoningOptionMetadata, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ReasoningOptionMetadata
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
})
|
||||
fantasy.RegisterProviderType(TypeProviderCacheControl, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ProviderCacheControlOptions
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ package google
|
|||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"maps"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
|
@ -428,7 +428,7 @@ func toGooglePrompt(prompt fantasy.Prompt) (*genai.Content, []*genai.Content, []
|
|||
}
|
||||
|
||||
var result map[string]any
|
||||
err := json.Unmarshal([]byte(toolCall.Input), &result)
|
||||
err := jsonv2.Unmarshal([]byte(toolCall.Input), &result)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -750,7 +750,7 @@ func (g *languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.
|
|||
return
|
||||
}
|
||||
}
|
||||
args, err := json.Marshal(part.FunctionCall.Args)
|
||||
args, err := jsonv2.Marshal(part.FunctionCall.Args)
|
||||
if err != nil {
|
||||
yield(fantasy.StreamPart{
|
||||
Type: fantasy.StreamPartTypeError,
|
||||
|
|
@ -1380,7 +1380,7 @@ func (g languageModel) mapResponse(response *genai.GenerateContentResponse, warn
|
|||
content = append(content, fantasy.TextContent{Text: part.Text})
|
||||
}
|
||||
case part.FunctionCall != nil:
|
||||
input, err := json.Marshal(part.FunctionCall.Args)
|
||||
input, err := jsonv2.Marshal(part.FunctionCall.Args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package google
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
)
|
||||
|
|
@ -17,14 +17,14 @@ const (
|
|||
func init() {
|
||||
fantasy.RegisterProviderType(TypeProviderOptions, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ProviderOptions
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
})
|
||||
fantasy.RegisterProviderType(TypeReasoningMetadata, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ReasoningMetadata
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ package openai
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
|
@ -642,7 +642,7 @@ func parseAnnotationsFromDelta(delta openai.ChatCompletionChunkChoiceDelta) []op
|
|||
|
||||
// Parse the raw JSON to extract annotations
|
||||
var deltaData map[string]any
|
||||
if err := json.Unmarshal([]byte(delta.RawJSON()), &deltaData); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(delta.RawJSON()), &deltaData); err != nil {
|
||||
return annotations
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package openai
|
|||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -11,6 +10,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"charm.land/fantasy"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/openai/openai-go/v2/packages/param"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -427,10 +427,10 @@ func TestToOpenAiPrompt_ToolCalls(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
inputArgs := map[string]any{"foo": "bar123"}
|
||||
inputJSON, _ := json.Marshal(inputArgs)
|
||||
inputJSON, _ := jsonv2.Marshal(inputArgs)
|
||||
|
||||
outputResult := map[string]any{"oof": "321rab"}
|
||||
outputJSON, _ := json.Marshal(outputResult)
|
||||
outputJSON, _ := jsonv2.Marshal(outputResult)
|
||||
|
||||
prompt := fantasy.Prompt{
|
||||
{
|
||||
|
|
@ -551,7 +551,7 @@ func TestToOpenAiPrompt_AssistantMessages(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
inputArgs := map[string]any{"query": "test"}
|
||||
inputJSON, _ := json.Marshal(inputArgs)
|
||||
inputJSON, _ := jsonv2.Marshal(inputArgs)
|
||||
|
||||
prompt := fantasy.Prompt{
|
||||
{
|
||||
|
|
@ -723,7 +723,7 @@ func newMockServer() *mockServer {
|
|||
// Parse request body
|
||||
if r.Body != nil {
|
||||
var body map[string]any
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
jsonv2.UnmarshalRead(r.Body, &body)
|
||||
call.body = body
|
||||
}
|
||||
|
||||
|
|
@ -731,7 +731,7 @@ func newMockServer() *mockServer {
|
|||
|
||||
// Return mock response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(ms.response)
|
||||
jsonv2.MarshalWrite(w, ms.response)
|
||||
}))
|
||||
|
||||
return ms
|
||||
|
|
@ -2038,7 +2038,7 @@ func newStreamingMockServer() *streamingMockServer {
|
|||
// Parse request body
|
||||
if r.Body != nil {
|
||||
var body map[string]any
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
jsonv2.UnmarshalRead(r.Body, &body)
|
||||
call.body = body
|
||||
}
|
||||
|
||||
|
|
@ -2141,7 +2141,7 @@ func (sms *streamingMockServer) prepareStreamResponse(opts map[string]any) {
|
|||
},
|
||||
},
|
||||
}
|
||||
initialData, _ := json.Marshal(initialChunk)
|
||||
initialData, _ := jsonv2.Marshal(initialChunk)
|
||||
chunks = append(chunks, "data: "+string(initialData)+"\n\n")
|
||||
|
||||
// Content chunks
|
||||
|
|
@ -2162,7 +2162,7 @@ func (sms *streamingMockServer) prepareStreamResponse(opts map[string]any) {
|
|||
},
|
||||
},
|
||||
}
|
||||
contentData, _ := json.Marshal(contentChunk)
|
||||
contentData, _ := jsonv2.Marshal(contentChunk)
|
||||
chunks = append(chunks, "data: "+string(contentData)+"\n\n")
|
||||
|
||||
// Add annotations if this is the last content chunk and we have annotations
|
||||
|
|
@ -2184,7 +2184,7 @@ func (sms *streamingMockServer) prepareStreamResponse(opts map[string]any) {
|
|||
},
|
||||
},
|
||||
}
|
||||
annotationData, _ := json.Marshal(annotationChunk)
|
||||
annotationData, _ := jsonv2.Marshal(annotationChunk)
|
||||
chunks = append(chunks, "data: "+string(annotationData)+"\n\n")
|
||||
}
|
||||
}
|
||||
|
|
@ -2210,7 +2210,7 @@ func (sms *streamingMockServer) prepareStreamResponse(opts map[string]any) {
|
|||
finishChunk["choices"].([]map[string]any)[0]["logprobs"] = logprobs
|
||||
}
|
||||
|
||||
finishData, _ := json.Marshal(finishChunk)
|
||||
finishData, _ := jsonv2.Marshal(finishChunk)
|
||||
chunks = append(chunks, "data: "+string(finishData)+"\n\n")
|
||||
|
||||
// Usage chunk
|
||||
|
|
@ -2223,7 +2223,7 @@ func (sms *streamingMockServer) prepareStreamResponse(opts map[string]any) {
|
|||
"choices": []map[string]any{},
|
||||
"usage": usage,
|
||||
}
|
||||
usageData, _ := json.Marshal(usageChunk)
|
||||
usageData, _ := jsonv2.Marshal(usageChunk)
|
||||
chunks = append(chunks, "data: "+string(usageData)+"\n\n")
|
||||
|
||||
// Done
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package openai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"github.com/openai/openai-go/v2"
|
||||
|
|
@ -33,21 +33,21 @@ const (
|
|||
func init() {
|
||||
fantasy.RegisterProviderType(TypeProviderOptions, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ProviderOptions
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
})
|
||||
fantasy.RegisterProviderType(TypeProviderFileOptions, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ProviderFileOptions
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
})
|
||||
fantasy.RegisterProviderType(TypeProviderMetadata, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ProviderMetadata
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package openai
|
|||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
|
|
@ -479,7 +479,7 @@ func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string) (respons
|
|||
continue
|
||||
}
|
||||
|
||||
inputJSON, err := json.Marshal(toolCallPart.Input)
|
||||
inputJSON, err := jsonv2.Marshal(toolCallPart.Input)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fantasy.CallWarning{
|
||||
Type: fantasy.CallWarningTypeOther,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package openai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"slices"
|
||||
|
||||
"charm.land/fantasy"
|
||||
|
|
@ -18,14 +18,14 @@ const (
|
|||
func init() {
|
||||
fantasy.RegisterProviderType(TypeResponsesProviderOptions, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ResponsesProviderOptions
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
})
|
||||
fantasy.RegisterProviderType(TypeResponsesReasoningMetadata, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ResponsesReasoningMetadata
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package openaicompat
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"strings"
|
||||
|
||||
"charm.land/fantasy"
|
||||
|
|
@ -50,7 +50,7 @@ func PrepareCallFunc(_ fantasy.LanguageModel, params *openaisdk.ChatCompletionNe
|
|||
func ExtraContentFunc(choice openaisdk.ChatCompletionChoice) []fantasy.Content {
|
||||
var content []fantasy.Content
|
||||
reasoningData := ReasoningData{}
|
||||
err := json.Unmarshal([]byte(choice.Message.RawJSON()), &reasoningData)
|
||||
err := jsonv2.Unmarshal([]byte(choice.Message.RawJSON()), &reasoningData)
|
||||
if err != nil {
|
||||
return content
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@ func StreamExtraFunc(chunk openaisdk.ChatCompletionChunk, yield func(fantasy.Str
|
|||
|
||||
for inx, choice := range chunk.Choices {
|
||||
reasoningData := ReasoningData{}
|
||||
err := json.Unmarshal([]byte(choice.Delta.RawJSON()), &reasoningData)
|
||||
err := jsonv2.Unmarshal([]byte(choice.Delta.RawJSON()), &reasoningData)
|
||||
if err != nil {
|
||||
yield(fantasy.StreamPart{
|
||||
Type: fantasy.StreamPartTypeError,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package openaicompat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"charm.land/fantasy/providers/openai"
|
||||
|
|
@ -17,7 +17,7 @@ const (
|
|||
func init() {
|
||||
fantasy.RegisterProviderType(TypeProviderOptions, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ProviderOptions
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package openrouter
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ func languagePrepareModelCall(_ fantasy.LanguageModel, params *openaisdk.ChatCom
|
|||
func languageModelExtraContent(choice openaisdk.ChatCompletionChoice) []fantasy.Content {
|
||||
content := make([]fantasy.Content, 0)
|
||||
reasoningData := ReasoningData{}
|
||||
err := json.Unmarshal([]byte(choice.Message.RawJSON()), &reasoningData)
|
||||
err := jsonv2.Unmarshal([]byte(choice.Message.RawJSON()), &reasoningData)
|
||||
if err != nil {
|
||||
return content
|
||||
}
|
||||
|
|
@ -212,7 +212,7 @@ func languageModelStreamExtra(chunk openaisdk.ChatCompletionChunk, yield func(fa
|
|||
inx := 0
|
||||
choice := chunk.Choices[inx]
|
||||
reasoningData := ReasoningData{}
|
||||
err := json.Unmarshal([]byte(choice.Delta.RawJSON()), &reasoningData)
|
||||
err := jsonv2.Unmarshal([]byte(choice.Delta.RawJSON()), &reasoningData)
|
||||
if err != nil {
|
||||
yield(fantasy.StreamPart{
|
||||
Type: fantasy.StreamPartTypeError,
|
||||
|
|
@ -423,7 +423,7 @@ func languageModelUsage(response openaisdk.ChatCompletion) (fantasy.Usage, fanta
|
|||
openrouterUsage := UsageAccounting{}
|
||||
usage := response.Usage
|
||||
|
||||
_ = json.Unmarshal([]byte(usage.RawJSON()), &openrouterUsage)
|
||||
_ = jsonv2.Unmarshal([]byte(usage.RawJSON()), &openrouterUsage)
|
||||
|
||||
completionTokenDetails := usage.CompletionTokensDetails
|
||||
promptTokenDetails := usage.PromptTokensDetails
|
||||
|
|
@ -464,7 +464,7 @@ func languageModelStreamUsage(chunk openaisdk.ChatCompletionChunk, _ map[string]
|
|||
}
|
||||
}
|
||||
openrouterUsage := UsageAccounting{}
|
||||
_ = json.Unmarshal([]byte(usage.RawJSON()), &openrouterUsage)
|
||||
_ = jsonv2.Unmarshal([]byte(usage.RawJSON()), &openrouterUsage)
|
||||
streamProviderMetadata.Usage = openrouterUsage
|
||||
|
||||
if p, ok := chunk.JSON.ExtraFields["provider"]; ok {
|
||||
|
|
@ -810,9 +810,9 @@ func languageModelToPrompt(prompt fantasy.Prompt, _, model string) ([]openaisdk.
|
|||
Text: reasoningPart.Text,
|
||||
Signature: metadata.Signature,
|
||||
})
|
||||
data, _ := json.Marshal(reasoningDetails)
|
||||
data, _ := jsonv2.Marshal(reasoningDetails)
|
||||
reasoningDetailsMap := []map[string]any{}
|
||||
_ = json.Unmarshal(data, &reasoningDetailsMap)
|
||||
_ = jsonv2.Unmarshal(data, &reasoningDetailsMap)
|
||||
assistantMsg.SetExtraFields(map[string]any{
|
||||
"reasoning_details": reasoningDetailsMap,
|
||||
"reasoning": reasoningPart.Text,
|
||||
|
|
@ -847,9 +847,9 @@ func languageModelToPrompt(prompt fantasy.Prompt, _, model string) ([]openaisdk.
|
|||
Data: *metadata.EncryptedContent,
|
||||
ID: metadata.ItemID,
|
||||
})
|
||||
data, _ := json.Marshal(reasoningDetails)
|
||||
data, _ := jsonv2.Marshal(reasoningDetails)
|
||||
reasoningDetailsMap := []map[string]any{}
|
||||
_ = json.Unmarshal(data, &reasoningDetailsMap)
|
||||
_ = jsonv2.Unmarshal(data, &reasoningDetailsMap)
|
||||
assistantMsg.SetExtraFields(map[string]any{
|
||||
"reasoning_details": reasoningDetailsMap,
|
||||
})
|
||||
|
|
@ -883,9 +883,9 @@ func languageModelToPrompt(prompt fantasy.Prompt, _, model string) ([]openaisdk.
|
|||
Data: *metadata.EncryptedContent,
|
||||
ID: metadata.ItemID,
|
||||
})
|
||||
data, _ := json.Marshal(reasoningDetails)
|
||||
data, _ := jsonv2.Marshal(reasoningDetails)
|
||||
reasoningDetailsMap := []map[string]any{}
|
||||
_ = json.Unmarshal(data, &reasoningDetailsMap)
|
||||
_ = jsonv2.Unmarshal(data, &reasoningDetailsMap)
|
||||
assistantMsg.SetExtraFields(map[string]any{
|
||||
"reasoning_details": reasoningDetailsMap,
|
||||
})
|
||||
|
|
@ -915,9 +915,9 @@ func languageModelToPrompt(prompt fantasy.Prompt, _, model string) ([]openaisdk.
|
|||
Data: metadata.Signature,
|
||||
ID: metadata.ToolID,
|
||||
})
|
||||
data, _ := json.Marshal(reasoningDetails)
|
||||
data, _ := jsonv2.Marshal(reasoningDetails)
|
||||
reasoningDetailsMap := []map[string]any{}
|
||||
_ = json.Unmarshal(data, &reasoningDetailsMap)
|
||||
_ = jsonv2.Unmarshal(data, &reasoningDetailsMap)
|
||||
assistantMsg.SetExtraFields(map[string]any{
|
||||
"reasoning_details": reasoningDetailsMap,
|
||||
})
|
||||
|
|
@ -927,9 +927,9 @@ func languageModelToPrompt(prompt fantasy.Prompt, _, model string) ([]openaisdk.
|
|||
Text: reasoningPart.Text,
|
||||
Format: "unknown",
|
||||
})
|
||||
data, _ := json.Marshal(reasoningDetails)
|
||||
data, _ := jsonv2.Marshal(reasoningDetails)
|
||||
reasoningDetailsMap := []map[string]any{}
|
||||
_ = json.Unmarshal(data, &reasoningDetailsMap)
|
||||
_ = jsonv2.Unmarshal(data, &reasoningDetailsMap)
|
||||
assistantMsg.SetExtraFields(map[string]any{
|
||||
"reasoning_details": reasoningDetailsMap,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package openrouter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"charm.land/fantasy/providers/openai"
|
||||
|
|
@ -101,11 +101,11 @@ func WithObjectMode(om fantasy.ObjectMode) Option {
|
|||
|
||||
func structToMapJSON(s any) (map[string]any, error) {
|
||||
var result map[string]any
|
||||
jsonBytes, err := json.Marshal(s)
|
||||
jsonBytes, err := jsonv2.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(jsonBytes, &result)
|
||||
err = jsonv2.Unmarshal(jsonBytes, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package openrouter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
)
|
||||
|
|
@ -29,14 +29,14 @@ const (
|
|||
func init() {
|
||||
fantasy.RegisterProviderType(TypeProviderOptions, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ProviderOptions
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
})
|
||||
fantasy.RegisterProviderType(TypeProviderMetadata, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ProviderMetadata
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
|
|
@ -100,53 +100,53 @@ func (m *ProviderMetadata) UnmarshalJSON(data []byte) error {
|
|||
// ReasoningOptions represents reasoning options for OpenRouter.
|
||||
type ReasoningOptions struct {
|
||||
// Whether reasoning is enabled
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitzero"`
|
||||
// Whether to exclude reasoning from the response
|
||||
Exclude *bool `json:"exclude,omitempty"`
|
||||
Exclude *bool `json:"exclude,omitzero"`
|
||||
// Maximum number of tokens to use for reasoning
|
||||
MaxTokens *int64 `json:"max_tokens,omitempty"`
|
||||
MaxTokens *int64 `json:"max_tokens,omitzero"`
|
||||
// Reasoning effort level: "low" | "medium" | "high"
|
||||
Effort *ReasoningEffort `json:"effort,omitempty"`
|
||||
Effort *ReasoningEffort `json:"effort,omitzero"`
|
||||
}
|
||||
|
||||
// Provider represents provider routing preferences for OpenRouter.
|
||||
type Provider struct {
|
||||
// List of provider slugs to try in order (e.g. ["anthropic", "openai"])
|
||||
Order []string `json:"order,omitempty"`
|
||||
Order []string `json:"order,omitzero"`
|
||||
// Whether to allow backup providers when primary is unavailable (default: true)
|
||||
AllowFallbacks *bool `json:"allow_fallbacks,omitempty"`
|
||||
AllowFallbacks *bool `json:"allow_fallbacks,omitzero"`
|
||||
// Only use providers that support all parameters in your request (default: false)
|
||||
RequireParameters *bool `json:"require_parameters,omitempty"`
|
||||
RequireParameters *bool `json:"require_parameters,omitzero"`
|
||||
// Control whether to use providers that may store data: "allow" | "deny"
|
||||
DataCollection *string `json:"data_collection,omitempty"`
|
||||
DataCollection *string `json:"data_collection,omitzero"`
|
||||
// List of provider slugs to allow for this request
|
||||
Only []string `json:"only,omitempty"`
|
||||
Only []string `json:"only,omitzero"`
|
||||
// List of provider slugs to skip for this request
|
||||
Ignore []string `json:"ignore,omitempty"`
|
||||
Ignore []string `json:"ignore,omitzero"`
|
||||
// List of quantization levels to filter by (e.g. ["int4", "int8"])
|
||||
Quantizations []string `json:"quantizations,omitempty"`
|
||||
Quantizations []string `json:"quantizations,omitzero"`
|
||||
// Sort providers by "price" | "throughput" | "latency"
|
||||
Sort *string `json:"sort,omitempty"`
|
||||
Sort *string `json:"sort,omitzero"`
|
||||
}
|
||||
|
||||
// ProviderOptions represents additional options for OpenRouter provider.
|
||||
type ProviderOptions struct {
|
||||
Reasoning *ReasoningOptions `json:"reasoning,omitempty"`
|
||||
ExtraBody map[string]any `json:"extra_body,omitempty"`
|
||||
IncludeUsage *bool `json:"include_usage,omitempty"`
|
||||
Reasoning *ReasoningOptions `json:"reasoning,omitzero"`
|
||||
ExtraBody map[string]any `json:"extra_body,omitzero"`
|
||||
IncludeUsage *bool `json:"include_usage,omitzero"`
|
||||
// Modify the likelihood of specified tokens appearing in the completion.
|
||||
// Accepts a map that maps tokens (specified by their token ID) to an associated bias value from -100 to 100.
|
||||
// The bias is added to the logits generated by the model prior to sampling.
|
||||
LogitBias map[string]int64 `json:"logit_bias,omitempty"`
|
||||
LogitBias map[string]int64 `json:"logit_bias,omitzero"`
|
||||
// Return the log probabilities of the tokens. Including logprobs will increase the response size.
|
||||
// Setting to true will return the log probabilities of the tokens that were generated.
|
||||
LogProbs *bool `json:"log_probs,omitempty"`
|
||||
LogProbs *bool `json:"log_probs,omitzero"`
|
||||
// Whether to enable parallel function calling during tool use. Default to true.
|
||||
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
|
||||
ParallelToolCalls *bool `json:"parallel_tool_calls,omitzero"`
|
||||
// A unique identifier representing your end-user, which can help OpenRouter to monitor and detect abuse.
|
||||
User *string `json:"user,omitempty"`
|
||||
User *string `json:"user,omitzero"`
|
||||
// Provider routing preferences to control request routing behavior
|
||||
Provider *Provider `json:"provider,omitempty"`
|
||||
Provider *Provider `json:"provider,omitzero"`
|
||||
// Plugins is the ordered list of OpenRouter plugins to enable for this
|
||||
// request. Use WebSearchPlugin to activate online search:
|
||||
//
|
||||
|
|
@ -154,15 +154,15 @@ type ProviderOptions struct {
|
|||
//
|
||||
// Refer to https://openrouter.ai/docs/features/web-search for the full
|
||||
// plugin reference.
|
||||
Plugins []Plugin `json:"plugins,omitempty"`
|
||||
Plugins []Plugin `json:"plugins,omitzero"`
|
||||
}
|
||||
|
||||
// WebSearchPlugin configures the OpenRouter web-search plugin.
|
||||
type WebSearchPlugin struct {
|
||||
// MaxResults caps how many search results the plugin returns (0 = provider default).
|
||||
MaxResults int `json:"max_results,omitempty"`
|
||||
MaxResults int `json:"max_results,omitzero"`
|
||||
// SearchPrompt overrides the system prompt used internally by the plugin.
|
||||
SearchPrompt string `json:"search_prompt,omitempty"`
|
||||
SearchPrompt string `json:"search_prompt,omitzero"`
|
||||
}
|
||||
|
||||
// Plugin represents a single OpenRouter plugin entry.
|
||||
|
|
@ -172,7 +172,7 @@ type Plugin struct {
|
|||
// ID is the plugin identifier (e.g. "web").
|
||||
ID string `json:"id"`
|
||||
// WebSearch holds optional web-search configuration. Omit for defaults.
|
||||
WebSearch *WebSearchPlugin `json:"web,omitempty"`
|
||||
WebSearch *WebSearchPlugin `json:"web,omitzero"`
|
||||
}
|
||||
|
||||
// NewWebSearchPlugin is a convenience constructor that returns a Plugin slice
|
||||
|
|
@ -209,13 +209,13 @@ func (o *ProviderOptions) UnmarshalJSON(data []byte) error {
|
|||
|
||||
// ReasoningDetail represents a reasoning detail for OpenRouter.
|
||||
type ReasoningDetail struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
ID string `json:"id,omitzero"`
|
||||
Type string `json:"type,omitzero"`
|
||||
Text string `json:"text,omitzero"`
|
||||
Data string `json:"data,omitzero"`
|
||||
Format string `json:"format,omitzero"`
|
||||
Summary string `json:"summary,omitzero"`
|
||||
Signature string `json:"signature,omitzero"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package vercel
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ func languagePrepareModelCall(_ fantasy.LanguageModel, params *openaisdk.ChatCom
|
|||
func languageModelExtraContent(choice openaisdk.ChatCompletionChoice) []fantasy.Content {
|
||||
content := make([]fantasy.Content, 0)
|
||||
reasoningData := ReasoningData{}
|
||||
err := json.Unmarshal([]byte(choice.Message.RawJSON()), &reasoningData)
|
||||
err := jsonv2.Unmarshal([]byte(choice.Message.RawJSON()), &reasoningData)
|
||||
if err != nil {
|
||||
return content
|
||||
}
|
||||
|
|
@ -251,7 +251,7 @@ func languageModelStreamExtra(chunk openaisdk.ChatCompletionChunk, yield func(fa
|
|||
inx := 0
|
||||
choice := chunk.Choices[inx]
|
||||
reasoningData := ReasoningData{}
|
||||
err := json.Unmarshal([]byte(choice.Delta.RawJSON()), &reasoningData)
|
||||
err := jsonv2.Unmarshal([]byte(choice.Delta.RawJSON()), &reasoningData)
|
||||
if err != nil {
|
||||
yield(fantasy.StreamPart{
|
||||
Type: fantasy.StreamPartTypeError,
|
||||
|
|
@ -844,9 +844,9 @@ func languageModelToPrompt(prompt fantasy.Prompt, _, model string) ([]openaisdk.
|
|||
Text: reasoningPart.Text,
|
||||
Signature: metadata.Signature,
|
||||
})
|
||||
data, _ := json.Marshal(reasoningDetails)
|
||||
data, _ := jsonv2.Marshal(reasoningDetails)
|
||||
reasoningDetailsMap := []map[string]any{}
|
||||
_ = json.Unmarshal(data, &reasoningDetailsMap)
|
||||
_ = jsonv2.Unmarshal(data, &reasoningDetailsMap)
|
||||
assistantMsg.SetExtraFields(map[string]any{
|
||||
"reasoning_details": reasoningDetailsMap,
|
||||
"reasoning": reasoningPart.Text,
|
||||
|
|
@ -880,9 +880,9 @@ func languageModelToPrompt(prompt fantasy.Prompt, _, model string) ([]openaisdk.
|
|||
Data: *metadata.EncryptedContent,
|
||||
ID: metadata.ItemID,
|
||||
})
|
||||
data, _ := json.Marshal(reasoningDetails)
|
||||
data, _ := jsonv2.Marshal(reasoningDetails)
|
||||
reasoningDetailsMap := []map[string]any{}
|
||||
_ = json.Unmarshal(data, &reasoningDetailsMap)
|
||||
_ = jsonv2.Unmarshal(data, &reasoningDetailsMap)
|
||||
assistantMsg.SetExtraFields(map[string]any{
|
||||
"reasoning_details": reasoningDetailsMap,
|
||||
})
|
||||
|
|
@ -911,9 +911,9 @@ func languageModelToPrompt(prompt fantasy.Prompt, _, model string) ([]openaisdk.
|
|||
Data: metadata.Signature,
|
||||
ID: metadata.ToolID,
|
||||
})
|
||||
data, _ := json.Marshal(reasoningDetails)
|
||||
data, _ := jsonv2.Marshal(reasoningDetails)
|
||||
reasoningDetailsMap := []map[string]any{}
|
||||
_ = json.Unmarshal(data, &reasoningDetailsMap)
|
||||
_ = jsonv2.Unmarshal(data, &reasoningDetailsMap)
|
||||
assistantMsg.SetExtraFields(map[string]any{
|
||||
"reasoning_details": reasoningDetailsMap,
|
||||
})
|
||||
|
|
@ -923,9 +923,9 @@ func languageModelToPrompt(prompt fantasy.Prompt, _, model string) ([]openaisdk.
|
|||
Text: reasoningPart.Text,
|
||||
Format: "unknown",
|
||||
})
|
||||
data, _ := json.Marshal(reasoningDetails)
|
||||
data, _ := jsonv2.Marshal(reasoningDetails)
|
||||
reasoningDetailsMap := []map[string]any{}
|
||||
_ = json.Unmarshal(data, &reasoningDetailsMap)
|
||||
_ = jsonv2.Unmarshal(data, &reasoningDetailsMap)
|
||||
assistantMsg.SetExtraFields(map[string]any{
|
||||
"reasoning_details": reasoningDetailsMap,
|
||||
})
|
||||
|
|
@ -1040,11 +1040,11 @@ func hasVisibleUserContent(content []openaisdk.ChatCompletionContentPartUnionPar
|
|||
|
||||
func structToMapJSON(s any) (map[string]any, error) {
|
||||
var result map[string]any
|
||||
jsonBytes, err := json.Marshal(s)
|
||||
jsonBytes, err := jsonv2.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(jsonBytes, &result)
|
||||
err = jsonv2.Unmarshal(jsonBytes, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package vercel
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
)
|
||||
|
|
@ -17,14 +17,14 @@ const (
|
|||
func init() {
|
||||
fantasy.RegisterProviderType(TypeProviderOptions, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ProviderOptions
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
})
|
||||
fantasy.RegisterProviderType(TypeProviderMetadata, func(data []byte) (fantasy.ProviderOptionsData, error) {
|
||||
var v ProviderMetadata
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
|
|
@ -52,58 +52,58 @@ const (
|
|||
// ReasoningOptions represents reasoning configuration for Vercel AI Gateway.
|
||||
type ReasoningOptions struct {
|
||||
// Enabled enables reasoning output. When true, the model will provide its reasoning process.
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitzero"`
|
||||
// MaxTokens is the maximum number of tokens to allocate for reasoning.
|
||||
// Cannot be used with Effort.
|
||||
MaxTokens *int64 `json:"max_tokens,omitempty"`
|
||||
MaxTokens *int64 `json:"max_tokens,omitzero"`
|
||||
// Effort controls reasoning effort level.
|
||||
// Mutually exclusive with MaxTokens.
|
||||
Effort *ReasoningEffort `json:"effort,omitempty"`
|
||||
Effort *ReasoningEffort `json:"effort,omitzero"`
|
||||
// Exclude excludes reasoning content from the response but still generates it internally.
|
||||
Exclude *bool `json:"exclude,omitempty"`
|
||||
Exclude *bool `json:"exclude,omitzero"`
|
||||
}
|
||||
|
||||
// GatewayProviderOptions represents provider routing preferences for Vercel AI Gateway.
|
||||
type GatewayProviderOptions struct {
|
||||
// Order is the list of provider slugs to try in order (e.g. ["vertex", "anthropic"]).
|
||||
Order []string `json:"order,omitempty"`
|
||||
Order []string `json:"order,omitzero"`
|
||||
// Models is the list of fallback models to try if the primary model fails.
|
||||
Models []string `json:"models,omitempty"`
|
||||
Models []string `json:"models,omitzero"`
|
||||
}
|
||||
|
||||
// BYOKCredential represents a single provider credential for BYOK.
|
||||
type BYOKCredential struct {
|
||||
APIKey string `json:"apiKey,omitempty"`
|
||||
APIKey string `json:"apiKey,omitzero"`
|
||||
}
|
||||
|
||||
// BYOKOptions represents Bring Your Own Key options for Vercel AI Gateway.
|
||||
type BYOKOptions struct {
|
||||
Anthropic map[string][]BYOKCredential `json:"anthropic,omitempty"`
|
||||
OpenAI map[string][]BYOKCredential `json:"openai,omitempty"`
|
||||
Vertex map[string][]BYOKCredential `json:"vertex,omitempty"`
|
||||
Bedrock map[string][]BYOKCredential `json:"bedrock,omitempty"`
|
||||
Anthropic map[string][]BYOKCredential `json:"anthropic,omitzero"`
|
||||
OpenAI map[string][]BYOKCredential `json:"openai,omitzero"`
|
||||
Vertex map[string][]BYOKCredential `json:"vertex,omitzero"`
|
||||
Bedrock map[string][]BYOKCredential `json:"bedrock,omitzero"`
|
||||
}
|
||||
|
||||
// ProviderOptions represents additional options for Vercel AI Gateway provider.
|
||||
type ProviderOptions struct {
|
||||
// Reasoning configuration for models that support extended thinking.
|
||||
Reasoning *ReasoningOptions `json:"reasoning,omitempty"`
|
||||
Reasoning *ReasoningOptions `json:"reasoning,omitzero"`
|
||||
// ProviderOptions for gateway routing preferences.
|
||||
ProviderOptions *GatewayProviderOptions `json:"providerOptions,omitempty"`
|
||||
ProviderOptions *GatewayProviderOptions `json:"providerOptions,omitzero"`
|
||||
// BYOK for request-scoped provider credentials.
|
||||
BYOK *BYOKOptions `json:"byok,omitempty"`
|
||||
BYOK *BYOKOptions `json:"byok,omitzero"`
|
||||
// User is a unique identifier representing your end-user.
|
||||
User *string `json:"user,omitempty"`
|
||||
User *string `json:"user,omitzero"`
|
||||
// LogitBias modifies the likelihood of specified tokens appearing in the completion.
|
||||
LogitBias map[string]int64 `json:"logit_bias,omitempty"`
|
||||
LogitBias map[string]int64 `json:"logit_bias,omitzero"`
|
||||
// LogProbs returns the log probabilities of the tokens.
|
||||
LogProbs *bool `json:"logprobs,omitempty"`
|
||||
LogProbs *bool `json:"logprobs,omitzero"`
|
||||
// TopLogProbs is the number of top log probabilities to return.
|
||||
TopLogProbs *int64 `json:"top_logprobs,omitempty"`
|
||||
TopLogProbs *int64 `json:"top_logprobs,omitzero"`
|
||||
// ParallelToolCalls enables parallel function calling during tool use.
|
||||
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
|
||||
ParallelToolCalls *bool `json:"parallel_tool_calls,omitzero"`
|
||||
// ExtraBody for additional request body fields.
|
||||
ExtraBody map[string]any `json:"extra_body,omitempty"`
|
||||
ExtraBody map[string]any `json:"extra_body,omitzero"`
|
||||
}
|
||||
|
||||
// Options implements the ProviderOptionsData interface for ProviderOptions.
|
||||
|
|
@ -128,7 +128,7 @@ func (o *ProviderOptions) UnmarshalJSON(data []byte) error {
|
|||
|
||||
// ProviderMetadata represents metadata from Vercel AI Gateway provider.
|
||||
type ProviderMetadata struct {
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Provider string `json:"provider,omitzero"`
|
||||
}
|
||||
|
||||
// Options implements the ProviderOptionsData interface for ProviderMetadata.
|
||||
|
|
@ -153,20 +153,20 @@ func (m *ProviderMetadata) UnmarshalJSON(data []byte) error {
|
|||
|
||||
// ReasoningDetail represents a reasoning detail from Vercel AI Gateway.
|
||||
type ReasoningDetail struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
ID string `json:"id,omitzero"`
|
||||
Type string `json:"type,omitzero"`
|
||||
Text string `json:"text,omitzero"`
|
||||
Data string `json:"data,omitzero"`
|
||||
Format string `json:"format,omitzero"`
|
||||
Summary string `json:"summary,omitzero"`
|
||||
Signature string `json:"signature,omitzero"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
// ReasoningData represents reasoning data from Vercel AI Gateway response.
|
||||
type ReasoningData struct {
|
||||
Reasoning string `json:"reasoning,omitempty"`
|
||||
ReasoningDetails []ReasoningDetail `json:"reasoning_details,omitempty"`
|
||||
Reasoning string `json:"reasoning,omitzero"`
|
||||
ReasoningDetails []ReasoningDetail `json:"reasoning_details,omitzero"`
|
||||
}
|
||||
|
||||
// ReasoningEffortOption creates a pointer to a ReasoningEffort value.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package providertests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
"charm.land/fantasy"
|
||||
|
|
@ -24,13 +24,13 @@ func TestProviderRegistry_Serialization_OpenAIOptions(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
var raw struct {
|
||||
ProviderOptions map[string]map[string]any `json:"provider_options"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(data, &raw))
|
||||
require.NoError(t, jsonv2.Unmarshal(data, &raw))
|
||||
|
||||
po, ok := raw.ProviderOptions[openai.Name]
|
||||
require.True(t, ok)
|
||||
|
|
@ -41,7 +41,7 @@ func TestProviderRegistry_Serialization_OpenAIOptions(t *testing.T) {
|
|||
require.Equal(t, "tester", inner["user"])
|
||||
|
||||
var decoded fantasy.Message
|
||||
require.NoError(t, json.Unmarshal(data, &decoded))
|
||||
require.NoError(t, jsonv2.Unmarshal(data, &decoded))
|
||||
|
||||
got, ok := decoded.ProviderOptions[openai.Name]
|
||||
require.True(t, ok)
|
||||
|
|
@ -66,14 +66,14 @@ func TestProviderRegistry_Serialization_OpenAIResponses(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// JSON should include the typed wrapper with constant TypeResponsesProviderOptions
|
||||
var raw struct {
|
||||
ProviderOptions map[string]map[string]any `json:"provider_options"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(data, &raw))
|
||||
require.NoError(t, jsonv2.Unmarshal(data, &raw))
|
||||
|
||||
po := raw.ProviderOptions[openai.Name]
|
||||
require.Equal(t, openai.TypeResponsesProviderOptions, po["type"]) // no magic strings
|
||||
|
|
@ -84,7 +84,7 @@ func TestProviderRegistry_Serialization_OpenAIResponses(t *testing.T) {
|
|||
|
||||
// Unmarshal back and assert concrete type
|
||||
var decoded fantasy.Message
|
||||
require.NoError(t, json.Unmarshal(data, &decoded))
|
||||
require.NoError(t, jsonv2.Unmarshal(data, &decoded))
|
||||
got := decoded.ProviderOptions[openai.Name]
|
||||
reqOpts, ok := got.(*openai.ResponsesProviderOptions)
|
||||
require.True(t, ok)
|
||||
|
|
@ -109,7 +109,7 @@ func TestProviderRegistry_Serialization_OpenAIResponsesReasoningMetadata(t *test
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(resp)
|
||||
data, err := jsonv2.Marshal(resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Ensure the provider metadata is wrapped with type using constant
|
||||
|
|
@ -119,7 +119,7 @@ func TestProviderRegistry_Serialization_OpenAIResponsesReasoningMetadata(t *test
|
|||
Data map[string]any `json:"data"`
|
||||
} `json:"content"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(data, &raw))
|
||||
require.NoError(t, jsonv2.Unmarshal(data, &raw))
|
||||
require.Greater(t, len(raw.Content), 0)
|
||||
tc := raw.Content[0]
|
||||
pm, ok := tc.Data["provider_metadata"].(map[string]any)
|
||||
|
|
@ -133,7 +133,7 @@ func TestProviderRegistry_Serialization_OpenAIResponsesReasoningMetadata(t *test
|
|||
|
||||
// Unmarshal back
|
||||
var decoded fantasy.Response
|
||||
require.NoError(t, json.Unmarshal(data, &decoded))
|
||||
require.NoError(t, jsonv2.Unmarshal(data, &decoded))
|
||||
pmDecoded := decoded.Content[0].(fantasy.TextContent).ProviderMetadata
|
||||
val, ok := pmDecoded[openai.Name]
|
||||
require.True(t, ok)
|
||||
|
|
@ -157,11 +157,11 @@ func TestProviderRegistry_Serialization_AnthropicOptions(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded fantasy.Message
|
||||
require.NoError(t, json.Unmarshal(data, &decoded))
|
||||
require.NoError(t, jsonv2.Unmarshal(data, &decoded))
|
||||
|
||||
got, ok := decoded.ProviderOptions[anthropic.Name]
|
||||
require.True(t, ok)
|
||||
|
|
@ -185,11 +185,11 @@ func TestProviderRegistry_Serialization_GoogleOptions(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded fantasy.Message
|
||||
require.NoError(t, json.Unmarshal(data, &decoded))
|
||||
require.NoError(t, jsonv2.Unmarshal(data, &decoded))
|
||||
|
||||
got, ok := decoded.ProviderOptions[google.Name]
|
||||
require.True(t, ok)
|
||||
|
|
@ -214,11 +214,11 @@ func TestProviderRegistry_Serialization_OpenRouterOptions(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded fantasy.Message
|
||||
require.NoError(t, json.Unmarshal(data, &decoded))
|
||||
require.NoError(t, jsonv2.Unmarshal(data, &decoded))
|
||||
|
||||
got, ok := decoded.ProviderOptions[openrouter.Name]
|
||||
require.True(t, ok)
|
||||
|
|
@ -245,11 +245,11 @@ func TestProviderRegistry_Serialization_OpenAICompatOptions(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded fantasy.Message
|
||||
require.NoError(t, json.Unmarshal(data, &decoded))
|
||||
require.NoError(t, jsonv2.Unmarshal(data, &decoded))
|
||||
|
||||
got, ok := decoded.ProviderOptions[openaicompat.Name]
|
||||
require.True(t, ok)
|
||||
|
|
@ -277,11 +277,11 @@ func TestProviderRegistry_MultiProvider(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded fantasy.Message
|
||||
require.NoError(t, json.Unmarshal(data, &decoded))
|
||||
require.NoError(t, jsonv2.Unmarshal(data, &decoded))
|
||||
|
||||
// Check OpenAI options
|
||||
openaiOpt, ok := decoded.ProviderOptions[openai.Name]
|
||||
|
|
@ -312,7 +312,7 @@ func TestProviderRegistry_ErrorHandling(t *testing.T) {
|
|||
}`
|
||||
|
||||
var msg fantasy.Message
|
||||
err := json.Unmarshal([]byte(invalidJSON), &msg)
|
||||
err := jsonv2.Unmarshal([]byte(invalidJSON), &msg)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unknown provider data type")
|
||||
})
|
||||
|
|
@ -327,7 +327,7 @@ func TestProviderRegistry_ErrorHandling(t *testing.T) {
|
|||
}`
|
||||
|
||||
var msg fantasy.Message
|
||||
err := json.Unmarshal([]byte(invalidJSON), &msg)
|
||||
err := jsonv2.Unmarshal([]byte(invalidJSON), &msg)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
|
@ -364,11 +364,11 @@ func TestProviderRegistry_AllTypesRegistered(t *testing.T) {
|
|||
}
|
||||
|
||||
// Marshal and unmarshal
|
||||
data, err := json.Marshal(msg)
|
||||
data, err := jsonv2.Marshal(msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded fantasy.Message
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
err = jsonv2.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the provider options exist
|
||||
|
|
@ -404,11 +404,11 @@ func TestProviderRegistry_AllTypesRegistered(t *testing.T) {
|
|||
}
|
||||
|
||||
// Marshal and unmarshal
|
||||
data, err := json.Marshal(resp)
|
||||
data, err := jsonv2.Marshal(resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded fantasy.Response
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
err = jsonv2.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the provider metadata exists
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ package schema
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
|
@ -259,7 +259,7 @@ func ParsePartialJSON(text string) (any, ParseState, error) {
|
|||
}
|
||||
|
||||
var result any
|
||||
if err := json.Unmarshal([]byte(text), &result); err == nil {
|
||||
if err := jsonv2.Unmarshal([]byte(text), &result); err == nil {
|
||||
return result, ParseStateSuccessful, nil
|
||||
}
|
||||
|
||||
|
|
@ -268,7 +268,7 @@ func ParsePartialJSON(text string) (any, ParseState, error) {
|
|||
return nil, ParseStateFailed, fmt.Errorf("json repair failed: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(repaired), &result); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(repaired), &result); err != nil {
|
||||
return nil, ParseStateFailed, fmt.Errorf("failed to parse repaired json: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -317,7 +317,7 @@ func ValidateAgainstSchema(obj any, schema Schema) error {
|
|||
// This is a convenience wrapper for use sites that hold the schema as a map rather
|
||||
// than the typed Schema struct.
|
||||
func ValidateAgainstSchemaMap(obj any, schemaMap map[string]any) error {
|
||||
schemaBytes, err := json.Marshal(schemaMap)
|
||||
schemaBytes, err := jsonv2.Marshal(schemaMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal schema map: %w", err)
|
||||
}
|
||||
|
|
@ -341,7 +341,7 @@ func ValidateAgainstSchemaMap(obj any, schemaMap map[string]any) error {
|
|||
}
|
||||
|
||||
func validateAgainstSchema(obj any, schema Schema) error {
|
||||
jsonSchemaBytes, err := json.Marshal(schema)
|
||||
jsonSchemaBytes, err := jsonv2.Marshal(schema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal schema: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package fantasy
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"reflect"
|
||||
|
||||
"charm.land/fantasy/schema"
|
||||
|
|
@ -78,7 +78,7 @@ func NewMediaResponse(data []byte, mediaType string) ToolResponse {
|
|||
// WithResponseMetadata adds metadata to a response.
|
||||
func WithResponseMetadata(response ToolResponse, metadata any) ToolResponse {
|
||||
if metadata != nil {
|
||||
metadataBytes, err := json.Marshal(metadata)
|
||||
metadataBytes, err := jsonv2.Marshal(metadata)
|
||||
if err != nil {
|
||||
return response
|
||||
}
|
||||
|
|
@ -167,7 +167,7 @@ func (w *funcToolWrapper[TInput]) Info() ToolInfo {
|
|||
|
||||
func (w *funcToolWrapper[TInput]) Run(ctx context.Context, params ToolCall) (ToolResponse, error) {
|
||||
var input TInput
|
||||
if err := json.Unmarshal([]byte(params.Input), &input); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(params.Input), &input); err != nil {
|
||||
return NewTextErrorResponse(fmt.Sprintf("invalid parameters: %s", err)), nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package fantasy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ func extractToolDependencies(input string) ([]string, error) {
|
|||
}
|
||||
|
||||
var v any
|
||||
if err := json.Unmarshal([]byte(input), &v); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(input), &v); err != nil {
|
||||
// If the tool input isn't valid JSON, treat it as having no dependencies.
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ package fantasy
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -233,7 +233,7 @@ func resolveToolRefsInInput(input string, results map[string]ToolResultContent)
|
|||
}
|
||||
|
||||
var v any
|
||||
if err := json.Unmarshal([]byte(input), &v); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(input), &v); err != nil {
|
||||
// Not JSON; nothing to resolve.
|
||||
return input, nil
|
||||
}
|
||||
|
|
@ -242,7 +242,7 @@ func resolveToolRefsInInput(input string, results map[string]ToolResultContent)
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b, err := json.Marshal(updated)
|
||||
b, err := jsonv2.Marshal(updated)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -328,7 +328,7 @@ func resolveToolRefValue(ref string, results map[string]ToolResultContent) (any,
|
|||
|
||||
// Path resolution: interpret base as JSON and walk.
|
||||
var cur any
|
||||
if err := json.Unmarshal([]byte(base), &cur); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(base), &cur); err != nil {
|
||||
return nil, fmt.Errorf("tool ref %q path requires JSON output, got non-JSON", ref)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ package conversations
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"strings"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/ids"
|
||||
|
|
@ -100,7 +100,7 @@ func (s *Store) EditMessage(ctx context.Context, p EditMessageParams) (sqlc.Agen
|
|||
return sqlc.AgentMessage{}, err
|
||||
}
|
||||
|
||||
metaJSON, _ := json.Marshal(p.Metadata)
|
||||
metaJSON, _ := jsonv2.Marshal(p.Metadata)
|
||||
|
||||
// Best-effort revision record — a failure here does not abort the edit.
|
||||
_, _ = s.q.AddAgentMessageRevision(ctx, sqlc.AddAgentMessageRevisionParams{
|
||||
|
|
@ -174,7 +174,7 @@ func (s *Store) ForkFromCheckpoint(ctx context.Context, p ForkFromCheckpointPara
|
|||
Messages []msgSnapshot `json:"messages"`
|
||||
}
|
||||
var snap snapshot
|
||||
_ = json.Unmarshal(runState.SnapshotJson, &snap)
|
||||
_ = jsonv2.Unmarshal([]byte(runState.SnapshotJson), &snap)
|
||||
|
||||
conv, err := s.q.CreateAgentConversation(ctx, sqlc.CreateAgentConversationParams{
|
||||
ID: ids.New(),
|
||||
|
|
@ -188,7 +188,7 @@ func (s *Store) ForkFromCheckpoint(ctx context.Context, p ForkFromCheckpointPara
|
|||
"checkpoint_name": cpName,
|
||||
"run_state_id": cp.RunStateID.String(),
|
||||
}
|
||||
forkMetaJSON, _ := json.Marshal(forkMeta)
|
||||
forkMetaJSON, _ := jsonv2.Marshal(forkMeta)
|
||||
|
||||
_, _ = s.q.CreateAgentConversationFork(ctx, sqlc.CreateAgentConversationForkParams{
|
||||
ID: ids.New(),
|
||||
|
|
@ -209,7 +209,7 @@ func (s *Store) ForkFromCheckpoint(ctx context.Context, p ForkFromCheckpointPara
|
|||
"checkpoint_name": cpName,
|
||||
"run_state_id": cp.RunStateID.String(),
|
||||
}
|
||||
seedMetaJSON, _ := json.Marshal(seedMeta)
|
||||
seedMetaJSON, _ := jsonv2.Marshal(seedMeta)
|
||||
|
||||
for _, m := range msgs {
|
||||
if m.Role != "user" && m.Role != "assistant" {
|
||||
|
|
@ -277,7 +277,7 @@ func (s *Store) MergeAsLinkedContext(ctx context.Context, p MergeAsLinkedContext
|
|||
}
|
||||
|
||||
meta := map[string]any{"source": "merge_as_linked_context"}
|
||||
metaJSON, _ := json.Marshal(meta)
|
||||
metaJSON, _ := jsonv2.Marshal(meta)
|
||||
|
||||
_, _ = s.q.CreateAgentConversationLink(ctx, sqlc.CreateAgentConversationLinkParams{
|
||||
ID: ids.New(),
|
||||
|
|
@ -321,7 +321,7 @@ type AncestryParams struct {
|
|||
// AncestryResult is the structured result of an Ancestry query.
|
||||
type AncestryResult struct {
|
||||
Conversation sqlc.AgentConversation `json:"conversation"`
|
||||
ForkParent *sqlc.AgentConversationFork `json:"fork_parent,omitempty"`
|
||||
ForkParent *sqlc.AgentConversationFork `json:"fork_parent,omitzero"`
|
||||
ForkChildren []sqlc.AgentConversationFork `json:"fork_children"`
|
||||
Links []sqlc.AgentConversationLink `json:"links"`
|
||||
}
|
||||
|
|
@ -434,7 +434,7 @@ type GraphParams struct {
|
|||
// GraphNode is a single conversation node in the fork/link graph.
|
||||
type GraphNode struct {
|
||||
ID string `json:"id"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Title *string `json:"title,omitzero"`
|
||||
}
|
||||
|
||||
// GraphEdge is a directed edge between two conversation nodes.
|
||||
|
|
@ -444,9 +444,9 @@ type GraphEdge struct {
|
|||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
// CheckpointID is set on fork edges.
|
||||
CheckpointID *string `json:"checkpoint_id,omitempty"`
|
||||
CheckpointID *string `json:"checkpoint_id,omitzero"`
|
||||
// Kind is set on link edges (e.g. "merge").
|
||||
Kind *string `json:"kind,omitempty"`
|
||||
Kind *string `json:"kind,omitzero"`
|
||||
}
|
||||
|
||||
// GraphResult is the full fork/link graph rooted at a conversation.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ package agent
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
memstore "github.com/sipeed/picoclaw/pkg/memory/store"
|
||||
"github.com/sipeed/picoclaw/pkg/tools"
|
||||
|
|
@ -84,7 +84,7 @@ func (t *MemGPTTool) Parameters() map[string]interface{} {
|
|||
|
||||
func (t *MemGPTTool) Execute(ctx context.Context, args map[string]interface{}) *tools.ToolResult {
|
||||
// Marshal the args back to JSON for the inner MemoryTool.Execute()
|
||||
input, err := json.Marshal(args)
|
||||
input, err := jsonv2.Marshal(args)
|
||||
if err != nil {
|
||||
return tools.ErrorResult("invalid arguments: " + err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ package mentions
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"strings"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/ids"
|
||||
|
|
@ -76,7 +76,7 @@ func (s *Store) Add(ctx context.Context, p AddParams) (sqlc.AgentMention, error)
|
|||
return sqlc.AgentMention{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse target_id %q", targetIDStr)
|
||||
}
|
||||
|
||||
metaJSON, _ := json.Marshal(p.Metadata)
|
||||
metaJSON, _ := jsonv2.Marshal(p.Metadata)
|
||||
|
||||
return s.q.AddAgentMention(ctx, sqlc.AddAgentMentionParams{
|
||||
ID: ids.New(),
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package agent
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ func (r OffloadingToolRuntime) Execute(ctx context.Context, tools []fantasy.Agen
|
|||
fullKey := toolResultFullKey(r.ConversationID, r.RunID, stepIndex, tc.ToolCallID)
|
||||
|
||||
payload, payloadType, payloadText := toolResultPayload(res)
|
||||
b, marshalErr := json.Marshal(payload)
|
||||
b, marshalErr := jsonv2.Marshal(payload)
|
||||
if marshalErr != nil {
|
||||
b = []byte(`{"error":"failed to marshal tool result payload"}`)
|
||||
payloadType = "error"
|
||||
|
|
@ -134,7 +134,7 @@ func (r OffloadingToolRuntime) Execute(ctx context.Context, tools []fantasy.Agen
|
|||
"result_type": payloadType,
|
||||
"step_index": stepIndex,
|
||||
}
|
||||
metaJSON, _ := json.Marshal(meta)
|
||||
metaJSON, _ := jsonv2.Marshal(meta)
|
||||
|
||||
var previewPtr *string
|
||||
if strings.TrimSpace(preview) != "" {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"github.com/sipeed/picoclaw/pkg/ids"
|
||||
"github.com/sipeed/picoclaw/pkg/memory/sqlc"
|
||||
|
|
@ -50,8 +52,8 @@ func (s *StateStore) UpdateRunStatus(ctx context.Context, runID ids.UUID, status
|
|||
|
||||
metaJSON := json.RawMessage(`{}`)
|
||||
if meta != nil {
|
||||
if b, err := json.Marshal(meta); err == nil {
|
||||
metaJSON = b
|
||||
if b, err := jsonv2.Marshal(meta); err == nil {
|
||||
metaJSON = json.RawMessage(b)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,8 +77,8 @@ func (s *StateStore) AddRunState(ctx context.Context, runID ids.UUID, stepIndex
|
|||
|
||||
snapJSON := json.RawMessage(`{}`)
|
||||
if snapshot != nil {
|
||||
if b, err := json.Marshal(snapshot); err == nil {
|
||||
snapJSON = b
|
||||
if b, err := jsonv2.Marshal(snapshot); err == nil {
|
||||
snapJSON = json.RawMessage(b)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,8 +101,8 @@ func (s *StateStore) AddTransition(ctx context.Context, runID ids.UUID, t fantas
|
|||
|
||||
metaJSON := json.RawMessage(`{}`)
|
||||
if t.Meta != nil {
|
||||
if b, err := json.Marshal(t.Meta); err == nil {
|
||||
metaJSON = b
|
||||
if b, err := jsonv2.Marshal(t.Meta); err == nil {
|
||||
metaJSON = json.RawMessage(b)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,8 +154,8 @@ func (s *CheckpointStore) CreateCheckpoint(ctx context.Context, conversationID i
|
|||
|
||||
metaJSON := json.RawMessage(`{}`)
|
||||
if meta != nil {
|
||||
if b, err := json.Marshal(meta); err == nil {
|
||||
metaJSON = b
|
||||
if b, err := jsonv2.Marshal(meta); err == nil {
|
||||
metaJSON = json.RawMessage(b)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ package threads
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"strings"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/ids"
|
||||
|
|
@ -45,7 +45,7 @@ func (s *Store) Create(ctx context.Context, p CreateParams) (sqlc.AgentThread, e
|
|||
return sqlc.AgentThread{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr)
|
||||
}
|
||||
|
||||
metaJSON, _ := json.Marshal(p.Metadata)
|
||||
metaJSON, _ := jsonv2.Marshal(p.Metadata)
|
||||
|
||||
return s.q.CreateAgentThread(ctx, sqlc.CreateAgentThreadParams{
|
||||
ID: ids.New(),
|
||||
|
|
@ -108,7 +108,7 @@ func (s *Store) AddMessage(ctx context.Context, p AddMessageParams) (sqlc.AgentT
|
|||
return sqlc.AgentThreadMessage{}, pcerrors.New(pcerrors.CodeInvalidArgument, "content is empty")
|
||||
}
|
||||
|
||||
metaJSON, _ := json.Marshal(p.Metadata)
|
||||
metaJSON, _ := jsonv2.Marshal(p.Metadata)
|
||||
|
||||
return s.q.AddAgentThreadMessage(ctx, sqlc.AddAgentThreadMessageParams{
|
||||
ID: ids.New(),
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ package agent
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"github.com/sipeed/picoclaw/pkg/ids"
|
||||
"github.com/sipeed/picoclaw/pkg/memory/sqlc"
|
||||
|
|
@ -13,23 +15,23 @@ import (
|
|||
)
|
||||
|
||||
type ToolResultSearchView struct {
|
||||
StartLine int `json:"start_line,omitempty" description:"Optional. 1-indexed start line (inclusive)."`
|
||||
EndLine int `json:"end_line,omitempty" description:"Optional. 1-indexed end line (inclusive)."`
|
||||
MaxLines int `json:"max_lines,omitempty" description:"Optional. Default 30, max 200."`
|
||||
StartChunk int `json:"start_chunk,omitempty" description:"Optional. 0-indexed start chunk (inclusive)."`
|
||||
EndChunk int `json:"end_chunk,omitempty" description:"Optional. 0-indexed end chunk (inclusive)."`
|
||||
MaxChunks int `json:"max_chunks,omitempty" description:"Optional. Default 3, max 20."`
|
||||
StartLine int `json:"start_line,omitzero" description:"Optional. 1-indexed start line (inclusive)."`
|
||||
EndLine int `json:"end_line,omitzero" description:"Optional. 1-indexed end line (inclusive)."`
|
||||
MaxLines int `json:"max_lines,omitzero" description:"Optional. Default 30, max 200."`
|
||||
StartChunk int `json:"start_chunk,omitzero" description:"Optional. 0-indexed start chunk (inclusive)."`
|
||||
EndChunk int `json:"end_chunk,omitzero" description:"Optional. 0-indexed end chunk (inclusive)."`
|
||||
MaxChunks int `json:"max_chunks,omitzero" description:"Optional. Default 3, max 20."`
|
||||
}
|
||||
|
||||
type ToolResultSearchInput struct {
|
||||
ConversationID string `json:"conversation_id,omitempty" description:"Optional. Agent conversation UUID."`
|
||||
RunID string `json:"run_id,omitempty" description:"Optional. Agent run UUID."`
|
||||
ToolCallID string `json:"tool_call_id,omitempty" description:"Optional. Tool call id to fetch (requires run_id)."`
|
||||
ToolName string `json:"tool_name,omitempty" description:"Optional. Filter by tool name."`
|
||||
Query string `json:"query,omitempty" description:"Optional. Case-insensitive substring match on tool_name/tool_call_id/summary."`
|
||||
ConversationID string `json:"conversation_id,omitzero" description:"Optional. Agent conversation UUID."`
|
||||
RunID string `json:"run_id,omitzero" description:"Optional. Agent run UUID."`
|
||||
ToolCallID string `json:"tool_call_id,omitzero" description:"Optional. Tool call id to fetch (requires run_id)."`
|
||||
ToolName string `json:"tool_name,omitzero" description:"Optional. Filter by tool name."`
|
||||
Query string `json:"query,omitzero" description:"Optional. Case-insensitive substring match on tool_name/tool_call_id/summary."`
|
||||
|
||||
Limit int `json:"limit,omitempty" description:"Optional. Default 5, max 50."`
|
||||
View *ToolResultSearchView `json:"view,omitempty" description:"Optional. File view range for each result."`
|
||||
Limit int `json:"limit,omitzero" description:"Optional. Default 5, max 50."`
|
||||
View *ToolResultSearchView `json:"view,omitzero" description:"Optional. File view range for each result."`
|
||||
}
|
||||
|
||||
// NewToolResultSearchTool creates the tool_result_search agent tool for
|
||||
|
|
@ -87,17 +89,17 @@ func NewToolResultSearchTool(q *sqlc.Queries, kv KVDelegate) fantasy.AgentTool {
|
|||
startChunk, endChunk := normalizeChunkView(view)
|
||||
|
||||
type item struct {
|
||||
ID string `json:"id"`
|
||||
RunID string `json:"run_id"`
|
||||
StepIndex int64 `json:"step_index"`
|
||||
ToolCallID string `json:"tool_call_id"`
|
||||
ToolName string `json:"tool_name"`
|
||||
Preview *string `json:"preview,omitempty"`
|
||||
FullKey string `json:"full_key"`
|
||||
ChunkCount int64 `json:"chunk_count"`
|
||||
View string `json:"view"`
|
||||
ViewRange map[string]int `json:"view_range"`
|
||||
Metadata json.RawMessage `json:"metadata_json"`
|
||||
ID string `json:"id"`
|
||||
RunID string `json:"run_id"`
|
||||
StepIndex int64 `json:"step_index"`
|
||||
ToolCallID string `json:"tool_call_id"`
|
||||
ToolName string `json:"tool_name"`
|
||||
Preview *string `json:"preview,omitzero"`
|
||||
FullKey string `json:"full_key"`
|
||||
ChunkCount int64 `json:"chunk_count"`
|
||||
View string `json:"view"`
|
||||
ViewRange map[string]int `json:"view_range"`
|
||||
Metadata jsontext.Value `json:"metadata_json"` // sqlc gives json.RawMessage; both are []byte
|
||||
}
|
||||
|
||||
out := struct {
|
||||
|
|
@ -131,11 +133,11 @@ func NewToolResultSearchTool(q *sqlc.Queries, kv KVDelegate) fantasy.AgentTool {
|
|||
ChunkCount: r.ChunkCount,
|
||||
View: sel,
|
||||
ViewRange: viewRange,
|
||||
Metadata: r.MetadataJson,
|
||||
Metadata: jsontext.Value(r.MetadataJson),
|
||||
})
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(out)
|
||||
b, _ := jsonv2.Marshal(out)
|
||||
return fantasy.NewTextResponse(string(b)), nil
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package agent_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -58,13 +58,13 @@ func invokeSearch(t *testing.T, tool fantasy.AgentTool, input agent.ToolResultSe
|
|||
require.False(t, resp.IsError, "search tool must not error: %s", resp.Content)
|
||||
|
||||
var out map[string]any
|
||||
require.NoError(t, json.Unmarshal([]byte(resp.Content), &out))
|
||||
require.NoError(t, jsonv2.Unmarshal([]byte(resp.Content), &out))
|
||||
return out
|
||||
}
|
||||
|
||||
func marshalInput(t *testing.T, v any) string {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(v)
|
||||
b, err := jsonv2.Marshal(v)
|
||||
require.NoError(t, err)
|
||||
return string(b)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
|
@ -16,6 +15,9 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
)
|
||||
|
||||
type OAuthProviderConfig struct {
|
||||
|
|
@ -128,12 +130,12 @@ type deviceCodeResponse struct {
|
|||
|
||||
func parseDeviceCodeResponse(body []byte) (deviceCodeResponse, error) {
|
||||
var raw struct {
|
||||
DeviceAuthID string `json:"device_auth_id"`
|
||||
UserCode string `json:"user_code"`
|
||||
Interval json.RawMessage `json:"interval"`
|
||||
DeviceAuthID string `json:"device_auth_id"`
|
||||
UserCode string `json:"user_code"`
|
||||
Interval jsontext.Value `json:"interval"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
if err := jsonv2.Unmarshal(body, &raw); err != nil {
|
||||
return deviceCodeResponse{}, err
|
||||
}
|
||||
|
||||
|
|
@ -149,18 +151,18 @@ func parseDeviceCodeResponse(body []byte) (deviceCodeResponse, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func parseFlexibleInt(raw json.RawMessage) (int, error) {
|
||||
func parseFlexibleInt(raw jsontext.Value) (int, error) {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var interval int
|
||||
if err := json.Unmarshal(raw, &interval); err == nil {
|
||||
if err := jsonv2.Unmarshal(raw, &interval); err == nil {
|
||||
return interval, nil
|
||||
}
|
||||
|
||||
var intervalStr string
|
||||
if err := json.Unmarshal(raw, &intervalStr); err == nil {
|
||||
if err := jsonv2.Unmarshal(raw, &intervalStr); err == nil {
|
||||
intervalStr = strings.TrimSpace(intervalStr)
|
||||
if intervalStr == "" {
|
||||
return 0, nil
|
||||
|
|
@ -172,7 +174,7 @@ func parseFlexibleInt(raw json.RawMessage) (int, error) {
|
|||
}
|
||||
|
||||
func LoginDeviceCode(cfg OAuthProviderConfig) (*AuthCredential, error) {
|
||||
reqBody, _ := json.Marshal(map[string]string{
|
||||
reqBody, _ := jsonv2.Marshal(map[string]string{
|
||||
"client_id": cfg.ClientID,
|
||||
})
|
||||
|
||||
|
|
@ -224,7 +226,7 @@ func LoginDeviceCode(cfg OAuthProviderConfig) (*AuthCredential, error) {
|
|||
}
|
||||
|
||||
func pollDeviceCode(cfg OAuthProviderConfig, deviceAuthID, userCode string) (*AuthCredential, error) {
|
||||
reqBody, _ := json.Marshal(map[string]string{
|
||||
reqBody, _ := jsonv2.Marshal(map[string]string{
|
||||
"device_auth_id": deviceAuthID,
|
||||
"user_code": userCode,
|
||||
})
|
||||
|
|
@ -250,7 +252,7 @@ func pollDeviceCode(cfg OAuthProviderConfig, deviceAuthID, userCode string) (*Au
|
|||
CodeChallenge string `json:"code_challenge"`
|
||||
CodeVerifier string `json:"code_verifier"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||
if err := jsonv2.Unmarshal(body, &tokenResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -349,7 +351,7 @@ func parseTokenResponse(body []byte, provider string) (*AuthCredential, error) {
|
|||
ExpiresIn int `json:"expires_in"`
|
||||
IDToken string `json:"id_token"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||
if err := jsonv2.Unmarshal(body, &tokenResp); err != nil {
|
||||
return nil, fmt.Errorf("parsing token response: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -435,7 +437,7 @@ func parseJWTClaims(token string) (map[string]interface{}, error) {
|
|||
}
|
||||
|
||||
var claims map[string]interface{}
|
||||
if err := json.Unmarshal(decoded, &claims); err != nil {
|
||||
if err := jsonv2.Unmarshal(decoded, &claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,19 +2,20 @@ package auth
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
func makeJWTForClaims(t *testing.T, claims map[string]interface{}) string {
|
||||
t.Helper()
|
||||
|
||||
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`))
|
||||
payloadJSON, err := json.Marshal(claims)
|
||||
payloadJSON, err := jsonv2.Marshal(claims)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal claims: %v", err)
|
||||
}
|
||||
|
|
@ -95,7 +96,7 @@ func TestParseTokenResponse(t *testing.T) {
|
|||
"expires_in": 3600,
|
||||
"id_token": "test-id-token",
|
||||
}
|
||||
body, _ := json.Marshal(resp)
|
||||
body, _ := jsonv2.Marshal(resp)
|
||||
|
||||
cred, err := parseTokenResponse(body, "openai")
|
||||
if err != nil {
|
||||
|
|
@ -127,7 +128,7 @@ func TestParseTokenResponseExtractsAccountIDFromIDToken(t *testing.T) {
|
|||
"expires_in": 3600,
|
||||
"id_token": idToken,
|
||||
}
|
||||
body, _ := json.Marshal(resp)
|
||||
body, _ := jsonv2.Marshal(resp)
|
||||
|
||||
cred, err := parseTokenResponse(body, "openai")
|
||||
if err != nil {
|
||||
|
|
@ -166,7 +167,7 @@ func TestParseTokenResponseAccountIDFromIDToken(t *testing.T) {
|
|||
"expires_in": 3600,
|
||||
"id_token": idToken,
|
||||
}
|
||||
body, _ := json.Marshal(resp)
|
||||
body, _ := jsonv2.Marshal(resp)
|
||||
|
||||
cred, err := parseTokenResponse(body, "openai")
|
||||
if err != nil {
|
||||
|
|
@ -206,7 +207,7 @@ func TestExchangeCodeForTokens(t *testing.T) {
|
|||
"refresh_token": "mock-refresh-token",
|
||||
"expires_in": 3600,
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
jsonv2.MarshalWrite(w, resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -245,7 +246,7 @@ func TestRefreshAccessToken(t *testing.T) {
|
|||
"refresh_token": "refreshed-refresh-token",
|
||||
"expires_in": 3600,
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
jsonv2.MarshalWrite(w, resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -294,7 +295,7 @@ func TestRefreshAccessTokenPreservesRefreshAndAccountID(t *testing.T) {
|
|||
"access_token": "new-access-token-only",
|
||||
"expires_in": 3600,
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
jsonv2.MarshalWrite(w, resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
)
|
||||
|
||||
type AuthCredential struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
AccountID string `json:"account_id,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitzero"`
|
||||
AccountID string `json:"account_id,omitzero"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitzero"`
|
||||
Provider string `json:"provider"`
|
||||
AuthMethod string `json:"auth_method"`
|
||||
}
|
||||
|
|
@ -50,7 +52,7 @@ func LoadStore() (*AuthStore, error) {
|
|||
}
|
||||
|
||||
var store AuthStore
|
||||
if err := json.Unmarshal(data, &store); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &store); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if store.Credentials == nil {
|
||||
|
|
@ -66,7 +68,7 @@ func SaveStore(store *AuthStore) error {
|
|||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(store, "", " ")
|
||||
data, err := jsonv2.Marshal(store, jsontext.WithIndent(" "))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ package channels
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
lark "github.com/larksuite/oapi-sdk-go/v3"
|
||||
larkdispatcher "github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
|
||||
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
|
||||
|
|
@ -97,7 +98,7 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
|||
return fmt.Errorf("chat ID is empty")
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(map[string]string{"text": msg.Content})
|
||||
payload, err := jsonv2.Marshal(map[string]string{"text": msg.Content})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal feishu content: %w", err)
|
||||
}
|
||||
|
|
@ -202,7 +203,7 @@ func extractFeishuMessageContent(message *larkim.EventMessage) string {
|
|||
var textPayload struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(*message.Content), &textPayload); err == nil {
|
||||
if err := jsonv2.Unmarshal([]byte(*message.Content), &textPayload); err == nil {
|
||||
return textPayload.Text
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
|
@ -15,6 +14,9 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
|
|
@ -140,7 +142,7 @@ func (c *LINEChannel) fetchBotInfo() error {
|
|||
BasicID string `json:"basicId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
if err := jsonv2.UnmarshalRead(resp.Body, &info); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -199,7 +201,7 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) {
|
|||
var payload struct {
|
||||
Events []lineEvent `json:"events"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
if err := jsonv2.Unmarshal(body, &payload); err != nil {
|
||||
logger.ErrorCF("line", "Failed to parse webhook payload", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
|
@ -230,11 +232,11 @@ func (c *LINEChannel) verifySignature(body []byte, signature string) bool {
|
|||
|
||||
// LINE webhook event types
|
||||
type lineEvent struct {
|
||||
Type string `json:"type"`
|
||||
ReplyToken string `json:"replyToken"`
|
||||
Source lineSource `json:"source"`
|
||||
Message json.RawMessage `json:"message"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Type string `json:"type"`
|
||||
ReplyToken string `json:"replyToken"`
|
||||
Source lineSource `json:"source"`
|
||||
Message jsontext.Value `json:"message"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type lineSource struct {
|
||||
|
|
@ -277,7 +279,7 @@ func (c *LINEChannel) processEvent(event lineEvent) {
|
|||
isGroup := event.Source.Type == "group" || event.Source.Type == "room"
|
||||
|
||||
var msg lineMessage
|
||||
if err := json.Unmarshal(event.Message, &msg); err != nil {
|
||||
if err := jsonv2.Unmarshal(event.Message, &msg); err != nil {
|
||||
logger.ErrorCF("line", "Failed to parse message", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
|
@ -558,7 +560,7 @@ func (c *LINEChannel) sendLoading(chatID string) {
|
|||
|
||||
// callAPI makes an authenticated POST request to the LINE API.
|
||||
func (c *LINEChannel) callAPI(ctx context.Context, endpoint string, payload interface{}) error {
|
||||
body, err := json.Marshal(payload)
|
||||
body, err := jsonv2.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal payload: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ package channels
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
|
|
@ -102,15 +103,13 @@ func (c *MaixCamChannel) handleConnection(conn net.Conn, ctx context.Context) {
|
|||
logger.DebugC("maixcam", "Connection closed")
|
||||
}()
|
||||
|
||||
decoder := json.NewDecoder(conn)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
var msg MaixCamMessage
|
||||
if err := decoder.Decode(&msg); err != nil {
|
||||
if err := jsonv2.UnmarshalRead(conn, &msg); err != nil {
|
||||
if err.Error() != "EOF" {
|
||||
logger.ErrorCF("maixcam", "Failed to decode message", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
|
|
@ -221,7 +220,7 @@ func (c *MaixCamChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
|||
"chat_id": msg.ChatID,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(response)
|
||||
data, err := jsonv2.Marshal(response)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ package channels
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
|
|
@ -31,21 +32,21 @@ type OneBotChannel struct {
|
|||
}
|
||||
|
||||
type oneBotRawEvent struct {
|
||||
PostType string `json:"post_type"`
|
||||
MessageType string `json:"message_type"`
|
||||
SubType string `json:"sub_type"`
|
||||
MessageID json.RawMessage `json:"message_id"`
|
||||
UserID json.RawMessage `json:"user_id"`
|
||||
GroupID json.RawMessage `json:"group_id"`
|
||||
RawMessage string `json:"raw_message"`
|
||||
Message json.RawMessage `json:"message"`
|
||||
Sender json.RawMessage `json:"sender"`
|
||||
SelfID json.RawMessage `json:"self_id"`
|
||||
Time json.RawMessage `json:"time"`
|
||||
MetaEventType string `json:"meta_event_type"`
|
||||
Echo string `json:"echo"`
|
||||
RetCode json.RawMessage `json:"retcode"`
|
||||
Status BotStatus `json:"status"`
|
||||
PostType string `json:"post_type"`
|
||||
MessageType string `json:"message_type"`
|
||||
SubType string `json:"sub_type"`
|
||||
MessageID jsontext.Value `json:"message_id"`
|
||||
UserID jsontext.Value `json:"user_id"`
|
||||
GroupID jsontext.Value `json:"group_id"`
|
||||
RawMessage string `json:"raw_message"`
|
||||
Message jsontext.Value `json:"message"`
|
||||
Sender jsontext.Value `json:"sender"`
|
||||
SelfID jsontext.Value `json:"self_id"`
|
||||
Time jsontext.Value `json:"time"`
|
||||
MetaEventType string `json:"meta_event_type"`
|
||||
Echo string `json:"echo"`
|
||||
RetCode jsontext.Value `json:"retcode"`
|
||||
Status BotStatus `json:"status"`
|
||||
}
|
||||
|
||||
type BotStatus struct {
|
||||
|
|
@ -54,9 +55,9 @@ type BotStatus struct {
|
|||
}
|
||||
|
||||
type oneBotSender struct {
|
||||
UserID json.RawMessage `json:"user_id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Card string `json:"card"`
|
||||
UserID jsontext.Value `json:"user_id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Card string `json:"card"`
|
||||
}
|
||||
|
||||
type oneBotEvent struct {
|
||||
|
|
@ -78,7 +79,7 @@ type oneBotEvent struct {
|
|||
type oneBotAPIRequest struct {
|
||||
Action string `json:"action"`
|
||||
Params interface{} `json:"params"`
|
||||
Echo string `json:"echo,omitempty"`
|
||||
Echo string `json:"echo,omitzero"`
|
||||
}
|
||||
|
||||
type oneBotSendPrivateMsgParams struct {
|
||||
|
|
@ -236,7 +237,7 @@ func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
|||
Echo: echo,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
data, err := jsonv2.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal OneBot request: %w", err)
|
||||
}
|
||||
|
|
@ -326,7 +327,7 @@ func (c *OneBotChannel) listen() {
|
|||
})
|
||||
|
||||
var raw oneBotRawEvent
|
||||
if err := json.Unmarshal(message, &raw); err != nil {
|
||||
if err := jsonv2.Unmarshal(message, &raw); err != nil {
|
||||
logger.WarnCF("onebot", "Failed to unmarshal raw event", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"payload": string(message),
|
||||
|
|
@ -354,29 +355,29 @@ func (c *OneBotChannel) listen() {
|
|||
}
|
||||
}
|
||||
|
||||
func parseJSONInt64(raw json.RawMessage) (int64, error) {
|
||||
func parseJSONInt64(raw jsontext.Value) (int64, error) {
|
||||
if len(raw) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var n int64
|
||||
if err := json.Unmarshal(raw, &n); err == nil {
|
||||
if err := jsonv2.Unmarshal(raw, &n); err == nil {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
if err := jsonv2.Unmarshal(raw, &s); err == nil {
|
||||
return strconv.ParseInt(s, 10, 64)
|
||||
}
|
||||
return 0, fmt.Errorf("cannot parse as int64: %s", string(raw))
|
||||
}
|
||||
|
||||
func parseJSONString(raw json.RawMessage) string {
|
||||
func parseJSONString(raw jsontext.Value) string {
|
||||
if len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
if err := jsonv2.Unmarshal(raw, &s); err == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
|
|
@ -388,13 +389,13 @@ type parseMessageResult struct {
|
|||
IsBotMentioned bool
|
||||
}
|
||||
|
||||
func parseMessageContentEx(raw json.RawMessage, selfID int64) parseMessageResult {
|
||||
func parseMessageContentEx(raw jsontext.Value, selfID int64) parseMessageResult {
|
||||
if len(raw) == 0 {
|
||||
return parseMessageResult{}
|
||||
}
|
||||
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
if err := jsonv2.Unmarshal(raw, &s); err == nil {
|
||||
mentioned := false
|
||||
if selfID > 0 {
|
||||
cqAt := fmt.Sprintf("[CQ:at,qq=%d]", selfID)
|
||||
|
|
@ -408,7 +409,7 @@ func parseMessageContentEx(raw json.RawMessage, selfID int64) parseMessageResult
|
|||
}
|
||||
|
||||
var segments []map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &segments); err == nil {
|
||||
if err := jsonv2.Unmarshal(raw, &segments); err == nil {
|
||||
var text string
|
||||
mentioned := false
|
||||
selfIDStr := strconv.FormatInt(selfID, 10)
|
||||
|
|
@ -497,7 +498,7 @@ func (c *OneBotChannel) normalizeMessageEvent(raw *oneBotRawEvent) (*oneBotEvent
|
|||
|
||||
var sender oneBotSender
|
||||
if len(raw.Sender) > 0 {
|
||||
if err := json.Unmarshal(raw.Sender, &sender); err != nil {
|
||||
if err := jsonv2.Unmarshal(raw.Sender, &sender); err != nil {
|
||||
logger.WarnCF("onebot", "Failed to parse sender", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"sender": string(raw.Sender),
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ package channels
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
|
|
@ -92,7 +92,7 @@ func (c *WhatsAppChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
|||
"content": msg.Content,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(payload)
|
||||
data, err := jsonv2.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal message: %w", err)
|
||||
}
|
||||
|
|
@ -127,7 +127,7 @@ func (c *WhatsAppChannel) listen(ctx context.Context) {
|
|||
}
|
||||
|
||||
var msg map[string]interface{}
|
||||
if err := json.Unmarshal(message, &msg); err != nil {
|
||||
if err := jsonv2.Unmarshal(message, &msg); err != nil {
|
||||
log.Printf("Failed to unmarshal WhatsApp message: %v", err)
|
||||
continue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
)
|
||||
|
||||
// FlexibleStringSlice is a []string that also accepts JSON numbers,
|
||||
|
|
@ -17,14 +18,14 @@ type FlexibleStringSlice []string
|
|||
func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
|
||||
// Try []string first
|
||||
var ss []string
|
||||
if err := json.Unmarshal(data, &ss); err == nil {
|
||||
if err := jsonv2.Unmarshal(data, &ss); err == nil {
|
||||
*f = ss
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try []interface{} to handle mixed types
|
||||
var raw []interface{}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -251,10 +252,10 @@ type ProvidersConfig struct {
|
|||
type ProviderConfig struct {
|
||||
APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"`
|
||||
APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
|
||||
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
|
||||
AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
|
||||
Timeout int `json:"timeout,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_TIMEOUT"` // seconds, 0 = default (120s)
|
||||
ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc`
|
||||
Proxy string `json:"proxy,omitzero" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
|
||||
AuthMethod string `json:"auth_method,omitzero" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
|
||||
Timeout int `json:"timeout,omitzero" env:"PICOCLAW_PROVIDERS_{{.Name}}_TIMEOUT"` // seconds, 0 = default (120s)
|
||||
ConnectMode string `json:"connect_mode,omitzero" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc`
|
||||
}
|
||||
|
||||
type OpenAIProviderConfig struct {
|
||||
|
|
@ -450,7 +451,7 @@ func LoadConfig(path string) (*Config, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -522,7 +523,7 @@ func SaveConfig(path string, cfg *Config) error {
|
|||
cfg.mu.RLock()
|
||||
defer cfg.mu.RUnlock()
|
||||
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
data, err := jsonv2.Marshal(cfg, jsontext.WithIndent(" "))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
|
@ -13,31 +12,34 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/adhocore/gronx"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/memory"
|
||||
)
|
||||
|
||||
type CronSchedule struct {
|
||||
Kind string `json:"kind"`
|
||||
AtMS *int64 `json:"atMs,omitempty"`
|
||||
EveryMS *int64 `json:"everyMs,omitempty"`
|
||||
Expr string `json:"expr,omitempty"`
|
||||
TZ string `json:"tz,omitempty"`
|
||||
AtMS *int64 `json:"atMs,omitzero"`
|
||||
EveryMS *int64 `json:"everyMs,omitzero"`
|
||||
Expr string `json:"expr,omitzero"`
|
||||
TZ string `json:"tz,omitzero"`
|
||||
}
|
||||
|
||||
type CronPayload struct {
|
||||
Kind string `json:"kind"`
|
||||
Message string `json:"message"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Command string `json:"command,omitzero"`
|
||||
Deliver bool `json:"deliver"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
To string `json:"to,omitempty"`
|
||||
Channel string `json:"channel,omitzero"`
|
||||
To string `json:"to,omitzero"`
|
||||
}
|
||||
|
||||
type CronJobState struct {
|
||||
NextRunAtMS *int64 `json:"nextRunAtMs,omitempty"`
|
||||
LastRunAtMS *int64 `json:"lastRunAtMs,omitempty"`
|
||||
LastStatus string `json:"lastStatus,omitempty"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
NextRunAtMS *int64 `json:"nextRunAtMs,omitzero"`
|
||||
LastRunAtMS *int64 `json:"lastRunAtMs,omitzero"`
|
||||
LastStatus string `json:"lastStatus,omitzero"`
|
||||
LastError string `json:"lastError,omitzero"`
|
||||
}
|
||||
|
||||
type CronJob struct {
|
||||
|
|
@ -349,7 +351,7 @@ func (cs *CronService) loadStore() error {
|
|||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, cs.store)
|
||||
return jsonv2.Unmarshal(data, cs.store)
|
||||
}
|
||||
|
||||
func (cs *CronService) loadStoreFromDelegate() error {
|
||||
|
|
@ -360,7 +362,7 @@ func (cs *CronService) loadStoreFromDelegate() error {
|
|||
if err != nil || val == "" {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal([]byte(val), cs.store)
|
||||
return jsonv2.Unmarshal([]byte(val), cs.store)
|
||||
}
|
||||
|
||||
func (cs *CronService) saveStoreUnsafe() error {
|
||||
|
|
@ -373,7 +375,7 @@ func (cs *CronService) saveStoreUnsafe() error {
|
|||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(cs.store, "", " ")
|
||||
data, err := jsonv2.Marshal(cs.store, jsontext.WithIndent(" "))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -382,7 +384,7 @@ func (cs *CronService) saveStoreUnsafe() error {
|
|||
}
|
||||
|
||||
func (cs *CronService) saveStoreToDelegate() error {
|
||||
data, err := json.Marshal(cs.store)
|
||||
data, err := jsonv2.Marshal(cs.store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ package fantasy
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"charm.land/fantasy"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
memstore "github.com/sipeed/picoclaw/pkg/memory/store"
|
||||
|
|
@ -170,7 +171,7 @@ func parseToolArgs(input string) (map[string]interface{}, error) {
|
|||
}
|
||||
|
||||
var args map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(input), &args); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(input), &args); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse tool arguments: %w", err)
|
||||
}
|
||||
return args, nil
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ package fantasy
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"iter"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"charm.land/fantasy"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
// claudeCliProvider implements fantasy.Provider using the claude CLI subprocess.
|
||||
|
|
@ -147,7 +147,7 @@ func extractTextFromCLIOutput(output []byte) string {
|
|||
Content string `json:"content"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
if err := json.Unmarshal(output, &results); err == nil {
|
||||
if err := jsonv2.Unmarshal(output, &results); err == nil {
|
||||
var texts []string
|
||||
for _, r := range results {
|
||||
switch {
|
||||
|
|
@ -167,7 +167,7 @@ func extractTextFromCLIOutput(output []byte) string {
|
|||
Result string `json:"result"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
if err := json.Unmarshal(output, &single); err == nil {
|
||||
if err := jsonv2.Unmarshal(output, &single); err == nil {
|
||||
if single.Result != "" {
|
||||
return single.Result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
package fantasy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"charm.land/fantasy"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/messages"
|
||||
)
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ func MessageToFantasy(msg messages.Message) fantasy.Message {
|
|||
input = tc.Function.Arguments
|
||||
} else if tc.Arguments != nil {
|
||||
// Fallback: serialize the map to JSON.
|
||||
data, _ := json.Marshal(tc.Arguments)
|
||||
data, _ := jsonv2.Marshal(tc.Arguments)
|
||||
input = string(data)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ package health
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
|
@ -20,14 +21,14 @@ type Server struct {
|
|||
type Check struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Message string `json:"message,omitzero"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
type StatusResponse struct {
|
||||
Status string `json:"status"`
|
||||
Uptime string `json:"uptime"`
|
||||
Checks map[string]Check `json:"checks,omitempty"`
|
||||
Checks map[string]Check `json:"checks,omitzero"`
|
||||
}
|
||||
|
||||
func NewServer(host string, port int) *Server {
|
||||
|
|
@ -113,7 +114,7 @@ func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
|
|||
Uptime: uptime.String(),
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
jsonv2.MarshalWrite(w, resp)
|
||||
}
|
||||
|
||||
func (s *Server) readyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -129,7 +130,7 @@ func (s *Server) readyHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
if !ready {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
json.NewEncoder(w).Encode(StatusResponse{
|
||||
jsonv2.MarshalWrite(w, StatusResponse{
|
||||
Status: "not ready",
|
||||
Checks: checks,
|
||||
})
|
||||
|
|
@ -139,7 +140,7 @@ func (s *Server) readyHandler(w http.ResponseWriter, r *http.Request) {
|
|||
for _, check := range checks {
|
||||
if check.Status == "fail" {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
json.NewEncoder(w).Encode(StatusResponse{
|
||||
jsonv2.MarshalWrite(w, StatusResponse{
|
||||
Status: "not ready",
|
||||
Checks: checks,
|
||||
})
|
||||
|
|
@ -149,7 +150,7 @@ func (s *Server) readyHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
uptime := time.Since(s.startTime)
|
||||
json.NewEncoder(w).Encode(StatusResponse{
|
||||
jsonv2.MarshalWrite(w, StatusResponse{
|
||||
Status: "ready",
|
||||
Uptime: uptime.String(),
|
||||
Checks: checks,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package ids
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
func TestNew_IsV7(t *testing.T) {
|
||||
|
|
@ -159,14 +160,14 @@ func TestScan_InvalidType(t *testing.T) {
|
|||
func TestJSON_RoundTrip(t *testing.T) {
|
||||
u := New()
|
||||
|
||||
b, err := json.Marshal(u)
|
||||
b, err := jsonv2.Marshal(u)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal: %v", err)
|
||||
}
|
||||
|
||||
// Should be a quoted string
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
if err := jsonv2.Unmarshal(b, &s); err != nil {
|
||||
t.Fatalf("Unmarshal to string: %v", err)
|
||||
}
|
||||
if s != u.String() {
|
||||
|
|
@ -174,7 +175,7 @@ func TestJSON_RoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
var parsed UUID
|
||||
if err := json.Unmarshal(b, &parsed); err != nil {
|
||||
if err := jsonv2.Unmarshal(b, &parsed); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if parsed != u {
|
||||
|
|
@ -184,7 +185,7 @@ func TestJSON_RoundTrip(t *testing.T) {
|
|||
|
||||
func TestJSON_ZeroUUID(t *testing.T) {
|
||||
var zero UUID
|
||||
b, err := json.Marshal(zero)
|
||||
b, err := jsonv2.Marshal(zero)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal zero: %v", err)
|
||||
}
|
||||
|
|
@ -201,13 +202,13 @@ func TestJSON_InStruct(t *testing.T) {
|
|||
}
|
||||
|
||||
r := record{ID: New(), Name: "test"}
|
||||
b, err := json.Marshal(r)
|
||||
b, err := jsonv2.Marshal(r)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal struct: %v", err)
|
||||
}
|
||||
|
||||
var decoded record
|
||||
if err := json.Unmarshal(b, &decoded); err != nil {
|
||||
if err := jsonv2.Unmarshal(b, &decoded); err != nil {
|
||||
t.Fatalf("Unmarshal struct: %v", err)
|
||||
}
|
||||
if decoded.ID != r.ID {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ package ids
|
|||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
// UUID represents a 16-byte RFC-9562 UUIDv7 value.
|
||||
|
|
@ -85,13 +86,13 @@ func (u *UUID) Scan(src interface{}) error {
|
|||
|
||||
// MarshalJSON encodes UUID as JSON string.
|
||||
func (u UUID) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(u.String())
|
||||
return jsonv2.Marshal(u.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes UUID from JSON string.
|
||||
func (u *UUID) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
if err := jsonv2.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
parsed, err := Parse(s)
|
||||
|
|
|
|||
|
|
@ -3,20 +3,15 @@
|
|||
// recursive decomposition operations — are serialized as ToolRequest /
|
||||
// ToolResponse pairs.
|
||||
//
|
||||
// The canonical schema lives in commands.fbs. This file provides the Go
|
||||
// types and encoding helpers that replace the raw flatc-generated code while
|
||||
// keeping the same wire format semantics. The encoding uses encoding/json
|
||||
// on the initial implementation path; the FlatBuffers binary encoding is
|
||||
// available via the flatbuffers package when performance demands it.
|
||||
//
|
||||
// Migration path: when the codebase moves to the full FlatBuffers binary
|
||||
// encoding, replace the JSON marshal/unmarshal calls with flatbuffers builder
|
||||
// calls without changing any call sites — the public types remain stable.
|
||||
// The canonical schema lives in commands.fbs. Wire encoding uses FlatBuffers
|
||||
// for all internal transport (socket, daemon, WASM). JSON encoding is
|
||||
// available via MarshalJSON / UnmarshalRequestJSON for the LLM boundary only.
|
||||
package itr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -113,6 +108,10 @@ type CodeExec struct {
|
|||
// command payload and declares dependencies on other nodes by ID.
|
||||
// Args may contain "#nodeN" references that are resolved to the output of
|
||||
// the dependency node at execution time.
|
||||
//
|
||||
// Implements json.Unmarshaler to deserialize Payload directly into the
|
||||
// concrete type indicated by Type, avoiding the map[string]interface{}
|
||||
// erasure that encoding/json applies to interface{} fields.
|
||||
type DAGNode struct {
|
||||
ID string `json:"id"`
|
||||
Type CommandType `json:"type"`
|
||||
|
|
@ -120,6 +119,92 @@ type DAGNode struct {
|
|||
DependsOn []string `json:"depends_on,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSONFrom implements jsonv2.UnmarshalerFrom. It defers Payload
|
||||
// deserialization via jsontext.Value until the Type discriminator is known,
|
||||
// then unmarshals directly into the concrete Go type.
|
||||
func (n *DAGNode) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
||||
var raw struct {
|
||||
ID string `json:"id"`
|
||||
Type CommandType `json:"type"`
|
||||
Payload jsontext.Value `json:"payload"`
|
||||
DependsOn []string `json:"depends_on,omitempty"`
|
||||
}
|
||||
if err := jsonv2.UnmarshalDecode(dec, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
n.ID = raw.ID
|
||||
n.Type = raw.Type
|
||||
n.DependsOn = raw.DependsOn
|
||||
|
||||
if len(raw.Payload) == 0 || string(raw.Payload) == "null" {
|
||||
return nil
|
||||
}
|
||||
|
||||
unmarshalPayload := func(dst any) error {
|
||||
return jsonv2.Unmarshal(raw.Payload, dst, jsonv2.MatchCaseInsensitiveNames(true))
|
||||
}
|
||||
|
||||
switch raw.Type {
|
||||
case CmdToolExec:
|
||||
var p ToolExec
|
||||
if err := unmarshalPayload(&p); err != nil {
|
||||
return fmt.Errorf("DAGNode %s payload: %w", raw.ID, err)
|
||||
}
|
||||
n.Payload = p
|
||||
case CmdToolSearch:
|
||||
var p ToolSearch
|
||||
if err := unmarshalPayload(&p); err != nil {
|
||||
return fmt.Errorf("DAGNode %s payload: %w", raw.ID, err)
|
||||
}
|
||||
n.Payload = p
|
||||
case CmdPeek:
|
||||
var p Peek
|
||||
if err := unmarshalPayload(&p); err != nil {
|
||||
return fmt.Errorf("DAGNode %s payload: %w", raw.ID, err)
|
||||
}
|
||||
n.Payload = p
|
||||
case CmdGrep:
|
||||
var p Grep
|
||||
if err := unmarshalPayload(&p); err != nil {
|
||||
return fmt.Errorf("DAGNode %s payload: %w", raw.ID, err)
|
||||
}
|
||||
n.Payload = p
|
||||
case CmdPartition:
|
||||
var p Partition
|
||||
if err := unmarshalPayload(&p); err != nil {
|
||||
return fmt.Errorf("DAGNode %s payload: %w", raw.ID, err)
|
||||
}
|
||||
n.Payload = p
|
||||
case CmdRecurse:
|
||||
var p Recurse
|
||||
if err := unmarshalPayload(&p); err != nil {
|
||||
return fmt.Errorf("DAGNode %s payload: %w", raw.ID, err)
|
||||
}
|
||||
n.Payload = p
|
||||
case CmdCodeExec:
|
||||
var p CodeExec
|
||||
if err := unmarshalPayload(&p); err != nil {
|
||||
return fmt.Errorf("DAGNode %s payload: %w", raw.ID, err)
|
||||
}
|
||||
n.Payload = p
|
||||
case CmdExecWasm:
|
||||
var p ExecWasm
|
||||
if err := unmarshalPayload(&p); err != nil {
|
||||
return fmt.Errorf("DAGNode %s payload: %w", raw.ID, err)
|
||||
}
|
||||
n.Payload = p
|
||||
case CmdFinal:
|
||||
var p Final
|
||||
if err := unmarshalPayload(&p); err != nil {
|
||||
return fmt.Errorf("DAGNode %s payload: %w", raw.ID, err)
|
||||
}
|
||||
n.Payload = p
|
||||
default:
|
||||
return fmt.Errorf("DAGNode %s: unknown type %q", raw.ID, raw.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DAGPlan is a composite command that contains multiple DAGNodes forming a
|
||||
// Directed Acyclic Graph. The executor dispatches nodes in topological order,
|
||||
// running independent nodes in parallel.
|
||||
|
|
@ -269,30 +354,77 @@ func NewLeakResponse(id, redactedResult string, redactedKeys []string) ToolRespo
|
|||
}
|
||||
}
|
||||
|
||||
// ── Serialisation helpers ────────────────────────────────────────────────────
|
||||
// ── Serialisation helpers (FlatBuffers default) ──────────────────────────────
|
||||
|
||||
// Marshal encodes a ToolRequest to JSON bytes for transmission.
|
||||
// Marshal encodes a ToolRequest to FlatBuffers bytes for internal transport.
|
||||
func (r ToolRequest) Marshal() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
return MarshalRequestFB(r)
|
||||
}
|
||||
|
||||
// Marshal encodes a ToolResponse to JSON bytes for transmission.
|
||||
// Marshal encodes a ToolResponse to FlatBuffers bytes for internal transport.
|
||||
func (r ToolResponse) Marshal() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
return MarshalResponseFB(r)
|
||||
}
|
||||
|
||||
// UnmarshalRequest decodes a ToolRequest from JSON bytes.
|
||||
// UnmarshalRequest decodes a ToolRequest from FlatBuffers bytes.
|
||||
func UnmarshalRequest(data []byte) (ToolRequest, error) {
|
||||
var raw struct {
|
||||
ID string `json:"id"`
|
||||
Type CommandType `json:"type"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Depth uint8 `json:"depth"`
|
||||
SessionKey string `json:"session_key"`
|
||||
ToolCallID string `json:"tool_call_id"`
|
||||
return UnmarshalRequestFB(data)
|
||||
}
|
||||
|
||||
// UnmarshalResponse decodes a ToolResponse from FlatBuffers bytes.
|
||||
func UnmarshalResponse(data []byte) (ToolResponse, error) {
|
||||
return UnmarshalResponseFB(data)
|
||||
}
|
||||
|
||||
// ── JSON helpers (LLM boundary only) ─────────────────────────────────────────
|
||||
|
||||
// MarshalJSONTo implements jsonv2.MarshalerTo. Use only at the LLM boundary.
|
||||
func (r ToolRequest) MarshalJSONTo(enc *jsontext.Encoder) error {
|
||||
type alias struct {
|
||||
ID string `json:"id"`
|
||||
Type CommandType `json:"type"`
|
||||
Payload interface{} `json:"payload"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Depth uint8 `json:"depth,omitzero"`
|
||||
SessionKey string `json:"session_key,omitzero"`
|
||||
ToolCallID string `json:"tool_call_id,omitzero"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return jsonv2.MarshalEncode(enc, alias{r.ID, r.Type, r.Payload, r.Timestamp, r.Depth, r.SessionKey, r.ToolCallID})
|
||||
}
|
||||
|
||||
// MarshalJSONTo implements jsonv2.MarshalerTo. Use only at the LLM boundary.
|
||||
func (r ToolResponse) MarshalJSONTo(enc *jsontext.Encoder) error {
|
||||
type alias struct {
|
||||
ID string `json:"id"`
|
||||
Result string `json:"result,omitzero"`
|
||||
IsError bool `json:"is_error,omitzero"`
|
||||
LeakDetected bool `json:"leak_detected,omitzero"`
|
||||
CostTokens uint32 `json:"cost_tokens,omitzero"`
|
||||
RedactedKeys []string `json:"redacted_keys,omitzero"`
|
||||
}
|
||||
return jsonv2.MarshalEncode(enc, alias{r.ID, r.Result, r.IsError, r.LeakDetected, r.CostTokens, r.RedactedKeys})
|
||||
}
|
||||
|
||||
// LLMJSONOpts returns json/v2 options suitable for unmarshaling LLM-generated JSON,
|
||||
// which may use inconsistent casing.
|
||||
func LLMJSONOpts() jsonv2.Options {
|
||||
return jsonv2.MatchCaseInsensitiveNames(true)
|
||||
}
|
||||
|
||||
// UnmarshalRequestJSON decodes a ToolRequest from JSON bytes. Use only at
|
||||
// the LLM boundary (planner output, tool schemas).
|
||||
func UnmarshalRequestJSON(data []byte) (ToolRequest, error) {
|
||||
opts := LLMJSONOpts()
|
||||
var raw struct {
|
||||
ID string `json:"id"`
|
||||
Type CommandType `json:"type"`
|
||||
Payload jsontext.Value `json:"payload"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Depth uint8 `json:"depth"`
|
||||
SessionKey string `json:"session_key"`
|
||||
ToolCallID string `json:"tool_call_id"`
|
||||
}
|
||||
if err := jsonv2.Unmarshal(data, &raw, opts); err != nil {
|
||||
return ToolRequest{}, err
|
||||
}
|
||||
|
||||
|
|
@ -305,47 +437,51 @@ func UnmarshalRequest(data []byte) (ToolRequest, error) {
|
|||
ToolCallID: raw.ToolCallID,
|
||||
}
|
||||
|
||||
unmarshalPayload := func(dst any) error {
|
||||
return jsonv2.Unmarshal(raw.Payload, dst, opts)
|
||||
}
|
||||
|
||||
var err error
|
||||
switch raw.Type {
|
||||
case CmdPeek:
|
||||
var p Peek
|
||||
err = json.Unmarshal(raw.Payload, &p)
|
||||
err = unmarshalPayload(&p)
|
||||
req.Payload = p
|
||||
case CmdGrep:
|
||||
var p Grep
|
||||
err = json.Unmarshal(raw.Payload, &p)
|
||||
err = unmarshalPayload(&p)
|
||||
req.Payload = p
|
||||
case CmdPartition:
|
||||
var p Partition
|
||||
err = json.Unmarshal(raw.Payload, &p)
|
||||
err = unmarshalPayload(&p)
|
||||
req.Payload = p
|
||||
case CmdRecurse:
|
||||
var p Recurse
|
||||
err = json.Unmarshal(raw.Payload, &p)
|
||||
err = unmarshalPayload(&p)
|
||||
req.Payload = p
|
||||
case CmdToolExec:
|
||||
var p ToolExec
|
||||
err = json.Unmarshal(raw.Payload, &p)
|
||||
err = unmarshalPayload(&p)
|
||||
req.Payload = p
|
||||
case CmdExecWasm:
|
||||
var p ExecWasm
|
||||
err = json.Unmarshal(raw.Payload, &p)
|
||||
err = unmarshalPayload(&p)
|
||||
req.Payload = p
|
||||
case CmdFinal:
|
||||
var p Final
|
||||
err = json.Unmarshal(raw.Payload, &p)
|
||||
err = unmarshalPayload(&p)
|
||||
req.Payload = p
|
||||
case CmdToolSearch:
|
||||
var p ToolSearch
|
||||
err = json.Unmarshal(raw.Payload, &p)
|
||||
err = unmarshalPayload(&p)
|
||||
req.Payload = p
|
||||
case CmdCodeExec:
|
||||
var p CodeExec
|
||||
err = json.Unmarshal(raw.Payload, &p)
|
||||
err = unmarshalPayload(&p)
|
||||
req.Payload = p
|
||||
case CmdDAGPlan:
|
||||
var p DAGPlan
|
||||
err = json.Unmarshal(raw.Payload, &p)
|
||||
err = unmarshalPayload(&p)
|
||||
req.Payload = p
|
||||
default:
|
||||
return ToolRequest{}, fmt.Errorf("unknown command type: %q", raw.Type)
|
||||
|
|
@ -354,8 +490,8 @@ func UnmarshalRequest(data []byte) (ToolRequest, error) {
|
|||
return req, err
|
||||
}
|
||||
|
||||
// UnmarshalResponse decodes a ToolResponse from JSON bytes.
|
||||
func UnmarshalResponse(data []byte) (ToolResponse, error) {
|
||||
// UnmarshalResponseJSON decodes a ToolResponse from JSON bytes.
|
||||
func UnmarshalResponseJSON(data []byte) (ToolResponse, error) {
|
||||
var r ToolResponse
|
||||
return r, json.Unmarshal(data, &r)
|
||||
return r, jsonv2.Unmarshal(data, &r)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ package dag
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
|
@ -36,11 +35,11 @@ type RLMExpandFunc func(ctx context.Context, sessionKey, query, contextContent s
|
|||
|
||||
// Executor runs a DAGPlan through the SecureBus with topological dispatch.
|
||||
type Executor struct {
|
||||
bus *securebus.Bus
|
||||
joiner JoinerFunc
|
||||
rlmExpand RLMExpandFunc
|
||||
rlmThresholdBytes int
|
||||
maxParallel int
|
||||
bus *securebus.Bus
|
||||
joiner JoinerFunc
|
||||
rlmExpand RLMExpandFunc
|
||||
rlmThresholdBytes int
|
||||
maxParallel int
|
||||
}
|
||||
|
||||
// ExecutorOption configures an Executor via the functional options pattern.
|
||||
|
|
@ -133,8 +132,9 @@ func (e *Executor) Execute(ctx context.Context, sessionKey string, plan *itr.DAG
|
|||
select {
|
||||
case <-depState.done:
|
||||
if _, depErr := depState.getResult(); depErr != nil {
|
||||
ns.setResult("", fmt.Errorf("dependency %s failed: %w", dep, depErr))
|
||||
errOnce.Do(func() { waveErr = depErr })
|
||||
wrapped := fmt.Errorf("dependency %s failed: %w", dep, depErr)
|
||||
ns.setResult("", wrapped)
|
||||
errOnce.Do(func() { waveErr = wrapped })
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
|
|
@ -215,34 +215,31 @@ func (e *Executor) Execute(ctx context.Context, sessionKey string, plan *itr.DAG
|
|||
// executeNode dispatches a single node through the SecureBus after resolving
|
||||
// dependency references in its payload.
|
||||
func (e *Executor) executeNode(ctx context.Context, sessionKey string, node *itr.DAGNode, states map[string]*nodeState) itr.ToolResponse {
|
||||
req := nodeToRequest(sessionKey, node, states)
|
||||
req, err := nodeToRequest(sessionKey, node, states)
|
||||
if err != nil {
|
||||
return itr.NewErrorResponse(node.ID, err.Error())
|
||||
}
|
||||
return e.bus.Execute(ctx, req)
|
||||
}
|
||||
|
||||
// nodeToRequest converts a DAGNode into a ToolRequest, resolving #nodeN
|
||||
// references in tool arguments.
|
||||
func nodeToRequest(sessionKey string, node *itr.DAGNode, states map[string]*nodeState) itr.ToolRequest {
|
||||
func nodeToRequest(sessionKey string, node *itr.DAGNode, states map[string]*nodeState) (itr.ToolRequest, error) {
|
||||
switch node.Type {
|
||||
case itr.CmdToolExec:
|
||||
te, ok := node.Payload.(itr.ToolExec)
|
||||
if !ok {
|
||||
if m, ok := node.Payload.(map[string]interface{}); ok {
|
||||
b, _ := json.Marshal(m)
|
||||
_ = json.Unmarshal(b, &te)
|
||||
}
|
||||
return itr.ToolRequest{}, fmt.Errorf("node %s: expected ToolExec payload, got %T", node.ID, node.Payload)
|
||||
}
|
||||
te.ArgsJSON = resolveToolExecArgs(te.ArgsJSON, states)
|
||||
return itr.NewToolExecRequest(node.ID, sessionKey, node.ID, te.ToolName, te.ArgsJSON)
|
||||
return itr.NewToolExecRequest(node.ID, sessionKey, node.ID, te.ToolName, te.ArgsJSON), nil
|
||||
|
||||
case itr.CmdToolSearch:
|
||||
ts, ok := node.Payload.(itr.ToolSearch)
|
||||
if !ok {
|
||||
if m, ok := node.Payload.(map[string]interface{}); ok {
|
||||
b, _ := json.Marshal(m)
|
||||
_ = json.Unmarshal(b, &ts)
|
||||
}
|
||||
return itr.ToolRequest{}, fmt.Errorf("node %s: expected ToolSearch payload, got %T", node.ID, node.Payload)
|
||||
}
|
||||
return itr.NewToolSearchRequest(node.ID, sessionKey, ts.Query, ts.MaxResults)
|
||||
return itr.NewToolSearchRequest(node.ID, sessionKey, ts.Query, ts.MaxResults), nil
|
||||
|
||||
default:
|
||||
return itr.ToolRequest{
|
||||
|
|
@ -250,7 +247,7 @@ func nodeToRequest(sessionKey string, node *itr.DAGNode, states map[string]*node
|
|||
Type: node.Type,
|
||||
Payload: node.Payload,
|
||||
SessionKey: sessionKey,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package dag_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/itr"
|
||||
|
|
@ -123,7 +123,11 @@ func TestExecutor_WithJoiner(t *testing.T) {
|
|||
defer bus.Close()
|
||||
|
||||
joiner := func(_ context.Context, _, userQuery string) (string, uint32, error) {
|
||||
return "synthesized: " + userQuery[:20], 50, nil
|
||||
n := len(userQuery)
|
||||
if n > 20 {
|
||||
n = 20
|
||||
}
|
||||
return "synthesized: " + userQuery[:n], 50, nil
|
||||
}
|
||||
|
||||
executor := dag.NewExecutor(bus, joiner)
|
||||
|
|
@ -173,7 +177,8 @@ func TestResolver_NodeRefSubstitution(t *testing.T) {
|
|||
|
||||
result, err := executor.Execute(context.Background(), "test-sess", plan)
|
||||
require.NoError(t, err)
|
||||
_ = result
|
||||
assert.Contains(t, result.NodeResults["prev"], "previous-output")
|
||||
assert.Contains(t, result.NodeResults["search"], "found")
|
||||
}
|
||||
|
||||
func TestRouter_SimpleQuerySelectsReAct(t *testing.T) {
|
||||
|
|
@ -225,7 +230,7 @@ func TestPlanner_ValidatePlan(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var plan itr.DAGPlan
|
||||
err := json.Unmarshal([]byte(tt.plan), &plan)
|
||||
err := jsonv2.Unmarshal([]byte(tt.plan), &plan)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use planner with a mock that returns the pre-built plan JSON
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package dag
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/itr"
|
||||
"github.com/sipeed/picoclaw/pkg/tools"
|
||||
|
|
@ -17,10 +17,10 @@ type PlannerFunc func(ctx context.Context, systemPrompt, userQuery string) (stri
|
|||
// LLM to produce a structured JSON plan. The LLM outputs a list of nodes
|
||||
// with dependency edges in a single inference pass (LLMCompiler pattern).
|
||||
type Planner struct {
|
||||
callModel PlannerFunc
|
||||
registry *tools.ToolRegistry
|
||||
maxParallel uint8
|
||||
tokenBudget uint32
|
||||
callModel PlannerFunc
|
||||
registry *tools.ToolRegistry
|
||||
maxParallel uint8
|
||||
tokenBudget uint32
|
||||
}
|
||||
|
||||
// PlannerConfig configures the DAG planner.
|
||||
|
|
@ -115,32 +115,10 @@ func parsePlanResponse(response string) (*itr.DAGPlan, error) {
|
|||
response = extractJSON(response)
|
||||
|
||||
var plan itr.DAGPlan
|
||||
if err := json.Unmarshal([]byte(response), &plan); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(response), &plan, itr.LLMJSONOpts()); err != nil {
|
||||
return nil, fmt.Errorf("JSON parse error: %w\nraw: %s", err, truncate(response, 500))
|
||||
}
|
||||
|
||||
// Unmarshal re-encodes Payload as map[string]interface{} via JSON round-trip.
|
||||
// Convert payload maps to their proper types.
|
||||
for i := range plan.Nodes {
|
||||
n := &plan.Nodes[i]
|
||||
m, ok := n.Payload.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
b, _ := json.Marshal(m)
|
||||
|
||||
switch n.Type {
|
||||
case itr.CmdToolExec:
|
||||
var te itr.ToolExec
|
||||
_ = json.Unmarshal(b, &te)
|
||||
n.Payload = te
|
||||
case itr.CmdToolSearch:
|
||||
var ts itr.ToolSearch
|
||||
_ = json.Unmarshal(b, &ts)
|
||||
n.Payload = ts
|
||||
}
|
||||
}
|
||||
|
||||
return &plan, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package dag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -67,11 +67,10 @@ func resolveToolExecArgs(argsJSON string, states map[string]*nodeState) string {
|
|||
|
||||
// escapeForJSON makes a string safe for embedding into a JSON value.
|
||||
func escapeForJSON(s string) string {
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
b, err := jsonv2.Marshal(s)
|
||||
if err != nil || len(b) < 2 {
|
||||
return s
|
||||
}
|
||||
// Strip surrounding quotes since we're replacing within an existing string.
|
||||
return string(b[1 : len(b)-1])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
|
@ -9,6 +8,8 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
type LogLevel int
|
||||
|
|
@ -43,10 +44,10 @@ type Logger struct {
|
|||
type LogEntry struct {
|
||||
Level string `json:"level"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Component string `json:"component,omitempty"`
|
||||
Component string `json:"component,omitzero"`
|
||||
Message string `json:"message"`
|
||||
Fields map[string]interface{} `json:"fields,omitempty"`
|
||||
Caller string `json:"caller,omitempty"`
|
||||
Fields map[string]interface{} `json:"fields,omitzero"`
|
||||
Caller string `json:"caller,omitzero"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -117,7 +118,7 @@ func logMessage(level LogLevel, component string, message string, fields map[str
|
|||
}
|
||||
|
||||
if logger.file != nil {
|
||||
jsonData, err := json.Marshal(entry)
|
||||
jsonData, err := jsonv2.Marshal(entry)
|
||||
if err == nil {
|
||||
logger.file.WriteString(string(jsonData) + "\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ package dag
|
|||
// BudgetConfig defines the percentage allocation for each context section.
|
||||
// All percentages should sum to 100.
|
||||
type BudgetConfig struct {
|
||||
SystemPromptPct int // % for system prompt (identity, rules, skills)
|
||||
ObservationsPct int // % for observation block
|
||||
KnowledgePct int // % for knowledge block (Focus completions)
|
||||
DAGSummariesPct int // % for DAG compressed history
|
||||
RawTailPct int // % for raw recent messages (uncompressed tail)
|
||||
ToolResultsPct int // % for tool call results
|
||||
SystemPromptPct int // % for system prompt (identity, rules, skills)
|
||||
ObservationsPct int // % for observation block
|
||||
KnowledgePct int // % for knowledge block (Focus completions)
|
||||
DAGSummariesPct int // % for DAG compressed history
|
||||
RawTailPct int // % for raw recent messages (uncompressed tail)
|
||||
ToolResultsPct int // % for tool call results
|
||||
}
|
||||
|
||||
// DefaultBudgetConfig returns a balanced allocation.
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ type Message struct {
|
|||
|
||||
// CompressorConfig controls the deterministic compression behavior.
|
||||
type CompressorConfig struct {
|
||||
ChunkSize int // Messages per chunk node (default 8)
|
||||
SectionSize int // Chunks per section node (default 4)
|
||||
MaxSentences int // Max sentences to extract per message (default 2)
|
||||
TargetRatio float64 // Target compression ratio (default 0.25 = 4:1)
|
||||
ChunkSize int // Messages per chunk node (default 8)
|
||||
SectionSize int // Chunks per section node (default 4)
|
||||
MaxSentences int // Max sentences to extract per message (default 2)
|
||||
TargetRatio float64 // Target compression ratio (default 0.25 = 4:1)
|
||||
}
|
||||
|
||||
// DefaultCompressorConfig returns sensible defaults.
|
||||
|
|
|
|||
|
|
@ -220,9 +220,9 @@ func TestSelectDAGLevel(t *testing.T) {
|
|||
|
||||
func TestTailMessageCount(t *testing.T) {
|
||||
assert.Equal(t, 4, TailMessageCount(100)) // Minimum
|
||||
assert.Equal(t, 20, TailMessageCount(1000)) // 1000/50
|
||||
assert.Equal(t, 4, TailMessageCount(0)) // Zero budget
|
||||
assert.Equal(t, 4, TailMessageCount(-1)) // Negative
|
||||
assert.Equal(t, 20, TailMessageCount(1000)) // 1000/50
|
||||
assert.Equal(t, 4, TailMessageCount(0)) // Zero budget
|
||||
assert.Equal(t, 4, TailMessageCount(-1)) // Negative
|
||||
}
|
||||
|
||||
func TestRenderDAGForBudget(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package delegate
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/ids"
|
||||
|
|
@ -38,7 +38,7 @@ func TestCronKVBackend_Roundtrip(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(store)
|
||||
data, err := jsonv2.Marshal(store)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, d.UpsertKV(ctx, agentID, kvKey, string(data)))
|
||||
|
|
@ -48,7 +48,7 @@ func TestCronKVBackend_Roundtrip(t *testing.T) {
|
|||
require.NotEmpty(t, raw)
|
||||
|
||||
var loaded cronStore
|
||||
require.NoError(t, json.Unmarshal([]byte(raw), &loaded))
|
||||
require.NoError(t, jsonv2.Unmarshal([]byte(raw), &loaded))
|
||||
|
||||
assert.Equal(t, 1, loaded.Version)
|
||||
assert.Len(t, loaded.Jobs, 2)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ package memory
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
@ -23,7 +23,7 @@ const migrationMarkerFile = ".sessions_migrated"
|
|||
type SessionFile struct {
|
||||
Key string `json:"key"`
|
||||
Messages []SessionMsg `json:"messages"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Summary string `json:"summary,omitzero"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ func MigrateFileSessions(ctx context.Context, del MemoryDelegate, agentID, sessi
|
|||
}
|
||||
|
||||
var sess SessionFile
|
||||
if err := json.Unmarshal(data, &sess); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &sess); err != nil {
|
||||
logger.WarnCF("migrate", "Failed to parse session file",
|
||||
map[string]interface{}{"path": sessPath, "error": err.Error()})
|
||||
result.Errors++
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package memory
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
|
@ -103,7 +103,7 @@ func (m *mockDelegate) CountAuditEntries(_ context.Context, _ string) (int, erro
|
|||
|
||||
func writeSessionFile(t *testing.T, dir, name string, sess SessionFile) {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(sess)
|
||||
data, err := jsonv2.Marshal(sess)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal session: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package memory
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -10,8 +10,8 @@ import (
|
|||
)
|
||||
|
||||
type legacyState struct {
|
||||
LastChannel string `json:"last_channel,omitempty"`
|
||||
LastChatID string `json:"last_chat_id,omitempty"`
|
||||
LastChannel string `json:"last_channel,omitzero"`
|
||||
LastChatID string `json:"last_chat_id,omitzero"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ func MigrateState(ctx context.Context, workspace string, delegate MemoryDelegate
|
|||
}
|
||||
|
||||
var s legacyState
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &s); err != nil {
|
||||
log.Printf("[WARN] migrate_state: failed to parse %s: %v", stateFile, err)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ type Manager struct {
|
|||
observer *Observer
|
||||
reflector *Reflector
|
||||
|
||||
mu sync.Mutex
|
||||
running map[string]bool // sessionKey -> running flag to prevent concurrent runs
|
||||
mu sync.Mutex
|
||||
running map[string]bool // sessionKey -> running flag to prevent concurrent runs
|
||||
}
|
||||
|
||||
// ManagerConfig bundles configuration for the observation system.
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
package observation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -25,8 +25,8 @@ type Priority string
|
|||
|
||||
const (
|
||||
PriorityCritical Priority = "critical" // 🔴
|
||||
PriorityNotable Priority = "notable" // 🟡
|
||||
PriorityInformational Priority = "informational" // 🔵
|
||||
PriorityNotable Priority = "notable" // 🟡
|
||||
PriorityInformational Priority = "informational" // 🔵
|
||||
)
|
||||
|
||||
func (p Priority) Emoji() string {
|
||||
|
|
@ -97,7 +97,7 @@ func FormatBlock(observations []Observation) string {
|
|||
|
||||
// MarshalObservations serializes observations to JSON for KV storage.
|
||||
func MarshalObservations(obs []Observation) (string, error) {
|
||||
data, err := json.Marshal(obs)
|
||||
data, err := jsonv2.Marshal(obs)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal observations: %w", err)
|
||||
}
|
||||
|
|
@ -110,7 +110,7 @@ func UnmarshalObservations(data string) ([]Observation, error) {
|
|||
return nil, nil
|
||||
}
|
||||
var obs []Observation
|
||||
if err := json.Unmarshal([]byte(data), &obs); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(data), &obs); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal observations: %w", err)
|
||||
}
|
||||
return obs, nil
|
||||
|
|
|
|||
|
|
@ -238,8 +238,8 @@ func TestParseKeptIndices(t *testing.T) {
|
|||
{"normal", "KEEP 0\nDROP 1\nKEEP 2", 2},
|
||||
{"all keep", "KEEP 0\nKEEP 1\nKEEP 2", 3},
|
||||
{"all drop", "DROP 0\nDROP 1\nDROP 2", 1}, // Fallback keeps critical
|
||||
{"invalid output", "blah blah", 1}, // Fallback keeps critical
|
||||
{"out of range", "KEEP 99", 1}, // Fallback keeps critical
|
||||
{"invalid output", "blah blah", 1}, // Fallback keeps critical
|
||||
{"out of range", "KEEP 99", 1}, // Fallback keeps critical
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -38,11 +38,12 @@ func TestNewEmbedderFromConfig_OpenAI_NoKey(t *testing.T) {
|
|||
|
||||
func TestNewEmbedderFromConfig_OpenAI_FallbackKey(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
respData, _ := jsonv2.Marshal(map[string]interface{}{
|
||||
"data": []map[string]interface{}{
|
||||
{"index": 0, "embedding": []float32{0.1, 0.2, 0.3}},
|
||||
},
|
||||
})
|
||||
w.Write(respData)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
|
|
@ -70,9 +71,10 @@ func TestNewEmbedderFromConfig_OpenAI_FallbackKey(t *testing.T) {
|
|||
|
||||
func TestNewEmbedderFromConfig_Ollama(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
respData, _ := jsonv2.Marshal(map[string]interface{}{
|
||||
"embeddings": [][]float32{{0.1, 0.2, 0.3}},
|
||||
})
|
||||
w.Write(respData)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
|
|
@ -97,9 +99,10 @@ func TestNewEmbedderFromConfig_Ollama(t *testing.T) {
|
|||
|
||||
func TestNewEmbedderFromConfig_Ollama_FallbackBase(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
respData, _ := jsonv2.Marshal(map[string]interface{}{
|
||||
"embeddings": [][]float32{{0.5}},
|
||||
})
|
||||
w.Write(respData)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
|
|
@ -119,9 +122,10 @@ func TestNewEmbedderFromConfig_Ollama_FallbackBase(t *testing.T) {
|
|||
|
||||
func TestNewEmbedderFromConfig_CaseInsensitive(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
respData, _ := jsonv2.Marshal(map[string]interface{}{
|
||||
"embeddings": [][]float32{{0.1}},
|
||||
})
|
||||
w.Write(respData)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package store
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
|
@ -93,7 +93,7 @@ func (o *OllamaEmbedder) embedBatch(ctx context.Context, texts []string) ([]memo
|
|||
}
|
||||
|
||||
func (o *OllamaEmbedder) embedSingle(ctx context.Context, text string) (memory.Embedding, error) {
|
||||
body, err := json.Marshal(ollamaEmbedRequest{
|
||||
body, err := jsonv2.Marshal(ollamaEmbedRequest{
|
||||
Model: o.model,
|
||||
Input: text,
|
||||
})
|
||||
|
|
@ -119,7 +119,7 @@ func (o *OllamaEmbedder) embedSingle(ctx context.Context, text string) (memory.E
|
|||
}
|
||||
|
||||
var result ollamaEmbedResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
if err := jsonv2.UnmarshalRead(resp.Body, &result); err != nil {
|
||||
return nil, fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package store
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -18,14 +18,15 @@ func TestOllamaEmbedder_Embed(t *testing.T) {
|
|||
}
|
||||
|
||||
var req ollamaEmbedRequest
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
jsonv2.UnmarshalRead(r.Body, &req)
|
||||
if req.Model != "test-model" {
|
||||
t.Errorf("expected model 'test-model', got %q", req.Model)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(ollamaEmbedResponse{
|
||||
respData, _ := jsonv2.Marshal(ollamaEmbedResponse{
|
||||
Embeddings: [][]float32{{0.1, 0.2, 0.3, 0.4}},
|
||||
})
|
||||
w.Write(respData)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
|
|
@ -55,9 +56,10 @@ func TestOllamaEmbedder_EmbedBatch(t *testing.T) {
|
|||
callCount := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
json.NewEncoder(w).Encode(ollamaEmbedResponse{
|
||||
respData, _ := jsonv2.Marshal(ollamaEmbedResponse{
|
||||
Embeddings: [][]float32{{float32(callCount) * 0.1}},
|
||||
})
|
||||
w.Write(respData)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
|
|
@ -91,7 +93,8 @@ func TestOllamaEmbedder_ServerError(t *testing.T) {
|
|||
|
||||
func TestOllamaEmbedder_EmptyResponse(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(ollamaEmbedResponse{Embeddings: [][]float32{}})
|
||||
respData, _ := jsonv2.Marshal(ollamaEmbedResponse{Embeddings: [][]float32{}})
|
||||
w.Write(respData)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package store
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
|
@ -59,7 +59,7 @@ func NewOpenAIEmbedder(cfg OpenAIEmbedderConfig) *OpenAIEmbedder {
|
|||
type openAIEmbedRequest struct {
|
||||
Input interface{} `json:"input"` // string or []string
|
||||
Model string `json:"model"`
|
||||
EncodingFormat string `json:"encoding_format,omitempty"`
|
||||
EncodingFormat string `json:"encoding_format,omitzero"`
|
||||
}
|
||||
|
||||
type openAIEmbedResponse struct {
|
||||
|
|
@ -100,7 +100,7 @@ func (o *OpenAIEmbedder) call(ctx context.Context, texts []string) ([]memory.Emb
|
|||
input = texts
|
||||
}
|
||||
|
||||
body, err := json.Marshal(openAIEmbedRequest{
|
||||
body, err := jsonv2.Marshal(openAIEmbedRequest{
|
||||
Input: input,
|
||||
Model: o.model,
|
||||
EncodingFormat: "float",
|
||||
|
|
@ -130,7 +130,7 @@ func (o *OpenAIEmbedder) call(ctx context.Context, texts []string) ([]memory.Emb
|
|||
}
|
||||
|
||||
var result openAIEmbedResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
if err := jsonv2.UnmarshalRead(resp.Body, &result); err != nil {
|
||||
return nil, fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package store
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -20,16 +20,17 @@ func TestOpenAIEmbedder_Embed(t *testing.T) {
|
|||
}
|
||||
|
||||
var req openAIEmbedRequest
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
jsonv2.UnmarshalRead(r.Body, &req)
|
||||
if req.Model != "test-embed" {
|
||||
t.Errorf("expected model 'test-embed', got %q", req.Model)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(openAIEmbedResponse{
|
||||
respData, _ := jsonv2.Marshal(openAIEmbedResponse{
|
||||
Data: []openAIEmbedData{
|
||||
{Index: 0, Embedding: []float32{0.5, 0.6, 0.7}},
|
||||
},
|
||||
})
|
||||
w.Write(respData)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ func TestOpenAIEmbedder_Embed(t *testing.T) {
|
|||
func TestOpenAIEmbedder_EmbedBatch(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req openAIEmbedRequest
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
jsonv2.UnmarshalRead(r.Body, &req)
|
||||
|
||||
// Batch request should send array
|
||||
texts, ok := req.Input.([]interface{})
|
||||
|
|
@ -73,7 +74,8 @@ func TestOpenAIEmbedder_EmbedBatch(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(openAIEmbedResponse{Data: data})
|
||||
respData, _ := jsonv2.Marshal(openAIEmbedResponse{Data: data})
|
||||
w.Write(respData)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
|
|
@ -94,16 +96,17 @@ func TestOpenAIEmbedder_EmbedBatch(t *testing.T) {
|
|||
func TestOpenAIEmbedder_SingleTextNotArray(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req openAIEmbedRequest
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
jsonv2.UnmarshalRead(r.Body, &req)
|
||||
|
||||
// Single text should be sent as string, not array
|
||||
if _, ok := req.Input.(string); !ok {
|
||||
t.Errorf("expected string input for single text, got %T", req.Input)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(openAIEmbedResponse{
|
||||
respData, _ := jsonv2.Marshal(openAIEmbedResponse{
|
||||
Data: []openAIEmbedData{{Index: 0, Embedding: []float32{1.0}}},
|
||||
})
|
||||
w.Write(respData)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
|
|
@ -140,9 +143,10 @@ func TestOpenAIEmbedder_NoAuth(t *testing.T) {
|
|||
if r.Header.Get("Authorization") != "" {
|
||||
t.Error("expected no auth header when key is empty")
|
||||
}
|
||||
json.NewEncoder(w).Encode(openAIEmbedResponse{
|
||||
respData, _ := jsonv2.Marshal(openAIEmbedResponse{
|
||||
Data: []openAIEmbedData{{Index: 0, Embedding: []float32{1.0}}},
|
||||
})
|
||||
w.Write(respData)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package store
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"strings"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/ids"
|
||||
|
|
@ -25,31 +25,31 @@ const (
|
|||
// MemoryToolRequest is the input to the memory tool.
|
||||
type MemoryToolRequest struct {
|
||||
Action MemoryToolAction `json:"action"`
|
||||
Query string `json:"query,omitempty"` // For search
|
||||
ID string `json:"id,omitempty"` // For read/update/delete
|
||||
Content string `json:"content,omitempty"` // For write/update
|
||||
Source string `json:"source,omitempty"` // For write
|
||||
Sector string `json:"sector,omitempty"` // For write: episodic/semantic/procedural/reflective
|
||||
Tags string `json:"tags,omitempty"` // For write: comma-separated
|
||||
Tier string `json:"tier,omitempty"` // "recall" or "archival" — defaults to "recall"
|
||||
Limit int `json:"limit,omitempty"` // For search — defaults to 5
|
||||
Query string `json:"query,omitzero"` // For search
|
||||
ID string `json:"id,omitzero"` // For read/update/delete
|
||||
Content string `json:"content,omitzero"` // For write/update
|
||||
Source string `json:"source,omitzero"` // For write
|
||||
Sector string `json:"sector,omitzero"` // For write: episodic/semantic/procedural/reflective
|
||||
Tags string `json:"tags,omitzero"` // For write: comma-separated
|
||||
Tier string `json:"tier,omitzero"` // "recall" or "archival" — defaults to "recall"
|
||||
Limit int `json:"limit,omitzero"` // For search — defaults to 5
|
||||
}
|
||||
|
||||
// MemoryToolResponse is the output of the memory tool.
|
||||
type MemoryToolResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Results []MemoryToolEntry `json:"results,omitempty"`
|
||||
Status *MemoryToolStatus `json:"status,omitempty"`
|
||||
Message string `json:"message,omitzero"`
|
||||
Results []MemoryToolEntry `json:"results,omitzero"`
|
||||
Status *MemoryToolStatus `json:"status,omitzero"`
|
||||
}
|
||||
|
||||
// MemoryToolEntry is a single memory entry in tool results.
|
||||
type MemoryToolEntry struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Sector string `json:"sector,omitempty"`
|
||||
Score float64 `json:"score,omitempty"`
|
||||
Source string `json:"source,omitzero"`
|
||||
Sector string `json:"sector,omitzero"`
|
||||
Score float64 `json:"score,omitzero"`
|
||||
}
|
||||
|
||||
// MemoryToolStatus summarizes the memory system state.
|
||||
|
|
@ -82,7 +82,7 @@ func NewMemoryTool(store *MemoryStore, agentID, session string) *MemoryTool {
|
|||
// Execute processes a memory tool request and returns a JSON response.
|
||||
func (t *MemoryTool) Execute(ctx context.Context, input string) (string, error) {
|
||||
var req MemoryToolRequest
|
||||
if err := json.Unmarshal([]byte(input), &req); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(input), &req); err != nil {
|
||||
return t.errorResponse("invalid input: " + err.Error()), nil
|
||||
}
|
||||
|
||||
|
|
@ -325,6 +325,6 @@ func (t *MemoryTool) errorResponse(msg string) string {
|
|||
}
|
||||
|
||||
func (t *MemoryTool) jsonResponse(resp *MemoryToolResponse) string {
|
||||
b, _ := json.Marshal(resp)
|
||||
b, _ := jsonv2.Marshal(resp)
|
||||
return string(b)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package store
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -22,7 +22,7 @@ func executeAndParse(t *testing.T, tool *MemoryTool, input string) *MemoryToolRe
|
|||
require.NoError(t, err)
|
||||
|
||||
var resp MemoryToolResponse
|
||||
require.NoError(t, json.Unmarshal([]byte(raw), &resp))
|
||||
require.NoError(t, jsonv2.Unmarshal([]byte(raw), &resp))
|
||||
return &resp
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
package migrate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ func LoadOpenClawConfig(configPath string) (map[string]interface{}, error) {
|
|||
}
|
||||
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &raw); err != nil {
|
||||
return nil, fmt.Errorf("parsing OpenClaw config: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package migrate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
|
|
@ -104,7 +105,7 @@ func TestLoadOpenClawConfig(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(openclawConfig)
|
||||
data, err := jsonv2.Marshal(openclawConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -583,7 +584,7 @@ func TestRunDryRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(configData)
|
||||
data, _ := jsonv2.Marshal(configData)
|
||||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
||||
|
||||
opts := Options{
|
||||
|
|
@ -638,7 +639,7 @@ func TestRunFullMigration(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(configData)
|
||||
data, _ := jsonv2.Marshal(configData)
|
||||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
||||
|
||||
opts := Options{
|
||||
|
|
@ -784,7 +785,7 @@ func TestRunConfigOnly(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(configData)
|
||||
data, _ := jsonv2.Marshal(configData)
|
||||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
||||
|
||||
opts := Options{
|
||||
|
|
@ -824,7 +825,7 @@ func TestRunWorkspaceOnly(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(configData)
|
||||
data, _ := jsonv2.Marshal(configData)
|
||||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
||||
|
||||
opts := Options{
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
package pcerrors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
// CLIHandler is the PicoClaw error lifecycle boundary for the CLI.
|
||||
|
|
@ -50,7 +51,7 @@ func (h CLIHandler) Handle(err error) int {
|
|||
if h.Verbose {
|
||||
record["detail"] = err.Error()
|
||||
}
|
||||
data, _ := json.Marshal(record)
|
||||
data, _ := jsonv2.Marshal(record)
|
||||
_, _ = fmt.Fprintln(w, string(data))
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(w, msg)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
package security
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -50,12 +50,12 @@ func ExtractJSON(text string, dest interface{}, opts *ExtractJSONOptions) error
|
|||
return ErrNoJSON
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(strings.NewReader(cleaned))
|
||||
var jsonOpts []jsonv2.Options
|
||||
if opts.DisallowUnknownFields {
|
||||
dec.DisallowUnknownFields()
|
||||
jsonOpts = append(jsonOpts, jsonv2.RejectUnknownMembers(true))
|
||||
}
|
||||
|
||||
if err := dec.Decode(dest); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(cleaned), dest, jsonOpts...); err != nil {
|
||||
return fmt.Errorf("json decode: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ func TestExtractJSON_InjectionAttempts(t *testing.T) {
|
|||
&ExtractJSONOptions{DisallowUnknownFields: true},
|
||||
func(t *testing.T, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unknown field")
|
||||
assert.Contains(t, err.Error(), "unknown object member name")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package security
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
|
@ -132,7 +133,7 @@ func (ss *SecretStore) load() error {
|
|||
return err
|
||||
}
|
||||
var entries []SecretEntry
|
||||
if err := json.Unmarshal(data, &entries); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &entries); err != nil {
|
||||
return fmt.Errorf("parse secret store: %w", err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
|
|
@ -150,7 +151,7 @@ func (ss *SecretStore) save() error {
|
|||
}
|
||||
ss.mu.RUnlock()
|
||||
|
||||
data, err := json.MarshalIndent(entries, "", " ")
|
||||
data, err := jsonv2.Marshal(entries, jsontext.WithIndent(" "))
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal secret store: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package securebus
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/itr"
|
||||
|
|
@ -62,6 +64,7 @@ type Bus struct {
|
|||
executor ToolExecutor
|
||||
toolSearch ToolSearchFunc // nil = no tool search support
|
||||
done chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
// New creates a Bus and starts background worker goroutines.
|
||||
|
|
@ -106,10 +109,12 @@ func (b *Bus) AuditLog() *AuditLog {
|
|||
return b.audit
|
||||
}
|
||||
|
||||
// Close shuts down the bus workers gracefully.
|
||||
// Close shuts down the bus workers gracefully. Safe to call multiple times.
|
||||
func (b *Bus) Close() {
|
||||
b.transport.Close()
|
||||
close(b.done)
|
||||
b.closeOnce.Do(func() {
|
||||
b.transport.Close()
|
||||
close(b.done)
|
||||
})
|
||||
}
|
||||
|
||||
// Execute is a convenience method for in-process callers that don't want to
|
||||
|
|
@ -155,12 +160,16 @@ func (b *Bus) dispatch(ctx context.Context, req itr.ToolRequest) itr.ToolRespons
|
|||
case itr.CmdToolSearch:
|
||||
resp := b.handleToolSearch(ctx, req)
|
||||
event.DurationMS = time.Since(start).Milliseconds()
|
||||
_ = b.audit.Append(event)
|
||||
if err := b.audit.Append(event); err != nil {
|
||||
log.Printf("securebus: audit append failed: %v", err)
|
||||
}
|
||||
return resp
|
||||
default:
|
||||
resp := b.handleRLMCommand(ctx, req)
|
||||
event.DurationMS = time.Since(start).Milliseconds()
|
||||
_ = b.audit.Append(event)
|
||||
if err := b.audit.Append(event); err != nil {
|
||||
log.Printf("securebus: audit append failed: %v", err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +177,9 @@ func (b *Bus) dispatch(ctx context.Context, req itr.ToolRequest) itr.ToolRespons
|
|||
if !ok {
|
||||
event.IsError = true
|
||||
event.DurationMS = time.Since(start).Milliseconds()
|
||||
_ = b.audit.Append(event)
|
||||
if err := b.audit.Append(event); err != nil {
|
||||
log.Printf("securebus: audit append failed: %v", err)
|
||||
}
|
||||
return itr.NewErrorResponse(req.ID, "internal: payload is not ToolExec")
|
||||
}
|
||||
event.ToolName = te.ToolName
|
||||
|
|
@ -184,17 +195,21 @@ func (b *Bus) dispatch(ctx context.Context, req itr.ToolRequest) itr.ToolRespons
|
|||
event.IsError = true
|
||||
event.PolicyViolation = err.Error()
|
||||
event.DurationMS = time.Since(start).Milliseconds()
|
||||
_ = b.audit.Append(event)
|
||||
if err := b.audit.Append(event); err != nil {
|
||||
log.Printf("securebus: audit append failed: %v", err)
|
||||
}
|
||||
return itr.NewErrorResponse(req.ID, "policy violation: "+err.Error())
|
||||
}
|
||||
|
||||
// 3. Deserialise args — always produce a non-nil map for safe injection.
|
||||
args := make(map[string]interface{})
|
||||
if te.ArgsJSON != "" && te.ArgsJSON != "null" {
|
||||
if err := json.Unmarshal([]byte(te.ArgsJSON), &args); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(te.ArgsJSON), &args); err != nil {
|
||||
event.IsError = true
|
||||
event.DurationMS = time.Since(start).Milliseconds()
|
||||
_ = b.audit.Append(event)
|
||||
if err := b.audit.Append(event); err != nil {
|
||||
log.Printf("securebus: audit append failed: %v", err)
|
||||
}
|
||||
return itr.NewErrorResponse(req.ID, fmt.Sprintf("invalid args JSON: %v", err))
|
||||
}
|
||||
}
|
||||
|
|
@ -204,7 +219,9 @@ func (b *Bus) dispatch(ctx context.Context, req itr.ToolRequest) itr.ToolRespons
|
|||
if err != nil {
|
||||
event.IsError = true
|
||||
event.DurationMS = time.Since(start).Milliseconds()
|
||||
_ = b.audit.Append(event)
|
||||
if err := b.audit.Append(event); err != nil {
|
||||
log.Printf("securebus: audit append failed: %v", err)
|
||||
}
|
||||
return itr.NewErrorResponse(req.ID, "secret injection failed: "+err.Error())
|
||||
}
|
||||
event.SecretsAccessed = injectedSecrets
|
||||
|
|
@ -232,7 +249,9 @@ func (b *Bus) dispatch(ctx context.Context, req itr.ToolRequest) itr.ToolRespons
|
|||
|
||||
event.IsError = resp.IsError
|
||||
event.DurationMS = time.Since(start).Milliseconds()
|
||||
_ = b.audit.Append(event)
|
||||
if err := b.audit.Append(event); err != nil {
|
||||
log.Printf("securebus: audit append failed: %v", err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package securebus_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/itr"
|
||||
|
|
@ -49,7 +49,7 @@ func makeArgsJSON(kv map[string]interface{}) string {
|
|||
if kv == nil {
|
||||
return "{}"
|
||||
}
|
||||
b, _ := json.Marshal(kv)
|
||||
b, _ := jsonv2.Marshal(kv)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
|
|
@ -248,3 +248,56 @@ func TestBus_RLMFinalCommand(t *testing.T) {
|
|||
assert.False(t, resp.IsError)
|
||||
assert.Equal(t, "the answer", resp.Result)
|
||||
}
|
||||
|
||||
func TestBus_CloseIdempotent(t *testing.T) {
|
||||
bus := makeBus(t, nil, nil)
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
bus.Close()
|
||||
bus.Close()
|
||||
bus.Close()
|
||||
}, "Close() must be safe to call multiple times")
|
||||
}
|
||||
|
||||
func TestBus_ToolSearch(t *testing.T) {
|
||||
bus := makeBus(t, nil, nil)
|
||||
defer bus.Close()
|
||||
|
||||
bus.SetToolSearch(func(query string, maxResults int) string {
|
||||
return `[{"name":"read_file","description":"reads a file"}]`
|
||||
})
|
||||
|
||||
req := itr.NewToolSearchRequest("req-search", "sess", "file operations", 5)
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
|
||||
assert.False(t, resp.IsError)
|
||||
assert.Contains(t, resp.Result, "read_file")
|
||||
}
|
||||
|
||||
func TestBus_ToolSearchNotConfigured(t *testing.T) {
|
||||
bus := makeBus(t, nil, nil)
|
||||
defer bus.Close()
|
||||
|
||||
req := itr.NewToolSearchRequest("req-search2", "sess", "anything", 5)
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
|
||||
assert.True(t, resp.IsError)
|
||||
assert.Contains(t, resp.Result, "not configured")
|
||||
}
|
||||
|
||||
func TestBus_NilToolResult(t *testing.T) {
|
||||
executor := func(ctx context.Context, name string, args map[string]interface{}) *tools.ToolResult {
|
||||
return nil
|
||||
}
|
||||
capLookup := func(name string) (tools.ToolCapabilities, bool) {
|
||||
return tools.ZeroCapabilities(), true
|
||||
}
|
||||
bus := securebus.New(securebus.DefaultBusConfig(), nil, capLookup, executor)
|
||||
defer bus.Close()
|
||||
|
||||
req := itr.NewToolExecRequest("req-nil", "sess", "tc", "something", "{}")
|
||||
resp := bus.Execute(context.Background(), req)
|
||||
|
||||
assert.False(t, resp.IsError)
|
||||
assert.Empty(t, resp.Result)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@ package session
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/cache"
|
||||
"github.com/sipeed/picoclaw/pkg/ids"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
|
|
@ -19,7 +21,7 @@ import (
|
|||
type Session struct {
|
||||
Key string `json:"key"`
|
||||
Messages []messages.Message `json:"messages"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Summary string `json:"summary,omitzero"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
|
@ -197,7 +199,7 @@ func (sm *SessionManager) loadSessionFromDisk(key string) *Session {
|
|||
return nil
|
||||
}
|
||||
var session Session
|
||||
if err := json.Unmarshal(data, &session); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &session); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &session
|
||||
|
|
@ -487,7 +489,7 @@ func snapshotSession(s *Session) Session {
|
|||
}
|
||||
|
||||
func (sm *SessionManager) writeSessionToDisk(key string, session *Session) error {
|
||||
data, err := json.MarshalIndent(session, "", " ")
|
||||
data, err := jsonv2.Marshal(session, jsontext.WithIndent(" "))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -558,7 +560,7 @@ func (sm *SessionManager) loadSessions() error {
|
|||
}
|
||||
|
||||
var session Session
|
||||
if err := json.Unmarshal(data, &session); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &session); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package skills
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
|
@ -10,6 +9,8 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
type SkillInstaller struct {
|
||||
|
|
@ -117,7 +118,7 @@ func (si *SkillInstaller) ListAvailableSkills(ctx context.Context) ([]AvailableS
|
|||
}
|
||||
|
||||
var skills []AvailableSkill
|
||||
if err := json.Unmarshal(body, &skills); err != nil {
|
||||
if err := jsonv2.Unmarshal(body, &skills); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse skills list: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package skills
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
|
@ -9,6 +8,8 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
var namePattern = regexp.MustCompile(`^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$`)
|
||||
|
|
@ -21,10 +22,10 @@ const (
|
|||
type SkillMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Links []string `json:"links,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
IsMOC bool `json:"is_moc,omitempty"`
|
||||
Tags []string `json:"tags,omitzero"`
|
||||
Links []string `json:"links,omitzero"`
|
||||
Domain string `json:"domain,omitzero"`
|
||||
IsMOC bool `json:"is_moc,omitzero"`
|
||||
}
|
||||
|
||||
type SkillInfo struct {
|
||||
|
|
@ -32,8 +33,8 @@ type SkillInfo struct {
|
|||
Path string `json:"path"`
|
||||
Source string `json:"source"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Tags []string `json:"tags,omitzero"`
|
||||
Domain string `json:"domain,omitzero"`
|
||||
}
|
||||
|
||||
func (info SkillInfo) validate() error {
|
||||
|
|
@ -124,18 +125,18 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo {
|
|||
continue
|
||||
}
|
||||
|
||||
info := SkillInfo{
|
||||
Name: dir.Name(),
|
||||
Path: skillFile,
|
||||
Source: "global",
|
||||
}
|
||||
metadata := sl.getSkillMetadata(skillFile)
|
||||
if metadata != nil {
|
||||
info.Description = metadata.Description
|
||||
info.Name = metadata.Name
|
||||
info.Tags = metadata.Tags
|
||||
info.Domain = metadata.Domain
|
||||
}
|
||||
info := SkillInfo{
|
||||
Name: dir.Name(),
|
||||
Path: skillFile,
|
||||
Source: "global",
|
||||
}
|
||||
metadata := sl.getSkillMetadata(skillFile)
|
||||
if metadata != nil {
|
||||
info.Description = metadata.Description
|
||||
info.Name = metadata.Name
|
||||
info.Tags = metadata.Tags
|
||||
info.Domain = metadata.Domain
|
||||
}
|
||||
if err := info.validate(); err != nil {
|
||||
slog.Warn("invalid skill from global", "name", info.Name, "error", err)
|
||||
continue
|
||||
|
|
@ -165,18 +166,18 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo {
|
|||
continue
|
||||
}
|
||||
|
||||
info := SkillInfo{
|
||||
Name: dir.Name(),
|
||||
Path: skillFile,
|
||||
Source: "builtin",
|
||||
}
|
||||
metadata := sl.getSkillMetadata(skillFile)
|
||||
if metadata != nil {
|
||||
info.Description = metadata.Description
|
||||
info.Name = metadata.Name
|
||||
info.Tags = metadata.Tags
|
||||
info.Domain = metadata.Domain
|
||||
}
|
||||
info := SkillInfo{
|
||||
Name: dir.Name(),
|
||||
Path: skillFile,
|
||||
Source: "builtin",
|
||||
}
|
||||
metadata := sl.getSkillMetadata(skillFile)
|
||||
if metadata != nil {
|
||||
info.Description = metadata.Description
|
||||
info.Name = metadata.Name
|
||||
info.Tags = metadata.Tags
|
||||
info.Domain = metadata.Domain
|
||||
}
|
||||
if err := info.validate(); err != nil {
|
||||
slog.Warn("invalid skill from builtin", "name", info.Name, "error", err)
|
||||
continue
|
||||
|
|
@ -281,7 +282,7 @@ func (sl *SkillsLoader) getSkillMetadata(skillPath string) *SkillMetadata {
|
|||
|
||||
// Try JSON first (for backward compatibility)
|
||||
var jsonMeta SkillMetadata
|
||||
if err := json.Unmarshal([]byte(frontmatter), &jsonMeta); err == nil && jsonMeta.Name != "" {
|
||||
if err := jsonv2.Unmarshal([]byte(frontmatter), &jsonMeta); err == nil && jsonMeta.Name != "" {
|
||||
return &jsonMeta
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package state
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
|
@ -10,6 +9,9 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/memory"
|
||||
)
|
||||
|
||||
|
|
@ -19,10 +21,10 @@ const kvAgentID = "picoclaw"
|
|||
// It includes information about the last active channel/chat.
|
||||
type State struct {
|
||||
// LastChannel is the last channel used for communication
|
||||
LastChannel string `json:"last_channel,omitempty"`
|
||||
LastChannel string `json:"last_channel,omitzero"`
|
||||
|
||||
// LastChatID is the last chat ID used for communication
|
||||
LastChatID string `json:"last_chat_id,omitempty"`
|
||||
LastChatID string `json:"last_chat_id,omitzero"`
|
||||
|
||||
// Timestamp is the last time this state was updated
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
|
|
@ -73,7 +75,7 @@ func NewManager(workspace string, opts ...Option) *Manager {
|
|||
|
||||
if _, err := os.Stat(stateFile); os.IsNotExist(err) {
|
||||
if data, err := os.ReadFile(oldStateFile); err == nil {
|
||||
if err := json.Unmarshal(data, sm.state); err == nil {
|
||||
if err := jsonv2.Unmarshal(data, sm.state); err == nil {
|
||||
sm.saveAtomic()
|
||||
log.Printf("[INFO] state: migrated state from %s to %s", oldStateFile, stateFile)
|
||||
}
|
||||
|
|
@ -180,7 +182,7 @@ func (sm *Manager) saveAtomic() error {
|
|||
tempFile := sm.stateFile + ".tmp"
|
||||
|
||||
// Marshal state to JSON
|
||||
data, err := json.MarshalIndent(sm.state, "", " ")
|
||||
data, err := jsonv2.Marshal(sm.state, jsontext.WithIndent(" "))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal state: %w", err)
|
||||
}
|
||||
|
|
@ -211,7 +213,7 @@ func (sm *Manager) load() error {
|
|||
return fmt.Errorf("failed to read state file: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, sm.state); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, sm.state); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal state: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
func TestAtomicSave(t *testing.T) {
|
||||
|
|
@ -162,7 +163,7 @@ func TestConcurrentAccess(t *testing.T) {
|
|||
}
|
||||
|
||||
var state State
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &state); err != nil {
|
||||
t.Errorf("State file contains invalid JSON: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ package tools
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
)
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ func (t *ToolCallTool) Execute(ctx context.Context, args map[string]interface{})
|
|||
if len(v) > maxArgsJSON {
|
||||
return ErrorResult(fmt.Sprintf("arguments JSON too large: %d bytes (max %d)", len(v), maxArgsJSON))
|
||||
}
|
||||
if err := json.Unmarshal([]byte(v), &toolArgs); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(v), &toolArgs); err != nil {
|
||||
return ErrorResult(fmt.Sprintf("invalid arguments JSON: %v", err))
|
||||
}
|
||||
case nil:
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ package tools
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
"github.com/sipeed/picoclaw/pkg/messages"
|
||||
"github.com/sipeed/picoclaw/pkg/session"
|
||||
|
|
@ -109,7 +110,7 @@ func (t *StartFocusTool) Execute(ctx context.Context, args map[string]interface{
|
|||
StartedAt: time.Now(),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(state)
|
||||
data, err := jsonv2.Marshal(state)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("marshal focus state: %v", err))
|
||||
}
|
||||
|
|
@ -181,7 +182,7 @@ func (t *CompleteFocusTool) Execute(ctx context.Context, args map[string]interfa
|
|||
}
|
||||
|
||||
var state FocusState
|
||||
if err := json.Unmarshal([]byte(raw), &state); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(raw), &state); err != nil {
|
||||
return ErrorResult(fmt.Sprintf("corrupt focus state: %v", err))
|
||||
}
|
||||
|
||||
|
|
@ -251,7 +252,7 @@ func (t *CompleteFocusTool) appendKnowledge(ctx context.Context, sessionKey, top
|
|||
kb := &KnowledgeBlock{}
|
||||
raw, err := t.delegate.GetKV(ctx, focusAgentID, kvKey)
|
||||
if err == nil && raw != "" {
|
||||
_ = json.Unmarshal([]byte(raw), kb)
|
||||
_ = jsonv2.Unmarshal([]byte(raw), kb)
|
||||
}
|
||||
|
||||
kb.Entries = append(kb.Entries, KnowledgeEntry{
|
||||
|
|
@ -260,7 +261,7 @@ func (t *CompleteFocusTool) appendKnowledge(ctx context.Context, sessionKey, top
|
|||
CreatedAt: time.Now(),
|
||||
})
|
||||
|
||||
data, err := json.Marshal(kb)
|
||||
data, err := jsonv2.Marshal(kb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -280,7 +281,7 @@ func LoadKnowledgeBlock(ctx context.Context, delegate KVStore, sessionKey string
|
|||
}
|
||||
|
||||
var kb KnowledgeBlock
|
||||
if err := json.Unmarshal([]byte(raw), &kb); err != nil {
|
||||
if err := jsonv2.Unmarshal([]byte(raw), &kb); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ package tools
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/messages"
|
||||
"github.com/sipeed/picoclaw/pkg/session"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -75,7 +76,7 @@ func TestStartFocus(t *testing.T) {
|
|||
require.NotEmpty(t, raw)
|
||||
|
||||
var state FocusState
|
||||
require.NoError(t, json.Unmarshal([]byte(raw), &state))
|
||||
require.NoError(t, jsonv2.Unmarshal([]byte(raw), &state))
|
||||
assert.Equal(t, "investigate auth bug", state.Topic)
|
||||
assert.Equal(t, 2, state.CheckpointIndex)
|
||||
}
|
||||
|
|
@ -134,7 +135,7 @@ func TestCompleteFocus(t *testing.T) {
|
|||
require.NotEmpty(t, knowledgeRaw)
|
||||
|
||||
var kb KnowledgeBlock
|
||||
require.NoError(t, json.Unmarshal([]byte(knowledgeRaw), &kb))
|
||||
require.NoError(t, jsonv2.Unmarshal([]byte(knowledgeRaw), &kb))
|
||||
require.Len(t, kb.Entries, 1)
|
||||
assert.Equal(t, "debug auth", kb.Entries[0].Topic)
|
||||
assert.Contains(t, kb.Entries[0].Summary, "token validation")
|
||||
|
|
@ -184,7 +185,7 @@ func TestCompleteFocus_MultipleKnowledgeEntries(t *testing.T) {
|
|||
|
||||
knowledgeRaw, _ := delegate.GetKV(ctx, focusAgentID, knowledgeKVPrefix+sk)
|
||||
var kb KnowledgeBlock
|
||||
require.NoError(t, json.Unmarshal([]byte(knowledgeRaw), &kb))
|
||||
require.NoError(t, jsonv2.Unmarshal([]byte(knowledgeRaw), &kb))
|
||||
require.Len(t, kb.Entries, 2)
|
||||
assert.Equal(t, "topic A", kb.Entries[0].Topic)
|
||||
assert.Equal(t, "topic B", kb.Entries[1].Topic)
|
||||
|
|
@ -286,7 +287,7 @@ func TestLoadKnowledgeBlock(t *testing.T) {
|
|||
{Topic: "Test", Summary: "Test summary"},
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(kb)
|
||||
data, _ := jsonv2.Marshal(kb)
|
||||
_ = delegate.UpsertKV(ctx, focusAgentID, knowledgeKVPrefix+"test-session", string(data))
|
||||
|
||||
block = LoadKnowledgeBlock(ctx, delegate, "test-session")
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package tools
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
)
|
||||
|
||||
// I2CTool provides I2C bus interaction for reading sensors and controlling peripherals.
|
||||
|
|
@ -111,7 +113,7 @@ func (t *I2CTool) detect() *ToolResult {
|
|||
}
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(buses, "", " ")
|
||||
result, _ := jsonv2.Marshal(buses, jsontext.WithIndent(" "))
|
||||
return SilentResult(fmt.Sprintf("Found %d I2C bus(es):\n%s", len(buses), string(result)))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
)
|
||||
|
||||
// I2C ioctl constants from Linux kernel headers (<linux/i2c-dev.h>, <linux/i2c.h>)
|
||||
|
|
@ -104,7 +106,7 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
|||
|
||||
type deviceEntry struct {
|
||||
Address string `json:"address"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Status string `json:"status,omitzero"`
|
||||
}
|
||||
|
||||
var found []deviceEntry
|
||||
|
|
@ -133,11 +135,11 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
|||
return SilentResult(fmt.Sprintf("No devices found on %s. Check wiring and pull-up resistors.", devPath))
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
||||
result, _ := jsonv2.Marshal(map[string]interface{}{
|
||||
"bus": devPath,
|
||||
"devices": found,
|
||||
"count": len(found),
|
||||
}, "", " ")
|
||||
}, jsontext.WithIndent(" "))
|
||||
return SilentResult(fmt.Sprintf("Scan of %s:\n%s", devPath, string(result)))
|
||||
}
|
||||
|
||||
|
|
@ -201,13 +203,13 @@ func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
|
|||
intBytes[i] = int(buf[i])
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
||||
result, _ := jsonv2.Marshal(map[string]interface{}{
|
||||
"bus": devPath,
|
||||
"address": fmt.Sprintf("0x%02x", addr),
|
||||
"bytes": intBytes,
|
||||
"hex": hexBytes,
|
||||
"length": n,
|
||||
}, "", " ")
|
||||
}, jsontext.WithIndent(" "))
|
||||
return SilentResult(string(result))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package tools
|
||||
|
||||
import "encoding/json"
|
||||
import jsonv2 "github.com/go-json-experiment/json"
|
||||
|
||||
// ToolResult represents the structured return value from tool execution.
|
||||
// It provides clear semantics for different types of results and supports
|
||||
|
|
@ -124,7 +124,7 @@ func UserResult(content string) *ToolResult {
|
|||
// The Err field is excluded from JSON output via the json:"-" tag.
|
||||
func (tr *ToolResult) MarshalJSON() ([]byte, error) {
|
||||
type Alias ToolResult
|
||||
return json.Marshal(&struct {
|
||||
return jsonv2.Marshal(&struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(tr),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
func TestNewToolResult(t *testing.T) {
|
||||
|
|
@ -125,14 +126,14 @@ func TestToolResultJSONSerialization(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Marshal to JSON
|
||||
data, err := json.Marshal(tt.result)
|
||||
data, err := jsonv2.Marshal(tt.result)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
// Unmarshal back
|
||||
var decoded ToolResult
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("Failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -168,13 +169,13 @@ func TestToolResultWithErrors(t *testing.T) {
|
|||
}
|
||||
|
||||
// Verify Err is not serialized
|
||||
data, marshalErr := json.Marshal(result)
|
||||
data, marshalErr := jsonv2.Marshal(result)
|
||||
if marshalErr != nil {
|
||||
t.Fatalf("Failed to marshal: %v", marshalErr)
|
||||
}
|
||||
|
||||
var decoded ToolResult
|
||||
if unmarshalErr := json.Unmarshal(data, &decoded); unmarshalErr != nil {
|
||||
if unmarshalErr := jsonv2.Unmarshal(data, &decoded); unmarshalErr != nil {
|
||||
t.Fatalf("Failed to unmarshal: %v", unmarshalErr)
|
||||
}
|
||||
|
||||
|
|
@ -186,14 +187,14 @@ func TestToolResultWithErrors(t *testing.T) {
|
|||
func TestToolResultJSONStructure(t *testing.T) {
|
||||
result := UserResult("test content")
|
||||
|
||||
data, err := json.Marshal(result)
|
||||
data, err := jsonv2.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
// Verify JSON structure
|
||||
var parsed map[string]interface{}
|
||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||
if err := jsonv2.Unmarshal(data, &parsed); err != nil {
|
||||
t.Fatalf("Failed to parse JSON: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue