diff --git a/.dockerignore b/.dockerignore index 411f4d121..8e4e8b1c0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,8 +13,6 @@ Dockerfile docs/* README.md README_CN.md -MANAGEMENT_API.md -MANAGEMENT_API_CN.md LICENSE # Runtime data folders (should be mounted as volumes) @@ -32,3 +30,4 @@ bin/* .agent/* .bmad/* _bmad/* +_bmad-output/* diff --git a/.gitignore b/.gitignore index b8abbf7e0..87e3a6427 100644 --- a/.gitignore +++ b/.gitignore @@ -12,11 +12,15 @@ bin/* logs/* conv/* temp/* +refs/* + +# Storage backends pgstore/* gitstore/* objectstore/* + +# Static assets static/* -refs/* # Authentication data auths/* @@ -36,6 +40,7 @@ GEMINI.md .agent/* .bmad/* _bmad/* +_bmad-output/* .mcp/cache/ # macOS diff --git a/internal/config/config.go b/internal/config/config.go index a23ad369e..a8027b2b8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -879,8 +879,8 @@ func getOrCreateMapValue(mapNode *yaml.Node, key string) *yaml.Node { } // mergeMappingPreserve merges keys from src into dst mapping node while preserving -// key order and comments of existing keys in dst. Unknown keys from src are appended -// to dst at the end, copying their node structure from src. +// key order and comments of existing keys in dst. New keys are only added if their +// value is non-zero to avoid polluting the config with defaults. func mergeMappingPreserve(dst, src *yaml.Node) { if dst == nil || src == nil { return @@ -891,20 +891,19 @@ func mergeMappingPreserve(dst, src *yaml.Node) { copyNodeShallow(dst, src) return } - // Build a lookup of existing keys in dst for i := 0; i+1 < len(src.Content); i += 2 { sk := src.Content[i] sv := src.Content[i+1] idx := findMapKeyIndex(dst, sk.Value) if idx >= 0 { - // Merge into existing value node + // Merge into existing value node (always update, even to zero values) dv := dst.Content[idx+1] mergeNodePreserve(dv, sv) } else { - if shouldSkipEmptyCollectionOnPersist(sk.Value, sv) { + // New key: only add if value is non-zero to avoid polluting config with defaults + if isZeroValueNode(sv) { continue } - // Append new key/value pair by deep-copying from src dst.Content = append(dst.Content, deepCopyNode(sk), deepCopyNode(sv)) } } @@ -987,32 +986,49 @@ func findMapKeyIndex(mapNode *yaml.Node, key string) int { return -1 } -func shouldSkipEmptyCollectionOnPersist(key string, node *yaml.Node) bool { - switch key { - case "generative-language-api-key", - "gemini-api-key", - "vertex-api-key", - "claude-api-key", - "codex-api-key", - "openai-compatibility": - return isEmptyCollectionNode(node) - default: - return false - } -} - -func isEmptyCollectionNode(node *yaml.Node) bool { +// isZeroValueNode returns true if the YAML node represents a zero/default value +// that should not be written as a new key to preserve config cleanliness. +// For mappings and sequences, recursively checks if all children are zero values. +func isZeroValueNode(node *yaml.Node) bool { if node == nil { return true } switch node.Kind { - case yaml.SequenceNode: - return len(node.Content) == 0 case yaml.ScalarNode: - return node.Tag == "!!null" - default: - return false + switch node.Tag { + case "!!bool": + return node.Value == "false" + case "!!int", "!!float": + return node.Value == "0" || node.Value == "0.0" + case "!!str": + return node.Value == "" + case "!!null": + return true + } + case yaml.SequenceNode: + if len(node.Content) == 0 { + return true + } + // Check if all elements are zero values + for _, child := range node.Content { + if !isZeroValueNode(child) { + return false + } + } + return true + case yaml.MappingNode: + if len(node.Content) == 0 { + return true + } + // Check if all values are zero values (values are at odd indices) + for i := 1; i < len(node.Content); i += 2 { + if !isZeroValueNode(node.Content[i]) { + return false + } + } + return true } + return false } // deepCopyNode creates a deep copy of a yaml.Node graph. diff --git a/internal/config/sdk_config.go b/internal/config/sdk_config.go index 7f0195200..596cbb2c8 100644 --- a/internal/config/sdk_config.go +++ b/internal/config/sdk_config.go @@ -30,13 +30,13 @@ type SDKConfig struct { // StreamingConfig holds server streaming behavior configuration. type StreamingConfig struct { // KeepAliveSeconds controls how often the server emits SSE heartbeats (": keep-alive\n\n"). - // nil means default (15 seconds). <= 0 disables keep-alives. - KeepAliveSeconds *int `yaml:"keepalive-seconds,omitempty" json:"keepalive-seconds,omitempty"` + // <= 0 disables keep-alives. Default is 0. + KeepAliveSeconds int `yaml:"keepalive-seconds,omitempty" json:"keepalive-seconds,omitempty"` // BootstrapRetries controls how many times the server may retry a streaming request before any bytes are sent, // to allow auth rotation / transient recovery. - // nil means default (2). 0 disables bootstrap retries. - BootstrapRetries *int `yaml:"bootstrap-retries,omitempty" json:"bootstrap-retries,omitempty"` + // <= 0 disables bootstrap retries. Default is 0. + BootstrapRetries int `yaml:"bootstrap-retries,omitempty" json:"bootstrap-retries,omitempty"` } // AccessConfig groups request authentication providers. diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go index ecabce958..d1403d7b5 100644 --- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go +++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go @@ -247,7 +247,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ } else if role == "assistant" { node := []byte(`{"role":"model","parts":[]}`) p := 0 - if content.Type == gjson.String { + if content.Type == gjson.String && content.String() != "" { node, _ = sjson.SetBytes(node, "parts.-1.text", content.String()) p++ } else if content.IsArray() { diff --git a/sdk/api/handlers/handlers.go b/sdk/api/handlers/handlers.go index 960e55ace..bd55cc817 100644 --- a/sdk/api/handlers/handlers.go +++ b/sdk/api/handlers/handlers.go @@ -104,8 +104,8 @@ func BuildErrorResponseBody(status int, errText string) []byte { // Returning 0 disables keep-alives (default when unset). func StreamingKeepAliveInterval(cfg *config.SDKConfig) time.Duration { seconds := defaultStreamingKeepAliveSeconds - if cfg != nil && cfg.Streaming.KeepAliveSeconds != nil { - seconds = *cfg.Streaming.KeepAliveSeconds + if cfg != nil { + seconds = cfg.Streaming.KeepAliveSeconds } if seconds <= 0 { return 0 @@ -116,8 +116,8 @@ func StreamingKeepAliveInterval(cfg *config.SDKConfig) time.Duration { // StreamingBootstrapRetries returns how many times a streaming request may be retried before any bytes are sent. func StreamingBootstrapRetries(cfg *config.SDKConfig) int { retries := defaultStreamingBootstrapRetries - if cfg != nil && cfg.Streaming.BootstrapRetries != nil { - retries = *cfg.Streaming.BootstrapRetries + if cfg != nil { + retries = cfg.Streaming.BootstrapRetries } if retries < 0 { retries = 0 diff --git a/sdk/api/handlers/handlers_stream_bootstrap_test.go b/sdk/api/handlers/handlers_stream_bootstrap_test.go index 39eefa842..f8ce6aeaf 100644 --- a/sdk/api/handlers/handlers_stream_bootstrap_test.go +++ b/sdk/api/handlers/handlers_stream_bootstrap_test.go @@ -94,10 +94,9 @@ func TestExecuteStreamWithAuthManager_RetriesBeforeFirstByte(t *testing.T) { registry.GetGlobalRegistry().UnregisterClient(auth2.ID) }) - bootstrapRetries := 1 handler := NewBaseAPIHandlers(&sdkconfig.SDKConfig{ Streaming: sdkconfig.StreamingConfig{ - BootstrapRetries: &bootstrapRetries, + BootstrapRetries: 1, }, }, manager) dataChan, errChan := handler.ExecuteStreamWithAuthManager(context.Background(), "openai", "test-model", []byte(`{"model":"test-model"}`), "")