From e2b33886d6ef3770c49711d7deb31441c8c235e8 Mon Sep 17 00:00:00 2001 From: Hesham Karm <24391550+hishamkaram@users.noreply.github.com> Date: Sun, 12 Jul 2026 05:31:05 +0200 Subject: [PATCH] feat(protocol): scope catalogs and type history errors --- history.go | 10 ++++++ history_test.go | 5 +-- provider_catalog.go | 4 +++ provider_catalog_test.go | 68 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 84 insertions(+), 3 deletions(-) diff --git a/history.go b/history.go index 3ce2eff..43493bc 100644 --- a/history.go +++ b/history.go @@ -18,6 +18,15 @@ const ( MsgHistoryPage = "history_page" ) +type HistoryErrorCode string + +const ( + HistoryErrorSessionNotFound HistoryErrorCode = "session_not_found" + HistoryErrorUnavailable HistoryErrorCode = "history_unavailable" + HistoryErrorBusy HistoryErrorCode = "history_busy" + HistoryErrorInvalidRequest HistoryErrorCode = "invalid_request" +) + // SessionHeadPayload is carried in an AgentMessage with Type MsgSessionHead. type SessionHeadPayload struct { SessionID string `json:"session_id"` @@ -43,6 +52,7 @@ type HistoryPagePayload struct { HasMore bool `json:"has_more"` RequestID string `json:"request_id"` Error string `json:"error,omitempty"` + ErrorCode HistoryErrorCode `json:"error_code,omitempty"` Trimmed bool `json:"trimmed,omitempty"` RetainedOldestSeq uint64 `json:"retained_oldest_seq,omitempty"` } diff --git a/history_test.go b/history_test.go index 60f2879..aa38447 100644 --- a/history_test.go +++ b/history_test.go @@ -88,6 +88,7 @@ func TestHistoryPagePayloadJSONRoundtrip(t *testing.T) { HasMore: true, RequestID: "request-123", Error: "history unavailable", + ErrorCode: HistoryErrorUnavailable, Trimmed: true, RetainedOldestSeq: 97, } @@ -96,7 +97,7 @@ func TestHistoryPagePayloadJSONRoundtrip(t *testing.T) { if err != nil { t.Fatalf("marshal HistoryPagePayload: %v", err) } - const wantJSON = `{"session_id":"session-123","messages":[{"type":"output","session_id":"session-123","seq":97},{"type":"output","session_id":"session-123","seq":98}],"oldest_seq":97,"has_more":true,"request_id":"request-123","error":"history unavailable","trimmed":true,"retained_oldest_seq":97}` + const wantJSON = `{"session_id":"session-123","messages":[{"type":"output","session_id":"session-123","seq":97},{"type":"output","session_id":"session-123","seq":98}],"oldest_seq":97,"has_more":true,"request_id":"request-123","error":"history unavailable","error_code":"history_unavailable","trimmed":true,"retained_oldest_seq":97}` if string(raw) != wantJSON { t.Fatalf("HistoryPagePayload JSON = %s, want %s", raw, wantJSON) } @@ -105,7 +106,7 @@ func TestHistoryPagePayloadJSONRoundtrip(t *testing.T) { if err := json.Unmarshal(raw, &out); err != nil { t.Fatalf("unmarshal HistoryPagePayload: %v", err) } - if out.SessionID != in.SessionID || out.OldestSeq != in.OldestSeq || out.HasMore != in.HasMore || out.RequestID != in.RequestID || out.Error != in.Error || out.Trimmed != in.Trimmed || out.RetainedOldestSeq != in.RetainedOldestSeq { + if out.SessionID != in.SessionID || out.OldestSeq != in.OldestSeq || out.HasMore != in.HasMore || out.RequestID != in.RequestID || out.Error != in.Error || out.ErrorCode != in.ErrorCode || out.Trimmed != in.Trimmed || out.RetainedOldestSeq != in.RetainedOldestSeq { t.Fatalf("HistoryPagePayload scalar roundtrip = %+v, want %+v", out, in) } if len(out.Messages) != len(in.Messages) || string(out.Messages[0]) != string(in.Messages[0]) || string(out.Messages[1]) != string(in.Messages[1]) { diff --git a/provider_catalog.go b/provider_catalog.go index f6e8387..fbdd58c 100644 --- a/provider_catalog.go +++ b/provider_catalog.go @@ -48,6 +48,7 @@ type ProviderModelInfo struct { ResolvedModel string `json:"resolved_model,omitempty"` DisplayName string `json:"display_name"` Description string `json:"description,omitempty"` + Disabled bool `json:"disabled,omitempty"` SupportsEffort bool `json:"supports_effort,omitempty"` SupportedEffortLevels []string `json:"supported_effort_levels,omitempty"` DefaultEffort string `json:"default_effort,omitempty"` @@ -61,6 +62,7 @@ type ProviderModelInfo struct { type ProviderRuntimeCatalog struct { Agent string `json:"agent"` Provider string `json:"provider"` + ScopeID string `json:"scope_id,omitempty"` State ProviderCatalogState `json:"state"` Generation string `json:"generation,omitempty"` Source ProviderCatalogSource `json:"source,omitempty"` @@ -75,10 +77,12 @@ type ProviderRuntimeCatalog struct { type ListProviderCatalogsRequest struct { Type string `json:"type"` RequestID string `json:"request_id"` + WorkDir string `json:"work_dir,omitempty"` } type ListProviderCatalogsResponse struct { Type string `json:"type"` RequestID string `json:"request_id,omitempty"` + ScopeID string `json:"scope_id,omitempty"` Catalogs []ProviderRuntimeCatalog `json:"catalogs"` } diff --git a/provider_catalog_test.go b/provider_catalog_test.go index e6f1c04..ed90137 100644 --- a/provider_catalog_test.go +++ b/provider_catalog_test.go @@ -12,10 +12,12 @@ func TestProviderRuntimeCatalogRoundTrip(t *testing.T) { want := ListProviderCatalogsResponse{ Type: MsgProviderCatalogList, RequestID: "catalog-1", + ScopeID: "scope-abc123", Catalogs: []ProviderRuntimeCatalog{ { Agent: "claude-code", Provider: "bedrock", + ScopeID: "scope-abc123", State: ProviderCatalogReady, Generation: "generation-1", Source: ProviderCatalogSourceSDK, @@ -26,6 +28,7 @@ func TestProviderRuntimeCatalogRoundTrip(t *testing.T) { Value: "provider-model-a", ResolvedModel: "provider-model-a-20260710", DisplayName: "Provider Model A", + Disabled: true, SupportsEffort: true, SupportedEffortLevels: []string{"low", "high"}, SupportsAdaptiveThinking: true, @@ -47,11 +50,74 @@ func TestProviderRuntimeCatalogRoundTrip(t *testing.T) { if err := json.Unmarshal(raw, &got); err != nil { t.Fatalf("Unmarshal: %v", err) } - if len(got.Catalogs) != 1 || got.Catalogs[0].Provider != "bedrock" || got.Catalogs[0].Models[0].ResolvedModel != "provider-model-a-20260710" { + if got.ScopeID != "scope-abc123" || + len(got.Catalogs) != 1 || + got.Catalogs[0].Provider != "bedrock" || + got.Catalogs[0].ScopeID != "scope-abc123" || + got.Catalogs[0].Models[0].ResolvedModel != "provider-model-a-20260710" || + !got.Catalogs[0].Models[0].Disabled { t.Fatalf("round trip = %+v", got) } } +func TestListProviderCatalogsResponseScopeIDCompatibility(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + payload string + wantScope string + }{ + { + name: "scoped response", + payload: `{"type":"provider_catalog_list","scope_id":"scope-abc123","catalogs":[]}`, + wantScope: "scope-abc123", + }, + { + name: "legacy response", + payload: `{"type":"provider_catalog_list","catalogs":[]}`, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var got ListProviderCatalogsResponse + if err := json.Unmarshal([]byte(tt.payload), &got); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if got.ScopeID != tt.wantScope { + t.Fatalf("scope_id = %q, want %q", got.ScopeID, tt.wantScope) + } + }) + } +} + +func TestListProviderCatalogsRequestRoundTripScopedWorkDir(t *testing.T) { + t.Parallel() + + in := ListProviderCatalogsRequest{ + Type: MsgListProviderCatalogs, + RequestID: "catalog-request-1", + WorkDir: "/workspace/project", + } + raw, err := json.Marshal(in) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + const wantJSON = `{"type":"list_provider_catalogs","request_id":"catalog-request-1","work_dir":"/workspace/project"}` + if string(raw) != wantJSON { + t.Fatalf("request JSON = %s, want %s", raw, wantJSON) + } + var got ListProviderCatalogsRequest + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if got != in { + t.Fatalf("round trip = %+v, want %+v", got, in) + } +} + func TestProviderCatalogUnavailableIsSanitized(t *testing.T) { t.Parallel()