Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7afe3ff48f | ||
|
|
6dec47c791 | ||
|
|
def0e4aff0 | ||
|
|
9a6ab6a9ca | ||
|
|
8f20326c07 | ||
|
|
a82592a130 | ||
|
|
feea909d69 | ||
|
|
b78a026ee1 |
@@ -430,6 +430,14 @@ func (r *CapabilityRepository) SaveCognitiveBinding(ctx context.Context, value c
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error) {
|
||||
return r.loadCognitiveCandidates(ctx, capabilityID, owner, "")
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) LoadCognitiveCandidatesForProfile(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID, profileID capability.TaskEvaluationProfileID) ([]capability.ResolvedCognitiveBinding, error) {
|
||||
return r.loadCognitiveCandidates(ctx, capabilityID, owner, profileID)
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) loadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID, profileID capability.TaskEvaluationProfileID) ([]capability.ResolvedCognitiveBinding, error) {
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
b.binding_id,b.revision,b.capability_id,b.owner_principal_id,b.provider_instance_id,b.provider_revision,b.model_descriptor_id,b.model_revision,b.data_policy,b.qualification,b.activation,b.health,b.limit_policy_ref,
|
||||
p.provider_instance_id,p.revision,p.provider_kind,p.endpoint_ref,COALESCE(p.credential_ref,''),p.enabled,p.qualification,COALESCE(ph.health,p.health),GREATEST(p.observed_at_unix_millis,COALESCE(ph.observed_at_unix_millis,0)),p.billing_mode,p.auth_mode,p.input_cost_micros,p.output_cost_micros,p.request_cost_micros,
|
||||
@@ -454,7 +462,7 @@ func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capa
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = r.loadCognitiveEvidence(ctx, &value); err != nil {
|
||||
if err = r.loadCognitiveEvidence(ctx, &value, profileID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
@@ -462,7 +470,7 @@ func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capa
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value *capability.ResolvedCognitiveBinding) error {
|
||||
func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value *capability.ResolvedCognitiveBinding, profileID capability.TaskEvaluationProfileID) error {
|
||||
var evaluation capability.EvaluatedCapability
|
||||
err := r.pool.QueryRow(ctx, `SELECT evaluation_id,binding_id,binding_revision,capability_id,result,quality_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis FROM capability.cognitive_evaluated_capabilities WHERE binding_id=$1 AND binding_revision=$2 ORDER BY observed_at_unix_millis DESC LIMIT 1`, value.Binding.ID, value.Binding.Revision).Scan(&evaluation.ID, &evaluation.BindingID, &evaluation.BindingRevision, &evaluation.CapabilityID, &evaluation.Result, &evaluation.QualityScoreMillis, &evaluation.CriticalFailureMillis, &evaluation.BenchmarkVersion, &evaluation.ArtifactID, &evaluation.ObservedAtUnixMillis)
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
@@ -479,6 +487,16 @@ func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value
|
||||
if err == nil {
|
||||
value.Performance = &performance
|
||||
}
|
||||
if profileID != "" {
|
||||
var taskEvaluation capability.TaskEvaluation
|
||||
err = r.pool.QueryRow(ctx, `SELECT evaluation_id,task_profile_id,binding_id,binding_revision,capability_id,result,quality_score_millis,reliability_score_millis,latency_score_millis,overall_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis FROM capability.cognitive_task_evaluations WHERE task_profile_id=$1 AND binding_id=$2 AND binding_revision=$3 ORDER BY observed_at_unix_millis DESC LIMIT 1`, profileID, value.Binding.ID, value.Binding.Revision).Scan(&taskEvaluation.ID, &taskEvaluation.ProfileID, &taskEvaluation.BindingID, &taskEvaluation.BindingRevision, &taskEvaluation.CapabilityID, &taskEvaluation.Result, &taskEvaluation.QualityScoreMillis, &taskEvaluation.ReliabilityScoreMillis, &taskEvaluation.LatencyScoreMillis, &taskEvaluation.OverallScoreMillis, &taskEvaluation.CriticalFailureMillis, &taskEvaluation.BenchmarkVersion, &taskEvaluation.ArtifactID, &taskEvaluation.ObservedAtUnixMillis)
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
value.TaskEvaluation = &taskEvaluation
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -592,3 +610,8 @@ func (r *CapabilityRepository) SavePerformanceEvidence(ctx context.Context, valu
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_performance_evidence (evidence_id,binding_id,binding_revision,model_descriptor_id,model_revision,hardware_profile,runtime,quantization,context_units,thinking_enabled,generation_tps_millis,prompt_tps_millis,ttft_micros,latency_micros,memory_bytes,reliability_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) ON CONFLICT DO NOTHING`, value.ID, value.BindingID, value.BindingRevision, value.ModelDescriptorID, value.ModelRevision, value.HardwareProfile, value.Runtime, value.Quantization, value.ContextUnits, value.ThinkingEnabled, value.GenerationTPSMillis, value.PromptTPSMillis, value.TTFTMicros, value.LatencyMicros, value.MemoryBytes, value.ReliabilityMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) SaveTaskEvaluation(ctx context.Context, value capability.TaskEvaluation) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_task_evaluations (evaluation_id,task_profile_id,binding_id,binding_revision,capability_id,result,quality_score_millis,reliability_score_millis,latency_score_millis,overall_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, value.ID, value.ProfileID, value.BindingID, value.BindingRevision, value.CapabilityID, value.Result, value.QualityScoreMillis, value.ReliabilityScoreMillis, value.LatencyScoreMillis, value.OverallScoreMillis, value.CriticalFailureMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
@@ -106,24 +107,28 @@ func (r *WorkRepository) SaveDispatchIntent(ctx context.Context, intent work.Age
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
_, err = tx.Exec(ctx, `INSERT INTO work.agent_dispatch_intents(dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(idempotency_key) DO NOTHING`, intent.DispatchID, intent.IdempotencyKey, intent.DelegationID, intent.RunID, intent.ImplementationID, intent.ImplementationRevision, intent.SourceRevision, intent.WorktreeID, intent.VerificationPlanRef, intent.CreatedAtUnixMillis)
|
||||
_, err = tx.Exec(ctx, `INSERT INTO work.agent_dispatch_intents(dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT(idempotency_key) DO NOTHING`, intent.DispatchID, intent.IdempotencyKey, intent.DelegationID, intent.RunID, intent.ImplementationID, intent.ImplementationRevision, intent.SourceRevision, intent.WorktreeID, intent.VerificationPlanRef, intent.CreatedAtUnixMillis, intent.SpecialistPurpose, intent.TaskEvaluationProfileID, intent.TaskEvaluationBindingID, intent.TaskEvaluationCaseIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("save dispatch intent: %w", err)
|
||||
}
|
||||
var existing work.AgentDispatchIntent
|
||||
err = tx.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE idempotency_key=$1`, intent.IdempotencyKey).Scan(&existing.DispatchID, &existing.IdempotencyKey, &existing.DelegationID, &existing.RunID, &existing.ImplementationID, &existing.ImplementationRevision, &existing.SourceRevision, &existing.WorktreeID, &existing.VerificationPlanRef, &existing.CreatedAtUnixMillis)
|
||||
err = tx.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids FROM work.agent_dispatch_intents WHERE idempotency_key=$1`, intent.IdempotencyKey).Scan(&existing.DispatchID, &existing.IdempotencyKey, &existing.DelegationID, &existing.RunID, &existing.ImplementationID, &existing.ImplementationRevision, &existing.SourceRevision, &existing.WorktreeID, &existing.VerificationPlanRef, &existing.CreatedAtUnixMillis, &existing.SpecialistPurpose, &existing.TaskEvaluationProfileID, &existing.TaskEvaluationBindingID, &existing.TaskEvaluationCaseIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read dispatch intent: %w", err)
|
||||
}
|
||||
if existing != intent {
|
||||
if !sameDispatchIntent(existing, intent) {
|
||||
return ErrDispatchIntentConflict
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func sameDispatchIntent(left, right work.AgentDispatchIntent) bool {
|
||||
return left.DispatchID == right.DispatchID && left.IdempotencyKey == right.IdempotencyKey && left.DelegationID == right.DelegationID && left.RunID == right.RunID && left.ImplementationID == right.ImplementationID && left.ImplementationRevision == right.ImplementationRevision && left.SourceRevision == right.SourceRevision && left.WorktreeID == right.WorktreeID && left.VerificationPlanRef == right.VerificationPlanRef && left.CreatedAtUnixMillis == right.CreatedAtUnixMillis && left.SpecialistPurpose == right.SpecialistPurpose && left.TaskEvaluationProfileID == right.TaskEvaluationProfileID && left.TaskEvaluationBindingID == right.TaskEvaluationBindingID && slices.Equal(left.TaskEvaluationCaseIDs, right.TaskEvaluationCaseIDs)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) LoadDispatchIntent(ctx context.Context, dispatchID string) (work.AgentDispatchIntent, error) {
|
||||
var intent work.AgentDispatchIntent
|
||||
err := r.pool.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE dispatch_id=$1`, dispatchID).Scan(&intent.DispatchID, &intent.IdempotencyKey, &intent.DelegationID, &intent.RunID, &intent.ImplementationID, &intent.ImplementationRevision, &intent.SourceRevision, &intent.WorktreeID, &intent.VerificationPlanRef, &intent.CreatedAtUnixMillis)
|
||||
err := r.pool.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids FROM work.agent_dispatch_intents WHERE dispatch_id=$1`, dispatchID).Scan(&intent.DispatchID, &intent.IdempotencyKey, &intent.DelegationID, &intent.RunID, &intent.ImplementationID, &intent.ImplementationRevision, &intent.SourceRevision, &intent.WorktreeID, &intent.VerificationPlanRef, &intent.CreatedAtUnixMillis, &intent.SpecialistPurpose, &intent.TaskEvaluationProfileID, &intent.TaskEvaluationBindingID, &intent.TaskEvaluationCaseIDs)
|
||||
if err != nil {
|
||||
return work.AgentDispatchIntent{}, fmt.Errorf("load dispatch intent: %w", err)
|
||||
}
|
||||
@@ -191,11 +196,11 @@ func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graphs(graph_id,revision,state,max_parallel,max_attempts) VALUES($1,$2,$3,$4,$5) ON CONFLICT(graph_id,revision) DO NOTHING`, graph.ID, graph.Revision, graph.State, graph.Budget.MaxParallel, graph.Budget.MaxAttempts); err != nil {
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graphs(graph_id,revision,state,max_parallel,max_attempts,parent_graph_id,parent_graph_revision,trigger_evidence_ref) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(graph_id,revision) DO NOTHING`, graph.ID, graph.Revision, graph.State, graph.Budget.MaxParallel, graph.Budget.MaxAttempts, graph.ParentGraphID, graph.ParentGraphRevision, graph.TriggerEvidenceRef); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range graph.Nodes {
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,dispatch_id,idempotency_key,state,attempts) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.DispatchID, node.IdempotencyKey, node.State, node.Attempts); err != nil {
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,dispatch_id,idempotency_key,state,attempts,purpose,task_profile_id,binding_id,case_ids) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.DispatchID, node.IdempotencyKey, node.State, node.Attempts, node.Purpose, node.TaskProfileID, node.BindingID, node.CaseIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -209,17 +214,17 @@ func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph
|
||||
|
||||
func (r *WorkRepository) LoadWorkGraph(ctx context.Context, graphID string, revision int64) (work.WorkGraph, error) {
|
||||
var graph work.WorkGraph
|
||||
err := r.pool.QueryRow(ctx, `SELECT graph_id,revision,state,max_parallel,max_attempts FROM work.graphs WHERE graph_id=$1 AND revision=$2`, graphID, revision).Scan(&graph.ID, &graph.Revision, &graph.State, &graph.Budget.MaxParallel, &graph.Budget.MaxAttempts)
|
||||
err := r.pool.QueryRow(ctx, `SELECT graph_id,revision,state,max_parallel,max_attempts,parent_graph_id,parent_graph_revision,trigger_evidence_ref FROM work.graphs WHERE graph_id=$1 AND revision=$2`, graphID, revision).Scan(&graph.ID, &graph.Revision, &graph.State, &graph.Budget.MaxParallel, &graph.Budget.MaxAttempts, &graph.ParentGraphID, &graph.ParentGraphRevision, &graph.TriggerEvidenceRef)
|
||||
if err != nil {
|
||||
return work.WorkGraph{}, fmt.Errorf("load graph: %w", err)
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT node_id,dispatch_id,idempotency_key,state,attempts FROM work.graph_nodes WHERE graph_id=$1 AND revision=$2 ORDER BY node_id`, graphID, revision)
|
||||
rows, err := r.pool.Query(ctx, `SELECT node_id,dispatch_id,idempotency_key,state,attempts,purpose,task_profile_id,binding_id,case_ids FROM work.graph_nodes WHERE graph_id=$1 AND revision=$2 ORDER BY node_id`, graphID, revision)
|
||||
if err != nil {
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var node work.GraphNode
|
||||
if err := rows.Scan(&node.ID, &node.DispatchID, &node.IdempotencyKey, &node.State, &node.Attempts); err != nil {
|
||||
if err := rows.Scan(&node.ID, &node.DispatchID, &node.IdempotencyKey, &node.State, &node.Attempts, &node.Purpose, &node.TaskProfileID, &node.BindingID, &node.CaseIDs); err != nil {
|
||||
rows.Close()
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
|
||||
@@ -842,7 +842,13 @@ func main() {
|
||||
http.Error(w, "invalid cognitive requirement", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
|
||||
var candidates []capability.ResolvedCognitiveBinding
|
||||
var err error
|
||||
if requirement.TaskProfileID != "" {
|
||||
candidates, err = capabilityRepository.LoadCognitiveCandidatesForProfile(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID, requirement.TaskProfileID)
|
||||
} else {
|
||||
candidates, err = capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
@@ -891,6 +897,20 @@ func main() {
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/cognitive-registry/task-evaluations", func(w http.ResponseWriter, r *http.Request) {
|
||||
var value capability.TaskEvaluation
|
||||
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&value); err != nil || capabilitydomain.ValidateTaskEvaluation(value) != nil {
|
||||
http.Error(w, "invalid task evaluation", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := capabilityRepository.SaveTaskEvaluation(r.Context(), value); err != nil {
|
||||
http.Error(w, "save task evaluation", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/cognitive-canary", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/modules/work/application"
|
||||
"git.cin.su/m/Assistant/platform/secrets"
|
||||
"git.cin.su/m/Assistant/runtime/health"
|
||||
@@ -26,6 +28,10 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer repository.Close()
|
||||
evidenceSink, err := newCoreTaskEvaluationSink(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 12 * time.Second})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "work-graph-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "work-graph-runtime", Probes: []health.Probe{{Name: "work-postgres", Check: func(ctx context.Context) error { return repository.Ping(ctx) }}}})
|
||||
@@ -42,6 +48,19 @@ func main() {
|
||||
}
|
||||
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/task-evaluations/schedule", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request application.TaskEvaluationScheduleRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
|
||||
http.Error(w, "invalid task evaluation graph request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
graph, ready, err := application.AdmitAndScheduleTaskEvaluation(r.Context(), repository, request)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/complete", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request application.GraphCompleteRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&request); err != nil {
|
||||
@@ -55,6 +74,19 @@ func main() {
|
||||
}
|
||||
writeJSON(w, graph)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/task-evaluations/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request work.TaskEvaluationEvidenceCallback
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
|
||||
http.Error(w, "invalid task evaluation callback", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
graph, err := application.CompleteTaskEvaluationNode(r.Context(), repository, evidenceSink, request)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSON(w, graph)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/synthesis", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request application.GraphSynthesisRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
|
||||
@@ -76,6 +108,13 @@ func main() {
|
||||
log.Fatal(http.ListenAndServe(addr, mux))
|
||||
}
|
||||
|
||||
func envOr(name, fallback string) string {
|
||||
if value := os.Getenv(name); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, value any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
type coreTaskEvaluationSink struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newCoreTaskEvaluationSink(baseURL string, client *http.Client) (*coreTaskEvaluationSink, error) {
|
||||
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||
if baseURL == "" {
|
||||
return nil, fmt.Errorf("core runtime URL is required")
|
||||
}
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return &coreTaskEvaluationSink{baseURL: baseURL, client: client}, nil
|
||||
}
|
||||
|
||||
func (s *coreTaskEvaluationSink) SaveTaskEvaluation(ctx context.Context, value capability.TaskEvaluation) error {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, s.baseURL+"/v1/cognitive-registry/task-evaluations", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := s.client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return fmt.Errorf("save task evaluation: status %s", response.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -160,28 +160,32 @@ type CognitiveRequirement struct {
|
||||
ExcludedBindingIDs []CapabilityBindingID
|
||||
Objective SelectionObjective
|
||||
MaxCostMicros *int64
|
||||
TaskProfileID TaskEvaluationProfileID
|
||||
}
|
||||
|
||||
// ResolvedCognitiveBinding is a provider-neutral invocation snapshot selected
|
||||
// by Capability. It contains references and declared metadata, never secrets.
|
||||
type ResolvedCognitiveBinding struct {
|
||||
Binding CognitiveBinding
|
||||
Provider ProviderInstance
|
||||
Model ModelDescriptor
|
||||
Evaluation *EvaluatedCapability
|
||||
Performance *PerformanceEvidence
|
||||
Rating *ModelRating
|
||||
Binding CognitiveBinding
|
||||
Provider ProviderInstance
|
||||
Model ModelDescriptor
|
||||
Evaluation *EvaluatedCapability
|
||||
Performance *PerformanceEvidence
|
||||
TaskEvaluation *TaskEvaluation
|
||||
Rating *ModelRating
|
||||
}
|
||||
|
||||
// ModelRating is a derived, provider-neutral explanation of why an eligible
|
||||
// model is routed. It never grants qualification or activation authority.
|
||||
type ModelRating struct {
|
||||
Category ModelRatingCategory `json:"category"`
|
||||
OverallScoreMillis int64 `json:"overall_score_millis"`
|
||||
QualityScoreMillis int64 `json:"quality_score_millis"`
|
||||
ReliabilityScoreMillis int64 `json:"reliability_score_millis"`
|
||||
LatencyScoreMillis int64 `json:"latency_score_millis"`
|
||||
EvidenceScoreMillis int64 `json:"evidence_score_millis"`
|
||||
TaskProfileID TaskEvaluationProfileID `json:"task_profile_id,omitempty"`
|
||||
TaskScoreMillis int64 `json:"task_score_millis,omitempty"`
|
||||
Category ModelRatingCategory `json:"category"`
|
||||
OverallScoreMillis int64 `json:"overall_score_millis"`
|
||||
QualityScoreMillis int64 `json:"quality_score_millis"`
|
||||
ReliabilityScoreMillis int64 `json:"reliability_score_millis"`
|
||||
LatencyScoreMillis int64 `json:"latency_score_millis"`
|
||||
EvidenceScoreMillis int64 `json:"evidence_score_millis"`
|
||||
}
|
||||
|
||||
type CognitiveResultClass string
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package capability
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/artifact"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
type TaskEvaluationProfileID ids.ID
|
||||
type TaskEvaluationID ids.ID
|
||||
type TaskEvaluationProfileStage string
|
||||
|
||||
const (
|
||||
TaskEvaluationGeneral TaskEvaluationProfileStage = "GENERAL"
|
||||
TaskEvaluationAdditional TaskEvaluationProfileStage = "ADDITIONAL"
|
||||
)
|
||||
|
||||
// TaskEvaluationProfile is the extensible, provider-neutral description of a
|
||||
// task-specific qualification run. Case payloads live in Artifact; the
|
||||
// profile stores only stable case references and scoring policy.
|
||||
type TaskEvaluationProfile struct {
|
||||
ID TaskEvaluationProfileID
|
||||
Revision int64
|
||||
CapabilityID CapabilityID
|
||||
Stage TaskEvaluationProfileStage
|
||||
ParentProfileID TaskEvaluationProfileID
|
||||
TriggerEvidenceRef string
|
||||
BenchmarkVersion string
|
||||
RequiredCaseIDs []string
|
||||
QualityWeightMillis int64
|
||||
ReliabilityWeightMillis int64
|
||||
LatencyWeightMillis int64
|
||||
}
|
||||
|
||||
func (p TaskEvaluationProfile) Validate() error {
|
||||
if p.ID == "" || p.Revision < 1 || p.CapabilityID == "" || (p.Stage != TaskEvaluationGeneral && p.Stage != TaskEvaluationAdditional) || p.BenchmarkVersion == "" || len(p.RequiredCaseIDs) == 0 {
|
||||
return errors.New("task evaluation profile identity and cases are incomplete")
|
||||
}
|
||||
if p.Stage == TaskEvaluationGeneral && (p.ParentProfileID != "" || p.TriggerEvidenceRef != "") {
|
||||
return errors.New("general task evaluation profile cannot have an expansion parent")
|
||||
}
|
||||
if p.Stage == TaskEvaluationAdditional && (p.ParentProfileID == "" || p.TriggerEvidenceRef == "") {
|
||||
return errors.New("additional task evaluation profile requires parent and trigger evidence")
|
||||
}
|
||||
if p.QualityWeightMillis < 0 || p.ReliabilityWeightMillis < 0 || p.LatencyWeightMillis < 0 || p.QualityWeightMillis+p.ReliabilityWeightMillis+p.LatencyWeightMillis != 1000 {
|
||||
return errors.New("task evaluation profile weights must be non-negative and sum to 1000")
|
||||
}
|
||||
seen := make(map[string]struct{}, len(p.RequiredCaseIDs))
|
||||
for _, caseID := range p.RequiredCaseIDs {
|
||||
if caseID == "" {
|
||||
return errors.New("task evaluation profile contains an empty case")
|
||||
}
|
||||
if _, ok := seen[caseID]; ok {
|
||||
return errors.New("task evaluation profile contains duplicate cases")
|
||||
}
|
||||
seen[caseID] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TaskEvaluationExpansionRequest is emitted after a task remains unresolved
|
||||
// with the current best eligible candidate. It creates only a bounded new
|
||||
// profile; it never silently rewrites the general benchmark.
|
||||
type TaskEvaluationExpansionRequest struct {
|
||||
ParentProfileID TaskEvaluationProfileID
|
||||
CapabilityID CapabilityID
|
||||
TriggerEvidenceRef string
|
||||
NewCaseIDs []string
|
||||
BenchmarkVersion string
|
||||
}
|
||||
|
||||
// TaskCaseObservation is produced by the bounded evaluator and verifier. A
|
||||
// score without a case result and evidence reference is not accepted as
|
||||
// qualification evidence.
|
||||
type TaskCaseObservation struct {
|
||||
CaseID string
|
||||
Result EvaluationResult
|
||||
QualityScoreMillis int64
|
||||
LatencyMicros int64
|
||||
CriticalFailure bool
|
||||
EvidenceArtifactRef string
|
||||
}
|
||||
|
||||
// TaskEvaluation is durable evidence for one model/binding revision under one
|
||||
// task profile. It is not activation authority.
|
||||
type TaskEvaluation struct {
|
||||
ID TaskEvaluationID
|
||||
ProfileID TaskEvaluationProfileID
|
||||
BindingID CapabilityBindingID
|
||||
BindingRevision int64
|
||||
CapabilityID CapabilityID
|
||||
Result EvaluationResult
|
||||
QualityScoreMillis int64
|
||||
ReliabilityScoreMillis int64
|
||||
LatencyScoreMillis int64
|
||||
OverallScoreMillis int64
|
||||
CriticalFailureMillis int64
|
||||
BenchmarkVersion string
|
||||
ArtifactID artifact.ArtifactID
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
+51
-11
@@ -1,11 +1,17 @@
|
||||
package work
|
||||
|
||||
import "git.cin.su/m/Assistant/contracts/capability"
|
||||
|
||||
type GraphNodeID string
|
||||
|
||||
type GraphState string
|
||||
type GraphNodeState string
|
||||
type GraphNodePurpose string
|
||||
|
||||
const (
|
||||
GraphNodePurposeTaskEvaluation GraphNodePurpose = "TASK_EVALUATION_SPECIALIST"
|
||||
GraphNodePurposeGeneric GraphNodePurpose = "GENERIC"
|
||||
|
||||
GraphDraft GraphState = "DRAFT"
|
||||
GraphAdmitted GraphState = "ADMITTED"
|
||||
GraphRunning GraphState = "RUNNING"
|
||||
@@ -31,11 +37,15 @@ type GraphBudget struct {
|
||||
}
|
||||
|
||||
type GraphNode struct {
|
||||
ID GraphNodeID `json:"id"`
|
||||
DispatchID string `json:"dispatch_id"`
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
State GraphNodeState `json:"state"`
|
||||
Attempts int `json:"attempts"`
|
||||
ID GraphNodeID `json:"id"`
|
||||
DispatchID string `json:"dispatch_id"`
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
State GraphNodeState `json:"state"`
|
||||
Attempts int `json:"attempts"`
|
||||
Purpose GraphNodePurpose `json:"purpose,omitempty"`
|
||||
TaskProfileID capability.TaskEvaluationProfileID `json:"task_profile_id,omitempty"`
|
||||
BindingID capability.CapabilityBindingID `json:"binding_id,omitempty"`
|
||||
CaseIDs []string `json:"case_ids,omitempty"`
|
||||
}
|
||||
|
||||
type GraphEdge struct {
|
||||
@@ -44,12 +54,42 @@ type GraphEdge struct {
|
||||
}
|
||||
|
||||
type WorkGraph struct {
|
||||
ID string `json:"id"`
|
||||
Revision int64 `json:"revision"`
|
||||
State GraphState `json:"state"`
|
||||
Nodes []GraphNode `json:"nodes"`
|
||||
Edges []GraphEdge `json:"edges"`
|
||||
Budget GraphBudget `json:"budget"`
|
||||
ID string `json:"id"`
|
||||
Revision int64 `json:"revision"`
|
||||
State GraphState `json:"state"`
|
||||
Nodes []GraphNode `json:"nodes"`
|
||||
Edges []GraphEdge `json:"edges"`
|
||||
Budget GraphBudget `json:"budget"`
|
||||
ParentGraphID string `json:"parent_graph_id,omitempty"`
|
||||
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
|
||||
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
|
||||
}
|
||||
|
||||
// TaskEvaluationGraphRequest describes a durable fan-out. The graph contains
|
||||
// one specialist node per binding; each node receives the profile's bounded
|
||||
// cases and reports evidence back to Capability for fan-in aggregation.
|
||||
type TaskEvaluationGraphRequest struct {
|
||||
GraphID string `json:"graph_id"`
|
||||
Profile capability.TaskEvaluationProfile `json:"profile"`
|
||||
BindingIDs []capability.CapabilityBindingID `json:"binding_ids"`
|
||||
DispatchIDs []string `json:"dispatch_ids"`
|
||||
IdempotencyPrefix string `json:"idempotency_prefix"`
|
||||
Budget GraphBudget `json:"budget"`
|
||||
ParentGraphID string `json:"parent_graph_id,omitempty"`
|
||||
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
|
||||
ParentGraphState GraphState `json:"parent_graph_state,omitempty"`
|
||||
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
|
||||
}
|
||||
|
||||
// TaskEvaluationEvidenceCallback is emitted by a specialist after it has
|
||||
// completed the bounded case set. Capability remains the evidence owner;
|
||||
// Work only advances its matching graph node after the evidence is accepted.
|
||||
type TaskEvaluationEvidenceCallback struct {
|
||||
GraphID string `json:"graph_id"`
|
||||
Revision int64 `json:"revision"`
|
||||
NodeID GraphNodeID `json:"node_id"`
|
||||
Evaluation capability.TaskEvaluation `json:"evaluation"`
|
||||
Succeeded bool `json:"succeeded"`
|
||||
}
|
||||
|
||||
type ResultCandidate struct {
|
||||
|
||||
+19
-11
@@ -1,6 +1,10 @@
|
||||
package work
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
type AgentLeaseID string
|
||||
type AgentFencingToken uint64
|
||||
@@ -50,14 +54,18 @@ type IndependentVerification struct {
|
||||
}
|
||||
|
||||
type AgentDispatchIntent struct {
|
||||
DispatchID string `json:"dispatch_id"`
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
DelegationID DelegationID `json:"delegation_id"`
|
||||
RunID AgentRunID `json:"run_id"`
|
||||
ImplementationID string `json:"implementation_id"`
|
||||
ImplementationRevision int64 `json:"implementation_revision"`
|
||||
SourceRevision string `json:"source_revision"`
|
||||
WorktreeID string `json:"worktree_id"`
|
||||
VerificationPlanRef string `json:"verification_plan_ref"`
|
||||
CreatedAtUnixMillis int64 `json:"created_at_unix_millis"`
|
||||
DispatchID string `json:"dispatch_id"`
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
DelegationID DelegationID `json:"delegation_id"`
|
||||
RunID AgentRunID `json:"run_id"`
|
||||
ImplementationID string `json:"implementation_id"`
|
||||
ImplementationRevision int64 `json:"implementation_revision"`
|
||||
SourceRevision string `json:"source_revision"`
|
||||
WorktreeID string `json:"worktree_id"`
|
||||
VerificationPlanRef string `json:"verification_plan_ref"`
|
||||
CreatedAtUnixMillis int64 `json:"created_at_unix_millis"`
|
||||
SpecialistPurpose string `json:"specialist_purpose,omitempty"`
|
||||
TaskEvaluationProfileID capability.TaskEvaluationProfileID `json:"task_evaluation_profile_id,omitempty"`
|
||||
TaskEvaluationBindingID capability.CapabilityBindingID `json:"task_evaluation_binding_id,omitempty"`
|
||||
TaskEvaluationCaseIDs []string `json:"task_evaluation_case_ids,omitempty"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
BEGIN;
|
||||
SET ROLE capability_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.cognitive_task_evaluations (
|
||||
evaluation_id TEXT PRIMARY KEY,
|
||||
task_profile_id TEXT NOT NULL,
|
||||
binding_id TEXT NOT NULL,
|
||||
binding_revision BIGINT NOT NULL,
|
||||
capability_id TEXT NOT NULL,
|
||||
result TEXT NOT NULL,
|
||||
quality_score_millis BIGINT NOT NULL,
|
||||
reliability_score_millis BIGINT NOT NULL,
|
||||
latency_score_millis BIGINT NOT NULL,
|
||||
overall_score_millis BIGINT NOT NULL,
|
||||
critical_failure_millis BIGINT NOT NULL,
|
||||
benchmark_version TEXT NOT NULL,
|
||||
artifact_id TEXT NOT NULL,
|
||||
observed_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (binding_revision > 0),
|
||||
CHECK (result IN ('UNKNOWN','SUPPORTED','UNSUPPORTED')),
|
||||
CHECK (quality_score_millis BETWEEN 0 AND 1000),
|
||||
CHECK (reliability_score_millis BETWEEN 0 AND 1000),
|
||||
CHECK (latency_score_millis BETWEEN 0 AND 1000),
|
||||
CHECK (overall_score_millis BETWEEN 0 AND 1000),
|
||||
CHECK (critical_failure_millis BETWEEN 0 AND 1000),
|
||||
CHECK (observed_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS cognitive_task_evaluations_lookup
|
||||
ON capability.cognitive_task_evaluations(task_profile_id, binding_id, binding_revision, observed_at_unix_millis DESC);
|
||||
|
||||
GRANT SELECT, INSERT ON capability.cognitive_task_evaluations TO capability_role;
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,23 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
ALTER TABLE work.agent_dispatch_intents
|
||||
ADD COLUMN IF NOT EXISTS specialist_purpose TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_profile_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_binding_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_case_ids TEXT[] NOT NULL DEFAULT '{}';
|
||||
|
||||
ALTER TABLE work.graphs
|
||||
ADD COLUMN IF NOT EXISTS parent_graph_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS parent_graph_revision BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS trigger_evidence_ref TEXT NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE work.graph_nodes
|
||||
ADD COLUMN IF NOT EXISTS purpose TEXT NOT NULL DEFAULT 'GENERIC',
|
||||
ADD COLUMN IF NOT EXISTS task_profile_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS binding_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS case_ids TEXT[] NOT NULL DEFAULT '{}';
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000207-gpu-invocation-qualification.json",
|
||||
"task_id": "DEV-000207",
|
||||
"task_file": "dev/tasks/DEV-000211-task-evaluator-callback.json",
|
||||
"task_id": "DEV-000211",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000208",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "b78a026ee169047feb8468f0799fa1b9f4e27562",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make Domovoy's model qualification and routing task-aware through parallel durable specialist work: typed evaluation profiles/evidence, durable task scores, and explainable selection without hardware/provider heuristics.",
|
||||
"requirement_ids": ["task-specific model evaluation", "parallel specialist evaluation", "no main execution thread", "model fleet/failover", "hardware-neutral routing", "explainable model rating"],
|
||||
"allowed_change_scope": {"modules": ["capability", "core-runtime"], "paths": ["contracts/capability/**", "modules/capability/**", "adapters/postgres/capability_repository.go", "cmd/core-runtime/main.go", "database/migrations/039_capability_task_evaluations.sql", "tests/model_qualification/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000208-task-aware-model-evaluation.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", ".gitea/**", "contracts/authority/**", "modules/identity/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
|
||||
"contracts": {"consume": ["CognitiveRequirement", "ResolvedCognitiveBinding", "existing capability evidence", "existing capability owner repository", "Work Graph durable fan-out/fan-in"], "may_change": ["task profile/evaluation contracts", "task-aware selection request/response serialization"]},
|
||||
"semantic_owner": "capability",
|
||||
"canonical_state": ["capability task evaluation evidence keyed by task profile and binding revision"],
|
||||
"state_transitions": ["GENERAL profile -> first bounded evaluation", "unresolved task evidence -> ADDITIONAL profile with parent and trigger evidence", "profile result UNKNOWN -> SUPPORTED/UNSUPPORTED; evidence is append-only by binding revision", "task-aware requirement -> eligible candidates with explainable task rating", "evaluation request -> durable fan-out -> specialist evidence -> fan-in aggregate"],
|
||||
"side_effects_and_privileges": ["core capability database role writes only; no provider calls and no secret access in this task"],
|
||||
"persistence_and_migrations": ["039 adds capability.cognitive_task_evaluations owned by capability"],
|
||||
"failure_retry_restart": ["missing, stale or unknown task evidence excludes a task-aware candidate; no score defaults to success", "one specialist failure does not block unrelated tasks; stale leases are recoverable and fan-in preserves partial/unknown state"],
|
||||
"assumptions": ["a later Work Graph evaluation runner will produce per-case evidence; this task establishes the durable contract and routing gate", "the chat/request path never waits synchronously for the full evaluation graph"],
|
||||
"acceptance_scenarios": ["general profile runs before additional profiles", "an unresolved task creates only a bounded additional profile linked to failure evidence", "task-aware requirement rejects a model without current supported task evidence", "task scores are included in explainable rating", "provider kind, endpoint and hardware do not affect score", "unknown/failed task evidence never silently becomes supported", "existing requirements without a task profile retain current behavior"],
|
||||
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "migration/schema checks", "CI", "production public health check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000209",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "a82592a1300c06ed7a568c058faca6560e085ab3",
|
||||
"risk_tier": "E",
|
||||
"intent": "Create durable Work Graph plans for task-aware model evaluation: run the general profile first, fan out independent model specialists in parallel, and permit bounded additional profiles linked to unresolved evidence without blocking the chat path.",
|
||||
"requirement_ids": ["general-first model evaluation", "parallel specialist evaluation", "bounded additional task tests", "fan-out/fan-in", "no synchronous chat blocking", "provider and hardware neutral"],
|
||||
"allowed_change_scope": {"modules": ["work"], "paths": ["contracts/work/**", "modules/work/**", "cmd/work-graph-runtime/**", "tests/model_qualification/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000209-task-evaluator-work-graph.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/devctl/**", "modules/identity/**", "database/**"]},
|
||||
"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": ["capability.TaskEvaluationProfile", "capability.TaskEvaluationProfileStage", "capability.TaskEvaluationProfileID", "existing Work Graph schedule contract"], "may_change": ["typed Work Graph node purpose and task-evaluation planning contract"]},
|
||||
"semantic_owner": "work",
|
||||
"canonical_state": ["work-owned durable graph and node lifecycle; capability remains owner of model evaluation evidence"],
|
||||
"state_transitions": ["GENERAL profile -> admitted evaluation graph", "eligible model bindings -> parallel specialist nodes", "all specialist nodes -> fan-in completion state", "ADDITIONAL profile -> a separate bounded graph linked by parent/trigger evidence"],
|
||||
"side_effects_and_privileges": ["none; graph planning only, no provider calls and no secret access"],
|
||||
"persistence_and_migrations": ["uses existing work graph persistence; no new schema"],
|
||||
"failure_retry_restart": ["each specialist remains independently retryable through existing node attempts/leases; partial or unknown graph state is preserved", "invalid stage/parent linkage is rejected before scheduling"],
|
||||
"assumptions": ["a later worker adapter executes each dispatched specialist and posts evidence to capability", "chat/request handlers enqueue the graph and return without waiting for all nodes"],
|
||||
"acceptance_scenarios": ["additional profile cannot be scheduled as a replacement for general profile", "one node is created per binding and profile with independent dispatch identity", "parallel budget is retained for fan-out", "graph metadata is sufficient for a worker to identify profile, binding and specialist role", "provider kind, endpoint and hardware are absent from planning inputs"],
|
||||
"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_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000210",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "9a6ab6a9caf68e65f302c95ab0859b259122dacf",
|
||||
"risk_tier": "E",
|
||||
"intent": "Carry task-evaluation specialist metadata through durable dispatch, persistence and recovery so workers can execute bounded model cases and return evidence without losing profile lineage.",
|
||||
"requirement_ids": ["worker execution context", "durable task metadata", "recovery-safe dispatch", "bounded case set", "no synchronous chat blocking"],
|
||||
"allowed_change_scope": {"modules": ["work"], "paths": ["contracts/work/**", "modules/work/**", "adapters/postgres/work_repository.go", "cmd/agent-worker/**", "database/migrations/040_task_evaluation_dispatch.sql", "tests/model_qualification/**", "tests/agent_execution/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000210-task-evaluator-worker.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/devctl/**", "modules/identity/**", "modules/capability/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
|
||||
"contracts": {"consume": ["Work Graph task-evaluation node metadata", "AgentExecutionRequest/Result", "existing lease and recovery contract"], "may_change": ["typed dispatch task-evaluation metadata"]},
|
||||
"semantic_owner": "work",
|
||||
"canonical_state": ["work-owned dispatch metadata and graph lineage; capability remains owner of evaluation evidence"],
|
||||
"state_transitions": ["graph specialist node -> enriched dispatch intent", "claimed dispatch -> executor receives profile/binding/cases", "stale claim -> recovery preserves task metadata for retry"],
|
||||
"side_effects_and_privileges": ["work role migration only; no provider calls and no secret access added"],
|
||||
"persistence_and_migrations": ["040 adds typed task metadata columns to existing work dispatch/graph tables"],
|
||||
"failure_retry_restart": ["metadata is persisted before enqueue; idempotency conflict rejects divergent task context", "lease timeout/recovery does not erase profile or case references"],
|
||||
"assumptions": ["an upcoming cognitive specialist executor uses the typed metadata to invoke the model gateway and post Capability evidence", "external bridge absence remains typed WAITING/local-fallback"],
|
||||
"acceptance_scenarios": ["task graph dispatch carries profile, binding and all cases", "persist/load round trip preserves metadata", "divergent idempotency retry is rejected", "legacy non-task dispatch remains valid", "recovery path does not claim success"],
|
||||
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "migration/schema checks", "CI", "production public health check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000211",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "6dec47c79163f456cf6b18085a268ebb2619c7bb",
|
||||
"risk_tier": "E",
|
||||
"intent": "Accept specialist task-evaluation evidence through a Work Graph callback: persist evidence through the Capability owner and complete the matching graph node only after typed lineage and revision checks pass.",
|
||||
"requirement_ids": ["evidence callback", "fan-in completion", "owner-local persistence", "revision safety", "no cross-owner DB access"],
|
||||
"allowed_change_scope": {"modules": ["work"], "paths": ["contracts/work/**", "modules/work/**", "cmd/work-graph-runtime/**", "tests/model_qualification/**", "tests/agent_execution/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000211-task-evaluator-callback.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/devctl/**", "modules/identity/**", "database/**", "modules/capability/**"]},
|
||||
"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": ["capability.TaskEvaluation", "existing Work Graph revision/completion contract", "Core Runtime capability evaluation HTTP boundary"], "may_change": ["typed task-evaluation callback contract"]},
|
||||
"semantic_owner": "work for graph lifecycle; capability for evaluation evidence",
|
||||
"canonical_state": ["Capability owns evaluation evidence; Work owns graph/node lifecycle and callback lineage"],
|
||||
"state_transitions": ["specialist result -> callback received", "callback -> Capability evidence persisted", "persisted evidence -> matching node completion and graph fan-in"],
|
||||
"side_effects_and_privileges": ["remote Core Runtime call through existing internal boundary; no direct foreign DB access and no new secrets"],
|
||||
"persistence_and_migrations": ["uses existing capability task-evaluation and work graph persistence; no migration"],
|
||||
"failure_retry_restart": ["Capability write failure leaves node incomplete and callback retryable", "stale graph revision is rejected; unknown/unsupported evaluation is not promoted by callback"],
|
||||
"assumptions": ["specialist executor has already produced evidence and artifact references", "the callback is asynchronous and never blocks the chat request"],
|
||||
"acceptance_scenarios": ["callback rejects non-task graph node", "callback rejects profile/binding lineage mismatch", "Capability write occurs before node completion", "sink failure leaves graph node unfinished", "valid callback advances graph fan-in", "duplicate evidence remains idempotent at Capability owner"],
|
||||
"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_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
@@ -148,6 +148,9 @@ func SelectCognitiveBinding(requirement capability.CognitiveRequirement, candida
|
||||
if candidate.Evaluation != nil && candidate.Evaluation.Result == capability.EvaluationUnsupported {
|
||||
continue
|
||||
}
|
||||
if requirement.TaskProfileID != "" && (candidate.TaskEvaluation == nil || candidate.TaskEvaluation.ProfileID != requirement.TaskProfileID || candidate.TaskEvaluation.Result != capability.EvaluationSupported) {
|
||||
continue
|
||||
}
|
||||
rating := RateCognitiveBinding(candidate)
|
||||
candidate.Rating = &rating
|
||||
if requirement.MaxCostMicros != nil {
|
||||
@@ -171,6 +174,19 @@ func SelectCognitiveBinding(requirement capability.CognitiveRequirement, candida
|
||||
// evidence. Provider kind, endpoint name and hardware are intentionally not
|
||||
// inputs to the score.
|
||||
func RateCognitiveBinding(candidate capability.ResolvedCognitiveBinding) capability.ModelRating {
|
||||
if candidate.TaskEvaluation != nil {
|
||||
evaluation := candidate.TaskEvaluation
|
||||
if evaluation.Result != capability.EvaluationSupported {
|
||||
return capability.ModelRating{TaskProfileID: evaluation.ProfileID, Category: capability.ModelRatingUnrated}
|
||||
}
|
||||
category := capability.ModelRatingBalanced
|
||||
if evaluation.OverallScoreMillis >= 850 && evaluation.QualityScoreMillis >= 800 {
|
||||
category = capability.ModelRatingQuality
|
||||
} else if evaluation.LatencyScoreMillis >= 800 && evaluation.OverallScoreMillis >= 700 {
|
||||
category = capability.ModelRatingFast
|
||||
}
|
||||
return capability.ModelRating{TaskProfileID: evaluation.ProfileID, TaskScoreMillis: evaluation.OverallScoreMillis, Category: category, OverallScoreMillis: evaluation.OverallScoreMillis, QualityScoreMillis: evaluation.QualityScoreMillis, ReliabilityScoreMillis: evaluation.ReliabilityScoreMillis, LatencyScoreMillis: evaluation.LatencyScoreMillis, EvidenceScoreMillis: 1000}
|
||||
}
|
||||
if candidate.Evaluation == nil || candidate.Evaluation.Result != capability.EvaluationSupported || candidate.Evaluation.QualityScoreMillis == nil || candidate.Evaluation.CriticalFailureMillis == nil {
|
||||
return capability.ModelRating{Category: capability.ModelRatingUnrated}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/artifact"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
var ErrInvalidTaskEvaluation = errors.New("invalid task evaluation")
|
||||
|
||||
func BuildAdditionalTaskEvaluationProfile(parent capability.TaskEvaluationProfile, request capability.TaskEvaluationExpansionRequest, profileID capability.TaskEvaluationProfileID, revision int64) (capability.TaskEvaluationProfile, error) {
|
||||
if err := parent.Validate(); err != nil || parent.Stage != capability.TaskEvaluationGeneral || request.ParentProfileID != parent.ID || request.CapabilityID != parent.CapabilityID || request.TriggerEvidenceRef == "" || len(request.NewCaseIDs) == 0 || profileID == "" || revision < 1 || request.BenchmarkVersion == "" {
|
||||
return capability.TaskEvaluationProfile{}, ErrInvalidTaskEvaluation
|
||||
}
|
||||
known := make(map[string]struct{}, len(parent.RequiredCaseIDs))
|
||||
for _, caseID := range parent.RequiredCaseIDs {
|
||||
known[caseID] = struct{}{}
|
||||
}
|
||||
seen := make(map[string]struct{}, len(request.NewCaseIDs))
|
||||
for _, caseID := range request.NewCaseIDs {
|
||||
if caseID == "" {
|
||||
return capability.TaskEvaluationProfile{}, ErrInvalidTaskEvaluation
|
||||
}
|
||||
if _, exists := known[caseID]; exists {
|
||||
return capability.TaskEvaluationProfile{}, ErrInvalidTaskEvaluation
|
||||
}
|
||||
if _, exists := seen[caseID]; exists {
|
||||
return capability.TaskEvaluationProfile{}, ErrInvalidTaskEvaluation
|
||||
}
|
||||
seen[caseID] = struct{}{}
|
||||
}
|
||||
return capability.TaskEvaluationProfile{ID: profileID, Revision: revision, CapabilityID: parent.CapabilityID, Stage: capability.TaskEvaluationAdditional, ParentProfileID: parent.ID, TriggerEvidenceRef: request.TriggerEvidenceRef, BenchmarkVersion: request.BenchmarkVersion, RequiredCaseIDs: append([]string(nil), request.NewCaseIDs...), QualityWeightMillis: parent.QualityWeightMillis, ReliabilityWeightMillis: parent.ReliabilityWeightMillis, LatencyWeightMillis: parent.LatencyWeightMillis}, nil
|
||||
}
|
||||
|
||||
// BuildTaskEvaluation aggregates independently observed case results. It is
|
||||
// intentionally deterministic: no provider name, hardware label or caller
|
||||
// supplied aggregate score can influence the result.
|
||||
func BuildTaskEvaluation(profile capability.TaskEvaluationProfile, evaluationID capability.TaskEvaluationID, bindingID capability.CapabilityBindingID, bindingRevision int64, capabilityID capability.CapabilityID, observations []capability.TaskCaseObservation, artifactID artifact.ArtifactID, observedAtUnixMillis int64) (capability.TaskEvaluation, error) {
|
||||
if err := profile.Validate(); err != nil || evaluationID == "" || bindingID == "" || bindingRevision < 1 || capabilityID == "" || capabilityID != profile.CapabilityID || artifactID == "" || observedAtUnixMillis < 1 || len(observations) != len(profile.RequiredCaseIDs) {
|
||||
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||
}
|
||||
required := make(map[string]struct{}, len(profile.RequiredCaseIDs))
|
||||
for _, caseID := range profile.RequiredCaseIDs {
|
||||
required[caseID] = struct{}{}
|
||||
}
|
||||
seen := make(map[string]struct{}, len(observations))
|
||||
qualityTotal, latencyTotal, passed, critical := int64(0), int64(0), int64(0), int64(0)
|
||||
unknown := false
|
||||
unsupported := false
|
||||
for _, observation := range observations {
|
||||
if _, ok := required[observation.CaseID]; !ok {
|
||||
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||
}
|
||||
if _, ok := seen[observation.CaseID]; ok || observation.EvidenceArtifactRef == "" || observation.QualityScoreMillis < 0 || observation.QualityScoreMillis > 1000 || observation.LatencyMicros < 0 {
|
||||
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||
}
|
||||
seen[observation.CaseID] = struct{}{}
|
||||
qualityTotal += observation.QualityScoreMillis
|
||||
latencyTotal += observation.LatencyMicros
|
||||
if observation.CriticalFailure {
|
||||
critical++
|
||||
}
|
||||
switch observation.Result {
|
||||
case capability.EvaluationSupported:
|
||||
passed++
|
||||
case capability.EvaluationUnsupported:
|
||||
unsupported = true
|
||||
case capability.EvaluationUnknown:
|
||||
unknown = true
|
||||
default:
|
||||
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||
}
|
||||
}
|
||||
if len(seen) != len(required) {
|
||||
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||
}
|
||||
count := int64(len(observations))
|
||||
quality := qualityTotal / count
|
||||
reliability := passed * 1000 / count
|
||||
latency := latencyScore(latencyTotal / count)
|
||||
criticalMillis := critical * 1000 / count
|
||||
overall := (quality*profile.QualityWeightMillis + reliability*profile.ReliabilityWeightMillis + latency*profile.LatencyWeightMillis) / 1000
|
||||
result := capability.EvaluationSupported
|
||||
if unknown {
|
||||
result = capability.EvaluationUnknown
|
||||
} else if unsupported {
|
||||
result = capability.EvaluationUnsupported
|
||||
}
|
||||
return capability.TaskEvaluation{ID: evaluationID, ProfileID: profile.ID, BindingID: bindingID, BindingRevision: bindingRevision, CapabilityID: capabilityID, Result: result, QualityScoreMillis: quality, ReliabilityScoreMillis: reliability, LatencyScoreMillis: latency, OverallScoreMillis: overall, CriticalFailureMillis: criticalMillis, BenchmarkVersion: profile.BenchmarkVersion, ArtifactID: artifactID, ObservedAtUnixMillis: observedAtUnixMillis}, nil
|
||||
}
|
||||
|
||||
func ValidateTaskEvaluation(value capability.TaskEvaluation) error {
|
||||
if value.ID == "" || value.ProfileID == "" || value.BindingID == "" || value.BindingRevision < 1 || value.CapabilityID == "" || value.BenchmarkVersion == "" || value.ArtifactID == "" || value.ObservedAtUnixMillis < 1 {
|
||||
return ErrInvalidTaskEvaluation
|
||||
}
|
||||
if value.Result != capability.EvaluationUnknown && value.Result != capability.EvaluationSupported && value.Result != capability.EvaluationUnsupported {
|
||||
return ErrInvalidTaskEvaluation
|
||||
}
|
||||
for _, score := range []int64{value.QualityScoreMillis, value.ReliabilityScoreMillis, value.LatencyScoreMillis, value.OverallScoreMillis, value.CriticalFailureMillis} {
|
||||
if score < 0 || score > 1000 {
|
||||
return ErrInvalidTaskEvaluation
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/artifact"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
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"},
|
||||
}, 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 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)
|
||||
if err != nil || value.Result != capability.EvaluationUnknown {
|
||||
t.Fatalf("evaluation=%+v err=%v", value, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAdditionalProfileRequiresUnresolvedEvidenceAndPreservesHistory(t *testing.T) {
|
||||
parent := capability.TaskEvaluationProfile{ID: "general-v1", Revision: 1, CapabilityID: "creator-advisory", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"general"}, QualityWeightMillis: 700, ReliabilityWeightMillis: 200, LatencyWeightMillis: 100}
|
||||
profile, err := BuildAdditionalTaskEvaluationProfile(parent, capability.TaskEvaluationExpansionRequest{ParentProfileID: parent.ID, CapabilityID: parent.CapabilityID, TriggerEvidenceRef: "experience-unsolved-1", NewCaseIDs: []string{"edge-case"}, BenchmarkVersion: "additional-v1"}, "additional-v1", 1)
|
||||
if err != nil || profile.Stage != capability.TaskEvaluationAdditional || profile.ParentProfileID != parent.ID || profile.RequiredCaseIDs[0] != "edge-case" {
|
||||
t.Fatalf("profile=%+v err=%v", profile, err)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ type Repository interface {
|
||||
SaveModelDescriptor(context.Context, capability.ModelDescriptor) error
|
||||
SaveCognitiveBinding(context.Context, capability.CognitiveBinding) error
|
||||
LoadCognitiveCandidates(context.Context, capability.CapabilityID, identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error)
|
||||
LoadCognitiveCandidatesForProfile(context.Context, capability.CapabilityID, identity.PrincipalID, capability.TaskEvaluationProfileID) ([]capability.ResolvedCognitiveBinding, error)
|
||||
LoadCognitiveCandidate(context.Context, capability.CapabilityBindingID, int64) (capability.ResolvedCognitiveBinding, error)
|
||||
SaveCognitiveCanaryPromotion(context.Context, capability.CognitiveCanaryPromotion) error
|
||||
SaveCognitiveActivationPromotion(context.Context, capability.CognitiveActivationPromotion) error
|
||||
@@ -25,4 +26,5 @@ type Repository interface {
|
||||
SaveCognitiveDiscovery(context.Context, capability.CognitiveDiscoveryRegistration) error
|
||||
SaveEvaluatedCapability(context.Context, capability.EvaluatedCapability) error
|
||||
SavePerformanceEvidence(context.Context, capability.PerformanceEvidence) error
|
||||
SaveTaskEvaluation(context.Context, capability.TaskEvaluation) error
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
)
|
||||
|
||||
var ErrGraphDispatchBinding = errors.New("graph node dispatch binding is missing")
|
||||
var ErrTaskEvaluationCallbackMismatch = errors.New("task evaluation callback does not match graph node")
|
||||
|
||||
type GraphScheduleRequest struct {
|
||||
Graph work.WorkGraph `json:"graph"`
|
||||
@@ -18,6 +19,12 @@ type GraphScheduleRequest struct {
|
||||
Now int64 `json:"now_unix_millis"`
|
||||
}
|
||||
|
||||
type TaskEvaluationScheduleRequest struct {
|
||||
Plan work.TaskEvaluationGraphRequest `json:"plan"`
|
||||
Intents []work.AgentDispatchIntent `json:"intents"`
|
||||
Now int64 `json:"now_unix_millis"`
|
||||
}
|
||||
|
||||
type GraphCompleteRequest struct {
|
||||
GraphID string `json:"graph_id"`
|
||||
Revision int64 `json:"revision"`
|
||||
@@ -36,6 +43,31 @@ type GraphSynthesisRequest struct {
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
func CompleteTaskEvaluationNode(ctx context.Context, graphStore ports.GraphStore, evidenceSink ports.TaskEvaluationSink, request work.TaskEvaluationEvidenceCallback) (work.WorkGraph, error) {
|
||||
if evidenceSink == nil || request.GraphID == "" || request.Revision < 1 || request.NodeID == "" || !request.Succeeded {
|
||||
return work.WorkGraph{}, ErrTaskEvaluationCallbackMismatch
|
||||
}
|
||||
graph, err := graphStore.LoadWorkGraph(ctx, request.GraphID, request.Revision)
|
||||
if err != nil {
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
matched := false
|
||||
for _, node := range graph.Nodes {
|
||||
if node.ID != request.NodeID {
|
||||
continue
|
||||
}
|
||||
matched = node.Purpose == work.GraphNodePurposeTaskEvaluation && node.TaskProfileID == request.Evaluation.ProfileID && node.BindingID == request.Evaluation.BindingID
|
||||
break
|
||||
}
|
||||
if !matched || request.Evaluation.BindingRevision < 1 || request.Evaluation.CapabilityID == "" {
|
||||
return work.WorkGraph{}, ErrTaskEvaluationCallbackMismatch
|
||||
}
|
||||
if err := evidenceSink.SaveTaskEvaluation(ctx, request.Evaluation); err != nil {
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
return CompleteGraphNode(ctx, graphStore, request.GraphID, request.Revision, request.NodeID, true)
|
||||
}
|
||||
|
||||
func SynthesizeGraph(ctx context.Context, store ports.GraphResultStore, request GraphSynthesisRequest) (work.ConflictSet, work.GraphSynthesisCandidate, error) {
|
||||
conflict, synthesis, err := domain.BuildVerifiedGraphSynthesis(request.Conflict, request.Candidates, request.SelectedCandidate, request.VerifierRunID, request.VerifierIdentity, request.EvidenceRefs, request.Summary, request.Verified)
|
||||
if err != nil {
|
||||
@@ -105,6 +137,32 @@ func AdmitAndSchedule(ctx context.Context, store ports.GraphStore, request Graph
|
||||
return next, ready, nil
|
||||
}
|
||||
|
||||
// AdmitAndScheduleTaskEvaluation creates the durable general-first or
|
||||
// bounded-expansion fan-out before handing it to the existing graph scheduler.
|
||||
// The request path only persists/enqueues work; specialist execution happens
|
||||
// independently through the dispatch workers.
|
||||
func AdmitAndScheduleTaskEvaluation(ctx context.Context, store ports.GraphStore, request TaskEvaluationScheduleRequest) (work.WorkGraph, []work.GraphNodeID, error) {
|
||||
graph, err := domain.BuildTaskEvaluationGraph(request.Plan)
|
||||
if err != nil {
|
||||
return work.WorkGraph{}, nil, err
|
||||
}
|
||||
intents := make([]work.AgentDispatchIntent, len(request.Intents))
|
||||
copy(intents, request.Intents)
|
||||
for i := range intents {
|
||||
for _, node := range graph.Nodes {
|
||||
if node.DispatchID != intents[i].DispatchID {
|
||||
continue
|
||||
}
|
||||
intents[i].SpecialistPurpose = string(node.Purpose)
|
||||
intents[i].TaskEvaluationProfileID = node.TaskProfileID
|
||||
intents[i].TaskEvaluationBindingID = node.BindingID
|
||||
intents[i].TaskEvaluationCaseIDs = append([]string(nil), node.CaseIDs...)
|
||||
break
|
||||
}
|
||||
}
|
||||
return AdmitAndSchedule(ctx, store, GraphScheduleRequest{Graph: graph, Intents: intents, Now: request.Now})
|
||||
}
|
||||
|
||||
func CompleteGraphNode(ctx context.Context, store ports.GraphStore, graphID string, revision int64, nodeID work.GraphNodeID, success bool) (work.WorkGraph, error) {
|
||||
graph, err := store.LoadWorkGraph(ctx, graphID, revision)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
type callbackGraphStore struct {
|
||||
graph work.WorkGraph
|
||||
saves int
|
||||
}
|
||||
|
||||
func (s *callbackGraphStore) SaveWorkGraph(_ context.Context, graph work.WorkGraph) error {
|
||||
s.graph = graph
|
||||
s.saves++
|
||||
return nil
|
||||
}
|
||||
func (s *callbackGraphStore) LoadWorkGraph(context.Context, string, int64) (work.WorkGraph, error) {
|
||||
return s.graph, nil
|
||||
}
|
||||
func (s *callbackGraphStore) SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error {
|
||||
return nil
|
||||
}
|
||||
func (s *callbackGraphStore) EnqueueDispatch(context.Context, work.AgentDispatchQueueItem) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type callbackSink struct {
|
||||
evaluation capability.TaskEvaluation
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *callbackSink) SaveTaskEvaluation(_ context.Context, value capability.TaskEvaluation) error {
|
||||
s.evaluation = value
|
||||
return s.err
|
||||
}
|
||||
|
||||
func callbackRequest() (work.WorkGraph, work.TaskEvaluationEvidenceCallback) {
|
||||
graph := work.WorkGraph{ID: "graph-1", Revision: 1, State: work.GraphRunning, Budget: work.GraphBudget{MaxParallel: 1, MaxAttempts: 1}, Nodes: []work.GraphNode{{ID: "node-1", DispatchID: "dispatch-1", IdempotencyKey: "idem-1", State: work.GraphNodeRunning, Purpose: work.GraphNodePurposeTaskEvaluation, TaskProfileID: "profile-1", BindingID: "binding-1"}}}
|
||||
evaluation := capability.TaskEvaluation{ID: "evaluation-1", ProfileID: "profile-1", BindingID: "binding-1", BindingRevision: 2, CapabilityID: "capability-1", Result: capability.EvaluationSupported}
|
||||
return graph, work.TaskEvaluationEvidenceCallback{GraphID: "graph-1", Revision: 1, NodeID: "node-1", Evaluation: evaluation, Succeeded: true}
|
||||
}
|
||||
|
||||
func TestCompleteTaskEvaluationPersistsEvidenceBeforeGraphCompletion(t *testing.T) {
|
||||
graph, callback := callbackRequest()
|
||||
store := &callbackGraphStore{graph: graph}
|
||||
sink := &callbackSink{}
|
||||
completed, err := CompleteTaskEvaluationNode(context.Background(), store, sink, callback)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sink.evaluation.ID != "evaluation-1" || store.saves != 1 || completed.State != work.GraphCompleted || completed.Nodes[0].State != work.GraphNodeSuccess {
|
||||
t.Fatalf("callback did not complete in order: evaluation=%+v saves=%d graph=%+v", sink.evaluation, store.saves, completed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteTaskEvaluationLeavesNodeOpenWhenCapabilityRejectsEvidence(t *testing.T) {
|
||||
graph, callback := callbackRequest()
|
||||
store := &callbackGraphStore{graph: graph}
|
||||
sink := &callbackSink{err: errors.New("capability unavailable")}
|
||||
if _, err := CompleteTaskEvaluationNode(context.Background(), store, sink, callback); err == nil {
|
||||
t.Fatal("expected capability failure")
|
||||
}
|
||||
if store.saves != 0 {
|
||||
t.Fatalf("graph was advanced after capability failure: saves=%d", store.saves)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteTaskEvaluationRejectsLineageMismatch(t *testing.T) {
|
||||
graph, callback := callbackRequest()
|
||||
callback.Evaluation.ProfileID = "profile-other"
|
||||
store := &callbackGraphStore{graph: graph}
|
||||
if _, err := CompleteTaskEvaluationNode(context.Background(), store, &callbackSink{}, callback); !errors.Is(err, ErrTaskEvaluationCallbackMismatch) {
|
||||
t.Fatalf("expected lineage rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
type taskGraphStore struct {
|
||||
graph work.WorkGraph
|
||||
intents []work.AgentDispatchIntent
|
||||
}
|
||||
|
||||
func (s *taskGraphStore) SaveWorkGraph(_ context.Context, graph work.WorkGraph) error {
|
||||
s.graph = graph
|
||||
return nil
|
||||
}
|
||||
func (s *taskGraphStore) LoadWorkGraph(context.Context, string, int64) (work.WorkGraph, error) {
|
||||
return s.graph, nil
|
||||
}
|
||||
func (s *taskGraphStore) SaveDispatchIntent(_ context.Context, intent work.AgentDispatchIntent) error {
|
||||
s.intents = append(s.intents, intent)
|
||||
return nil
|
||||
}
|
||||
func (s *taskGraphStore) EnqueueDispatch(context.Context, work.AgentDispatchQueueItem) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAdmitAndScheduleTaskEvaluationCarriesSpecialistContext(t *testing.T) {
|
||||
store := &taskGraphStore{}
|
||||
request := TaskEvaluationScheduleRequest{
|
||||
Plan: work.TaskEvaluationGraphRequest{
|
||||
GraphID: "graph-task", Profile: capability.TaskEvaluationProfile{ID: "profile-general", Revision: 1, CapabilityID: "capability-chat", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-a", "case-b"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100},
|
||||
BindingIDs: []capability.CapabilityBindingID{"binding-a"}, DispatchIDs: []string{"dispatch-a"}, IdempotencyPrefix: "eval", Budget: work.GraphBudget{MaxParallel: 1, MaxAttempts: 2},
|
||||
},
|
||||
Intents: []work.AgentDispatchIntent{{DispatchID: "dispatch-a", IdempotencyKey: "idem-a", DelegationID: "delegation-a", RunID: "run-a", ImplementationID: "impl", ImplementationRevision: 1, SourceRevision: "source", WorktreeID: "tree", VerificationPlanRef: "verify", CreatedAtUnixMillis: 1}},
|
||||
}
|
||||
if _, _, err := AdmitAndScheduleTaskEvaluation(context.Background(), store, request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(store.intents) != 1 || store.intents[0].TaskEvaluationProfileID != "profile-general" || store.intents[0].TaskEvaluationBindingID != "binding-a" || len(store.intents[0].TaskEvaluationCaseIDs) != 2 || store.intents[0].SpecialistPurpose != string(work.GraphNodePurposeTaskEvaluation) {
|
||||
t.Fatalf("specialist context was not carried into dispatch: %+v", store.intents)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
var ErrInvalidTaskEvaluationGraph = errors.New("invalid task evaluation graph request")
|
||||
|
||||
// BuildTaskEvaluationGraph turns one bounded profile into durable specialist
|
||||
// nodes. It deliberately has no provider, endpoint or hardware inputs: those
|
||||
// are resolved later by the cognitive worker for each binding.
|
||||
func BuildTaskEvaluationGraph(request work.TaskEvaluationGraphRequest) (work.WorkGraph, error) {
|
||||
if request.GraphID == "" || request.IdempotencyPrefix == "" || len(request.BindingIDs) == 0 || len(request.BindingIDs) != len(request.DispatchIDs) || request.Budget.MaxParallel < 1 || request.Budget.MaxAttempts < 1 {
|
||||
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||
}
|
||||
if err := request.Profile.Validate(); err != nil {
|
||||
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||
}
|
||||
if request.Profile.Stage == capability.TaskEvaluationGeneral {
|
||||
if request.ParentGraphID != "" || request.ParentGraphRevision != 0 || request.ParentGraphState != "" || request.TriggerEvidenceRef != "" {
|
||||
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||
}
|
||||
} else {
|
||||
if request.ParentGraphID == "" || request.ParentGraphRevision < 1 || request.TriggerEvidenceRef == "" || (request.ParentGraphState != work.GraphCompleted && request.ParentGraphState != work.GraphPartial) {
|
||||
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||
}
|
||||
if request.Profile.ParentProfileID == "" {
|
||||
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||
}
|
||||
}
|
||||
seenBindings := make(map[capability.CapabilityBindingID]struct{}, len(request.BindingIDs))
|
||||
seenDispatches := make(map[string]struct{}, len(request.DispatchIDs))
|
||||
nodes := make([]work.GraphNode, 0, len(request.BindingIDs))
|
||||
for i, bindingID := range request.BindingIDs {
|
||||
if bindingID == "" || request.DispatchIDs[i] == "" {
|
||||
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||
}
|
||||
if _, ok := seenBindings[bindingID]; ok {
|
||||
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||
}
|
||||
if _, ok := seenDispatches[request.DispatchIDs[i]]; ok {
|
||||
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||
}
|
||||
seenBindings[bindingID] = struct{}{}
|
||||
seenDispatches[request.DispatchIDs[i]] = struct{}{}
|
||||
nodes = append(nodes, work.GraphNode{
|
||||
ID: work.GraphNodeID(fmt.Sprintf("%s-node-%d", request.GraphID, i+1)),
|
||||
DispatchID: request.DispatchIDs[i],
|
||||
IdempotencyKey: fmt.Sprintf("%s:%s:%d", request.IdempotencyPrefix, bindingID, request.Profile.Revision),
|
||||
State: work.GraphNodeBlocked,
|
||||
Purpose: work.GraphNodePurposeTaskEvaluation,
|
||||
TaskProfileID: request.Profile.ID,
|
||||
BindingID: bindingID,
|
||||
CaseIDs: append([]string(nil), request.Profile.RequiredCaseIDs...),
|
||||
})
|
||||
}
|
||||
return work.WorkGraph{ID: request.GraphID, Revision: 1, State: work.GraphDraft, Nodes: nodes, Budget: request.Budget, ParentGraphID: request.ParentGraphID, ParentGraphRevision: request.ParentGraphRevision, TriggerEvidenceRef: request.TriggerEvidenceRef}, nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
func evaluationGraphRequest(stage capability.TaskEvaluationProfileStage) work.TaskEvaluationGraphRequest {
|
||||
profile := capability.TaskEvaluationProfile{ID: "profile-1", Revision: 1, CapabilityID: "capability-1", Stage: stage, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-a", "case-b"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
|
||||
request := work.TaskEvaluationGraphRequest{GraphID: "graph-1", Profile: profile, BindingIDs: []capability.CapabilityBindingID{"binding-a", "binding-b"}, DispatchIDs: []string{"dispatch-a", "dispatch-b"}, IdempotencyPrefix: "task-eval", Budget: work.GraphBudget{MaxParallel: 2, MaxAttempts: 2}}
|
||||
if stage == capability.TaskEvaluationAdditional {
|
||||
request.Profile.ParentProfileID = "profile-general"
|
||||
request.Profile.TriggerEvidenceRef = "artifact:unresolved-1"
|
||||
request.Profile.Stage = capability.TaskEvaluationAdditional
|
||||
request.ParentGraphID, request.ParentGraphRevision, request.ParentGraphState, request.TriggerEvidenceRef = "graph-general", 2, work.GraphPartial, "artifact:unresolved-1"
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
func TestBuildTaskEvaluationGraphFansOutOneNodePerBinding(t *testing.T) {
|
||||
graph, err := BuildTaskEvaluationGraph(evaluationGraphRequest(capability.TaskEvaluationGeneral))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(graph.Nodes) != 2 || graph.Nodes[0].Purpose != work.GraphNodePurposeTaskEvaluation || graph.Nodes[0].TaskProfileID != "profile-1" || len(graph.Nodes[0].CaseIDs) != 2 {
|
||||
t.Fatalf("unexpected task graph nodes: %+v", graph.Nodes)
|
||||
}
|
||||
if graph.ParentGraphID != "" || graph.TriggerEvidenceRef != "" {
|
||||
t.Fatalf("general graph unexpectedly has expansion lineage: %+v", graph)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTaskEvaluationGraphRequiresCompletedOrPartialGeneralParent(t *testing.T) {
|
||||
request := evaluationGraphRequest(capability.TaskEvaluationAdditional)
|
||||
request.ParentGraphState = work.GraphRunning
|
||||
if _, err := BuildTaskEvaluationGraph(request); err == nil {
|
||||
t.Fatal("expected additional graph to require a finished parent graph")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTaskEvaluationGraphRejectsGeneralReplacement(t *testing.T) {
|
||||
request := evaluationGraphRequest(capability.TaskEvaluationGeneral)
|
||||
request.ParentGraphID = "old-graph"
|
||||
request.ParentGraphRevision = 2
|
||||
request.ParentGraphState = work.GraphPartial
|
||||
request.TriggerEvidenceRef = "artifact:unresolved"
|
||||
if _, err := BuildTaskEvaluationGraph(request); err == nil {
|
||||
t.Fatal("expected general graph replacement to be rejected")
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package ports
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
@@ -16,3 +17,9 @@ type GraphStore interface {
|
||||
type GraphResultStore interface {
|
||||
SaveGraphSynthesis(context.Context, work.ConflictSet, []work.ResultCandidate, work.GraphSynthesisCandidate) error
|
||||
}
|
||||
|
||||
// TaskEvaluationSink is a remote-capable boundary to Capability's owner.
|
||||
// Work never writes the capability schema directly.
|
||||
type TaskEvaluationSink interface {
|
||||
SaveTaskEvaluation(context.Context, capability.TaskEvaluation) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package agent_execution
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTaskEvaluationDispatchMigrationPersistsWorkerContext(t *testing.T) {
|
||||
payload, err := os.ReadFile("../../database/migrations/040_task_evaluation_dispatch.sql")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sql := strings.ToLower(string(payload))
|
||||
for _, marker := range []string{"work.agent_dispatch_intents", "specialist_purpose", "task_evaluation_profile_id", "task_evaluation_binding_id", "task_evaluation_case_ids", "work.graphs", "parent_graph_id", "work.graph_nodes", "case_ids", "set role work_role"} {
|
||||
if !strings.Contains(sql, strings.ToLower(marker)) {
|
||||
t.Fatalf("migration missing %q", marker)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user