diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 1759b15..228eae3 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -278,6 +278,10 @@ not collapse runtime explicit zero or broaden accepted configuration shapes. ## Stage 3: Project Resolved Backend And Profile Resources +**Completed:** Exact-ID resource lookups reuse captured resolved indexes; focused +configuration tests cover partial inheritance, explicit values, backend defaults, +derived queue capacity, diagnostics, missing IDs, and cancellation. + **Scope:** Configuration-owned views and exact-ID resource lookup. 1. Add private-adapter result structs in `internal/configuration`, with fields diff --git a/internal/configuration/load.go b/internal/configuration/load.go index e026b60..d518176 100644 --- a/internal/configuration/load.go +++ b/internal/configuration/load.go @@ -122,6 +122,8 @@ type loaded struct { promptMetadata map[string][]promptInputPresence prompts map[string]*promptEntry registry *backend.Registry + backendRecords map[string]backendConfig + profileRecords map[string]profileConfig indexedProfiles map[string]domain.ExecutionProfile backendIDs []string profileIDs []string diff --git a/internal/configuration/resources.go b/internal/configuration/resources.go new file mode 100644 index 0000000..4cc96b1 --- /dev/null +++ b/internal/configuration/resources.go @@ -0,0 +1,71 @@ +package configuration + +import ( + "context" + "fmt" + + "go.maximumdirect.net/promptkit/internal/backend" + "go.maximumdirect.net/promptkit/internal/domain" + "go.maximumdirect.net/promptkit/internal/profile" +) + +// ProfileResource is the resolved profile before execution completion. +// Its fields borrow immutable configuration captured by the adapter. +type ProfileResource struct { + ID string + Backend, Endpoint, Model *string + Temperature, TopP *float64 + MaxTokens, TimeoutSeconds *int + ServiceTier, ReasoningEffort *string + APIKeyEnv *string + APIKeyRequired *bool + ExtraParams map[string]any +} + +// BackendResource is the resolved backend and its configured diagnostics. +// Its fields borrow immutable configuration captured by the adapter. +type BackendResource struct { + ID, Endpoint string + APIKeyEnv *string + ConcurrencyLimit, QueueCapacity *int + ExtraParams map[string]any + SmokeTests *domain.BackendSmokeTestConfig +} + +func (state *loaded) InspectProfile(ctx context.Context, id string) (*ProfileResource, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if state == nil { + return nil, fmt.Errorf("%w: %q", profile.ErrProfileNotFound, id) + } + config, ok := state.profileRecords[id] + if !ok { + return nil, fmt.Errorf("%w: %q", profile.ErrProfileNotFound, id) + } + return &ProfileResource{ + ID: id, Backend: config.Backend, Endpoint: config.Endpoint, Model: config.Model, + Temperature: config.Temperature, TopP: config.TopP, MaxTokens: config.MaxTokens, + TimeoutSeconds: config.TimeoutSeconds, ServiceTier: config.ServiceTier, + ReasoningEffort: config.ReasoningEffort, APIKeyEnv: config.APIKeyEnv, + APIKeyRequired: config.APIKeyRequired, ExtraParams: config.ExtraParams, + }, nil +} + +func (state *loaded) InspectBackend(ctx context.Context, id string) (*BackendResource, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if state == nil { + return nil, fmt.Errorf("%w: %q", backend.ErrBackendNotFound, id) + } + config, ok := state.backendRecords[id] + if !ok { + return nil, fmt.Errorf("%w: %q", backend.ErrBackendNotFound, id) + } + return &BackendResource{ + ID: id, Endpoint: valueOrZero(config.Endpoint), APIKeyEnv: config.APIKeyEnv, + ConcurrencyLimit: config.ConcurrencyLimit, QueueCapacity: config.QueueCapacity, + ExtraParams: config.ExtraParams, SmokeTests: captureBackend(id, config).SmokeTests, + }, nil +} diff --git a/internal/configuration/resources_test.go b/internal/configuration/resources_test.go new file mode 100644 index 0000000..d022687 --- /dev/null +++ b/internal/configuration/resources_test.go @@ -0,0 +1,53 @@ +package configuration + +import ( + "context" + "errors" + "testing" + + "go.maximumdirect.net/promptkit/internal/backend" + "go.maximumdirect.net/promptkit/internal/profile" +) + +func TestResolvedBackendAndProfileResources(t *testing.T) { + state, err := load(mapping("resources", map[string]any{ + "backends": []any{map[string]any{"id": "local", "endpoint": "https://example.test/v1", "concurrency_limit": 2, + "extra_params": map[string]any{"backend": true}, "smoke_tests": map[string]any{"default_profile": "missing", "reachability": map[string]any{}}}}, + "profiles": []any{ + map[string]any{"id": "parent", "backend": "local", "temperature": 0.4}, + map[string]any{"id": "child", "extends": map[string]any{"id": "parent"}, "temperature": 0, "endpoint": "", "extra_params": map[string]any{}}, + }, + }), nil) + if err != nil { + t.Fatal(err) + } + resource, err := state.InspectProfile(context.Background(), "child") + if err != nil { + t.Fatal(err) + } + if resource.ID != "child" || resource.Backend == nil || *resource.Backend != "local" || resource.Endpoint == nil || *resource.Endpoint != "" || resource.Temperature == nil || *resource.Temperature != 0 || resource.Model != nil || resource.ExtraParams == nil || len(resource.ExtraParams) != 0 { + t.Fatalf("profile resource = %+v", resource) + } + target, err := state.GetProfile(context.Background(), "child") + if err != nil || target.ResolvedTarget.ExtraParams["backend"] != true { + t.Fatalf("execution target = %+v, %v", target, err) + } + resourceBackend, err := state.InspectBackend(context.Background(), "local") + if err != nil { + t.Fatal(err) + } + if resourceBackend.Endpoint != "https://example.test/v1" || resourceBackend.QueueCapacity == nil || *resourceBackend.QueueCapacity != 1024 || resourceBackend.SmokeTests == nil || resourceBackend.SmokeTests.DefaultProfile == nil || *resourceBackend.SmokeTests.DefaultProfile != "missing" || resourceBackend.SmokeTests.Reachability == nil || resourceBackend.SmokeTests.Reachability.Endpoint != nil { + t.Fatalf("backend resource = %+v", resourceBackend) + } + if _, err := state.InspectBackend(context.Background(), "missing"); !errors.Is(err, backend.ErrBackendNotFound) { + t.Fatalf("backend error = %v", err) + } + if _, err := state.InspectProfile(context.Background(), "missing"); !errors.Is(err, profile.ErrProfileNotFound) { + t.Fatalf("profile error = %v", err) + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + if _, err := state.InspectBackend(ctx, "local"); !errors.Is(err, context.Canceled) { + t.Fatalf("cancellation = %v", err) + } +} diff --git a/internal/configuration/targets.go b/internal/configuration/targets.go index 06eb4f1..d059401 100644 --- a/internal/configuration/targets.go +++ b/internal/configuration/targets.go @@ -14,6 +14,8 @@ import ( func (state *loaded) buildExecutionProfiles() error { backendConfigs := indexedBackends(&state.result.Config) profileConfigs := indexedProfiles(&state.result.Config) + state.backendRecords = backendConfigs + state.profileRecords = profileConfigs definitions := make(map[string]domain.Backend, len(backendConfigs)) for _, id := range sortedKeys(backendConfigs) { config := backendConfigs[id]