Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae5848d799 | ||
|
|
ec79f19a79 | ||
|
|
f63c4bb40c | ||
|
|
22f839df99 | ||
|
|
c41634a065 | ||
|
|
2609f31e59 | ||
|
|
7aa41b3be2 | ||
|
|
1f6ca363ee | ||
|
|
02471990ef | ||
|
|
40b56798e2 | ||
|
|
c53d7ded06 | ||
|
|
e080c34a77 | ||
|
|
306f53d484 | ||
|
|
c4411afaaf | ||
|
|
c508941a8a | ||
|
|
ee68e2207e | ||
|
|
3d9a5bccba | ||
|
|
d736500d98 | ||
|
|
33b51a4445 | ||
|
|
0aab10863c | ||
|
|
b17caa3ea9 | ||
|
|
66ed523248 |
@@ -7,26 +7,26 @@ import (
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
// localFallbackExecutor keeps the durable work path alive when no external
|
||||
// executor is reachable. It never claims that an external action happened.
|
||||
// The work owner records the WAITING callback and can retry after a bridge is
|
||||
// localExecutor provides the bounded local execution capability. It never
|
||||
// claims that an unsupported or external action happened. The work owner
|
||||
// records the WAITING callback and can retry after a suitable executor is
|
||||
// configured or a human chooses the next step.
|
||||
type localFallbackExecutor struct {
|
||||
type localExecutor struct {
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func newLocalFallbackExecutor() localFallbackExecutor {
|
||||
return localFallbackExecutor{now: time.Now}
|
||||
func newLocalExecutor() localExecutor {
|
||||
return localExecutor{now: time.Now}
|
||||
}
|
||||
|
||||
func (e localFallbackExecutor) Execute(_ context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
|
||||
func (e localExecutor) Execute(_ context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
|
||||
now := time.Now()
|
||||
if e.now != nil {
|
||||
now = e.now()
|
||||
}
|
||||
return work.AgentExecutionResult{
|
||||
State: work.AgentExecutionWaiting,
|
||||
Summary: "Внешний Agent Execution Bridge недоступен; локальный fallback сохранил durable работу и ожидает исполнителя.",
|
||||
Summary: "Локальный исполнитель не поддерживает эту операцию; durable работа сохранена и ожидает подходящего исполнителя.",
|
||||
ObservedAtUnixMillis: now.UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
func TestLocalFallbackNeverClaimsExternalExecution(t *testing.T) {
|
||||
func TestLocalExecutorNeverClaimsUnsupportedExecution(t *testing.T) {
|
||||
when := time.UnixMilli(1_700_000_000_000)
|
||||
executor := localFallbackExecutor{now: func() time.Time { return when }}
|
||||
executor := localExecutor{now: func() time.Time { return when }}
|
||||
result, err := executor.Execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-1"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -19,6 +19,6 @@ func TestLocalFallbackNeverClaimsExternalExecution(t *testing.T) {
|
||||
t.Fatalf("unexpected fallback result: %+v", result)
|
||||
}
|
||||
if result.OutcomeRef != "" || result.Summary == "" {
|
||||
t.Fatalf("fallback must not claim an outcome: %+v", result)
|
||||
t.Fatalf("local executor must not claim an outcome: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,10 +49,11 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
// The local evaluator keeps task-qualification work alive, but it is not
|
||||
// an external Agent Execution Bridge and must never be reported as one.
|
||||
// The local executor keeps supported task-qualification work alive, but
|
||||
// it is not an external Agent Execution Bridge and must never be reported
|
||||
// as one. Unsupported work remains durably WAITING.
|
||||
localFallback = true
|
||||
executor = newTaskEvaluationExecutor(envOr("COGNITIVE_WORKER_URL", "http://cognitive-worker:8083"), envOr("WORK_GRAPH_RUNTIME_URL", "http://work-graph-runtime:8088"), &http.Client{Timeout: *leaseTTL})
|
||||
executor = newTaskEvaluationExecutor(envOr("COGNITIVE_WORKER_URL", "http://cognitive-worker:8083"), envOr("WORK_GRAPH_RUNTIME_URL", "http://work-graph-runtime:8088"), envOr("TASK_VERIFIER_URL", "http://task-verifier:8091"), &http.Client{Timeout: *leaseTTL})
|
||||
fmt.Fprintln(os.Stderr, "protected agent execution bridge URL is not configured; task evaluations use the local durable evaluator, other work remains in fallback")
|
||||
}
|
||||
go func() {
|
||||
|
||||
@@ -6,12 +6,14 @@ import (
|
||||
)
|
||||
|
||||
type workerStatusResponse struct {
|
||||
Runtime string `json:"runtime"`
|
||||
ExecutorConfigured bool `json:"executor_configured"`
|
||||
Mode string `json:"mode"`
|
||||
Ready bool `json:"ready"`
|
||||
Reason string `json:"reason"`
|
||||
LocalFallback bool `json:"local_fallback"`
|
||||
Runtime string `json:"runtime"`
|
||||
ExecutorConfigured bool `json:"executor_configured"`
|
||||
Mode string `json:"mode"`
|
||||
Ready bool `json:"ready"`
|
||||
Reason string `json:"reason"`
|
||||
LocalFallback bool `json:"local_fallback"`
|
||||
LocalExecutor bool `json:"local_executor"`
|
||||
Capabilities []string `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
func workerStatusHandler(executorConfigured bool, localFallback ...bool) http.Handler {
|
||||
@@ -23,10 +25,12 @@ func workerStatusHandler(executorConfigured bool, localFallback ...bool) http.Ha
|
||||
Ready: false,
|
||||
Reason: "protected agent execution bridge URL is not configured",
|
||||
LocalFallback: local,
|
||||
LocalExecutor: local,
|
||||
}
|
||||
if local {
|
||||
status.Mode = "local-fallback"
|
||||
status.Reason = "external bridge unavailable; durable work is retained without external mutation"
|
||||
status.Mode = "local-executor"
|
||||
status.Reason = "bounded local task-evaluation executor is active; unsupported work remains durably waiting"
|
||||
status.Capabilities = []string{"task-evaluation"}
|
||||
}
|
||||
if executorConfigured {
|
||||
status.Mode = "execution"
|
||||
|
||||
@@ -38,16 +38,19 @@ func TestWorkerStatusReportsRecoveryOnlyWithoutExecutor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerStatusReportsLocalFallback(t *testing.T) {
|
||||
func TestWorkerStatusReportsLocalExecutor(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
workerStatusHandler(false, true).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
|
||||
var value workerStatusResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.Code != http.StatusOK || value.Ready || value.Mode != "local-fallback" || !value.LocalFallback {
|
||||
if response.Code != http.StatusOK || value.Ready || value.Mode != "local-executor" || !value.LocalFallback || !value.LocalExecutor {
|
||||
t.Fatalf("unexpected status: code=%d value=%+v", response.Code, value)
|
||||
}
|
||||
if len(value.Capabilities) != 1 || value.Capabilities[0] != "task-evaluation" {
|
||||
t.Fatalf("unexpected local capabilities: %+v", value.Capabilities)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerStatusDoesNotCallLocalFallbackAnExternalExecutor(t *testing.T) {
|
||||
@@ -57,7 +60,7 @@ func TestWorkerStatusDoesNotCallLocalFallbackAnExternalExecutor(t *testing.T) {
|
||||
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if value.ExecutorConfigured || value.Ready || !value.LocalFallback || value.Mode != "local-fallback" {
|
||||
if value.ExecutorConfigured || value.Ready || !value.LocalFallback || !value.LocalExecutor || value.Mode != "local-executor" {
|
||||
t.Fatalf("local fallback was presented as external execution: %+v", value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,16 @@ import (
|
||||
type taskEvaluationExecutor struct {
|
||||
cognitiveURL string
|
||||
callbackURL string
|
||||
verifierURL string
|
||||
client *http.Client
|
||||
fallback localFallbackExecutor
|
||||
fallback localExecutor
|
||||
}
|
||||
|
||||
func newTaskEvaluationExecutor(cognitiveURL, callbackURL string, client *http.Client) taskEvaluationExecutor {
|
||||
func newTaskEvaluationExecutor(cognitiveURL, callbackURL, verifierURL string, client *http.Client) taskEvaluationExecutor {
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return taskEvaluationExecutor{cognitiveURL: strings.TrimRight(cognitiveURL, "/"), callbackURL: strings.TrimRight(callbackURL, "/"), client: client, fallback: newLocalFallbackExecutor()}
|
||||
return taskEvaluationExecutor{cognitiveURL: strings.TrimRight(cognitiveURL, "/"), callbackURL: strings.TrimRight(callbackURL, "/"), verifierURL: strings.TrimRight(verifierURL, "/"), client: client, fallback: newLocalExecutor()}
|
||||
}
|
||||
|
||||
func (e taskEvaluationExecutor) Execute(ctx context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
|
||||
@@ -38,7 +39,7 @@ func (e taskEvaluationExecutor) Execute(ctx context.Context, request work.AgentE
|
||||
if execution.GraphID == "" || execution.NodeID == "" || execution.BindingRevision < 1 || execution.ActorID == "" || execution.PrincipalID == "" || execution.ScopeID == "" {
|
||||
return e.fallback.Execute(ctx, request)
|
||||
}
|
||||
if e.cognitiveURL == "" || e.callbackURL == "" {
|
||||
if e.cognitiveURL == "" || e.callbackURL == "" || e.verifierURL == "" {
|
||||
return e.fallback.Execute(ctx, request)
|
||||
}
|
||||
payload := capability.TaskEvaluationExecutionRequest{Profile: *intent.TaskEvaluationProfile, CapabilityID: intent.TaskEvaluationProfile.CapabilityID, BindingID: intent.TaskEvaluationBindingID, BindingRevision: execution.BindingRevision, ActorID: execution.ActorID, PrincipalID: execution.PrincipalID, ScopeID: execution.ScopeID, Cases: intent.TaskEvaluationCases, MaxOutputBytes: 4096}
|
||||
@@ -46,13 +47,29 @@ func (e taskEvaluationExecutor) Execute(ctx context.Context, request work.AgentE
|
||||
if err != nil {
|
||||
return work.AgentExecutionResult{}, err
|
||||
}
|
||||
if len(response.Observations) == 0 || response.ObservedAtUnixMillis < 1 {
|
||||
if len(response.Responses) == 0 || response.ObservedAtUnixMillis < 1 {
|
||||
return work.AgentExecutionResult{}, fmt.Errorf("task evaluation returned no observations")
|
||||
}
|
||||
if len(intent.TaskEvaluationCases) == 0 {
|
||||
return work.AgentExecutionResult{}, fmt.Errorf("task evaluation cases are missing")
|
||||
}
|
||||
evaluation, err := capabilitydomain.BuildTaskEvaluation(*intent.TaskEvaluationProfile, capability.TaskEvaluationID("evaluation:"+intent.DispatchID), response.BindingID, response.BindingRevision, intent.TaskEvaluationProfile.CapabilityID, response.Observations, artifact.ArtifactID(intent.TaskEvaluationCases[0].EvidenceArtifactRef), response.ObservedAtUnixMillis)
|
||||
caseByID := make(map[string]capability.TaskEvaluationCase, len(intent.TaskEvaluationCases))
|
||||
for _, item := range intent.TaskEvaluationCases {
|
||||
caseByID[item.CaseID] = item
|
||||
}
|
||||
observations := make([]capability.TaskCaseObservation, 0, len(response.Responses))
|
||||
for _, item := range response.Responses {
|
||||
caseValue, ok := caseByID[item.CaseID]
|
||||
if !ok || caseValue.Verification.Mode == "" {
|
||||
return e.fallback.Execute(ctx, request)
|
||||
}
|
||||
verified, verifyErr := postJSON[capability.TaskCaseVerificationResponse](ctx, e.client, e.verifierURL+"/v1/task-evaluations/verify", capability.TaskCaseVerificationRequest{CaseID: item.CaseID, ResponseText: item.ResponseText, LatencyMicros: item.LatencyMicros, EvidenceArtifactRef: item.EvidenceArtifactRef, Verification: caseValue.Verification})
|
||||
if verifyErr != nil {
|
||||
return e.fallback.Execute(ctx, request)
|
||||
}
|
||||
observations = append(observations, capability.TaskCaseObservation{CaseID: verified.CaseID, Result: verified.Result, QualityScoreMillis: verified.QualityScoreMillis, LatencyMicros: verified.LatencyMicros, CriticalFailure: verified.CriticalFailure, EvidenceArtifactRef: verified.EvidenceArtifactRef, VerificationEvidenceRef: verified.VerificationEvidenceRef, VerifierIdentity: verified.VerifierIdentity})
|
||||
}
|
||||
evaluation, err := capabilitydomain.BuildTaskEvaluation(*intent.TaskEvaluationProfile, capability.TaskEvaluationID("evaluation:"+intent.DispatchID), response.BindingID, response.BindingRevision, intent.TaskEvaluationProfile.CapabilityID, observations, artifact.ArtifactID(intent.TaskEvaluationCases[0].EvidenceArtifactRef), response.ObservedAtUnixMillis)
|
||||
if err != nil {
|
||||
return work.AgentExecutionResult{}, err
|
||||
}
|
||||
|
||||
@@ -13,9 +13,14 @@ import (
|
||||
|
||||
func TestTaskEvaluationExecutorInvokesAggregatesAndCallbacks(t *testing.T) {
|
||||
profile := capability.TaskEvaluationProfile{ID: "profile-1", Revision: 1, CapabilityID: "capability-1", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-1"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
|
||||
verification := capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/v1/task-evaluations/execute" {
|
||||
_ = json.NewEncoder(w).Encode(capability.TaskEvaluationExecutionResponse{ProfileID: profile.ID, BindingID: "binding-1", BindingRevision: 3, Observations: []capability.TaskCaseObservation{{CaseID: "case-1", Result: capability.EvaluationSupported, EvidenceArtifactRef: "artifact-1"}}, ObservedAtUnixMillis: 10})
|
||||
_ = json.NewEncoder(w).Encode(capability.TaskEvaluationExecutionResponse{ProfileID: profile.ID, BindingID: "binding-1", BindingRevision: 3, Responses: []capability.TaskCaseResponse{{CaseID: "case-1", ResponseText: "answer", LatencyMicros: 2, EvidenceArtifactRef: "artifact-1"}}, ObservedAtUnixMillis: 10})
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/v1/task-evaluations/verify" {
|
||||
_ = json.NewEncoder(w).Encode(capability.TaskCaseVerificationResponse{CaseID: "case-1", Result: capability.EvaluationSupported, QualityScoreMillis: 900, LatencyMicros: 2, EvidenceArtifactRef: "artifact-1", VerificationEvidenceRef: "verify-1", VerifierIdentity: "verifier-1"})
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/v1/work-graphs/task-evaluations/callback" {
|
||||
@@ -30,8 +35,8 @@ func TestTaskEvaluationExecutorInvokesAggregatesAndCallbacks(t *testing.T) {
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
executor := newTaskEvaluationExecutor(server.URL, server.URL, server.Client())
|
||||
result, err := executor.Execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-1", TaskEvaluationBindingID: "binding-1", TaskEvaluationProfile: &profile, TaskEvaluationCases: []capability.TaskEvaluationCase{{CaseID: "case-1", Prompt: "answer", EvidenceArtifactRef: "artifact-1"}}, TaskEvaluationExecution: &work.TaskEvaluationDispatchContext{GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", BindingRevision: 3, ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1"}}})
|
||||
executor := newTaskEvaluationExecutor(server.URL, server.URL, server.URL, server.Client())
|
||||
result, err := executor.Execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-1", TaskEvaluationBindingID: "binding-1", TaskEvaluationProfile: &profile, TaskEvaluationCases: []capability.TaskEvaluationCase{{CaseID: "case-1", Prompt: "answer", EvidenceArtifactRef: "artifact-1", Verification: verification}}, TaskEvaluationExecution: &work.TaskEvaluationDispatchContext{GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", BindingRevision: 3, ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1"}}})
|
||||
if err != nil || result.State != work.AgentExecutionCompleted || result.OutcomeRef == "" {
|
||||
t.Fatalf("result=%+v err=%v", result, err)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
@@ -17,6 +18,10 @@ type discoveryService struct {
|
||||
}
|
||||
|
||||
func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/v1/provider-discovery/status" {
|
||||
s.statusHandler(w, r)
|
||||
return
|
||||
}
|
||||
registration, ok := s.providers[r.PathValue("provider")]
|
||||
if !ok || registration.Discovery == nil {
|
||||
http.Error(w, "provider discovery not found", http.StatusNotFound)
|
||||
@@ -35,3 +40,42 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
type discoveryStatus struct {
|
||||
Provider string `json:"provider"`
|
||||
ProviderKind capability.ProviderKind `json:"provider_kind"`
|
||||
Health capability.HealthState `json:"health"`
|
||||
FailureClass capability.CognitiveFailureType `json:"failure_class,omitempty"`
|
||||
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
|
||||
Models []discoveredModelStatus `json:"models"`
|
||||
}
|
||||
|
||||
type discoveredModelStatus struct {
|
||||
ProviderModelID capability.ProviderModelID `json:"provider_model_id"`
|
||||
Fingerprint capability.ModelFingerprint `json:"fingerprint,omitempty"`
|
||||
Capabilities []capability.ModelCapability `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
func (s discoveryService) statusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
providers := make([]string, 0, len(s.providers))
|
||||
for name := range s.providers {
|
||||
providers = append(providers, name)
|
||||
}
|
||||
sort.Strings(providers)
|
||||
result := make([]discoveryStatus, 0, len(providers))
|
||||
for _, name := range providers {
|
||||
registration := s.providers[name]
|
||||
status := discoveryStatus{Provider: name, ProviderKind: registration.Kind, Health: capability.HealthUnknown, ObservedAtUnixMillis: s.now().UnixMilli(), Models: []discoveredModelStatus{}}
|
||||
if registration.Discovery != nil {
|
||||
models, probe := registration.Discovery.Discover(r.Context())
|
||||
status.Health = probe.Health
|
||||
status.FailureClass = capability.CognitiveFailureType(probe.Failure)
|
||||
for _, model := range models {
|
||||
status.Models = append(status.Models, discoveredModelStatus{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, Capabilities: model.Capabilities})
|
||||
}
|
||||
}
|
||||
result = append(result, status)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/models"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
type discoveryTestProvider struct{}
|
||||
|
||||
func (discoveryTestProvider) Probe(context.Context) models.ProviderProbe {
|
||||
return models.ProviderProbe{Health: capability.HealthAvailable}
|
||||
}
|
||||
func (discoveryTestProvider) Discover(context.Context) ([]models.DiscoveredModel, models.ProviderProbe) {
|
||||
return []models.DiscoveredModel{{ProviderModelID: "local-model", Fingerprint: "fingerprint-1", Capabilities: []capability.ModelCapability{capability.CapabilityAdvisory}}}, models.ProviderProbe{Health: capability.HealthAvailable}
|
||||
}
|
||||
func (discoveryTestProvider) Advise(context.Context, cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
|
||||
return cognitive.AdvisoryResponse{}, nil
|
||||
}
|
||||
|
||||
func TestDiscoveryStatusIsReadOnlyAndDoesNotExposeCredentialMetadata(t *testing.T) {
|
||||
service := discoveryService{
|
||||
providers: map[string]providerRegistration{"cpu": {ProviderInstanceID: "provider-1", Kind: "openai-compatible-chat-v1", CredentialRef: "credential://must-not-be-returned", Discovery: discoveryTestProvider{}}},
|
||||
owner: identity.PrincipalID("owner-1"),
|
||||
now: func() time.Time { return time.UnixMilli(1234) },
|
||||
}
|
||||
response := httptest.NewRecorder()
|
||||
service.handler(response, httptest.NewRequest(http.MethodGet, "/v1/provider-discovery/status", nil))
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d", response.Code)
|
||||
}
|
||||
body := response.Body.String()
|
||||
var result []discoveryStatus
|
||||
if err := json.NewDecoder(strings.NewReader(body)).Decode(&result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result) != 1 || result[0].Provider != "cpu" || result[0].Health != capability.HealthAvailable || len(result[0].Models) != 1 || result[0].ObservedAtUnixMillis != 1234 {
|
||||
t.Fatalf("unexpected discovery status: %+v", result)
|
||||
}
|
||||
if strings.Contains(body, "must-not-be-returned") || strings.Contains(body, "credential://") {
|
||||
t.Fatalf("credential metadata leaked in discovery status: %s", body)
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,7 @@ func main() {
|
||||
mux.HandleFunc("POST /v1/task-evaluations/execute", gateway.taskEvaluationHandler)
|
||||
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
|
||||
mux.HandleFunc("POST /v1/provider-discovery/{provider}", discovery.handler)
|
||||
mux.HandleFunc("GET /v1/provider-discovery/status", discovery.statusHandler)
|
||||
refreshProviderDiscoveries(resolver, discovery)
|
||||
log.Printf("cognitive-worker listening on %s", addr)
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
|
||||
@@ -44,6 +44,7 @@ func (g cognitiveGateway) taskEvaluationHandler(w http.ResponseWriter, r *http.R
|
||||
caseSet[id] = struct{}{}
|
||||
}
|
||||
observations := make([]capability.TaskCaseObservation, len(request.Cases))
|
||||
responses := make([]capability.TaskCaseResponse, len(request.Cases))
|
||||
var wg sync.WaitGroup
|
||||
seenCases := make(map[string]struct{}, len(request.Cases))
|
||||
for i, item := range request.Cases {
|
||||
@@ -67,6 +68,7 @@ func (g cognitiveGateway) taskEvaluationHandler(w http.ResponseWriter, r *http.R
|
||||
invocation := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("task-evaluation:%s:%s", request.Profile.ID, item.CaseID)), ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: item.Prompt, MaxOutputBytes: int(request.MaxOutputBytes), ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
|
||||
response, invokeErr := g.invokeAdmitted(r.Context(), string(invocation.RequestID), provider, invocation, int(request.MaxOutputBytes))
|
||||
observation := capability.TaskCaseObservation{CaseID: item.CaseID, LatencyMicros: time.Since(started).Microseconds(), EvidenceArtifactRef: item.EvidenceArtifactRef}
|
||||
responses[index] = capability.TaskCaseResponse{CaseID: item.CaseID, ResponseText: response.AdvisoryText, LatencyMicros: observation.LatencyMicros, EvidenceArtifactRef: item.EvidenceArtifactRef}
|
||||
if invokeErr == nil && response.FinishReason == cognitive.FinishCompleted && response.AdvisoryText != "" {
|
||||
// Invocation success proves reachability only. Without an
|
||||
// independent verifier and answer evidence, semantic support and
|
||||
@@ -85,7 +87,7 @@ func (g cognitiveGateway) taskEvaluationHandler(w http.ResponseWriter, r *http.R
|
||||
return
|
||||
}
|
||||
wg.Wait()
|
||||
writeTaskEvaluationJSON(w, capability.TaskEvaluationExecutionResponse{ProfileID: request.Profile.ID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, Observations: observations, ObservedAtUnixMillis: time.Now().UnixMilli()})
|
||||
writeTaskEvaluationJSON(w, capability.TaskEvaluationExecutionResponse{ProfileID: request.Profile.ID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, Observations: observations, Responses: responses, ObservedAtUnixMillis: time.Now().UnixMilli()})
|
||||
}
|
||||
|
||||
func writeTaskEvaluationJSON(w http.ResponseWriter, value any) {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
workdomain "git.cin.su/m/Assistant/modules/work/domain"
|
||||
)
|
||||
|
||||
var errDevelopmentGateCreate = errors.New("development gate must start at evidence collected with revision one")
|
||||
|
||||
func prepareDevelopmentGate(value work.DevelopmentGate, now time.Time) (work.DevelopmentGate, error) {
|
||||
if value.Stage != work.DevelopmentEvidenceCollected || value.Revision != 1 {
|
||||
return work.DevelopmentGate{}, errDevelopmentGateCreate
|
||||
}
|
||||
value.UpdatedAtUnixMillis = now.UnixMilli()
|
||||
if err := workdomain.ValidateDevelopmentGate(value); err != nil {
|
||||
return work.DevelopmentGate{}, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func advanceDevelopmentGate(value work.DevelopmentGate, next work.DevelopmentStage, now time.Time) (work.DevelopmentGate, error) {
|
||||
advanced, err := workdomain.AdvanceDevelopmentGate(value, next)
|
||||
if err != nil {
|
||||
return work.DevelopmentGate{}, err
|
||||
}
|
||||
advanced.Revision = value.Revision + 1
|
||||
advanced.UpdatedAtUnixMillis = now.UnixMilli()
|
||||
return advanced, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
func TestDevelopmentGateControllerRequiresSequentialVerifiedEvidence(t *testing.T) {
|
||||
now := time.UnixMilli(10)
|
||||
gate, err := prepareDevelopmentGate(work.DevelopmentGate{ID: "gate-1", Revision: 1, Stage: work.DevelopmentEvidenceCollected, EvidenceRefs: []string{"evidence-1"}}, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gate.ProposalRef = "proposal-1"
|
||||
gate, err = advanceDevelopmentGate(gate, work.DevelopmentProposalCreated, now)
|
||||
if err != nil || gate.Revision != 2 {
|
||||
t.Fatalf("proposal transition failed: %#v %v", gate, err)
|
||||
}
|
||||
gate.TaskPacketRef = "task-1"
|
||||
gate, err = advanceDevelopmentGate(gate, work.DevelopmentTaskAdmitted, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gate.WorktreeRef = "isolated-worktree"
|
||||
gate, err = advanceDevelopmentGate(gate, work.DevelopmentIsolated, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gate.ProducerRunRefs = []work.AgentRunID{"producer"}
|
||||
gate.VerifierRunRefs = []work.AgentRunID{"verifier"}
|
||||
gate.VerificationEvidence = "verification-1"
|
||||
gate, err = advanceDevelopmentGate(gate, work.DevelopmentVerified, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := advanceDevelopmentGate(gate, work.DevelopmentPromoted, now); err == nil {
|
||||
t.Fatal("promotion bypassed canary")
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,10 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
researchClient, err := newResearchRuntimeClient(envOr("RESEARCH_RUNTIME_URL", "http://research-runtime:8087"), &http.Client{Timeout: 12 * time.Second})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
|
||||
mux := http.NewServeMux()
|
||||
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
@@ -178,6 +182,25 @@ func main() {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(session)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/research/web", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
var request epistemics.WebResearchRequest
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 128<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid web research request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
result, err := researchClient.EvaluateWeb(r.Context(), request)
|
||||
if err != nil {
|
||||
http.Error(w, "web research unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/development/proposals", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
@@ -233,6 +256,61 @@ func main() {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(scaffold)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/development/gates", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
var gate work.DevelopmentGate
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&gate); err != nil {
|
||||
http.Error(w, "invalid development gate", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
gate, err := prepareDevelopmentGate(gate, time.Now())
|
||||
if err != nil {
|
||||
http.Error(w, "development gate rejected", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
if err := workRepository.SaveDevelopmentGate(r.Context(), gate, 0); err != nil {
|
||||
http.Error(w, "development gate conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(gate)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/development/gates/{id}/advance", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
ExpectedRevision int64 `json:"expected_revision"`
|
||||
NextStage work.DevelopmentStage `json:"next_stage"`
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil || request.ExpectedRevision < 1 || request.NextStage == "" {
|
||||
http.Error(w, "invalid development gate transition", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
gate, err := workRepository.LoadDevelopmentGate(r.Context(), r.PathValue("id"))
|
||||
if err != nil || gate.Revision != request.ExpectedRevision {
|
||||
http.Error(w, "development gate revision conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
gate, err = advanceDevelopmentGate(gate, request.NextStage, time.Now())
|
||||
if err != nil {
|
||||
http.Error(w, "development gate transition rejected", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
if err := workRepository.SaveDevelopmentGate(r.Context(), gate, request.ExpectedRevision); err != nil {
|
||||
http.Error(w, "development gate conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(gate)
|
||||
})
|
||||
mux.HandleFunc("GET /v1/creator/focus", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
)
|
||||
|
||||
type researchRuntimeClient struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newResearchRuntimeClient(rawURL string, client *http.Client) (researchRuntimeClient, error) {
|
||||
base := strings.TrimRight(strings.TrimSpace(rawURL), "/")
|
||||
if base == "" {
|
||||
return researchRuntimeClient{}, fmt.Errorf("research runtime URL is empty")
|
||||
}
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return researchRuntimeClient{baseURL: base, client: client}, nil
|
||||
}
|
||||
|
||||
func (c researchRuntimeClient) EvaluateWeb(ctx context.Context, request epistemics.WebResearchRequest) (epistemics.ResearchResult, error) {
|
||||
payload, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/research/web/evaluate", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return epistemics.ResearchResult{}, fmt.Errorf("research runtime status %d", response.StatusCode)
|
||||
}
|
||||
var result epistemics.ResearchResult
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
)
|
||||
|
||||
func TestResearchRuntimeClientForwardsBoundedWebRequest(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/v1/research/web/evaluate" || r.Header.Get("Content-Type") != "application/json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
var request epistemics.WebResearchRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil || request.URL != "https://assistant.cin.su/healthz" {
|
||||
t.Fatalf("unexpected request body: %#v %v", request, err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(epistemics.ResearchResult{PlanID: request.Plan.ID, Findings: []epistemics.ResearchFinding{{PropositionKey: request.PropositionKey, State: epistemics.FindingSupported}}})
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := newResearchRuntimeClient(server.URL, server.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := client.EvaluateWeb(context.Background(), epistemics.WebResearchRequest{Plan: epistemics.ResearchPlan{ID: "plan-1"}, PropositionKey: "health", URL: "https://assistant.cin.su/healthz"})
|
||||
if err != nil || result.PlanID != "plan-1" || len(result.Findings) != 1 || result.Findings[0].State != epistemics.FindingSupported {
|
||||
t.Fatalf("unexpected result: %#v %v", result, err)
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/" || r.URL.Path == "/chat" || r.URL.Path == "/chat.html" {
|
||||
if r.URL.Path == "/" || r.URL.Path == "/login" || r.URL.Path == "/chat" || r.URL.Path == "/chat.html" {
|
||||
g.serveEmbeddedHTML(w, "chat.html")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -354,6 +354,18 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginRouteServesChatShell(t *testing.T) {
|
||||
handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", "http://commit-runtime:8082", http.DefaultClient)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/login", nil))
|
||||
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "id=\"username\"") || !strings.Contains(response.Body.String(), "id=\"password\"") {
|
||||
t.Fatalf("login response = %d", response.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdvisoryModeRejectsInvalidConfiguration(t *testing.T) {
|
||||
_, err := newHandlerWithAdvisoryMode("http://core.invalid", "http://continuity.invalid", "http://commit.invalid", http.DefaultClient, "browser-selected")
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid advisory binding mode") {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
const verifierIdentity = "deterministic-task-verifier-v1"
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"runtime":"task-verifier","status":"ok"}`))
|
||||
})
|
||||
mux.HandleFunc("POST /v1/task-evaluations/verify", verifyHandler)
|
||||
addr := ":8091"
|
||||
if value := strings.TrimSpace(os.Getenv("LISTEN_ADDR")); value != "" {
|
||||
addr = value
|
||||
}
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var request capability.TaskCaseVerificationRequest
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil || request.CaseID == "" || request.ResponseText == "" || request.EvidenceArtifactRef == "" || request.LatencyMicros < 0 || (request.Verification.Mode != capability.TaskCaseVerificationExactText && request.Verification.Mode != capability.TaskCaseVerificationJSONFieldEquals) || (request.Verification.Mode == capability.TaskCaseVerificationJSONFieldEquals && request.Verification.FieldPath == "") || request.Verification.ExpectedText == "" || request.Verification.MinimumQualityMillis < 0 || request.Verification.MinimumQualityMillis > 1000 {
|
||||
http.Error(w, "invalid task verification request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
matched := matches(request)
|
||||
quality := int64(0)
|
||||
result := capability.EvaluationUnsupported
|
||||
if matched {
|
||||
quality = request.Verification.MinimumQualityMillis
|
||||
if quality == 0 {
|
||||
quality = 1000
|
||||
}
|
||||
result = capability.EvaluationSupported
|
||||
}
|
||||
evidence := verificationEvidence(request, matched)
|
||||
writeJSON(w, capability.TaskCaseVerificationResponse{CaseID: request.CaseID, Result: result, QualityScoreMillis: quality, LatencyMicros: request.LatencyMicros, EvidenceArtifactRef: request.EvidenceArtifactRef, VerificationEvidenceRef: evidence, VerifierIdentity: verifierIdentity})
|
||||
}
|
||||
|
||||
func verificationEvidence(request capability.TaskCaseVerificationRequest, matched bool) string {
|
||||
payload := fmt.Sprintf("%s|%s|%s|%s|%s|%t", verifierIdentity, request.CaseID, request.EvidenceArtifactRef, request.Verification.Mode, request.Verification.FieldPath, matched)
|
||||
digest := sha256.Sum256([]byte(payload))
|
||||
return "verification:" + hex.EncodeToString(digest[:])
|
||||
}
|
||||
|
||||
func matches(request capability.TaskCaseVerificationRequest) bool {
|
||||
if request.Verification.Mode == capability.TaskCaseVerificationExactText {
|
||||
return strings.TrimSpace(request.ResponseText) == strings.TrimSpace(request.Verification.ExpectedText)
|
||||
}
|
||||
var document map[string]json.RawMessage
|
||||
if json.Unmarshal([]byte(request.ResponseText), &document) != nil {
|
||||
return false
|
||||
}
|
||||
value, ok := document[request.Verification.FieldPath]
|
||||
return ok && strings.TrimSpace(string(value)) == strings.TrimSpace(request.Verification.ExpectedText)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, value any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
func TestVerifyExactTextProducesIndependentEvidence(t *testing.T) {
|
||||
payload, _ := json.Marshal(capability.TaskCaseVerificationRequest{CaseID: "case-1", ResponseText: " answer ", EvidenceArtifactRef: "case-artifact", Verification: capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}})
|
||||
recorder := httptest.NewRecorder()
|
||||
verifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/verify", bytes.NewReader(payload)))
|
||||
var response capability.TaskCaseVerificationResponse
|
||||
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.Result != capability.EvaluationSupported || response.QualityScoreMillis != 900 || response.VerifierIdentity == "" || response.VerificationEvidenceRef == "" {
|
||||
t.Fatalf("unexpected verification response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyMismatchIsUnsupportedWithEvidence(t *testing.T) {
|
||||
payload, _ := json.Marshal(capability.TaskCaseVerificationRequest{CaseID: "case-1", ResponseText: "wrong", EvidenceArtifactRef: "case-artifact", Verification: capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}})
|
||||
recorder := httptest.NewRecorder()
|
||||
verifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/verify", bytes.NewReader(payload)))
|
||||
var response capability.TaskCaseVerificationResponse
|
||||
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.Result != capability.EvaluationUnsupported || response.QualityScoreMillis != 0 || response.VerificationEvidenceRef == "" {
|
||||
t.Fatalf("unexpected mismatch response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyJSONFieldEquals(t *testing.T) {
|
||||
payload, _ := json.Marshal(capability.TaskCaseVerificationRequest{CaseID: "json-1", ResponseText: `{"answer":42}`, EvidenceArtifactRef: "case-artifact", Verification: capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationJSONFieldEquals, FieldPath: "answer", ExpectedText: "42", MinimumQualityMillis: 850}})
|
||||
recorder := httptest.NewRecorder()
|
||||
verifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/verify", bytes.NewReader(payload)))
|
||||
var response capability.TaskCaseVerificationResponse
|
||||
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.Result != capability.EvaluationSupported || response.QualityScoreMillis != 850 {
|
||||
t.Fatalf("unexpected JSON verification response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,22 @@ type TaskEvaluationExpansionRequest struct {
|
||||
BenchmarkVersion string
|
||||
}
|
||||
|
||||
type TaskCaseVerificationMode string
|
||||
|
||||
const (
|
||||
TaskCaseVerificationExactText TaskCaseVerificationMode = "EXACT_TEXT"
|
||||
TaskCaseVerificationJSONFieldEquals TaskCaseVerificationMode = "JSON_FIELD_EQUALS"
|
||||
)
|
||||
|
||||
// TaskCaseVerification is a bounded, task-owned rubric. The expected text is
|
||||
// test data, not model output, and is never sent to the cognitive provider.
|
||||
type TaskCaseVerification struct {
|
||||
Mode TaskCaseVerificationMode
|
||||
FieldPath string
|
||||
ExpectedText string
|
||||
MinimumQualityMillis int64
|
||||
}
|
||||
|
||||
// TaskCaseObservation is produced by the bounded evaluator and verifier. A
|
||||
// score without a case result and evidence reference is not accepted as
|
||||
// qualification evidence.
|
||||
@@ -80,6 +96,10 @@ type TaskCaseObservation struct {
|
||||
LatencyMicros int64
|
||||
CriticalFailure bool
|
||||
EvidenceArtifactRef string
|
||||
// VerificationEvidenceRef and VerifierIdentity are required whenever an
|
||||
// observation claims semantic support or a non-zero quality score.
|
||||
VerificationEvidenceRef string
|
||||
VerifierIdentity string
|
||||
}
|
||||
|
||||
// TaskEvaluation is durable evidence for one model/binding revision under one
|
||||
|
||||
@@ -11,6 +11,33 @@ type TaskEvaluationCase struct {
|
||||
CaseID string
|
||||
Prompt string
|
||||
EvidenceArtifactRef string
|
||||
Verification TaskCaseVerification
|
||||
}
|
||||
|
||||
type TaskCaseResponse struct {
|
||||
CaseID string
|
||||
ResponseText string
|
||||
LatencyMicros int64
|
||||
EvidenceArtifactRef string
|
||||
}
|
||||
|
||||
type TaskCaseVerificationRequest struct {
|
||||
CaseID string
|
||||
ResponseText string
|
||||
LatencyMicros int64
|
||||
EvidenceArtifactRef string
|
||||
Verification TaskCaseVerification
|
||||
}
|
||||
|
||||
type TaskCaseVerificationResponse struct {
|
||||
CaseID string
|
||||
Result EvaluationResult
|
||||
QualityScoreMillis int64
|
||||
LatencyMicros int64
|
||||
CriticalFailure bool
|
||||
EvidenceArtifactRef string
|
||||
VerificationEvidenceRef string
|
||||
VerifierIdentity string
|
||||
}
|
||||
|
||||
type TaskEvaluationExecutionRequest struct {
|
||||
@@ -31,5 +58,6 @@ type TaskEvaluationExecutionResponse struct {
|
||||
BindingID CapabilityBindingID
|
||||
BindingRevision int64
|
||||
Observations []TaskCaseObservation
|
||||
Responses []TaskCaseResponse
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
|
||||
@@ -118,10 +118,24 @@ services:
|
||||
WORK_POSTGRES_CREDENTIAL_REF: credential://domovoy/work/db-password/v1
|
||||
WORK_POSTGRES_PASSWORD_FILE: /run/secrets/work_db_password
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
depends_on: { postgres: { condition: service_healthy } }
|
||||
TASK_VERIFIER_URL: http://task-verifier:8091
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
task-verifier: { condition: service_healthy }
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
|
||||
task-verifier:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: task-verifier, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LISTEN_ADDR: :8091
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8091/healthz >/dev/null"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
work-graph-runtime:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: work-graph-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000219-reachability-not-quality.json",
|
||||
"task_id": "DEV-000219",
|
||||
"task_file": "dev/tasks/DEV-000230-development-gate-controller.json",
|
||||
"task_id": "DEV-000230",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000220",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "66ed523248ea6cf0a41383e2206d9a70eeaafda8",
|
||||
"risk_tier": "E",
|
||||
"intent": "Require explicit independent verifier identity and evidence before task evaluation can claim semantic support or non-zero quality.",
|
||||
"requirement_ids": ["independent verifier evidence", "no false quality", "typed model qualification"],
|
||||
"allowed_change_scope": {"modules": ["capability", "agent-worker"], "paths": ["contracts/capability/task_evaluation.go", "modules/capability/domain/task_evaluation.go", "modules/capability/domain/task_evaluation_test.go", "cmd/agent-worker/task_evaluation_executor_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000220-verifier-evidence-boundary.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "cmd/cognitive-worker/**", "LLM-local/**"]},
|
||||
"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": ["TaskCaseObservation", "TaskEvaluation", "existing Work Graph callback"], "may_change": ["typed observation fields for verifier evidence"]},
|
||||
"semantic_owner": "capability owns qualification evidence; verifier remains independent of provider invocation",
|
||||
"canonical_state": ["existing capability task-evaluation evidence"],
|
||||
"state_transitions": ["reachability UNKNOWN -> separately verified SUPPORTED only with evidence"],
|
||||
"side_effects_and_privileges": ["no provider invocation or external mutation"],
|
||||
"persistence_and_migrations": ["existing task evaluation persistence is unchanged"],
|
||||
"failure_retry_restart": ["missing verifier fields fail closed; unknown reachability remains non-promotable"],
|
||||
"assumptions": ["a later verifier runtime will produce VerificationEvidenceRef and VerifierIdentity"],
|
||||
"acceptance_scenarios": ["supported without verifier is rejected", "unknown with zero quality remains valid", "verified supported observation aggregates normally", "no hardware/provider name bypasses the boundary"],
|
||||
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000221",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "0aab10863cb23824688029c0bab6734f100b4610",
|
||||
"risk_tier": "E",
|
||||
"intent": "Provide an independent deterministic task-verifier runtime and wire model task evaluation through response evidence rather than provider self-assertion.",
|
||||
"requirement_ids": ["independent verifier runtime", "typed answer evidence", "quality qualification boundary", "degraded verifier fallback"],
|
||||
"allowed_change_scope": {"modules": ["capability", "cognitive-worker", "agent-worker", "task-verifier", "deployment"], "paths": ["contracts/capability/task_evaluation.go", "contracts/capability/task_evaluation_execution.go", "cmd/cognitive-worker/task_evaluation.go", "cmd/agent-worker/main.go", "cmd/agent-worker/task_evaluation_executor.go", "cmd/agent-worker/task_evaluation_executor_test.go", "cmd/task-verifier/**", "deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000221-task-verifier-runtime.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "modules/**", "LLM-local/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["TaskEvaluationCase", "TaskEvaluationExecutionRequest/Response", "TaskCaseObservation", "existing worker callback"], "may_change": ["typed verification spec, response envelope and verifier result" ]},
|
||||
"semantic_owner": "capability owns evidence; task-verifier is a replaceable independent verifier adapter and does not own canonical state",
|
||||
"canonical_state": ["existing capability task-evaluation evidence only; verifier has no database"],
|
||||
"state_transitions": ["provider response -> bounded response evidence -> verifier result -> TaskCaseObservation -> TaskEvaluation"],
|
||||
"side_effects_and_privileges": ["local network calls only; no secrets; no external mutation; no authority expansion"],
|
||||
"persistence_and_migrations": ["none; verifier evidence is carried through existing callback and capability persistence"],
|
||||
"failure_retry_restart": ["verifier unavailable returns local fallback/waiting; no quality or promotion claim is made"],
|
||||
"assumptions": ["initial verifier mode is exact-text for bounded deterministic cases; future verifiers may add typed modes under separate task packets"],
|
||||
"acceptance_scenarios": ["exact response produces supported evidence with verifier identity", "mismatch produces unsupported evidence", "cognitive worker never asserts support from invocation alone", "worker wires verifier result before callback", "verifier process has health endpoint and production compose dependency", "verifier loss does not produce false quality"],
|
||||
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health and verifier health check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000222",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "d736500d9818a7748c3715c442edb695c5aed1100",
|
||||
"risk_tier": "E",
|
||||
"intent": "Extend the independent task verifier with a structured JSON field mode and add a bounded case builder driven by unresolved evidence.",
|
||||
"requirement_ids": ["task-aware verification", "additional cases during lifetime", "general-first bounded expansion"],
|
||||
"allowed_change_scope": {"modules": ["capability", "task-verifier"], "paths": ["contracts/capability/task_evaluation.go", "cmd/task-verifier/main.go", "cmd/task-verifier/main_test.go", "modules/capability/domain/task_evaluation.go", "modules/capability/domain/task_evaluation_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000222-verifier-modes-and-case-builder.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "cmd/core-runtime/**", "cmd/creator-web/**", "cmd/agent-worker/**", "cmd/cognitive-worker/**", "modules/identity/**", "LLM-local/**"]},
|
||||
"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": ["TaskCaseVerification", "TaskEvaluationCase", "unresolved outcome evidence"], "may_change": ["verification modes and bounded case construction" ]},
|
||||
"semantic_owner": "capability owns evaluation policy; verifier remains stateless adapter",
|
||||
"canonical_state": ["none; case builder returns candidates and does not persist or activate them"],
|
||||
"state_transitions": ["unresolved evidence -> two bounded additional case candidates", "structured response -> verifier evidence -> supported/unsupported"],
|
||||
"side_effects_and_privileges": ["no model calls, no external mutation, no secrets"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["invalid unresolved evidence fails closed; verifier remains stateless and repeatable"],
|
||||
"assumptions": ["generated cases still require an explicit verification spec before quality can be claimed"],
|
||||
"acceptance_scenarios": ["JSON field match verifies with evidence", "JSON mismatch is unsupported", "unresolved evidence yields exactly two bounded probes", "generated cases do not bypass verifier or promotion gate"],
|
||||
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000223",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "ee68e2207eff5535c79cc594637a971cea03d71c",
|
||||
"risk_tier": "E",
|
||||
"intent": "Reconcile the status projection with the merged DEV-000222 verifier stage and preserve explicit remaining runtime and UI gaps.",
|
||||
"requirement_ids": ["current audit evidence", "task lineage accuracy", "negative-state transparency"],
|
||||
"allowed_change_scope": {"modules": ["operations-evidence"], "paths": ["docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000223-status-reconciliation.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "database/**", "deploy/**", "LLM-local/**"]},
|
||||
"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": ["merged task lineage", "existing redacted live evidence"], "may_change": ["operations status projection"]},
|
||||
"semantic_owner": "operations-evidence projection",
|
||||
"canonical_state": ["none; docs are rebuildable projections"],
|
||||
"state_transitions": ["stale task pointer -> merged verifier stage pointer"],
|
||||
"side_effects_and_privileges": ["none; documentation only"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["historical evidence remains immutable"],
|
||||
"assumptions": ["DEV-000222 main revision ee68e22 is the current deployed baseline"],
|
||||
"acceptance_scenarios": ["status names DEV-000222 as last completed", "remaining bridge and current authenticated UI gaps remain explicit", "no secret value is added"],
|
||||
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000224",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "c4411afaaf21886cba9b5ead586afb26d697a355",
|
||||
"risk_tier": "E",
|
||||
"intent": "Record the current production model-fleet readiness and dynamic tier selection without claiming quality or promotion.",
|
||||
"requirement_ids": ["model fleet/failover", "GPU is not intelligence priority", "truthful operational evidence"],
|
||||
"allowed_change_scope": {"modules": ["operations-evidence"], "paths": ["docs/PROJECT_STATUS.md", "docs/evidence/fleet-readiness-live-2026-08-23-v2.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000224-live-fleet-evidence.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "database/**", "deploy/**", "LLM-local/**"]},
|
||||
"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": ["live cognitive-worker fleet readiness response"], "may_change": ["redacted evidence and status projection"]},
|
||||
"semantic_owner": "operations-evidence projection",
|
||||
"canonical_state": ["none; evidence is a rebuildable projection"],
|
||||
"state_transitions": ["live fleet readiness -> redacted evidence record"],
|
||||
"side_effects_and_privileges": ["read-only production inspection"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["unavailable providers remain explicit; no missing data is promoted to ready"],
|
||||
"assumptions": ["fleet endpoint response is authoritative for this observation window"],
|
||||
"acceptance_scenarios": ["FAST unavailable selects available MEDIUM", "HEAVY availability does not imply priority", "LIFEBOAT remains fallback", "secret values are absent"],
|
||||
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health and fleet live check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000225",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "e080c34a772caa5d256ab9f4ae0d2568bb8a7fb9",
|
||||
"risk_tier": "E",
|
||||
"intent": "Serve the existing chat/login shell at the direct /login route so users can reach authentication without a 404.",
|
||||
"requirement_ids": ["UI login direct route", "authenticated acceptance entrypoint"],
|
||||
"allowed_change_scope": {"modules": ["creator-web"], "paths": ["cmd/creator-web/server.go", "cmd/creator-web/server_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000225-login-route-fix.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "modules/**", "LLM-local/**"]},
|
||||
"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 embedded chat/login shell"], "may_change": ["HTTP route mapping only"]},
|
||||
"semantic_owner": "creator-web route projection",
|
||||
"canonical_state": ["none"],
|
||||
"state_transitions": ["GET /login -> existing chat shell"],
|
||||
"side_effects_and_privileges": ["none; GET only"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["route remains deterministic and stateless"],
|
||||
"assumptions": ["authentication behavior remains unchanged and is handled by the existing chat shell"],
|
||||
"acceptance_scenarios": ["GET /login returns 200", "login inputs are present", "root route remains unchanged", "no credential values are added"],
|
||||
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health and /login check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000226",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "40b56798e23d852d26156b0bc2abbc518fd6b9b6",
|
||||
"risk_tier": "E",
|
||||
"intent": "Record current authenticated desktop/mobile UI and operations acceptance evidence without mutating production state.",
|
||||
"requirement_ids": ["UI operations acceptance", "authenticated browser evidence", "responsive acceptance"],
|
||||
"allowed_change_scope": {"modules": ["operations-evidence"], "paths": ["docs/PROJECT_STATUS.md", "docs/evidence/authenticated-ui-acceptance-2026-08-23-v2.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000226-ui-acceptance-evidence.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "database/**", "deploy/**", "LLM-local/**"]},
|
||||
"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": ["current authenticated browser DOM and visual state", "live production UI responses"], "may_change": ["redacted UI acceptance evidence and status projection"]},
|
||||
"semantic_owner": "operations-evidence projection",
|
||||
"canonical_state": ["none; evidence is rebuildable and redacted"],
|
||||
"state_transitions": ["current browser acceptance -> evidence record"],
|
||||
"side_effects_and_privileges": ["read-only navigation and section inspection; no production mutation"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["missing authenticated session or unavailable section must remain explicit"],
|
||||
"assumptions": ["browser evidence reflects the deployed revision shown in the page metadata"],
|
||||
"acceptance_scenarios": ["root chat authenticated", "diagnostics 7/7", "fleet 4/4 with fallback visible", "learning/reflection visible", "390x844 layout readable", "no secret values retained"],
|
||||
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health and authenticated browser evidence"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"task_id": "DEV-000227",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "1f6ca363ee241b41a89bff1f1b603f15e9873b00",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make the existing bounded local task-evaluation path an explicit local executor capability while preserving truthful durable waiting for unsupported work and keeping the external bridge optional.",
|
||||
"requirement_ids": ["local executor", "truthful degraded execution", "optional external bridge"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["agent-worker", "operations-status"],
|
||||
"paths": [
|
||||
"cmd/agent-worker/local_fallback.go",
|
||||
"cmd/agent-worker/local_fallback_test.go",
|
||||
"cmd/agent-worker/task_evaluation_executor.go",
|
||||
"cmd/agent-worker/status.go",
|
||||
"cmd/agent-worker/status_test.go",
|
||||
"cmd/agent-worker/main.go",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000227-local-executor.json",
|
||||
"docs/PROJECT_STATUS.md"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "contracts/**", "database/**", "deploy/**", "LLM-local/**"]
|
||||
},
|
||||
"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": ["work.AgentExecutor", "typed task-evaluation runtime endpoints"],
|
||||
"may_change": ["agent-worker status projection adds local executor capability metadata"]
|
||||
},
|
||||
"semantic_owner": "work agent-worker runtime",
|
||||
"canonical_state": ["existing durable Work dispatch, lease and callback state"],
|
||||
"state_transitions": ["unsupported local request -> WAITING", "supported task evaluation -> verified callback"],
|
||||
"side_effects_and_privileges": ["existing bounded internal task-evaluation calls and Work callback only; no external mutation authority"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["unsupported work remains durable WAITING; existing lease recovery remains authoritative"],
|
||||
"assumptions": ["task-evaluation execution and verification endpoints are already deployed internal capabilities", "external bridge may remain absent"],
|
||||
"acceptance_scenarios": ["local status names bounded local executor and capability", "unsupported work never claims completion", "task evaluation still executes, verifies and callbacks", "external bridge status remains separate", "no secret or new privilege is introduced"],
|
||||
"required_tests": ["go test ./cmd/agent-worker/...", "go vet ./cmd/agent-worker/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production status smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000228",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "2609f31e59d3914cdf4adcf23ec618705277d12a",
|
||||
"risk_tier": "E",
|
||||
"intent": "Expose a read-only, provider-neutral discovery status view so Domovoy can inspect available optional providers and models before requesting qualification, without exposing credential metadata or activating anything.",
|
||||
"requirement_ids": ["controlled discovery", "provider qualification visibility", "optional external tools"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-worker", "operations-status"], "paths": ["cmd/cognitive-worker/discovery.go", "cmd/cognitive-worker/discovery_test.go", "cmd/cognitive-worker/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000228-controlled-discovery.json", "docs/PROJECT_STATUS.md"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "contracts/**", "database/**", "deploy/**", "LLM-local/**"]},
|
||||
"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 LocalProvider discovery probes", "existing capability model metadata"], "may_change": ["read-only cognitive-worker discovery status projection"]},
|
||||
"semantic_owner": "cognitive-worker observation projection",
|
||||
"canonical_state": ["existing Capability discovery registration remains the canonical persisted state"],
|
||||
"state_transitions": ["provider probe -> observed discovery status", "discovery registration remains DISCOVERED/INACTIVE until qualification and promotion"],
|
||||
"side_effects_and_privileges": ["read-only provider probe on explicit status request; no activation, mutation or credential disclosure"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["unavailable provider is reported with health/failure state; persisted discovery remains available through existing owner"],
|
||||
"assumptions": ["provider discovery adapters already enforce bounded requests", "qualification remains a separate task-evaluation and promotion step"],
|
||||
"acceptance_scenarios": ["status lists registered providers deterministically", "model inventory and probe health are visible", "credential references are not returned", "status does not activate or persist a provider", "unknown provider remains absent or unavailable"],
|
||||
"required_tests": ["go test ./cmd/cognitive-worker/...", "go vet ./cmd/cognitive-worker/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production status smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000229",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "22f839df99d8f6abd95f00cf99d34b4b45ace692",
|
||||
"risk_tier": "E",
|
||||
"intent": "Connect authenticated Domovoy/Core requests to the existing bounded Truth/Research web runtime while preserving source freshness, exact citations, conflicts and read-only connector policy.",
|
||||
"requirement_ids": ["Truth/Research runtime", "authenticated internet research", "source evidence"],
|
||||
"allowed_change_scope": {"modules": ["core-runtime", "research-runtime-bridge"], "paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/research_client.go", "cmd/core-runtime/research_client_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000229-research-runtime-bridge.json", "docs/PROJECT_STATUS.md"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "contracts/**", "database/**", "deploy/**", "LLM-local/**"]},
|
||||
"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 epistemics.WebResearchRequest and ResearchResult", "existing authenticated Creator session"], "may_change": ["authenticated Core-to-research runtime adapter route"]},
|
||||
"semantic_owner": "epistemics research runtime; Core is an authenticated transport boundary",
|
||||
"canonical_state": ["epistemics runtime remains sole owner of persisted truth bundles, claims, beliefs and citations"],
|
||||
"state_transitions": ["authenticated request -> bounded web connector -> evaluated persisted finding"],
|
||||
"side_effects_and_privileges": ["read-only outbound source fetches to preconfigured allowlisted hosts; authenticated user request persists evidence in epistemics owner"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["research runtime unavailable returns explicit gateway failure; no claim is emitted by Core"],
|
||||
"assumptions": ["research runtime allowlist is configured outside code", "web connector retains exact source snapshot and does not invent citations"],
|
||||
"acceptance_scenarios": ["unauthenticated research request is denied", "authenticated request reaches research runtime", "source evidence and finding are returned without fabricated claims", "research runtime remains bounded by allowlist", "no secret is forwarded or returned"],
|
||||
"required_tests": ["go test ./cmd/core-runtime/...", "go vet ./cmd/core-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health and research-runtime readiness smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000230",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "ec79f19a7935e1eb8f0f59c4c9ce2940b97c4855",
|
||||
"risk_tier": "E",
|
||||
"intent": "Expose the existing Work-owned governed development state machine through authenticated optimistic-concurrency routes without executing code, changing protected source or bypassing promotion gates.",
|
||||
"requirement_ids": ["governed self-development", "durable gate progression", "canary rollback control"],
|
||||
"allowed_change_scope": {"modules": ["core-runtime", "governed-development"], "paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/development_gate.go", "cmd/core-runtime/development_gate_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000230-development-gate-controller.json", "docs/PROJECT_STATUS.md"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "contracts/**", "database/**", "deploy/**", "LLM-local/**"]},
|
||||
"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": ["Work DevelopmentGate state machine", "authenticated Creator session", "optimistic revision persistence"], "may_change": ["authenticated Core gate controller routes"]},
|
||||
"semantic_owner": "Work governed development gate",
|
||||
"canonical_state": ["Work-owned development gate and existing proposal/scaffold records"],
|
||||
"state_transitions": ["EVIDENCE_COLLECTED -> PROPOSAL_CREATED -> TASK_ADMITTED -> ISOLATED -> VERIFIED -> CANARY -> PROMOTED", "CANARY -> ROLLED_BACK"],
|
||||
"side_effects_and_privileges": ["authenticated durable gate creation/transition only; no checkout, code execution, secret access, production mutation or automatic promotion"],
|
||||
"persistence_and_migrations": ["none; existing development_gates table and repository are reused"],
|
||||
"failure_retry_restart": ["optimistic revision conflict is explicit; invalid or skipped transitions are rejected; rollback remains available only from canary"],
|
||||
"assumptions": ["Creator authentication is the explicit governance boundary for these operator routes", "actual worktree execution and deployment remain separate controlled systems"],
|
||||
"acceptance_scenarios": ["gate creation requires evidence-collected revision one", "sequential transitions require their typed references", "verified cannot jump directly to promoted", "stale revision is rejected", "canary rollback is allowed", "unauthenticated requests are denied"],
|
||||
"required_tests": ["go test ./cmd/core-runtime/...", "go vet ./cmd/core-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## Current audit delta — 2026-08-23
|
||||
|
||||
- `LAST_COMPLETED = DEV-000222`; independent task verification now supports exact text and structured top-level JSON-field checks, and unresolved outcomes can produce two bounded additional case candidates after the general evaluation stage. The task packet is merged to `main` and deployed; quality remains unclaimed unless verifier evidence is present.
|
||||
|
||||
- `CURRENT_PRODUCTION_STATUS = LIVE_HEALTHY`; public `/healthz` is healthy. The exact deployed revision is authoritative in the deployment log and merge record, not duplicated in this mutable status projection. Evidence: `docs/evidence/audit-runtime-status-2026-08-23-v1.json`.
|
||||
- `FAST_PROVIDER_STATUS = LIVE_SMOKE_SUPPORTED`: local CPU gateway authentication is configured through a protected file reference; four bounded local-only advisory cases completed through cognitive-worker. Evidence: `docs/evidence/wp-601-fast-gateway-live-smoke-2026-08-22-v1.json`.
|
||||
- `MEDIUM_PROVIDER_STATUS = LIVE_BOUNDED_SMOKE_SUPPORTED_UNPROMOTED`; four bounded local-provider cases passed with template thinking disabled. This is availability/response-shape evidence only; lifecycle qualification, activation and promotion remain unclaimed. Evidence: `docs/evidence/medium-live-bounded-smoke-2026-08-23-v1.json`.
|
||||
@@ -14,14 +16,16 @@
|
||||
- `GOVERNED_SELF_DEVELOPMENT_STATUS = DURABLE_GATE_PERSISTED`; Work-owned revisioned gate storage and optimistic-concurrency persistence are live. No bridge-backed source mutation, promotion or rollback is claimed. Evidence: `docs/evidence/governed-development-gate-persistence-2026-08-23-v1.json`.
|
||||
- `UI_AUTHENTICATED_ACCEPTANCE = AUTHENTICATED_QUICK_SMOKE_PASS`; browser-driven smoke completed in the active production session: chat response received, admin login accepted, diagnostics 7/7 passed, operations fleet 4/4 READY, and action history loaded. Deep mutation, delegation-specific and long-running acceptance remain intentionally deferred.
|
||||
- `CURRENT_UI_SESSION_STATUS = NO_CURRENT_AUTHENTICATED_SESSION`; the historical quick-smoke evidence above is retained, but the current acceptance run has no signed-in browser session, so no new authenticated visual result is claimed. Public cabinet shell and unauthenticated protection were checked separately.
|
||||
- `AGENT_WORKER_STATUS_CORRECTION = LIVE_TRUTHFUL_LOCAL_FALLBACK`; after DEV-000216, absence of the protected bridge is reported as `executor_configured=false`, `mode=local-fallback`, `ready=false`; local task evaluation is not represented as external agent execution. Evidence: `docs/evidence/audit-runtime-status-2026-08-23-v2.json`.
|
||||
- `AGENT_WORKER_STATUS_CORRECTION = LIVE_TRUTHFUL_LOCAL_EXECUTOR`; absence of the protected bridge is reported as `executor_configured=false`, `mode=local-executor`, `ready=false`, with `task-evaluation` as the only advertised local capability; unsupported work remains durably waiting and is not represented as external agent execution. Evidence: `docs/evidence/audit-runtime-status-2026-08-23-v2.json` (pre-DEV-000227 field shape remains historical).
|
||||
- `FLEET_READINESS_LIVE = MEDIUM_SELECTED_BY_AVAILABILITY`; the live cognitive worker observed FAST unavailable, MEDIUM available, HEAVY available and LIFEBOAT available; it selected MEDIUM while the configured preference remained FAST. This is routing evidence only, not a quality or promotion claim. Evidence: `docs/evidence/fleet-readiness-live-2026-08-23-v2.json`.
|
||||
- `UI_AUTHENTICATED_ACCEPTANCE = CURRENT_LIVE_PASS`; current signed-in browser evidence covers root chat session, admin diagnostics 7/7, operations fleet 4/4 READY with FAST selected, truthful local-executor agent status, action history and learning/reflection sections; the 390x844 console layout remained readable without horizontal overflow. Evidence: `docs/evidence/authenticated-ui-acceptance-2026-08-23-v2.json`.
|
||||
|
||||
## Current governance checkpoint
|
||||
|
||||
- `COMPLETION_MATRIX = docs/evidence/audit-completion-matrix-2026-08-23-v3.json`; all seven objective areas are mapped to current repository/production proof, with external bridge, GPU promotion/quality and deep-acceptance gaps explicit.
|
||||
|
||||
- `ACTIVE_TASK = dev/tasks/DEV-000217-current-status-evidence.json revision 1`
|
||||
- `ACTIVE_TASK = dev/tasks/DEV-000230-development-gate-controller.json revision 1`; authenticated Core now exposes Work-owned durable development gate creation and optimistic-concurrency progression. Code execution, protected-main access, automatic promotion and production mutation remain outside this controller.
|
||||
- `LINEAGE_DECISION = docs/GOVERNANCE_LINEAGE_DECISION_2026-08-22.md`; historical packets remain provenance and are not current authority.
|
||||
- `REGISTRY_STATUS = YAML and JSON semantic mirrors agree`
|
||||
- `PRODUCTION_DOMAIN = docker.cin.su`; shared docker-test is not used for this project.
|
||||
- `NEXT_WORK = provision/qualify an independently operated Agent Execution bridge when available, complete deferred deep browser acceptance with a signed-in session, and add task-quality verification only when live work produces an unresolved case; retain explicit evidence for all remaining negative directions.`
|
||||
- `NEXT_WORK = qualify an independently operated Agent Execution bridge when available and complete deferred deep browser acceptance with a current signed-in session; retain local fallback and general-first task evaluation when either capability is unavailable.`
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"observed_on": "2026-08-23",
|
||||
"source": "current authenticated in-app browser session against assistant.cin.su",
|
||||
"production_revision_visible": "40b56798e23d",
|
||||
"chat": {"authenticated": true, "session_state": "active", "message_surface": "visible"},
|
||||
"admin": {
|
||||
"authenticated": true,
|
||||
"diagnostics": {"passed": 7, "total": 7},
|
||||
"operations": {"fleet_ready": 4, "fleet_total": 4, "selected_tier": "FAST", "fallback_tier": "LIFEBOAT"},
|
||||
"agent_worker": {"mode": "local-fallback", "executor_configured": false, "external_mutation": false},
|
||||
"action_history_records_visible": 25,
|
||||
"learning_signals_visible": 17,
|
||||
"reflection_state_visible": "RESOLVED",
|
||||
"skill_state_visible": "SUSPENDED"
|
||||
},
|
||||
"responsive": {"viewport": "390x844", "horizontal_overflow_observed": false, "sections_readable": true},
|
||||
"mutations_performed": false,
|
||||
"secrets_exposed": false,
|
||||
"limitations": ["no delegation-specific mutation was performed", "no external bridge was configured"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"observed_on": "2026-08-23",
|
||||
"source": "production cognitive-worker read-only endpoint on docker.cin.su",
|
||||
"endpoint": "/v1/fleet/readiness",
|
||||
"preferred_tier": "FAST",
|
||||
"selected_tier": "MEDIUM",
|
||||
"ready_tiers": ["MEDIUM", "HEAVY", "LIFEBOAT"],
|
||||
"fallback_tier": "LIFEBOAT",
|
||||
"members": [
|
||||
{"tier": "FAST", "ready": false, "reason_class": "provider connection refused"},
|
||||
{"tier": "MEDIUM", "ready": true, "reason_class": "available"},
|
||||
{"tier": "HEAVY", "ready": true, "reason_class": "available"},
|
||||
{"tier": "LIFEBOAT", "ready": true, "reason_class": "available"}
|
||||
],
|
||||
"interpretation": "Selection follows current readiness and preserves LIFEBOAT fallback; GPU/HEAVY availability does not by itself override the preferred tier or prove superior quality.",
|
||||
"secret_policy": "values omitted; only typed states and reason classes retained"
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package domain
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/artifact"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
@@ -10,6 +11,28 @@ import (
|
||||
|
||||
var ErrInvalidTaskEvaluation = errors.New("invalid task evaluation")
|
||||
|
||||
type UnresolvedTaskEvidence struct {
|
||||
ID string
|
||||
OriginalPrompt string
|
||||
FailureSummary string
|
||||
EvidenceArtifactRef string
|
||||
}
|
||||
|
||||
// BuildAdditionalTaskEvaluationCases creates two bounded probes from one
|
||||
// unresolved outcome. It creates prompts only; quality still requires a
|
||||
// verification spec and independent verifier evidence.
|
||||
func BuildAdditionalTaskEvaluationCases(value UnresolvedTaskEvidence) ([]capability.TaskEvaluationCase, error) {
|
||||
if strings.TrimSpace(value.ID) == "" || strings.TrimSpace(value.OriginalPrompt) == "" || strings.TrimSpace(value.FailureSummary) == "" || strings.TrimSpace(value.EvidenceArtifactRef) == "" {
|
||||
return nil, ErrInvalidTaskEvaluation
|
||||
}
|
||||
base := strings.TrimSpace(value.OriginalPrompt)
|
||||
failure := strings.TrimSpace(value.FailureSummary)
|
||||
return []capability.TaskEvaluationCase{
|
||||
{CaseID: value.ID + ":reproduce", Prompt: base, EvidenceArtifactRef: value.EvidenceArtifactRef},
|
||||
{CaseID: value.ID + ":address-failure", Prompt: base + "\n\nAddress this observed unresolved condition without assuming it is true: " + failure, EvidenceArtifactRef: value.EvidenceArtifactRef},
|
||||
}, nil
|
||||
}
|
||||
|
||||
const automaticExpansionMinimumOverallScore int64 = 700
|
||||
|
||||
// AutomaticTaskEvaluationExpansion decides whether a completed general
|
||||
@@ -85,6 +108,12 @@ func BuildTaskEvaluation(profile capability.TaskEvaluationProfile, evaluationID
|
||||
if _, ok := seen[observation.CaseID]; ok || observation.EvidenceArtifactRef == "" || observation.QualityScoreMillis < 0 || observation.QualityScoreMillis > 1000 || observation.LatencyMicros < 0 {
|
||||
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||
}
|
||||
if observation.Result == capability.EvaluationSupported && (observation.VerificationEvidenceRef == "" || observation.VerifierIdentity == "") {
|
||||
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||
}
|
||||
if observation.Result == capability.EvaluationUnknown && observation.QualityScoreMillis != 0 {
|
||||
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||
}
|
||||
seen[observation.CaseID] = struct{}{}
|
||||
qualityTotal += observation.QualityScoreMillis
|
||||
latencyTotal += observation.LatencyMicros
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/artifact"
|
||||
@@ -10,14 +11,22 @@ import (
|
||||
func TestBuildTaskEvaluationAggregatesOnlyProfileCases(t *testing.T) {
|
||||
profile := capability.TaskEvaluationProfile{ID: "research-v1", Revision: 1, CapabilityID: "creator-advisory", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "research-v1", RequiredCaseIDs: []string{"claim", "uncertainty"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
|
||||
value, err := BuildTaskEvaluation(profile, "evaluation-1", "binding-1", 2, "creator-advisory", []capability.TaskCaseObservation{
|
||||
{CaseID: "claim", Result: capability.EvaluationSupported, QualityScoreMillis: 900, LatencyMicros: 400_000, EvidenceArtifactRef: "artifact-case-1"},
|
||||
{CaseID: "uncertainty", Result: capability.EvaluationSupported, QualityScoreMillis: 800, LatencyMicros: 600_000, EvidenceArtifactRef: "artifact-case-2"},
|
||||
{CaseID: "claim", Result: capability.EvaluationSupported, QualityScoreMillis: 900, LatencyMicros: 400_000, EvidenceArtifactRef: "artifact-case-1", VerificationEvidenceRef: "verify-case-1", VerifierIdentity: "verifier-1"},
|
||||
{CaseID: "uncertainty", Result: capability.EvaluationSupported, QualityScoreMillis: 800, LatencyMicros: 600_000, EvidenceArtifactRef: "artifact-case-2", VerificationEvidenceRef: "verify-case-2", VerifierIdentity: "verifier-1"},
|
||||
}, artifact.ArtifactID("artifact-summary"), 10)
|
||||
if err != nil || value.Result != capability.EvaluationSupported || value.OverallScoreMillis == 0 || value.ProfileID != profile.ID {
|
||||
t.Fatalf("evaluation=%+v err=%v", value, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTaskEvaluationRejectsSupportedWithoutIndependentVerifier(t *testing.T) {
|
||||
profile := capability.TaskEvaluationProfile{ID: "quality-v1", Revision: 1, CapabilityID: "creator-advisory", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "quality-v1", RequiredCaseIDs: []string{"case"}, QualityWeightMillis: 700, ReliabilityWeightMillis: 200, LatencyWeightMillis: 100}
|
||||
_, err := BuildTaskEvaluation(profile, "evaluation-unverified", "binding-1", 1, profile.CapabilityID, []capability.TaskCaseObservation{{CaseID: "case", Result: capability.EvaluationSupported, QualityScoreMillis: 900, EvidenceArtifactRef: "artifact-case"}}, artifact.ArtifactID("artifact-summary"), 1)
|
||||
if err == nil {
|
||||
t.Fatal("supported observation without verifier evidence was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTaskEvaluationDoesNotPromoteUnknown(t *testing.T) {
|
||||
profile := capability.TaskEvaluationProfile{ID: "coding-v1", Revision: 1, CapabilityID: "creator-advisory", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "coding-v1", RequiredCaseIDs: []string{"compile"}, QualityWeightMillis: 700, ReliabilityWeightMillis: 200, LatencyWeightMillis: 100}
|
||||
value, err := BuildTaskEvaluation(profile, "evaluation-2", "binding-1", 2, "creator-advisory", []capability.TaskCaseObservation{{CaseID: "compile", Result: capability.EvaluationUnknown, QualityScoreMillis: 0, LatencyMicros: 0, EvidenceArtifactRef: "artifact-case"}}, artifact.ArtifactID("artifact-summary"), 11)
|
||||
@@ -51,3 +60,10 @@ func TestAutomaticExpansionDoesNotRunWhenAQualifiedCandidateExists(t *testing.T)
|
||||
t.Fatalf("qualified candidate must suppress expansion: profile=%+v expanded=%t err=%v", value, expanded, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAdditionalTaskEvaluationCasesFromUnresolvedEvidence(t *testing.T) {
|
||||
cases, err := BuildAdditionalTaskEvaluationCases(UnresolvedTaskEvidence{ID: "outcome-1", OriginalPrompt: "compute answer", FailureSummary: "the answer was not verified", EvidenceArtifactRef: "artifact-outcome-1"})
|
||||
if err != nil || len(cases) != 2 || cases[0].CaseID != "outcome-1:reproduce" || !strings.Contains(cases[1].Prompt, "not verified") {
|
||||
t.Fatalf("unexpected generated cases: cases=%+v err=%v", cases, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user