diff --git a/Makefile b/Makefile index 2fc565c..8d6424b 100644 --- a/Makefile +++ b/Makefile @@ -23,11 +23,12 @@ GOLANGCI := $(GOBIN_DIR)/golangci-lint GOFUMPT := $(GOBIN_DIR)/gofumpt GOIMPORTS := $(GOBIN_DIR)/goimports GOVULNCHECK := $(GOBIN_DIR)/govulncheck +OAPICODEGEN := $(GOBIN_DIR)/oapi-codegen # --------------------------------------------------------------------------- # Phony declarations (alphabetical). # --------------------------------------------------------------------------- -.PHONY: all bench boundary-guard build ci clean cover fmt help lint lint-fix \ +.PHONY: all bench boundary-guard build ci clean cover fmt gen help lint lint-fix \ security test test-10x test-race tidy version vet # --------------------------------------------------------------------------- @@ -96,7 +97,7 @@ tidy: ## Tidy go.mod / go.sum. # --------------------------------------------------------------------------- # Composite gate used by CI and pre-push. # --------------------------------------------------------------------------- -ci: tidy fmt vet boundary-guard lint test-race security ## Run everything CI runs. +ci: tidy gen fmt vet boundary-guard lint test-race security ## Run everything CI runs. @echo "All CI checks passed." # --------------------------------------------------------------------------- @@ -107,6 +108,10 @@ version: ## Print the version that will be embedded. @echo "Commit: $(COMMIT)" @echo "Date: $(DATE)" +gen: ## Generate code from the OpenAPI spec (requires oapi-codegen). + go generate ./internal/spec/ + @echo "Code generation complete." + clean: ## Remove build artefacts. rm -rf coverage.out coverage.html go clean -testcache diff --git a/agent_test.go b/agent_test.go index 94217b2..a61fd2b 100644 --- a/agent_test.go +++ b/agent_test.go @@ -55,7 +55,7 @@ func TestAgent_ChatWithTools(t *testing.T) { json.NewEncoder(w).Encode(ChatWithToolsResponse{ ChatResponse: ChatResponse{SessionID: "s-tools"}, ToolCalls: []ToolCall{ - {ID: "tc-1", Name: "greet", Arguments: `{"name":"world"}`}, + {ID: "tc-1", Name: "greet", Arguments: map[string]interface{}{"name": "world"}}, }, }) } else { @@ -80,9 +80,10 @@ func TestAgent_ChatWithTools(t *testing.T) { Description: "Greets someone", Parameters: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"}}}`), }, - Run: func(ctx context.Context, args string) (string, error) { + Run: func(ctx context.Context, args map[string]interface{}) (string, error) { var p struct{ Name string } - json.Unmarshal([]byte(args), &p) + b, _ := json.Marshal(args) + json.Unmarshal(b, &p) return "Hello, " + p.Name + "!", nil }, }, diff --git a/benchmark_test.go b/benchmark_test.go index d3a6440..9b400bf 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -258,7 +258,7 @@ func BenchmarkChatWithTools(b *testing.B) { if round%2 == 1 { json.NewEncoder(w).Encode(ChatWithToolsResponse{ ChatResponse: ChatResponse{SessionID: "s-1", Response: ""}, - ToolCalls: []ToolCall{{ID: "tc-1", Name: "echo", Arguments: `{"msg":"hi"}`}}, + ToolCalls: []ToolCall{{ID: "tc-1", Name: "echo", Arguments: map[string]interface{}{"msg": "hi"}}}, FinishReason: "tool_calls", }) } else { @@ -274,7 +274,7 @@ func BenchmarkChatWithTools(b *testing.B) { tools := []Tool{ { Schema: ToolSchema{Name: "echo", Description: "echo", Parameters: json.RawMessage(`{"type":"object"}`)}, - Run: func(_ context.Context, args string) (string, error) { return "echoed", nil }, + Run: func(_ context.Context, args map[string]interface{}) (string, error) { return "echoed", nil }, }, } ctx := context.Background() diff --git a/codegen_tools.go b/codegen_tools.go new file mode 100644 index 0000000..1abd3dd --- /dev/null +++ b/codegen_tools.go @@ -0,0 +1,7 @@ +//go:build tools + +package hawksdk + +import ( + _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" +) diff --git a/go.mod b/go.mod index 2e08f16..9b5a383 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,28 @@ module github.com/GrayCodeAI/hawk-sdk-go go 1.26.4 + +require github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 + +require ( + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/getkin/kin-openapi v0.135.0 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oasdiff/yaml v0.0.9 // indirect + github.com/oasdiff/yaml3 v0.0.9 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/speakeasy-api/jsonpath v0.6.3 // indirect + github.com/speakeasy-api/openapi v1.19.2 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/tools v0.42.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d84bb17 --- /dev/null +++ b/go.sum @@ -0,0 +1,181 @@ +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.135.0 h1:751SjYfbiwqukYuVjwYEIKNfrSwS5YpA7DZnKSwQgtg= +github.com/getkin/kin-openapi v0.135.0/go.mod h1:6dd5FJl6RdX4usBtFBaQhk9q62Yb2J0Mk5IhUO/QqFI= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 h1:a7Ab7YlpqkVG5HKrTaeFstm32Z5QOnyjnbsCO0jiMYM= +github.com/oapi-codegen/oapi-codegen/v2 v2.7.1/go.mod h1:qzFy6iuobJw/hD1aRILee4G87/ShmhR0xYCwcUtZMCw= +github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48= +github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM= +github.com/oasdiff/yaml3 v0.0.9 h1:rWPrKccrdUm8J0F3sGuU+fuh9+1K/RdJlWF7O/9yw2g= +github.com/oasdiff/yaml3 v0.0.9/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/speakeasy-api/jsonpath v0.6.3 h1:c+QPwzAOdrWvzycuc9HFsIZcxKIaWcNpC+xhOW9rJxU= +github.com/speakeasy-api/jsonpath v0.6.3/go.mod h1:2cXloNuQ+RSXi5HTRaeBh7JEmjRXTiaKpFTdZiL7URI= +github.com/speakeasy-api/openapi v1.19.2 h1:md90tE71/M8jS3cuRlsuWP5Aed4xoG5PSRvXeZgCv/M= +github.com/speakeasy-api/openapi v1.19.2/go.mod h1:UfKa7FqE4jgexJZuj51MmdHAFGmDv0Zaw3+yOd81YKU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/spec/api_gen.gen.go b/internal/spec/api_gen.gen.go new file mode 100644 index 0000000..1513b67 --- /dev/null +++ b/internal/spec/api_gen.gen.go @@ -0,0 +1,115 @@ +// Package spec provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.7.1 DO NOT EDIT. +package spec + +import ( + "time" +) + +const ( + ApiKeyAuthScopes apiKeyAuthContextKey = "ApiKeyAuth.Scopes" + BearerAuthScopes bearerAuthContextKey = "BearerAuth.Scopes" +) + +// Defines values for MessageRole. +const ( + Assistant MessageRole = "assistant" + Tool MessageRole = "tool" + User MessageRole = "user" +) + +// Valid indicates whether the value is a known member of the MessageRole enum. +func (e MessageRole) Valid() bool { + switch e { + case Assistant: + return true + case Tool: + return true + case User: + return true + default: + return false + } +} + +// ChatRequest defines model for ChatRequest. +type ChatRequest struct { + // Message The user message to send to the agent + Message string `json:"message"` + + // Model Optional model override + Model *string `json:"model,omitempty"` + + // SessionId Continue an existing session (omit to start a new one) + SessionId *string `json:"session_id,omitempty"` + + // Stream Use SSE streaming for the response + Stream *bool `json:"stream,omitempty"` +} + +// ChatResponse defines model for ChatResponse. +type ChatResponse struct { + Model *string `json:"model,omitempty"` + Response *string `json:"response,omitempty"` + SessionId *string `json:"session_id,omitempty"` + TokensIn *int `json:"tokens_in,omitempty"` + TokensOut *int `json:"tokens_out,omitempty"` +} + +// Error defines model for Error. +type Error struct { + Code *string `json:"code,omitempty"` + Error *string `json:"error,omitempty"` +} + +// Message defines model for Message. +type Message struct { + Content *string `json:"content,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + Id *string `json:"id,omitempty"` + Role *MessageRole `json:"role,omitempty"` + SessionId *string `json:"session_id,omitempty"` +} + +// MessageRole defines model for Message.Role. +type MessageRole string + +// Session defines model for Session. +type Session struct { + CreatedAt *time.Time `json:"created_at,omitempty"` + Id *string `json:"id,omitempty"` + MessageCount *int `json:"message_count,omitempty"` + Model *string `json:"model,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` +} + +// Stats defines model for Stats. +type Stats struct { + ActiveSessions *int `json:"active_sessions,omitempty"` + TokensIn *int `json:"tokens_in,omitempty"` + TokensOut *int `json:"tokens_out,omitempty"` + TotalMessages *int `json:"total_messages,omitempty"` + TotalSessions *int `json:"total_sessions,omitempty"` +} + +// apiKeyAuthContextKey is the context key for ApiKeyAuth security scheme +type apiKeyAuthContextKey string + +// bearerAuthContextKey is the context key for BearerAuth security scheme +type bearerAuthContextKey string + +// GetV1SessionsParams defines parameters for GetV1Sessions. +type GetV1SessionsParams struct { + Limit *int `form:"limit,omitempty" json:"limit,omitempty"` + Offset *int `form:"offset,omitempty" json:"offset,omitempty"` +} + +// GetV1SessionsIdMessagesParams defines parameters for GetV1SessionsIdMessages. +type GetV1SessionsIdMessagesParams struct { + Limit *int `form:"limit,omitempty" json:"limit,omitempty"` + Offset *int `form:"offset,omitempty" json:"offset,omitempty"` +} + +// PostV1ChatJSONRequestBody defines body for PostV1Chat for application/json ContentType. +type PostV1ChatJSONRequestBody = ChatRequest diff --git a/internal/spec/generate.go b/internal/spec/generate.go new file mode 100644 index 0000000..1737721 --- /dev/null +++ b/internal/spec/generate.go @@ -0,0 +1,3 @@ +package spec + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=oapi-codegen.yaml ../../../hawk/api/openapi.yaml diff --git a/internal/spec/oapi-codegen.yaml b/internal/spec/oapi-codegen.yaml new file mode 100644 index 0000000..54d9f57 --- /dev/null +++ b/internal/spec/oapi-codegen.yaml @@ -0,0 +1,6 @@ +package: spec +generate: + models: true + client: false + embedded-spec: false +output: api_gen.gen.go diff --git a/internal/spec/spec_test.go b/internal/spec/spec_test.go new file mode 100644 index 0000000..7f9d1f2 --- /dev/null +++ b/internal/spec/spec_test.go @@ -0,0 +1,66 @@ +package spec + +import ( + "encoding/json" + "testing" +) + +func TestSpecTypesCompileAndJSONRoundTrip(t *testing.T) { + // Verify ChatRequest JSON round-trip matches OpenAPI schema expectations. + req := ChatRequest{ + Message: "hello", + } + b, err := json.Marshal(req) + if err != nil { + t.Fatalf("Marshal ChatRequest: %v", err) + } + var got ChatRequest + if err := json.Unmarshal(b, &got); err != nil { + t.Fatalf("Unmarshal ChatRequest: %v", err) + } + if got.Message != "hello" { + t.Errorf("Message = %q, want %q", got.Message, "hello") + } +} + +func TestSpecEnumValues(t *testing.T) { + if User != "user" { + t.Errorf("User = %q, want %q", User, "user") + } + if Assistant != "assistant" { + t.Errorf("Assistant = %q, want %q", Assistant, "assistant") + } + if Tool != "tool" { + t.Errorf("Tool = %q, want %q", Tool, "tool") + } +} + +func TestSpecTypesNotEmpty(t *testing.T) { + var chatReq ChatRequest + if chatReq.Message != "" { + t.Error("zero value ChatRequest.Message should be empty") + } + var sess Session + if sess.Id != nil { + t.Error("zero value Session.Id should be nil") + } + var msg Message + if msg.Role != nil { + t.Error("zero value Message.Role should be nil") + } +} + +func TestSpecMessageRoleValid(t *testing.T) { + if !User.Valid() { + t.Error("User.Valid() should be true") + } + if !Assistant.Valid() { + t.Error("Assistant.Valid() should be true") + } + if !Tool.Valid() { + t.Error("Tool.Valid() should be true") + } + if MessageRole("bogus").Valid() { + t.Error("bogus role Valid() should be false") + } +} diff --git a/resolution.go b/resolution.go new file mode 100644 index 0000000..4bb23e3 --- /dev/null +++ b/resolution.go @@ -0,0 +1,72 @@ +package hawksdk + +// ResolutionPhase identifies which phase of the Agentless pipeline produced +// a given result. Matches the Phase type in hawk/internal/pipeline and +// hawk-core-contracts/sessions. +type ResolutionPhase string + +const ( + ResolutionPhaseLocalize ResolutionPhase = "localize" + ResolutionPhaseRepair ResolutionPhase = "repair" + ResolutionPhaseValidate ResolutionPhase = "validate" + ResolutionPhaseReview ResolutionPhase = "review" + ResolutionPhasePlanning ResolutionPhase = "planning" + ResolutionPhaseUnknown ResolutionPhase = "" +) + +// ResolutionRequest is the SDK-level request to run a multi-phase +// localize → repair → validate (+ review, planning) resolution against a repository. +type ResolutionRequest struct { + // SessionID links this request to an ongoing session for cost attribution. + SessionID string `json:"session_id,omitempty"` + // RootDir is the absolute path to the repository root on the server. + RootDir string `json:"root_dir"` + // Query is the natural-language problem description (bug report, task). + Query string `json:"query"` + // MaxFiles limits how many files the localize phase considers. + MaxFiles int `json:"max_files,omitempty"` + // MaxSymbols limits how many symbols per file the localize phase returns. + MaxSymbols int `json:"max_symbols,omitempty"` + // Language restricts localization to one language ("go", "python", etc.). + Language string `json:"language,omitempty"` +} + +// PatchCandidate is a single proposed code change returned by the repair phase. +type PatchCandidate struct { + // FilePath is the repository-relative path to the file to change. + FilePath string `json:"file_path"` + // Symbol is the function or type containing the change. + Symbol string `json:"symbol,omitempty"` + // OriginalBody is the existing source of the symbol. + OriginalBody string `json:"original_body,omitempty"` + // PatchedBody is the proposed replacement source. + PatchedBody string `json:"patched_body,omitempty"` + // Confidence is in [0, 1]; higher means the candidate is more likely correct. + Confidence float64 `json:"confidence"` +} + +// PhaseMetrics records token spend and timing for a single pipeline phase. +type PhaseMetrics struct { + Phase ResolutionPhase `json:"phase"` + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` + ElapsedMs int64 `json:"elapsed_ms"` +} + +// ResolutionResult is the complete output of a three-phase resolution run. +type ResolutionResult struct { + // SessionID echoes the request SessionID for correlation. + SessionID string `json:"session_id,omitempty"` + // Candidates are the patch candidates produced by the repair phase. + Candidates []PatchCandidate `json:"candidates"` + // ValidationPassed is true when the applied patch passes all checks. + ValidationPassed bool `json:"validation_passed"` + // ValidationFailures lists test or lint failures; empty when ValidationPassed is true. + ValidationFailures []string `json:"validation_failures,omitempty"` + // PhaseMetrics records per-phase token spend. + PhaseMetrics []PhaseMetrics `json:"phase_metrics"` + // TotalInputTokens is the sum of InputTokens across all phases. + TotalInputTokens int `json:"total_input_tokens"` + // TotalOutputTokens is the sum of OutputTokens across all phases. + TotalOutputTokens int `json:"total_output_tokens"` +} diff --git a/resolution_test.go b/resolution_test.go new file mode 100644 index 0000000..b35276a --- /dev/null +++ b/resolution_test.go @@ -0,0 +1,94 @@ +package hawksdk_test + +import ( + "encoding/json" + "testing" + + hawksdk "github.com/GrayCodeAI/hawk-sdk-go" +) + +func TestResolutionPhaseConstants(t *testing.T) { + phases := []hawksdk.ResolutionPhase{ + hawksdk.ResolutionPhaseLocalize, + hawksdk.ResolutionPhaseRepair, + hawksdk.ResolutionPhaseValidate, + hawksdk.ResolutionPhaseReview, + hawksdk.ResolutionPhasePlanning, + } + for _, p := range phases { + if string(p) == "" { + t.Errorf("ResolutionPhase constant must not be empty") + } + } + if string(hawksdk.ResolutionPhaseUnknown) != "" { + t.Error("ResolutionPhaseUnknown should be empty") + } +} + +func TestResolutionRequestJSONRoundTrip(t *testing.T) { + req := hawksdk.ResolutionRequest{ + SessionID: "sess-123", + RootDir: "/repo", + Query: "fix the authentication bug", + MaxFiles: 10, + MaxSymbols: 5, + Language: "go", + } + b, err := json.Marshal(req) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + var got hawksdk.ResolutionRequest + if err := json.Unmarshal(b, &got); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if got.Query != req.Query { + t.Errorf("Query = %q, want %q", got.Query, req.Query) + } + if got.SessionID != req.SessionID { + t.Errorf("SessionID = %q, want %q", got.SessionID, req.SessionID) + } +} + +func TestResolutionResultJSONRoundTrip(t *testing.T) { + result := hawksdk.ResolutionResult{ + SessionID: "sess-123", + Candidates: []hawksdk.PatchCandidate{ + { + FilePath: "auth.go", + Symbol: "ValidateToken", + PatchedBody: "func ValidateToken(t string) error { return nil }", + Confidence: 0.9, + }, + }, + ValidationPassed: true, + PhaseMetrics: []hawksdk.PhaseMetrics{ + {Phase: hawksdk.ResolutionPhaseLocalize, InputTokens: 1000, OutputTokens: 200, ElapsedMs: 50}, + {Phase: hawksdk.ResolutionPhaseRepair, InputTokens: 3000, OutputTokens: 500, ElapsedMs: 200}, + {Phase: hawksdk.ResolutionPhaseValidate, InputTokens: 500, OutputTokens: 100, ElapsedMs: 20}, + }, + TotalInputTokens: 4500, + TotalOutputTokens: 800, + } + + b, err := json.Marshal(result) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + var got hawksdk.ResolutionResult + if err := json.Unmarshal(b, &got); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if !got.ValidationPassed { + t.Error("ValidationPassed should be true after round-trip") + } + if len(got.Candidates) != 1 { + t.Errorf("Candidates len = %d, want 1", len(got.Candidates)) + } + if len(got.PhaseMetrics) != 3 { + t.Errorf("PhaseMetrics len = %d, want 3", len(got.PhaseMetrics)) + } + if got.PhaseMetrics[0].Phase != hawksdk.ResolutionPhaseLocalize { + t.Errorf("first phase = %q, want %q", got.PhaseMetrics[0].Phase, hawksdk.ResolutionPhaseLocalize) + } +} diff --git a/tools.go b/tools.go index 9e77e63..bcdff81 100644 --- a/tools.go +++ b/tools.go @@ -23,8 +23,8 @@ type Tool struct { // Schema describes the tool for the model. Schema ToolSchema - // Run executes the tool with the given JSON arguments and returns a result string. - Run func(ctx context.Context, arguments string) (string, error) + // Run executes the tool with the given arguments and returns a result string. + Run func(ctx context.Context, arguments map[string]interface{}) (string, error) } // ToolCall represents a tool invocation requested by the model. @@ -35,8 +35,8 @@ type ToolCall struct { // Name is the function name to invoke. Name string `json:"name"` - // Arguments is the JSON-encoded arguments string. - Arguments string `json:"arguments"` + // Arguments is the arguments object for the tool call. + Arguments map[string]interface{} `json:"arguments"` } // ToolResult holds the result of executing a tool call. diff --git a/tools_test.go b/tools_test.go index debb918..6815e39 100644 --- a/tools_test.go +++ b/tools_test.go @@ -29,7 +29,7 @@ func TestChatWithTools_NoToolCalls(t *testing.T) { Description: "Says hello", Parameters: json.RawMessage(`{"type":"object","properties":{}}`), }, - Run: func(ctx context.Context, args string) (string, error) { + Run: func(ctx context.Context, args map[string]interface{}) (string, error) { return "hi", nil }, }, @@ -54,7 +54,7 @@ func TestChatWithTools_ExecutesTools(t *testing.T) { json.NewEncoder(w).Encode(ChatWithToolsResponse{ ChatResponse: ChatResponse{SessionID: "s-1", Response: ""}, ToolCalls: []ToolCall{ - {ID: "tc-1", Name: "add", Arguments: `{"a":1,"b":2}`}, + {ID: "tc-1", Name: "add", Arguments: map[string]interface{}{"a": float64(1), "b": float64(2)}}, }, FinishReason: "tool_calls", }) @@ -76,12 +76,7 @@ func TestChatWithTools_ExecutesTools(t *testing.T) { Description: "Adds two numbers", Parameters: json.RawMessage(`{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}}}`), }, - Run: func(ctx context.Context, args string) (string, error) { - var params struct { - A int `json:"a"` - B int `json:"b"` - } - json.Unmarshal([]byte(args), ¶ms) + Run: func(ctx context.Context, args map[string]interface{}) (string, error) { return "3", nil }, }, @@ -105,7 +100,7 @@ func TestChatWithTools_MaxRoundsExceeded(t *testing.T) { json.NewEncoder(w).Encode(ChatWithToolsResponse{ ChatResponse: ChatResponse{SessionID: "s-1", Response: ""}, ToolCalls: []ToolCall{ - {ID: "tc-1", Name: "loop", Arguments: `{}`}, + {ID: "tc-1", Name: "loop", Arguments: map[string]interface{}{}}, }, FinishReason: "tool_calls", }) @@ -116,7 +111,7 @@ func TestChatWithTools_MaxRoundsExceeded(t *testing.T) { tools := []Tool{ { Schema: ToolSchema{Name: "loop", Description: "loops"}, - Run: func(ctx context.Context, args string) (string, error) { + Run: func(ctx context.Context, args map[string]interface{}) (string, error) { return "again", nil }, }, @@ -137,7 +132,7 @@ func TestChatWithTools_UnknownTool(t *testing.T) { json.NewEncoder(w).Encode(ChatWithToolsResponse{ ChatResponse: ChatResponse{SessionID: "s-1"}, ToolCalls: []ToolCall{ - {ID: "tc-1", Name: "unknown_tool", Arguments: `{}`}, + {ID: "tc-1", Name: "unknown_tool", Arguments: map[string]interface{}{}}, }, FinishReason: "tool_calls", }) @@ -154,7 +149,7 @@ func TestChatWithTools_UnknownTool(t *testing.T) { tools := []Tool{ { Schema: ToolSchema{Name: "known", Description: "a known tool"}, - Run: func(ctx context.Context, args string) (string, error) { + Run: func(ctx context.Context, args map[string]interface{}) (string, error) { return "ok", nil }, }, @@ -174,7 +169,7 @@ func TestChatWithTools_ContextCancelled(t *testing.T) { json.NewEncoder(w).Encode(ChatWithToolsResponse{ ChatResponse: ChatResponse{SessionID: "s-1"}, ToolCalls: []ToolCall{ - {ID: "tc-1", Name: "slow", Arguments: `{}`}, + {ID: "tc-1", Name: "slow", Arguments: map[string]interface{}{}}, }, }) })) @@ -187,7 +182,7 @@ func TestChatWithTools_ContextCancelled(t *testing.T) { tools := []Tool{ { Schema: ToolSchema{Name: "slow"}, - Run: func(ctx context.Context, args string) (string, error) { + Run: func(ctx context.Context, args map[string]interface{}) (string, error) { return "done", nil }, }, diff --git a/types.go b/types.go index fa5720b..ebacfd5 100644 --- a/types.go +++ b/types.go @@ -64,9 +64,10 @@ type Message struct { // CreateSessionRequest is the request body for POST /v1/sessions. type CreateSessionRequest struct { - Model string `json:"model,omitempty"` - CWD string `json:"cwd,omitempty"` - Name string `json:"name,omitempty"` + Model string `json:"model,omitempty"` + CWD string `json:"cwd,omitempty"` + Name string `json:"name,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` } // StatsResponse is the response from GET /v1/stats.