Observe built-in cognitive usage
This commit is contained in:
@@ -184,7 +184,18 @@ func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.Gateway
|
||||
func (g cognitiveGateway) invokeBuiltinAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) cognitive.GatewayAdvisoryResponse {
|
||||
if g.builtinLocal != nil && g.builtinLocal.Provider != nil {
|
||||
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
|
||||
started := g.now()
|
||||
response, err := g.builtinLocal.Provider.Advise(ctx, invocation)
|
||||
if response.RequestID == "" {
|
||||
response.RequestID = request.RequestID
|
||||
}
|
||||
if response.Version == "" {
|
||||
response.Version = request.Version
|
||||
}
|
||||
if err != nil {
|
||||
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, FinishReason: cognitive.FinishInvalidProviderResponse}
|
||||
}
|
||||
g.observeBuiltin(ctx, g.builtinLocal, response, started)
|
||||
if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" {
|
||||
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}
|
||||
}
|
||||
@@ -237,6 +248,21 @@ func (g cognitiveGateway) observe(ctx context.Context, selected capability.Resol
|
||||
}
|
||||
}
|
||||
|
||||
// observeBuiltin gives the always-available local provider the same evidence
|
||||
// path as a resolved binding. The identity is stable and contains references
|
||||
// only; no endpoint, credential, prompt, or response text is persisted here.
|
||||
func (g cognitiveGateway) observeBuiltin(ctx context.Context, registration *providerRegistration, response cognitive.AdvisoryResponse, started time.Time) {
|
||||
if registration == nil {
|
||||
return
|
||||
}
|
||||
selected := capability.ResolvedCognitiveBinding{
|
||||
Binding: capability.CognitiveBinding{ID: "builtin-local-binding-v1", Revision: 1},
|
||||
Provider: capability.ProviderInstance{ID: "builtin-local-lifeboat-v1", Revision: 1, Kind: registration.Kind},
|
||||
Model: capability.ModelDescriptor{ID: "builtin-local-model-v1", Revision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ProviderModelID: "domovoy-lifeboat"},
|
||||
}
|
||||
g.observe(ctx, selected, response, started)
|
||||
}
|
||||
|
||||
func fallbackEligible(reason cognitive.FinishReason) bool {
|
||||
switch reason {
|
||||
case cognitive.FinishProviderUnavailable, cognitive.FinishModelUnavailable, cognitive.FinishTimeout, cognitive.FinishDegraded:
|
||||
|
||||
@@ -188,6 +188,37 @@ func TestNoActiveBindingInvokesBuiltInLocalProvider(t *testing.T) {
|
||||
if response.Code != http.StatusOK || !provider.called || !strings.Contains(response.Body.String(), "локальный ответ") {
|
||||
t.Fatalf("status=%d provider_called=%t body=%s", response.Code, provider.called, response.Body.String())
|
||||
}
|
||||
if registry.observation == nil || registry.observation.BindingID != "builtin-local-binding-v1" || registry.observation.ProviderInstanceID != "builtin-local-lifeboat-v1" || registry.observation.ModelDescriptorID != "builtin-local-model-v1" || registry.observation.ResultClass != capability.CognitiveResultClass(cognitive.FinishCompleted) {
|
||||
t.Fatalf("built-in usage observation=%+v", registry.observation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltInProviderFailureIsObservedWithoutBlockingFallback(t *testing.T) {
|
||||
registry := ®istryStub{err: errors.New("no active binding")}
|
||||
provider := &stubAdvisoryProvider{err: errors.New("local timeout")}
|
||||
builtin := providerRegistration{Kind: "ollama-v1", Provider: provider}
|
||||
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
|
||||
response := httptest.NewRecorder()
|
||||
gateway.advisoryHandler(response, request)
|
||||
if response.Code != http.StatusOK || registry.observation == nil {
|
||||
t.Fatalf("status=%d observation=%+v body=%s", response.Code, registry.observation, response.Body.String())
|
||||
}
|
||||
if registry.observation.ResultClass != capability.CognitiveResultClass(cognitive.FinishInvalidProviderResponse) || registry.observation.FailureType != capability.CognitiveFailureType(cognitive.FinishInvalidProviderResponse) {
|
||||
t.Fatalf("failure observation=%+v", registry.observation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltInInterpretationIsObserved(t *testing.T) {
|
||||
registry := ®istryStub{err: errors.New("no active binding")}
|
||||
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{FinishReason: cognitive.FinishCompleted, AdvisoryText: `{"primary_speech_act":"QUESTION","secondary_speech_act":"","confidence_millis":900,"applicability":"APPLICABLE","requested_outcome":"","needs_cognition":false,"clarification":"NOT_REQUIRED","selection_reason":"local","capability_hints":[]}`}}
|
||||
builtin := providerRegistration{Kind: "openai-compatible-chat-v1", Provider: provider}
|
||||
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
|
||||
request := cognitive.GatewayInterpretationRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "interpretation-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", Message: "что доступно", MaxOutputBytes: 4096, DeterministicSpeechAct: "QUESTION", DeterministicApplicability: cognitive.InterpretationApplicable}
|
||||
result := gateway.interpretWithBuiltin(context.Background(), request, request.MaxOutputBytes)
|
||||
if result.FinishReason != cognitive.FinishCompleted || registry.observation == nil || registry.observation.RequestID != "interpretation-1" {
|
||||
t.Fatalf("result=%+v observation=%+v", result, registry.observation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderLossBetweenRoutingAndInvocationFallsBackOnce(t *testing.T) {
|
||||
|
||||
@@ -83,7 +83,18 @@ func (g cognitiveGateway) interpretWithBuiltin(ctx context.Context, request cogn
|
||||
return deterministicInterpretationResponse(request)
|
||||
}
|
||||
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: interpretationPrompt(request), Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
|
||||
started := g.now()
|
||||
response, err := g.builtinLocal.Provider.Advise(ctx, invocation)
|
||||
if response.RequestID == "" {
|
||||
response.RequestID = request.RequestID
|
||||
}
|
||||
if response.Version == "" {
|
||||
response.Version = request.Version
|
||||
}
|
||||
if err != nil {
|
||||
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, FinishReason: cognitive.FinishInvalidProviderResponse}
|
||||
}
|
||||
g.observeBuiltin(ctx, g.builtinLocal, response, started)
|
||||
if err != nil || response.FinishReason != cognitive.FinishCompleted {
|
||||
return deterministicInterpretationResponse(request)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000095-chat-build-metadata-rendering.json",
|
||||
"task_id": "DEV-000095",
|
||||
"task_file": "dev/tasks/DEV-000096-builtin-cognitive-usage-telemetry.json",
|
||||
"task_id": "DEV-000096",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"task_id": "DEV-000096",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "305a55a",
|
||||
"risk_tier": "E",
|
||||
"intent": "Record usage observations for the built-in Domovoy cognitive provider so routing and learning can account for local fallback success, latency, usage, and failure.",
|
||||
"requirement_ids": ["Built-in cognitive calls are observable", "Telemetry failure does not block local chat"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cognitive-worker", "development-governance"],
|
||||
"paths": ["cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/gateway_registry_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000096-builtin-cognitive-usage-telemetry.json"]
|
||||
},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "cmd/cognitive-worker/interpretation_live_trace_test.go"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing CognitiveUsageObservation and bindingResolver.Observe"], "may_change": []},
|
||||
"semantic_owner": "capability registry owns cognitive usage observations; cognitive-worker is an observer",
|
||||
"canonical_state": [],
|
||||
"state_transitions": [],
|
||||
"side_effects_and_privileges": ["Adds only an existing usage-observation side effect for built-in provider calls; no external mutation or credential access"],
|
||||
"persistence_and_migrations": ["No schema or migration changes; uses existing observation endpoint and repository"],
|
||||
"failure_retry_restart": ["Observation errors remain non-fatal; provider response and deterministic fallback behavior remain unchanged"],
|
||||
"assumptions": ["The built-in provider identity remains the existing stable local lifeboat identity", "docker-test remains prohibited"],
|
||||
"acceptance_scenarios": ["Successful built-in advisory call produces an observation", "Failed built-in advisory call produces a non-success observation", "Built-in interpretation call is observed", "Chat behavior remains available when observation persistence fails", "No secrets are included in observations"],
|
||||
"required_tests": ["go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
Reference in New Issue
Block a user