Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b17caa3ea9 | ||
|
|
66ed523248 | ||
|
|
c5b5055ff1 | ||
|
|
7476f43ffa | ||
|
|
be29ed5776 | ||
|
|
ab840878fe | ||
|
|
aebc932b97 | ||
|
|
05a246ea3f | ||
|
|
c847988630 | ||
|
|
62f0c529b3 | ||
|
|
54958d0902 | ||
|
|
8d3e7f7cac | ||
|
|
c9eb398857 | ||
|
|
284ea88553 | ||
|
|
f9b9765100 | ||
|
|
863a379048 | ||
|
|
a86d1994c3 | ||
|
|
699eac00eb | ||
|
|
dff6c91c8e | ||
|
|
7afe3ff48f | ||
|
|
6dec47c791 | ||
|
|
def0e4aff0 | ||
|
|
9a6ab6a9ca | ||
|
|
8f20326c07 | ||
|
|
a82592a130 | ||
|
|
feea909d69 | ||
|
|
b78a026ee1 | ||
|
|
cf35c0ae25 | ||
|
|
9f96393fb9 | ||
|
|
beee8deb34 | ||
|
|
faab913227 | ||
|
|
346a8c8467 | ||
|
|
288d18a4fb | ||
|
|
97ed2e90ee | ||
|
|
b4d166480e |
@@ -430,6 +430,32 @@ 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) LoadCognitiveCandidate(ctx context.Context, bindingID capability.CapabilityBindingID, revision int64) (capability.ResolvedCognitiveBinding, error) {
|
||||
var capabilityID capability.CapabilityID
|
||||
var owner identity.PrincipalID
|
||||
if err := r.pool.QueryRow(ctx, `SELECT capability_id,owner_principal_id FROM capability.cognitive_bindings WHERE binding_id=$1 AND revision=$2`, bindingID, revision).Scan(&capabilityID, &owner); err != nil {
|
||||
return capability.ResolvedCognitiveBinding{}, err
|
||||
}
|
||||
candidates, err := r.loadCognitiveCandidates(ctx, capabilityID, owner, "")
|
||||
if err != nil {
|
||||
return capability.ResolvedCognitiveBinding{}, err
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if candidate.Binding.ID == bindingID && candidate.Binding.Revision == revision {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return capability.ResolvedCognitiveBinding{}, pgx.ErrNoRows
|
||||
}
|
||||
|
||||
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 +480,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 +488,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 +505,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 +628,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
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
@@ -14,10 +18,11 @@ import (
|
||||
type WorkRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
var (
|
||||
ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict")
|
||||
ErrAgentLeaseUnavailable = errors.New("agent lease unavailable")
|
||||
ErrAgentCallbackRejected = errors.New("agent callback rejected")
|
||||
ErrDevelopmentGateConflict = errors.New("development gate revision conflict")
|
||||
ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict")
|
||||
ErrAgentLeaseUnavailable = errors.New("agent lease unavailable")
|
||||
ErrAgentCallbackRejected = errors.New("agent callback rejected")
|
||||
ErrDevelopmentGateConflict = errors.New("development gate revision conflict")
|
||||
ErrDevelopmentCandidateConflict = errors.New("development candidate conflict")
|
||||
)
|
||||
|
||||
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
|
||||
@@ -100,33 +105,85 @@ func (r *WorkRepository) SaveDevelopmentGate(ctx context.Context, gate work.Deve
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveExtensionProposal(ctx context.Context, value work.ExtensionProposal) error {
|
||||
result, err := r.pool.Exec(ctx, `INSERT INTO work.extension_proposals(proposal_id,requested_by_principal_id,stage,evidence_refs,deduplicated_signal_keys,desired_outcome,smallest_change_layer,affected_owners,affected_contracts,risk_tier,evaluation_plan,alternatives,rollback_plan,assumptions,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT(proposal_id) DO NOTHING`, value.ID, value.RequestedByPrincipalID, value.Stage, value.EvidenceRefs, value.DeduplicatedSignalKeys, value.DesiredOutcome, value.SmallestChangeLayer, value.AffectedOwners, value.AffectedContracts, value.RiskTier, value.EvaluationPlan, value.Alternatives, value.RollbackPlan, value.Assumptions, value.CreatedAtUnixMillis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() != 1 {
|
||||
return ErrDevelopmentCandidateConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveDevelopmentScaffold(ctx context.Context, value work.DevelopmentScaffold) error {
|
||||
result, err := r.pool.Exec(ctx, `INSERT INTO work.development_scaffolds(scaffold_id,requested_by_principal_id,stage,proposal_ref,base_repo_revision,worktree_ref,task_intent,modules,allowed_paths,forbidden_paths,required_tests,artifact_lineage_refs,protected_main_access,production_access,secret_access,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ON CONFLICT(scaffold_id) DO NOTHING`, value.ID, value.RequestedByPrincipalID, value.Stage, value.ProposalRef, value.BaseRepoRevision, value.WorktreeRef, value.TaskIntent, value.Modules, value.AllowedPaths, value.ForbiddenPaths, value.RequiredTests, value.ArtifactLineageRefs, value.ProtectedMainAccess, value.ProductionAccess, value.SecretAccess, value.CreatedAtUnixMillis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() != 1 {
|
||||
return ErrDevelopmentCandidateConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveDispatchIntent(ctx context.Context, intent work.AgentDispatchIntent) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
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)
|
||||
profileJSON, casesJSON, err := marshalTaskPayload(intent.TaskEvaluationProfile, intent.TaskEvaluationCases)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
executionJSON, err := json.Marshal(intent.TaskEvaluationExecution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, 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,task_evaluation_profile,task_evaluation_cases,task_evaluation_execution) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) 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, profileJSON, casesJSON, executionJSON)
|
||||
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)
|
||||
var profileJSONExisting, casesJSONExisting, executionJSONExisting []byte
|
||||
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,task_evaluation_profile,task_evaluation_cases,task_evaluation_execution 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, &profileJSONExisting, &casesJSONExisting, &executionJSONExisting)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read dispatch intent: %w", err)
|
||||
}
|
||||
if existing != intent {
|
||||
if err := unmarshalTaskPayload(profileJSONExisting, casesJSONExisting, &existing.TaskEvaluationProfile, &existing.TaskEvaluationCases); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(executionJSONExisting) > 0 && string(executionJSONExisting) != "null" {
|
||||
if err := json.Unmarshal(executionJSONExisting, &existing.TaskEvaluationExecution); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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) && reflect.DeepEqual(left.TaskEvaluationProfile, right.TaskEvaluationProfile) && slices.Equal(left.TaskEvaluationCases, right.TaskEvaluationCases) && reflect.DeepEqual(left.TaskEvaluationExecution, right.TaskEvaluationExecution)
|
||||
}
|
||||
|
||||
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)
|
||||
var profileJSON, casesJSON, executionJSON []byte
|
||||
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,task_evaluation_profile,task_evaluation_cases,task_evaluation_execution 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, &profileJSON, &casesJSON, &executionJSON)
|
||||
if err != nil {
|
||||
return work.AgentDispatchIntent{}, fmt.Errorf("load dispatch intent: %w", err)
|
||||
}
|
||||
if err := unmarshalTaskPayload(profileJSON, casesJSON, &intent.TaskEvaluationProfile, &intent.TaskEvaluationCases); err != nil {
|
||||
return work.AgentDispatchIntent{}, err
|
||||
}
|
||||
if len(executionJSON) > 0 && string(executionJSON) != "null" {
|
||||
if err := json.Unmarshal(executionJSON, &intent.TaskEvaluationExecution); err != nil {
|
||||
return work.AgentDispatchIntent{}, err
|
||||
}
|
||||
}
|
||||
return intent, nil
|
||||
}
|
||||
|
||||
@@ -142,7 +199,7 @@ func (r *WorkRepository) ClaimNextDispatch(ctx context.Context, workerID string,
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
var item work.AgentDispatchQueueItem
|
||||
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
|
||||
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 AND attempts < retry_limit ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return work.AgentDispatchQueueItem{}, nil
|
||||
@@ -185,17 +242,36 @@ func (r *WorkRepository) CompleteDispatch(ctx context.Context, dispatchID, worke
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) RetryDispatch(ctx context.Context, dispatchID, workerID string, nowUnixMillis int64, delay time.Duration) error {
|
||||
if delay < 0 {
|
||||
return fmt.Errorf("invalid retry delay")
|
||||
}
|
||||
availableAt := nowUnixMillis + delay.Milliseconds()
|
||||
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=CASE WHEN attempts >= retry_limit THEN 'FAILED' ELSE 'QUEUED' END,available_at_unix_millis=$1,claimed_by='',updated_at_unix_millis=$2 WHERE dispatch_id=$3 AND state='CLAIMED' AND claimed_by=$4`, availableAt, nowUnixMillis, dispatchID, workerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if command.RowsAffected() != 1 {
|
||||
return ErrAgentCallbackRejected
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
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 {
|
||||
profileJSON, casesJSON, err := marshalTaskPayload(node.TaskProfile, node.TaskCases)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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,task_evaluation_profile,task_evaluation_cases) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) 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, profileJSON, casesJSON); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -209,17 +285,22 @@ 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,task_evaluation_profile,task_evaluation_cases 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 {
|
||||
var profileJSON, casesJSON []byte
|
||||
if err := rows.Scan(&node.ID, &node.DispatchID, &node.IdempotencyKey, &node.State, &node.Attempts, &node.Purpose, &node.TaskProfileID, &node.BindingID, &node.CaseIDs, &profileJSON, &casesJSON); err != nil {
|
||||
rows.Close()
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
if err := unmarshalTaskPayload(profileJSON, casesJSON, &node.TaskProfile, &node.TaskCases); err != nil {
|
||||
rows.Close()
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
@@ -271,10 +352,21 @@ func (r *WorkRepository) SaveGraphSynthesis(ctx context.Context, conflict work.C
|
||||
}
|
||||
|
||||
func (r *WorkRepository) RecoverStaleClaims(ctx context.Context, nowUnixMillis, staleAfterMillis int64) (int64, error) {
|
||||
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state='QUEUED',claimed_by='',updated_at_unix_millis=$1 WHERE state='CLAIMED' AND updated_at_unix_millis <= $2`, nowUnixMillis, nowUnixMillis-staleAfterMillis)
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err := tx.Exec(ctx, `UPDATE work.agent_leases AS l SET state='EXPIRED' FROM work.agent_dispatch_queue AS q JOIN work.agent_dispatch_intents AS i ON i.dispatch_id=q.dispatch_id WHERE q.state='CLAIMED' AND q.updated_at_unix_millis <= $1 AND l.run_id=i.run_id AND l.state='ACTIVE'`, nowUnixMillis-staleAfterMillis); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
command, err := tx.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=CASE WHEN attempts >= retry_limit THEN 'FAILED' ELSE 'QUEUED' END,available_at_unix_millis=$1,claimed_by='',updated_at_unix_millis=$2 WHERE state='CLAIMED' AND updated_at_unix_millis <= $3`, nowUnixMillis, nowUnixMillis, nowUnixMillis-staleAfterMillis)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return command.RowsAffected(), nil
|
||||
}
|
||||
|
||||
@@ -565,3 +657,32 @@ func latestRowErr(err error) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func marshalTaskPayload(profile *capability.TaskEvaluationProfile, cases []capability.TaskEvaluationCase) ([]byte, []byte, error) {
|
||||
var profileJSON []byte
|
||||
var err error
|
||||
if profile != nil {
|
||||
profileJSON, err = json.Marshal(profile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
casesJSON, err := json.Marshal(cases)
|
||||
return profileJSON, casesJSON, err
|
||||
}
|
||||
|
||||
func unmarshalTaskPayload(profileJSON, casesJSON []byte, profile **capability.TaskEvaluationProfile, cases *[]capability.TaskEvaluationCase) error {
|
||||
if len(profileJSON) > 0 && string(profileJSON) != "null" {
|
||||
var value capability.TaskEvaluationProfile
|
||||
if err := json.Unmarshal(profileJSON, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
*profile = &value
|
||||
}
|
||||
if len(casesJSON) > 0 && string(casesJSON) != "null" {
|
||||
if err := json.Unmarshal(casesJSON, cases); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ func (s *memoryStore) SaveDispatchIntent(context.Context, work.AgentDispatchInte
|
||||
s.writes++
|
||||
return nil
|
||||
}
|
||||
func (s *memoryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
|
||||
func (s *memoryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
|
||||
func (s *memoryStore) RenewAgentLease(context.Context, work.AgentLease, time.Time) error { return nil }
|
||||
func (s *memoryStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
|
||||
s.writes++
|
||||
return nil
|
||||
|
||||
@@ -49,9 +49,11 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
executor = newLocalFallbackExecutor()
|
||||
// The local evaluator keeps task-qualification work alive, but it is not
|
||||
// an external Agent Execution Bridge and must never be reported as one.
|
||||
localFallback = true
|
||||
fmt.Fprintln(os.Stderr, "protected agent execution bridge URL is not configured; local fallback preserves durable work without external mutation")
|
||||
executor = newTaskEvaluationExecutor(envOr("COGNITIVE_WORKER_URL", "http://cognitive-worker:8083"), envOr("WORK_GRAPH_RUNTIME_URL", "http://work-graph-runtime:8088"), &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() {
|
||||
if serveErr := http.ListenAndServe(*statusAddr, workerStatusHandler(executor != nil && !localFallback, localFallback)); serveErr != nil {
|
||||
@@ -107,6 +109,13 @@ func loadProtectedBridgeURLFrom(root, path string) (string, bool) {
|
||||
return value, true
|
||||
}
|
||||
|
||||
func envOr(name, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func resolveDatabasePassword(name, refName string, consumer secretcontracts.RuntimeConsumer) (string, error) {
|
||||
if _, ok := os.LookupEnv(name); ok {
|
||||
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
|
||||
|
||||
@@ -50,6 +50,18 @@ func TestWorkerStatusReportsLocalFallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerStatusDoesNotCallLocalFallbackAnExternalExecutor(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 value.ExecutorConfigured || value.Ready || !value.LocalFallback || value.Mode != "local-fallback" {
|
||||
t.Fatalf("local fallback was presented as external execution: %+v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerReadyzRequiresExecutor(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
workerStatusHandler(false).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/artifact"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
|
||||
)
|
||||
|
||||
type taskEvaluationExecutor struct {
|
||||
cognitiveURL string
|
||||
callbackURL string
|
||||
client *http.Client
|
||||
fallback localFallbackExecutor
|
||||
}
|
||||
|
||||
func newTaskEvaluationExecutor(cognitiveURL, callbackURL 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()}
|
||||
}
|
||||
|
||||
func (e taskEvaluationExecutor) Execute(ctx context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
|
||||
intent := request.Intent
|
||||
if intent.TaskEvaluationExecution == nil || intent.TaskEvaluationProfile == nil || len(intent.TaskEvaluationCases) == 0 {
|
||||
return e.fallback.Execute(ctx, request)
|
||||
}
|
||||
execution := intent.TaskEvaluationExecution
|
||||
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 == "" {
|
||||
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}
|
||||
response, err := postJSON[capability.TaskEvaluationExecutionResponse](ctx, e.client, e.cognitiveURL+"/v1/task-evaluations/execute", payload)
|
||||
if err != nil {
|
||||
return work.AgentExecutionResult{}, err
|
||||
}
|
||||
if len(response.Observations) == 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)
|
||||
if err != nil {
|
||||
return work.AgentExecutionResult{}, err
|
||||
}
|
||||
callback := work.TaskEvaluationEvidenceCallback{GraphID: execution.GraphID, Revision: execution.GraphRevision, NodeID: execution.NodeID, Evaluation: evaluation, Succeeded: true}
|
||||
if _, err := postJSON[json.RawMessage](ctx, e.client, e.callbackURL+"/v1/work-graphs/task-evaluations/callback", callback); err != nil {
|
||||
return work.AgentExecutionResult{}, err
|
||||
}
|
||||
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "task evaluation executed, aggregated and accepted by Work Graph callback", OutcomeRef: string(evaluation.ID), ArtifactRefs: []string{string(evaluation.ArtifactID)}, ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
|
||||
}
|
||||
|
||||
func postJSON[T any](ctx context.Context, client *http.Client, url string, payload any) (T, error) {
|
||||
var result T
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return result, fmt.Errorf("POST %s: status %s", url, resp.Status)
|
||||
}
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
return result, nil
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return result, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
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}
|
||||
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, QualityScoreMillis: 800, EvidenceArtifactRef: "artifact-1", VerificationEvidenceRef: "verify-1", VerifierIdentity: "verifier-1"}}, ObservedAtUnixMillis: 10})
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/v1/work-graphs/task-evaluations/callback" {
|
||||
var callback work.TaskEvaluationEvidenceCallback
|
||||
if err := json.NewDecoder(r.Body).Decode(&callback); err != nil || callback.GraphID != "graph-1" || callback.NodeID != "node-1" || callback.Evaluation.Result != capability.EvaluationSupported {
|
||||
http.Error(w, "invalid callback", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
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"}}})
|
||||
if err != nil || result.State != work.AgentExecutionCompleted || result.OutcomeRef == "" {
|
||||
t.Fatalf("result=%+v err=%v", result, err)
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,10 @@ type bindingResolver interface {
|
||||
RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error)
|
||||
}
|
||||
|
||||
type candidateBindingResolver interface {
|
||||
ResolveCandidate(context.Context, capability.CapabilityBindingID, int64) (capability.ResolvedCognitiveBinding, error)
|
||||
}
|
||||
|
||||
type admissionClient interface {
|
||||
Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error)
|
||||
Release(context.Context, capability.AdmissionLease) error
|
||||
@@ -341,6 +345,26 @@ func (r *httpBindingResolver) Resolve(ctx context.Context, value capability.Cogn
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (r *httpBindingResolver) ResolveCandidate(ctx context.Context, bindingID capability.CapabilityBindingID, revision int64) (capability.ResolvedCognitiveBinding, error) {
|
||||
var result capability.ResolvedCognitiveBinding
|
||||
target := *r.base
|
||||
target.Path = "/v1/cognitive-bindings/" + url.PathEscape(string(bindingID)) + "/revisions/" + fmt.Sprint(revision) + "/snapshot"
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
response, err := r.client.Do(request)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return result, fmt.Errorf("candidate status %d", response.StatusCode)
|
||||
}
|
||||
err = json.NewDecoder(io.LimitReader(response.Body, 64<<10)).Decode(&result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (r *httpBindingResolver) Observe(ctx context.Context, value capability.CognitiveUsageObservation) error {
|
||||
return r.post(ctx, "/v1/cognitive-usage-observations", value, nil)
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ func main() {
|
||||
mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler)
|
||||
mux.HandleFunc("POST /v1/advisories", gateway.advisoryHandler)
|
||||
mux.HandleFunc("POST /v1/interpretations", gateway.interpretationHandler)
|
||||
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)
|
||||
refreshProviderDiscoveries(resolver, discovery)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
)
|
||||
|
||||
func (g cognitiveGateway) taskEvaluationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var request capability.TaskEvaluationExecutionRequest
|
||||
if !decodeJSON(w, r, 256<<10, &request) {
|
||||
return
|
||||
}
|
||||
if err := request.Profile.Validate(); err != nil || request.CapabilityID == "" || request.CapabilityID != request.Profile.CapabilityID || request.BindingID == "" || request.BindingRevision < 1 || request.ActorID == "" || request.PrincipalID == "" || request.ScopeID == "" || request.MaxOutputBytes < 1 || len(request.Cases) != len(request.Profile.RequiredCaseIDs) {
|
||||
http.Error(w, "invalid task evaluation execution request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
resolver, ok := g.resolver.(candidateBindingResolver)
|
||||
if !ok {
|
||||
http.Error(w, "exact candidate resolver unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
selected, err := resolver.ResolveCandidate(r.Context(), request.BindingID, request.BindingRevision)
|
||||
if err != nil || selected.Binding.ID != request.BindingID || selected.Binding.Revision != request.BindingRevision || selected.Binding.CapabilityID != request.CapabilityID || selected.Binding.OwnerPrincipalID != request.PrincipalID {
|
||||
http.Error(w, "cognitive binding snapshot unavailable", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if selected.Binding.DataPolicy != capability.DataPolicyLocalOnly && !request.EgressAcknowledged {
|
||||
http.Error(w, "external egress acknowledgement required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
provider := g.provider(selected)
|
||||
if provider == nil {
|
||||
http.Error(w, "cognitive provider unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
caseSet := make(map[string]struct{}, len(request.Profile.RequiredCaseIDs))
|
||||
for _, id := range request.Profile.RequiredCaseIDs {
|
||||
caseSet[id] = struct{}{}
|
||||
}
|
||||
observations := make([]capability.TaskCaseObservation, len(request.Cases))
|
||||
var wg sync.WaitGroup
|
||||
seenCases := make(map[string]struct{}, len(request.Cases))
|
||||
for i, item := range request.Cases {
|
||||
if item.CaseID == "" || item.Prompt == "" || item.EvidenceArtifactRef == "" {
|
||||
http.Error(w, "invalid task evaluation case", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if _, exists := caseSet[item.CaseID]; !exists {
|
||||
http.Error(w, "task evaluation case is not in profile", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if _, exists := seenCases[item.CaseID]; exists {
|
||||
http.Error(w, "duplicate task evaluation case", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
seenCases[item.CaseID] = struct{}{}
|
||||
wg.Add(1)
|
||||
go func(index int, item capability.TaskEvaluationCase) {
|
||||
defer wg.Done()
|
||||
started := time.Now()
|
||||
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}
|
||||
if invokeErr == nil && response.FinishReason == cognitive.FinishCompleted && response.AdvisoryText != "" {
|
||||
// Invocation success proves reachability only. Without an
|
||||
// independent verifier and answer evidence, semantic support and
|
||||
// quality must remain UNKNOWN rather than becoming false evidence.
|
||||
observation.Result = capability.EvaluationUnknown
|
||||
observation.QualityScoreMillis = 0
|
||||
} else {
|
||||
observation.Result = capability.EvaluationUnknown
|
||||
observation.CriticalFailure = invokeErr != nil
|
||||
}
|
||||
observations[index] = observation
|
||||
}(i, item)
|
||||
}
|
||||
if len(seenCases) != len(caseSet) {
|
||||
http.Error(w, "task evaluation case set is incomplete", http.StatusBadRequest)
|
||||
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()})
|
||||
}
|
||||
|
||||
func writeTaskEvaluationJSON(w http.ResponseWriter, value any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
type candidateRegistryStub struct {
|
||||
registryStub
|
||||
selected capability.ResolvedCognitiveBinding
|
||||
}
|
||||
|
||||
func (s *candidateRegistryStub) ResolveCandidate(context.Context, capability.CapabilityBindingID, int64) (capability.ResolvedCognitiveBinding, error) {
|
||||
return s.selected, nil
|
||||
}
|
||||
|
||||
func TestTaskEvaluationHandlerInvokesEveryBoundedCase(t *testing.T) {
|
||||
selected := gatewaySelection()
|
||||
registry := &candidateRegistryStub{selected: selected}
|
||||
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, FinishReason: cognitive.FinishCompleted, AdvisoryText: "observed response"}}
|
||||
gateway := cognitiveGateway{
|
||||
resolver: registry,
|
||||
providers: map[capability.ProviderKind][]providerRegistration{
|
||||
selected.Provider.Kind: {{ProviderInstanceID: selected.Provider.ID, Kind: selected.Provider.Kind, EndpointRef: selected.Provider.EndpointRef, CredentialRef: selected.Provider.CredentialRef, Provider: provider}},
|
||||
},
|
||||
now: func() time.Time { return time.UnixMilli(1000) },
|
||||
}
|
||||
profile := capability.TaskEvaluationProfile{ID: "profile-general", Revision: 1, CapabilityID: selected.Binding.CapabilityID, Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-a", "case-b"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
|
||||
request := capability.TaskEvaluationExecutionRequest{Profile: profile, CapabilityID: selected.Binding.CapabilityID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, ActorID: identity.ActorID("actor-1"), PrincipalID: selected.Binding.OwnerPrincipalID, ScopeID: authority.ScopeID("scope-1"), MaxOutputBytes: 512, EgressAcknowledged: true, Cases: []capability.TaskEvaluationCase{{CaseID: "case-a", Prompt: "one", EvidenceArtifactRef: "artifact:a"}, {CaseID: "case-b", Prompt: "two", EvidenceArtifactRef: "artifact:b"}}}
|
||||
payload, _ := json.Marshal(request)
|
||||
recorder := httptest.NewRecorder()
|
||||
gateway.taskEvaluationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/execute", bytes.NewReader(payload)))
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var response capability.TaskEvaluationExecutionResponse
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(response.Observations) != 2 || response.Observations[0].Result != capability.EvaluationUnknown || response.Observations[1].Result != capability.EvaluationUnknown {
|
||||
t.Fatalf("unexpected observations: %+v", response.Observations)
|
||||
}
|
||||
for _, observation := range response.Observations {
|
||||
if observation.QualityScoreMillis != 0 || observation.CriticalFailure {
|
||||
t.Fatalf("reachability-only evidence must not carry quality or failure claims: %+v", observation)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,6 +178,61 @@ func main() {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(session)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/development/proposals", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Evidence []work.ExtensionEvidence `json:"evidence"`
|
||||
Proposal work.ExtensionProposalRequest `json:"proposal"`
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid development proposal", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
proposal, err := workdomain.BuildExtensionProposal(request.Evidence, request.Proposal)
|
||||
if err != nil {
|
||||
http.Error(w, "development proposal rejected", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
proposal.RequestedByPrincipalID = string(session.PrincipalID)
|
||||
if err := workRepository.SaveExtensionProposal(r.Context(), proposal); err != nil {
|
||||
http.Error(w, "development proposal conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(proposal)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/development/scaffolds", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request work.DevelopmentScaffoldRequest
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid development scaffold", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
scaffold, err := workdomain.BuildDevelopmentScaffold(request)
|
||||
if err != nil {
|
||||
http.Error(w, "development scaffold rejected", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
scaffold.RequestedByPrincipalID = string(session.PrincipalID)
|
||||
if err := workRepository.SaveDevelopmentScaffold(r.Context(), scaffold); err != nil {
|
||||
http.Error(w, "development scaffold conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(scaffold)
|
||||
})
|
||||
mux.HandleFunc("GET /v1/creator/focus", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
@@ -834,6 +889,20 @@ func main() {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(binding)
|
||||
})
|
||||
mux.HandleFunc("GET /v1/cognitive-bindings/{id}/revisions/{revision}/snapshot", func(w http.ResponseWriter, r *http.Request) {
|
||||
revision, err := strconv.ParseInt(r.PathValue("revision"), 10, 64)
|
||||
if err != nil || revision < 1 {
|
||||
http.Error(w, "invalid cognitive binding revision", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
snapshot, err := capabilityRepository.LoadCognitiveCandidate(r.Context(), capability.CapabilityBindingID(r.PathValue("id")), revision)
|
||||
if err != nil {
|
||||
http.Error(w, "cognitive binding snapshot not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(snapshot)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/cognitive-bindings/resolve", func(w http.ResponseWriter, r *http.Request) {
|
||||
var requirement capability.CognitiveRequirement
|
||||
decoder := json.NewDecoder(io.LimitReader(r.Body, 16*1024+1))
|
||||
@@ -842,7 +911,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 +966,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,106 @@
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package capability
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
// TaskEvaluationCase carries one bounded prompt selected by the evaluator.
|
||||
// The prompt is an input artifact, not a model-produced fact.
|
||||
type TaskEvaluationCase struct {
|
||||
CaseID string
|
||||
Prompt string
|
||||
EvidenceArtifactRef string
|
||||
}
|
||||
|
||||
type TaskEvaluationExecutionRequest struct {
|
||||
Profile TaskEvaluationProfile
|
||||
CapabilityID CapabilityID
|
||||
BindingID CapabilityBindingID
|
||||
BindingRevision int64
|
||||
ActorID identity.ActorID
|
||||
PrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
Cases []TaskEvaluationCase
|
||||
MaxOutputBytes int64
|
||||
EgressAcknowledged bool
|
||||
}
|
||||
|
||||
type TaskEvaluationExecutionResponse struct {
|
||||
ProfileID TaskEvaluationProfileID
|
||||
BindingID CapabilityBindingID
|
||||
BindingRevision int64
|
||||
Observations []TaskCaseObservation
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
+58
-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,17 @@ 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"`
|
||||
TaskProfile *capability.TaskEvaluationProfile `json:"task_profile,omitempty"`
|
||||
TaskCases []capability.TaskEvaluationCase `json:"task_cases,omitempty"`
|
||||
}
|
||||
|
||||
type GraphEdge struct {
|
||||
@@ -44,12 +56,47 @@ 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"`
|
||||
Cases []capability.TaskEvaluationCase `json:"cases,omitempty"`
|
||||
BindingRevision int64 `json:"binding_revision,omitempty"`
|
||||
ActorID string `json:"actor_id,omitempty"`
|
||||
PrincipalID string `json:"principal_id,omitempty"`
|
||||
ScopeID string `json:"scope_id,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 {
|
||||
|
||||
+34
-11
@@ -1,6 +1,12 @@
|
||||
package work
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
type AgentLeaseID string
|
||||
type AgentFencingToken uint64
|
||||
@@ -50,14 +56,31 @@ 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"`
|
||||
TaskEvaluationProfile *capability.TaskEvaluationProfile `json:"task_evaluation_profile,omitempty"`
|
||||
TaskEvaluationCases []capability.TaskEvaluationCase `json:"task_evaluation_cases,omitempty"`
|
||||
TaskEvaluationExecution *TaskEvaluationDispatchContext `json:"task_evaluation_execution,omitempty"`
|
||||
}
|
||||
|
||||
type TaskEvaluationDispatchContext struct {
|
||||
GraphID string `json:"graph_id"`
|
||||
GraphRevision int64 `json:"graph_revision"`
|
||||
NodeID GraphNodeID `json:"node_id"`
|
||||
BindingRevision int64 `json:"binding_revision"`
|
||||
ActorID identity.ActorID `json:"actor_id"`
|
||||
PrincipalID identity.PrincipalID `json:"principal_id"`
|
||||
ScopeID authority.ScopeID `json:"scope_id"`
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ type ExtensionProposalRequest struct {
|
||||
// authority grant, implementation body or promotion decision.
|
||||
type ExtensionProposal struct {
|
||||
ID string
|
||||
RequestedByPrincipalID string
|
||||
Stage ExtensionProposalStage
|
||||
EvidenceRefs []string
|
||||
DeduplicatedSignalKeys []string
|
||||
@@ -113,19 +114,20 @@ type DevelopmentScaffoldRequest struct {
|
||||
// DevelopmentScaffold is a provenance-linked task-packet candidate. It has
|
||||
// no executable instruction, credential, checkout handle or authority grant.
|
||||
type DevelopmentScaffold struct {
|
||||
ID string
|
||||
Stage DevelopmentScaffoldStage
|
||||
ProposalRef string
|
||||
BaseRepoRevision string
|
||||
WorktreeRef string
|
||||
TaskIntent string
|
||||
Modules []string
|
||||
AllowedPaths []string
|
||||
ForbiddenPaths []string
|
||||
RequiredTests []string
|
||||
ArtifactLineageRefs []string
|
||||
ProtectedMainAccess string
|
||||
ProductionAccess string
|
||||
SecretAccess string
|
||||
CreatedAtUnixMillis int64
|
||||
ID string
|
||||
RequestedByPrincipalID string
|
||||
Stage DevelopmentScaffoldStage
|
||||
ProposalRef string
|
||||
BaseRepoRevision string
|
||||
WorktreeRef string
|
||||
TaskIntent string
|
||||
Modules []string
|
||||
AllowedPaths []string
|
||||
ForbiddenPaths []string
|
||||
RequiredTests []string
|
||||
ArtifactLineageRefs []string
|
||||
ProtectedMainAccess string
|
||||
ProductionAccess string
|
||||
SecretAccess string
|
||||
CreatedAtUnixMillis int64
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,16 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
ALTER TABLE work.agent_dispatch_intents
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_profile JSONB,
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_cases JSONB NOT NULL DEFAULT '[]',
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_execution JSONB;
|
||||
|
||||
ALTER TABLE work.graph_nodes
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_profile JSONB,
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_cases JSONB NOT NULL DEFAULT '[]',
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_execution JSONB;
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,15 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
ALTER TABLE work.agent_dispatch_queue
|
||||
ADD COLUMN IF NOT EXISTS retry_limit INTEGER NOT NULL DEFAULT 3;
|
||||
|
||||
ALTER TABLE work.agent_dispatch_queue
|
||||
DROP CONSTRAINT IF EXISTS agent_dispatch_queue_retry_limit_check;
|
||||
|
||||
ALTER TABLE work.agent_dispatch_queue
|
||||
ADD CONSTRAINT agent_dispatch_queue_retry_limit_check CHECK (retry_limit > 0);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,45 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.extension_proposals (
|
||||
proposal_id TEXT PRIMARY KEY,
|
||||
requested_by_principal_id TEXT NOT NULL,
|
||||
stage TEXT NOT NULL,
|
||||
evidence_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
deduplicated_signal_keys TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
desired_outcome TEXT NOT NULL,
|
||||
smallest_change_layer TEXT NOT NULL,
|
||||
affected_owners TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
affected_contracts TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
risk_tier TEXT NOT NULL,
|
||||
evaluation_plan TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
alternatives TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
rollback_plan TEXT NOT NULL,
|
||||
assumptions TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (proposal_id <> ''), CHECK (requested_by_principal_id <> ''), CHECK (stage = 'PROPOSED'), CHECK (desired_outcome <> ''), CHECK (smallest_change_layer <> ''), CHECK (risk_tier <> ''), CHECK (rollback_plan <> ''), CHECK (created_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.development_scaffolds (
|
||||
scaffold_id TEXT PRIMARY KEY,
|
||||
requested_by_principal_id TEXT NOT NULL,
|
||||
stage TEXT NOT NULL,
|
||||
proposal_ref TEXT NOT NULL,
|
||||
base_repo_revision TEXT NOT NULL,
|
||||
worktree_ref TEXT NOT NULL,
|
||||
task_intent TEXT NOT NULL,
|
||||
modules TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
allowed_paths TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
forbidden_paths TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
required_tests TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
artifact_lineage_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
protected_main_access TEXT NOT NULL,
|
||||
production_access TEXT NOT NULL,
|
||||
secret_access TEXT NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (scaffold_id <> ''), CHECK (requested_by_principal_id <> ''), CHECK (stage = 'SANDBOXED'), CHECK (proposal_ref <> ''), CHECK (base_repo_revision <> ''), CHECK (worktree_ref <> ''), CHECK (task_intent <> ''), CHECK (protected_main_access = 'DENIED'), CHECK (production_access = 'DENIED'), CHECK (secret_access = 'DENIED'), CHECK (created_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000203-local-agent-execution-fallback.json",
|
||||
"task_id": "DEV-000203",
|
||||
"task_file": "dev/tasks/DEV-000220-verifier-evidence-boundary.json",
|
||||
"task_id": "DEV-000220",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000204",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "b4d166480e5d06049718618ffeea0ccdbb993336",
|
||||
"risk_tier": "E",
|
||||
"intent": "Record protected GPU model discovery without equating hardware availability with intelligence, qualification or routing priority.",
|
||||
"requirement_ids": ["WP-200", "model fleet/failover", "hardware-neutral routing"],
|
||||
"allowed_change_scope": {"modules": ["governance status", "evidence"], "paths": ["docs/PROJECT_STATUS.md", "docs/evidence/gpu-discovery-2026-08-23-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000204-gpu-discovery-evidence.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "cmd/**", "modules/**", "contracts/**", ".gitea/**"]},
|
||||
"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": ["protected provider discovery", "model fleet policy"], "may_change": ["status and evidence projection only"]},
|
||||
"semantic_owner": "governance",
|
||||
"canonical_state": ["capability discovery projection"],
|
||||
"state_transitions": ["HEAVY degraded -> provider AVAILABLE; models remain DISCOVERED/INACTIVE"],
|
||||
"side_effects_and_privileges": ["protected read/discovery request only"],
|
||||
"persistence_and_migrations": ["no migration"],
|
||||
"failure_retry_restart": ["retain unqualified/unpromoted state until task-specific evidence exists"],
|
||||
"assumptions": ["cognitive-worker uses the configured protected GPU credential"],
|
||||
"acceptance_scenarios": ["two GPU models are observed", "both remain unpromoted", "status rejects GPU-as-intelligence inference"],
|
||||
"required_tests": ["go test ./cmd/cognitive-worker", "go vet ./cmd/cognitive-worker", "protected provider discovery smoke", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "production public health"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000205",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "288d18a4fba0535e3e2d15643de5b78499beb961",
|
||||
"risk_tier": "E",
|
||||
"intent": "Record a bounded protected GPU invocation smoke result without promoting or activating an unqualified model.",
|
||||
"requirement_ids": ["WP-200", "WP-201", "model fleet/failover", "truthful negative evidence"],
|
||||
"allowed_change_scope": {"modules": ["governance status", "evidence"], "paths": ["docs/evidence/gpu-invocation-smoke-2026-08-23-v1.json", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000205-gpu-invocation-smoke-evidence.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["protected GPU gateway OpenAI-compatible endpoint", "existing GPU discovery evidence"], "may_change": []},
|
||||
"semantic_owner": "governance evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["discovery available -> invocation smoke not observed; no activation"],
|
||||
"side_effects_and_privileges": ["one bounded synthetic request through an already configured protected credential"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence only"],
|
||||
"failure_retry_restart": ["no retry; 400 remains an explicit negative observation"],
|
||||
"assumptions": ["the gateway credential is resolved inside the protected runtime and is not copied into evidence"],
|
||||
"acceptance_scenarios": ["model discovery remains visible", "failed invocation is recorded without claiming quality", "GPU remains inactive and cannot become routing priority from discovery alone"],
|
||||
"required_tests": ["JSON validation", "go test", "go vet", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl 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-000206",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "faab913227f985fc06436618b5747376d4495487",
|
||||
"risk_tier": "E",
|
||||
"intent": "Refresh the audit completion matrix against the current production revision and preserve explicit evidence for every remaining gap.",
|
||||
"requirement_ids": ["AUDIT-COMPLETION-TRUTH", "all objective areas", "current production evidence"],
|
||||
"allowed_change_scope": {"modules": ["governance status", "evidence"], "paths": ["docs/evidence/audit-completion-matrix-2026-08-23-v2.json", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000206-completion-matrix-refresh.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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 production status and evidence artifacts"], "may_change": []},
|
||||
"semantic_owner": "governance evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["none; refreshes an append-only audit projection"],
|
||||
"side_effects_and_privileges": ["none beyond repository evidence and status"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence only"],
|
||||
"failure_retry_restart": ["retain explicit NOT_COMPLETE result when any required evidence is missing"],
|
||||
"assumptions": ["production revision faab913 is the deployed main artifact", "deep acceptance remains deferred by current testing policy"],
|
||||
"acceptance_scenarios": ["all seven objective areas have current statuses", "repository and production proof are distinguished", "remaining external gaps are explicit", "no secret values appear"],
|
||||
"required_tests": ["JSON validation", "go test", "go vet", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "independent", "git diff --check", "CI", "production public health check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000207",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "9f96393fb9a7ddce19eda3f90cbdeecb024029e6",
|
||||
"risk_tier": "E",
|
||||
"intent": "Record a bounded production GPU invocation using the exact Domovoy adapter request shape without automatic promotion.",
|
||||
"requirement_ids": ["WP-200", "WP-201", "model fleet/failover", "hardware-neutral routing"],
|
||||
"allowed_change_scope": {"modules": ["governance status", "evidence"], "paths": ["docs/evidence/gpu-invocation-qualification-2026-08-23-v1.json", "docs/evidence/audit-completion-matrix-2026-08-23-v3.json", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000207-gpu-invocation-qualification.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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 OpenAI-compatible local adapter request contract", "protected GPU gateway", "current completion matrix"], "may_change": []},
|
||||
"semantic_owner": "governance evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["discovered/inactive -> bounded invocation observed; remains unpromoted"],
|
||||
"side_effects_and_privileges": ["one bounded synthetic request through an existing protected credential"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence only"],
|
||||
"failure_retry_restart": ["no retry and no automatic activation"],
|
||||
"assumptions": ["the protected runtime credential is used in place and is not copied into repository artifacts"],
|
||||
"acceptance_scenarios": ["exact adapter payload returns one bounded completion", "quality remains unassessed", "GPU does not receive routing priority merely because it is available"],
|
||||
"required_tests": ["JSON validation", "go test", "go vet", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "independent", "git diff --check", "CI", "production public health check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000212",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "dff6c91c8e08611d73231a04a1af67efeebed22b",
|
||||
"risk_tier": "E",
|
||||
"intent": "Execute bounded task-evaluation cases against an exact Capability binding through Cognitive Gateway and return typed observations for the Work Graph callback.",
|
||||
"requirement_ids": ["actual model invocation", "exact binding evaluation", "bounded cases", "provider-neutral evidence", "unknown on provider failure"],
|
||||
"allowed_change_scope": {"modules": ["capability", "core-runtime", "cognitive-worker"], "paths": ["contracts/capability/**", "adapters/postgres/capability_repository.go", "cmd/core-runtime/**", "cmd/cognitive-worker/**", "tests/model_qualification/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000212-task-evaluator-invocation.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/devctl/**", "database/**", "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": 0},
|
||||
"contracts": {"consume": ["TaskEvaluationProfile", "TaskCaseObservation", "Cognitive Gateway provider boundary", "Capability cognitive binding snapshot boundary"], "may_change": ["task evaluation execution request/response"]},
|
||||
"semantic_owner": "capability owns evaluation meaning; cognitive-worker owns provider invocation adapter boundary",
|
||||
"canonical_state": ["none; this task returns observations only and does not promote or persist activation"],
|
||||
"state_transitions": ["execution request -> exact binding snapshot", "case -> provider invocation -> SUPPORTED or UNKNOWN observation", "all observations -> Work Graph callback input"],
|
||||
"side_effects_and_privileges": ["provider calls only through existing cognitive adapter; no direct DB and no new secret access"],
|
||||
"persistence_and_migrations": ["none; durable persistence remains callback/Core Capability owner"],
|
||||
"failure_retry_restart": ["provider timeout/auth/unavailable maps to UNKNOWN observation with evidence reference; caller may retry as a new attempt", "partial case set is never returned as supported aggregate"],
|
||||
"assumptions": ["specialist supplies case prompts and artifact references from the approved bounded profile", "quality scoring beyond bounded invocation is performed by a verifier or later evaluator and is not fabricated here"],
|
||||
"acceptance_scenarios": ["exact binding snapshot is required", "all supplied cases are invoked", "successful non-empty response yields supported observation with latency", "provider failure yields unknown observation", "unknown is never silently converted to supported", "no provider or hardware field participates in scoring"],
|
||||
"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-000213",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "a86d1994c3f48db86592eb87fb54533297e0671c",
|
||||
"risk_tier": "E",
|
||||
"intent": "Carry the complete bounded task-evaluation profile and case payload through durable Work Graph dispatch so agent-worker can invoke Cognitive Worker, aggregate observations, and return the existing Work Graph callback after restart-safe recovery.",
|
||||
"requirement_ids": ["dispatch-to-evaluator link", "payload durability", "worker recovery", "observation aggregation", "callback integration"],
|
||||
"allowed_change_scope": {"modules": ["work", "agent-worker"], "paths": ["contracts/work/**", "modules/work/**", "adapters/postgres/work_repository.go", "cmd/agent-worker/**", "cmd/work-graph-runtime/**", "database/migrations/041_task_evaluation_payload.sql", "tests/agent_execution/**", "tests/model_qualification/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000213-worker-evaluator-link.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": ["TaskEvaluationExecutionRequest/Response", "Work Graph callback", "existing AgentExecutor and recovery contract"], "may_change": ["typed dispatch payload and worker evaluator executor"]},
|
||||
"semantic_owner": "work owns dispatch lifecycle; capability owns evaluation evidence; cognitive-worker owns provider invocation",
|
||||
"canonical_state": ["work dispatch payload is durable input; Capability remains evidence owner"],
|
||||
"state_transitions": ["task graph node -> durable payload dispatch", "worker claim -> evaluator invocation", "response -> aggregate TaskEvaluation", "aggregate -> Work Graph evidence callback"],
|
||||
"side_effects_and_privileges": ["HTTP calls to internal cognitive/core/work boundaries; no direct foreign DB and no new secret"],
|
||||
"persistence_and_migrations": ["041 adds JSONB payload columns to work graph nodes and dispatch intents"],
|
||||
"failure_retry_restart": ["payload is loaded after stale claim recovery; evaluator failure leaves dispatch retryable", "callback only follows a persisted aggregate; unknown remains unknown"],
|
||||
"assumptions": ["Cognitive Worker endpoint is available as optional local execution path", "external bridge remains preferred for non-evaluation agent tasks"],
|
||||
"acceptance_scenarios": ["profile/cases survive save-load", "task dispatch invokes evaluator with original prompt payload", "all observations aggregate deterministically", "callback receives matching binding/profile/artifact", "evaluator or callback failure does not complete dispatch", "legacy dispatch remains unchanged"],
|
||||
"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-000214",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "f9b9765100df02bead16280d2db9c1af478a4589",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make Agent Execution recovery restart-safe: renew active leases with managed heartbeats, fence stale workers, cap retries, and requeue transient execution failures without losing durable dispatch state.",
|
||||
"requirement_ids": ["durable heartbeat", "lease fencing", "bounded retry", "restart recovery", "transient failure requeue"],
|
||||
"allowed_change_scope": {"modules": ["work", "agent-worker"], "paths": ["contracts/work/**", "modules/work/**", "adapters/postgres/work_repository.go", "cmd/agent-worker/**", "cmd/agent-canary/**", "database/migrations/042_agent_execution_recovery.sql", "tests/agent_execution/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000214-agent-execution-recovery.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/devctl/**", "modules/identity/**", "modules/capability/**", "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": 1},
|
||||
"contracts": {"consume": ["existing AgentExecutor", "existing lease/callback contract", "durable dispatch queue"], "may_change": ["WorkerStore retry/recovery port"]},
|
||||
"semantic_owner": "work owns dispatch, lease, callback and retry lifecycle",
|
||||
"canonical_state": ["work agent dispatch queue and lease tables remain canonical; heartbeat is durable evidence of liveness"],
|
||||
"state_transitions": ["CLAIMED -> QUEUED on transient failure or stale recovery", "CLAIMED -> FAILED after retry limit", "ACTIVE lease -> EXPIRED before stale claim requeue", "heartbeat renews lease and callback sequence"],
|
||||
"side_effects_and_privileges": ["work_role writes only work-owned queue and lease state; no external mutation"],
|
||||
"persistence_and_migrations": ["042 adds bounded retry limit to dispatch queue"],
|
||||
"failure_retry_restart": ["stale recovery expires the old lease before requeue", "transient execution failure uses durable delayed retry", "retry limit prevents hot-loop poison dispatch", "heartbeat failure cancels execution and leaves a retryable dispatch"],
|
||||
"assumptions": ["operational heartbeat ticker is lifecycle-managed by the bounded worker call and durable state remains authoritative"],
|
||||
"acceptance_scenarios": ["long execution renews lease and emits ordered heartbeats", "stale claim is fenced and requeued", "third failed attempt becomes FAILED", "waiting/error dispatch is not left CLAIMED", "legacy dispatch behavior remains compatible"],
|
||||
"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-000215",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "8d3e7f7cac236aa50919bd1cd854012dcc8500a5",
|
||||
"risk_tier": "E",
|
||||
"intent": "Expose a durable, authenticated control-plane boundary for governed self-development proposals and isolated scaffold candidates without executing source mutation, bypassing verification, or granting promotion authority.",
|
||||
"requirement_ids": ["durable proposal intake", "isolated scaffold contract", "authenticated operator boundary", "no implicit promotion"],
|
||||
"allowed_change_scope": {"modules": ["work", "core-runtime"], "paths": ["contracts/work/self_development.go", "modules/work/domain/**", "cmd/core-runtime/main.go", "adapters/postgres/work_repository.go", "database/migrations/043_governed_development_candidates.sql", "tests/semantic_evolution/**", "tests/work/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000215-governed-development-control-plane.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/devctl/**", "modules/identity/**", "modules/capability/**", "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": 1},
|
||||
"contracts": {"consume": ["ADR-0022 governed self-development boundary", "creator authentication", "work development gate"], "may_change": ["work-owned proposal/scaffold persistence and authenticated control-plane endpoints"]},
|
||||
"semantic_owner": "work owns proposal, scaffold and development gate lineage",
|
||||
"canonical_state": ["work-owned extension proposal and scaffold candidates are durable; source tree and production remain outside this task"],
|
||||
"state_transitions": ["repeated evidence -> PROPOSED", "proposal -> SANDBOXED scaffold candidate", "gate remains explicit and cannot be promoted by these endpoints"],
|
||||
"side_effects_and_privileges": ["authenticated creator session required; database writes stay within work owner; no checkout, source mutation, deployment or secret access"],
|
||||
"persistence_and_migrations": ["043 adds durable work-owned proposal/scaffold candidate records"],
|
||||
"failure_retry_restart": ["idempotent candidate IDs preserve lineage; duplicate IDs are rejected; no endpoint advances promotion stage"],
|
||||
"assumptions": ["an independent executor and verifier remain separate future capabilities"],
|
||||
"acceptance_scenarios": ["unauthenticated requests are rejected", "repeated evidence creates a durable proposal", "protected paths cannot create a scaffold", "valid scaffold is durable and remains SANDBOXED", "no promotion or source mutation occurs"],
|
||||
"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-000216",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "62f0c529b3f8b3c92e4a69fcc6298d59b3015fde",
|
||||
"risk_tier": "E",
|
||||
"intent": "Keep agent-worker status truthful when the protected external execution bridge is absent while retaining local task-evaluation fallback.",
|
||||
"requirement_ids": ["WP-301", "truthful degraded status", "local fallback"],
|
||||
"allowed_change_scope": {"modules": ["agent-worker"], "paths": ["cmd/agent-worker/main.go", "cmd/agent-worker/status_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000216-agent-worker-status-truth.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "deploy/**", "security/**", "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": ["protected bridge file", "agent-worker status response", "local task-evaluation fallback"], "may_change": []},
|
||||
"semantic_owner": "work",
|
||||
"canonical_state": ["existing worker status projection and durable dispatch lifecycle"],
|
||||
"state_transitions": ["bridge absent -> local-fallback status", "bridge present -> execution status"],
|
||||
"side_effects_and_privileges": ["no external mutation; no new credentials; no persistence change"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["bridge absence remains explicit after restart; local evaluator remains available for bounded qualification work"],
|
||||
"assumptions": ["task-evaluation fallback is not general external agent execution"],
|
||||
"acceptance_scenarios": ["worker marks local_fallback when protected bridge URL is absent", "status never reports executor_configured=true for local fallback", "configured bridge path remains execution mode", "no secret value is exposed"],
|
||||
"required_tests": ["go test ./cmd/agent-worker", "go vet ./cmd/agent-worker", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production worker status"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000217",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "05a246ea3f240a77be769969ad62a236fba33bac",
|
||||
"risk_tier": "E",
|
||||
"intent": "Refresh the project status projection and redacted live evidence after the agent-worker truthful fallback fix without changing runtime behavior.",
|
||||
"requirement_ids": ["current audit evidence", "task lineage accuracy", "negative-state transparency"],
|
||||
"allowed_change_scope": {"modules": ["operations-evidence"], "paths": ["docs/PROJECT_STATUS.md", "docs/evidence/audit-runtime-status-2026-08-23-v2.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000217-current-status-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 health/readiness/status responses", "merged task lineage"], "may_change": ["operations status projection and redacted evidence artifact"]},
|
||||
"semantic_owner": "operations-evidence projection",
|
||||
"canonical_state": ["none; docs and evidence are rebuildable projections"],
|
||||
"state_transitions": ["stale status projection -> current evidence projection"],
|
||||
"side_effects_and_privileges": ["read-only live checks; no secrets, credentials or runtime mutation"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["evidence records unavailable/degraded states explicitly; no success inferred from missing data"],
|
||||
"assumptions": ["historical evidence remains immutable and is not rewritten"],
|
||||
"acceptance_scenarios": ["active task points to the current merged packet", "bridge absence is reported as local fallback", "GPU timeout is reported as degraded without changing FAST preference", "authenticated UI availability is not claimed without a current signed-in session", "no secret value appears in the projection"],
|
||||
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health and runtime status"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000218",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "ab840878fe7d21a0a6aeb69193a3d58ea23a56e0",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add a conservative deterministic policy gate for creating additional model-evaluation profiles only after general evaluation evidence remains unresolved.",
|
||||
"requirement_ids": ["general-first evaluation", "bounded additional evaluation", "no unnecessary fleet-wide tests"],
|
||||
"allowed_change_scope": {"modules": ["capability"], "paths": ["modules/capability/domain/task_evaluation.go", "modules/capability/domain/task_evaluation_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000218-automatic-expansion-policy.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "cmd/**", "contracts/**", "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": ["TaskEvaluationProfile", "TaskEvaluation", "TaskEvaluationExpansionRequest"], "may_change": []},
|
||||
"semantic_owner": "capability owns model qualification evidence and expansion policy",
|
||||
"canonical_state": ["none; this packet creates no persistence or activation authority"],
|
||||
"state_transitions": ["general COMPLETED/PARTIAL unresolved evidence -> bounded additional profile candidate", "qualified candidate -> no expansion"],
|
||||
"side_effects_and_privileges": ["pure deterministic decision; no provider call, no model invocation, no mutation"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["missing trigger evidence or additional cases fails closed; repeated decision is deterministic"],
|
||||
"assumptions": ["a later task supplies task-specific additional cases from an unresolved outcome and schedules them through the existing Work Graph"],
|
||||
"acceptance_scenarios": ["general graph not completed/partial does not expand", "unknown/unsupported/low-scoring candidates expand only with evidence and new cases", "one qualified candidate suppresses expansion", "no provider or hardware label changes the decision"],
|
||||
"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-000219",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "7476f43ffa57d55f5a5a9bddc7f1c74e9b87ae0e",
|
||||
"risk_tier": "E",
|
||||
"intent": "Prevent task-evaluation invocation success from being recorded as semantic model quality or support without independent verifier evidence.",
|
||||
"requirement_ids": ["truthful model evidence", "independent verification", "no false qualification"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/task_evaluation.go", "cmd/cognitive-worker/task_evaluation_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000219-reachability-not-quality.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": ["TaskEvaluationExecutionRequest/Response", "TaskCaseObservation", "Cognitive Gateway"], "may_change": []},
|
||||
"semantic_owner": "capability evidence remains owner; cognitive-worker only records invocation observation",
|
||||
"canonical_state": ["existing task-evaluation evidence; no new persistence"],
|
||||
"state_transitions": ["successful invocation -> UNKNOWN reachability observation", "independent verifier evidence -> future supported/quality observation"],
|
||||
"side_effects_and_privileges": ["no additional provider calls or privileges"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["unknown evidence remains retryable/eligible for a separately verified evaluation; no promotion follows invocation alone"],
|
||||
"assumptions": ["a later verifier task will attach answer-quality evidence and score cases"],
|
||||
"acceptance_scenarios": ["nonempty provider response is not SUPPORTED", "quality score remains zero", "provider invocation failure remains UNKNOWN with critical failure", "no model/hardware label can manufacture quality evidence"],
|
||||
"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-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"]
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
- `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`.
|
||||
- `HEAVY_PROVIDER_STATUS = LIVE_QUALIFIED_UNACTIVATED`; the qualified GPU binding remains inactive. Evidence: `docs/evidence/wp-200-heavy-qualification-2026-08-22-v2.json`.
|
||||
- `HEAVY_PROVIDER_STATUS = LIVE_BOUNDED_INVOCATION_SUPPORTED_UNPROMOTED`; the exact Domovoy adapter payload produced one bounded Qwen completion in 0.583s. Quality is not assessed, lifecycle promotion is not performed, and GPU availability is not treated as intelligence or routing priority. Evidence: `docs/evidence/gpu-discovery-2026-08-23-v1.json`, `docs/evidence/gpu-invocation-qualification-2026-08-23-v1.json`.
|
||||
- `AGENT_EXECUTION_STATUS = DURABLE_DRY_RUN_CANARY_VERIFIED`; the explicit production canary persisted a dispatch intent, an ACTIVE lease and callback head sequence 2. No independently operated bridge endpoint is configured, so bridge-backed execution remains unavailable. Evidence: `docs/evidence/agent-execution-dry-run-canary-2026-08-23-v1.json`.
|
||||
- `FLEET_EXECUTION_EVIDENCE = docs/evidence/audit-runtime-status-2026-08-23-v1.json`; FAST, MEDIUM and LIFEBOAT are ready, HEAVY is explicitly degraded by provider timeout, FAST remains preferred/selected, and LIFEBOAT is the fallback tier.
|
||||
- `TRUTH_RESEARCH_STATUS = LIVE_CANARY_PERSISTED`; the web research canary preserved an OBSERVED/COMPLETE/NO-CONFLICT source chain while leaving the claim UNVERIFIED and belief PROVISIONAL. Evidence: `docs/evidence/truth-runtime-canary-2026-08-23-v1.json`.
|
||||
@@ -13,11 +13,15 @@
|
||||
- `BACKUP_RESTORE_STATUS = VERIFIED`; latest checksum and isolated restore evidence is retained in `docs/evidence/wp-002-postgres-restore-2026-08-22-v1.json`.
|
||||
- `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`.
|
||||
|
||||
## Current governance checkpoint
|
||||
|
||||
- `ACTIVE_TASK = dev/tasks/DEV-000202-authenticated-ui-quick-smoke.json revision 1`
|
||||
- `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`
|
||||
- `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 = close browser-driven interaction, external Agent Execution bridge and GPU gaps; retain explicit evidence for all remaining negative directions.`
|
||||
- `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.`
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"evidence_id": "audit-completion-matrix-2026-08-23-v2",
|
||||
"captured_at": "2026-08-23T08:25:00Z",
|
||||
"production_revision": "faab913227f985fc06436618b5747376d4495487",
|
||||
"objective": "Довести оставшиеся пункты аудита Domovoy до проверяемого состояния",
|
||||
"matrix": [
|
||||
{
|
||||
"area": "Agent Execution runtime and recovery",
|
||||
"status": "PARTIAL_EXTERNAL_GAP",
|
||||
"repository_proof": ["adapters/agentbridge/http.go", "cmd/agent-worker/local_fallback.go", "modules/work/application/worker.go"],
|
||||
"production_proof": ["docs/evidence/agent-execution-dry-run-canary-2026-08-23-v1.json", "docs/evidence/authenticated-ui-quick-smoke-2026-08-23-v1.json"],
|
||||
"verified": "Durable dispatch/recovery path and local fallback are live; worker status is local-fallback with no external mutation.",
|
||||
"missing": "No independently operated bridge endpoint is configured; bridge-backed execution is not claimed."
|
||||
},
|
||||
{
|
||||
"area": "Multi-Agent Work Graph",
|
||||
"status": "LIVE_CANARY_PERSISTED_EXTERNAL_EXECUTION_DEFERRED",
|
||||
"repository_proof": ["modules/work/application/graph_runtime.go", "contracts/work/graph.go", "tests/agent_execution/graph_runtime_test.go"],
|
||||
"production_proof": ["docs/evidence/audit-runtime-status-2026-08-23-v1.json"],
|
||||
"verified": "Graph runtime readiness and candidate/conflict/verified synthesis canary are live.",
|
||||
"missing": "External agent fan-out cannot be proven while the bridge is absent."
|
||||
},
|
||||
{
|
||||
"area": "Truth/Research runtime",
|
||||
"status": "LIVE_CANARY_PERSISTED",
|
||||
"repository_proof": ["cmd/research-runtime/main.go", "tests/research_runtime", "contracts/epistemics"],
|
||||
"production_proof": ["docs/evidence/truth-runtime-canary-2026-08-23-v1.json", "docs/evidence/audit-runtime-status-2026-08-23-v1.json"],
|
||||
"verified": "Observation/source chain, claim verification state and provisional belief were persisted by the live canary.",
|
||||
"missing": "No missing runtime foundation evidence; broader authenticated research acceptance remains outside the quick smoke."
|
||||
},
|
||||
{
|
||||
"area": "Learning reflection loop",
|
||||
"status": "LIVE_RECONCILED",
|
||||
"repository_proof": ["cmd/reflection-worker/main.go", "database/migrations/032_reflection_candidate_leases.sql", "tests/research_contracts/reflection_worker_sql_test.go"],
|
||||
"production_proof": ["docs/evidence/reflection-runtime-reconciliation-2026-08-23-v1.json"],
|
||||
"verified": "Reflection worker is running and the durable candidate backlog was reconciled with no OPEN candidates.",
|
||||
"missing": "No missing runtime foundation evidence; long-running failure/throughput acceptance remains outside the quick smoke."
|
||||
},
|
||||
{
|
||||
"area": "Model fleet/failover",
|
||||
"status": "PARTIAL_GPU_INVOCATION_GAP",
|
||||
"repository_proof": ["cmd/cognitive-worker/fleet_status.go", "modules/capability/domain/fleet.go", "adapters/models/openai_compatible_local.go"],
|
||||
"production_proof": ["docs/evidence/audit-runtime-status-2026-08-23-v1.json", "docs/evidence/gpu-discovery-2026-08-23-v1.json", "docs/evidence/gpu-invocation-smoke-2026-08-23-v1.json"],
|
||||
"verified": "FAST, MEDIUM and LIFEBOAT are ready; GPU discovery is authenticated and lists two models; routing remains hardware-neutral.",
|
||||
"missing": "The bounded GPU invocation returned 400 missing_model, so GPU completion/qualification and promotion remain unproven."
|
||||
},
|
||||
{
|
||||
"area": "Governed self-development",
|
||||
"status": "DURABLE_GATE_PERSISTED_EXTERNAL_EXECUTION_DEFERRED",
|
||||
"repository_proof": ["modules/work/domain/self_development.go", "tests/semantic_evolution/self_development_contract_test.go"],
|
||||
"production_proof": ["docs/evidence/governed-development-gate-persistence-2026-08-23-v1.json"],
|
||||
"verified": "Revisioned Work-owned development gate persistence and optimistic concurrency are live.",
|
||||
"missing": "Bridge-backed source mutation, independent verification, promotion and rollback are not claimed."
|
||||
},
|
||||
{
|
||||
"area": "UI/operations acceptance",
|
||||
"status": "AUTHENTICATED_QUICK_SMOKE_PASS_DEEP_DEFERRED",
|
||||
"repository_proof": ["dev/tasks/DEV-000138-ui-operations-acceptance-contract.json", "cmd/creator-web/static/assets/console.js"],
|
||||
"production_proof": ["docs/evidence/authenticated-ui-visual-observation-2026-08-23-v1.json", "docs/evidence/authenticated-ui-quick-smoke-2026-08-23-v1.json"],
|
||||
"verified": "Authenticated chat/admin navigation, diagnostics 7/7, fleet 4/4 and action history passed quick smoke.",
|
||||
"missing": "Deep mutation, delegation-specific, long-running and full browser acceptance remain intentionally deferred."
|
||||
}
|
||||
],
|
||||
"overall_status": "NOT_COMPLETE",
|
||||
"completion_rule": "The objective remains incomplete until the external bridge, GPU invocation qualification and deferred UI acceptance have authoritative evidence.",
|
||||
"secrets": "No secret values included."
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"evidence_id": "audit-completion-matrix-2026-08-23-v3",
|
||||
"captured_at": "2026-08-23T08:30:00Z",
|
||||
"production_revision_before_release": "9f96393fb9a7ddce19eda3f90cbdeecb024029e6",
|
||||
"supersedes": "docs/evidence/audit-completion-matrix-2026-08-23-v2.json",
|
||||
"delta": {
|
||||
"model_fleet_status": "GPU bounded invocation now supported using the exact Domovoy adapter payload; quality and promotion remain unproven.",
|
||||
"evidence": "docs/evidence/gpu-invocation-qualification-2026-08-23-v1.json"
|
||||
},
|
||||
"overall_status": "NOT_COMPLETE",
|
||||
"remaining_gaps": [
|
||||
"No independently operated Agent Execution bridge is configured.",
|
||||
"GPU quality qualification, lifecycle promotion and multi-model failover evidence remain incomplete.",
|
||||
"Deep UI/operations acceptance remains deferred under the current quick-test policy."
|
||||
],
|
||||
"secrets": "No secret values included."
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"observed_on": "2026-08-23",
|
||||
"source": "production docker.cin.su read-only live checks",
|
||||
"public": {
|
||||
"healthz": {"status": 200, "runtime": "creator-web"}
|
||||
},
|
||||
"agent_worker": {
|
||||
"runtime": "agent-worker",
|
||||
"executor_configured": false,
|
||||
"mode": "local-fallback",
|
||||
"ready": false,
|
||||
"local_fallback": true,
|
||||
"reason": "external bridge unavailable; durable work is retained without external mutation"
|
||||
},
|
||||
"fleet": {
|
||||
"preferred_tier": "FAST",
|
||||
"selected_tier": "FAST",
|
||||
"ready_tiers": ["FAST", "MEDIUM", "LIFEBOAT"],
|
||||
"fallback_tier": "LIFEBOAT",
|
||||
"heavy": {"ready": false, "reason_class": "provider timeout"}
|
||||
},
|
||||
"runtimes": {
|
||||
"core": "ready",
|
||||
"work_graph": "ready",
|
||||
"research": "ready",
|
||||
"reflection_worker": "running"
|
||||
},
|
||||
"ui": {
|
||||
"public_admin_shell": "available",
|
||||
"authenticated_browser_session": "not_available_for_current_acceptance"
|
||||
},
|
||||
"secret_policy": "values omitted; only typed states and reason classes retained"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"captured_at": "2026-08-23",
|
||||
"production_revision": "b4d166480e5d06049718618ffeea0ccdbb993336",
|
||||
"provider_instance_id": "local-gpu-opportunistic",
|
||||
"provider_health": "AVAILABLE",
|
||||
"credential_source": "protected Domovoy credential reference; secret value not exposed",
|
||||
"models": [
|
||||
{
|
||||
"provider_model_id": "Qwen3.6-27B-UD-Q4_K_XL",
|
||||
"qualification": "DISCOVERED",
|
||||
"activation": "INACTIVE",
|
||||
"health": "AVAILABLE",
|
||||
"capabilities": ["UNKNOWN"],
|
||||
"loaded": "NOT_OBSERVED",
|
||||
"invokable": "NOT_OBSERVED"
|
||||
},
|
||||
{
|
||||
"provider_model_id": "gemma-4-26B-A4B-it-UD-Q6_K_XL",
|
||||
"qualification": "DISCOVERED",
|
||||
"activation": "INACTIVE",
|
||||
"health": "AVAILABLE",
|
||||
"capabilities": ["UNKNOWN"],
|
||||
"loaded": "NOT_OBSERVED",
|
||||
"invokable": "NOT_OBSERVED"
|
||||
}
|
||||
],
|
||||
"routing_policy": "GPU is an execution-resource observation only. Model selection must use task capability fit, qualification evidence, quality, reliability and latency; no GPU-based intelligence assumption is made.",
|
||||
"secrets_exposed": false
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"evidence_id": "gpu-invocation-qualification-2026-08-23-v1",
|
||||
"captured_at": "2026-08-23T08:30:00Z",
|
||||
"environment": "production",
|
||||
"endpoint_class": "protected OpenAI-compatible GPU gateway",
|
||||
"model": "Qwen3.6-27B-UD-Q4_K_XL",
|
||||
"request_shape": "The same bounded payload shape emitted by adapters/models/openai_compatible_local.go, including chat_template_kwargs.enable_thinking=false.",
|
||||
"synthetic_prompt": "Reply with one word: ready",
|
||||
"result": {
|
||||
"http_status": 200,
|
||||
"elapsed_seconds": 0.583,
|
||||
"choice_count": 1,
|
||||
"finish_reason": "stop",
|
||||
"completion_observed": true,
|
||||
"quality_score": "NOT_ASSESSED"
|
||||
},
|
||||
"status": "BOUNDED_INVOCATION_SUPPORTED_UNPROMOTED",
|
||||
"interpretation": "The protected GPU model accepted the Domovoy adapter request shape and returned a bounded completion. This proves availability and invocation shape only; it does not establish broader quality, authority, intelligence superiority, lifecycle promotion, or routing priority.",
|
||||
"secrets": "No credential values included."
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"evidence_id": "gpu-invocation-smoke-2026-08-23-v1",
|
||||
"captured_at": "2026-08-23T08:16:05Z",
|
||||
"environment": "production",
|
||||
"endpoint_class": "protected OpenAI-compatible GPU gateway",
|
||||
"discovery": {
|
||||
"request": "GET /v1/models with protected runtime credential",
|
||||
"http_status": 200,
|
||||
"models_observed": [
|
||||
"Qwen3.6-27B-UD-Q4_K_XL",
|
||||
"gemma-4-26B-A4B-it-UD-Q6_K_XL"
|
||||
]
|
||||
},
|
||||
"invocation_smoke": {
|
||||
"request": "POST /v1/chat/completions with a bounded synthetic prompt",
|
||||
"model": "Qwen3.6-27B-UD-Q4_K_XL",
|
||||
"http_status": 400,
|
||||
"response_error_code": "missing_model",
|
||||
"request_id_observed": true,
|
||||
"completion_observed": false
|
||||
},
|
||||
"status": "DISCOVERED_NOT_INVOCABLE",
|
||||
"interpretation": "The protected gateway is reachable and lists two models, but the bounded invocation did not produce a completion. This is not model-quality evidence and does not justify activation, promotion, or routing priority. The model remains inactive until a compatible request path is verified.",
|
||||
"secrets": "No credential values included."
|
||||
}
|
||||
@@ -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,142 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/artifact"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
var ErrInvalidTaskEvaluation = errors.New("invalid task evaluation")
|
||||
|
||||
const automaticExpansionMinimumOverallScore int64 = 700
|
||||
|
||||
// AutomaticTaskEvaluationExpansion decides whether a completed general
|
||||
// evaluation has evidence that warrants a bounded additional profile. It is
|
||||
// deliberately conservative: one supported candidate above the quality
|
||||
// threshold suppresses expansion, while unknown/unsupported/low-scoring or
|
||||
// critically failing candidates keep the unresolved path explicit.
|
||||
func AutomaticTaskEvaluationExpansion(parent capability.TaskEvaluationProfile, parentGraphState string, evaluations []capability.TaskEvaluation, request capability.TaskEvaluationExpansionRequest, profileID capability.TaskEvaluationProfileID, revision int64) (capability.TaskEvaluationProfile, bool, error) {
|
||||
if parentGraphState != "COMPLETED" && parentGraphState != "PARTIAL" {
|
||||
return capability.TaskEvaluationProfile{}, false, nil
|
||||
}
|
||||
if len(evaluations) == 0 {
|
||||
return capability.TaskEvaluationProfile{}, false, nil
|
||||
}
|
||||
for _, evaluation := range evaluations {
|
||||
if evaluation.Result == capability.EvaluationSupported && evaluation.OverallScoreMillis >= automaticExpansionMinimumOverallScore && evaluation.CriticalFailureMillis == 0 {
|
||||
return capability.TaskEvaluationProfile{}, false, nil
|
||||
}
|
||||
}
|
||||
if request.TriggerEvidenceRef == "" || len(request.NewCaseIDs) == 0 {
|
||||
return capability.TaskEvaluationProfile{}, false, fmt.Errorf("unresolved task evaluation requires trigger evidence and additional cases")
|
||||
}
|
||||
profile, err := BuildAdditionalTaskEvaluationProfile(parent, request, profileID, revision)
|
||||
if err != nil {
|
||||
return capability.TaskEvaluationProfile{}, false, err
|
||||
}
|
||||
return profile, true, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
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,61 @@
|
||||
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", 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutomaticExpansionWaitsForGeneralGraphAndUnresolvedCandidates(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}
|
||||
request := capability.TaskEvaluationExpansionRequest{ParentProfileID: parent.ID, CapabilityID: parent.CapabilityID, TriggerEvidenceRef: "outcome-unsolved", NewCaseIDs: []string{"edge-case"}, BenchmarkVersion: "additional-v1"}
|
||||
value, expanded, err := AutomaticTaskEvaluationExpansion(parent, "COMPLETED", []capability.TaskEvaluation{{ID: "evaluation-1", ProfileID: parent.ID, BindingID: "binding-1", BindingRevision: 1, CapabilityID: parent.CapabilityID, Result: capability.EvaluationUnknown, OverallScoreMillis: 0, CriticalFailureMillis: 1000, BenchmarkVersion: parent.BenchmarkVersion, ArtifactID: "artifact-1", ObservedAtUnixMillis: 1}}, request, "additional-v1", 1)
|
||||
if err != nil || !expanded || value.Stage != capability.TaskEvaluationAdditional || value.ParentProfileID != parent.ID {
|
||||
t.Fatalf("expected bounded expansion, profile=%+v expanded=%t err=%v", value, expanded, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutomaticExpansionDoesNotRunWhenAQualifiedCandidateExists(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}
|
||||
request := capability.TaskEvaluationExpansionRequest{ParentProfileID: parent.ID, CapabilityID: parent.CapabilityID, TriggerEvidenceRef: "outcome-unsolved", NewCaseIDs: []string{"edge-case"}, BenchmarkVersion: "additional-v1"}
|
||||
value, expanded, err := AutomaticTaskEvaluationExpansion(parent, "PARTIAL", []capability.TaskEvaluation{{ID: "evaluation-1", ProfileID: parent.ID, BindingID: "binding-1", BindingRevision: 1, CapabilityID: parent.CapabilityID, Result: capability.EvaluationSupported, OverallScoreMillis: 850, CriticalFailureMillis: 0, BenchmarkVersion: parent.BenchmarkVersion, ArtifactID: "artifact-1", ObservedAtUnixMillis: 1}}, request, "additional-v1", 1)
|
||||
if err != nil || expanded || value.ID != "" {
|
||||
t.Fatalf("qualified candidate must suppress expansion: profile=%+v expanded=%t err=%v", value, expanded, 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
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
@@ -33,16 +35,75 @@ func ExecuteAgentDispatch(ctx context.Context, store ports.ExecutionStore, execu
|
||||
if err := store.AcceptAgentCallback(ctx, heartbeat); err != nil {
|
||||
return work.AgentExecutionResult{}, err
|
||||
}
|
||||
result, err := executor.Execute(ctx, work.AgentExecutionRequest{Intent: intent, WorkerID: workerID, FencingToken: token, LeaseExpiresAt: lease.ExpiresAt})
|
||||
executionCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
var stateMu sync.Mutex
|
||||
currentLease := lease
|
||||
lastSequence := heartbeat.Sequence
|
||||
heartbeatDone := make(chan struct{})
|
||||
heartbeatStopped := make(chan struct{})
|
||||
heartbeatFailure := make(chan error, 1)
|
||||
interval := ttl / 3
|
||||
if interval <= 0 {
|
||||
return work.AgentExecutionResult{}, fmt.Errorf("invalid heartbeat interval")
|
||||
}
|
||||
go func() {
|
||||
defer close(heartbeatStopped)
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-heartbeatDone:
|
||||
return
|
||||
case observedAt := <-ticker.C:
|
||||
stateMu.Lock()
|
||||
nextLease, renewErr := domain.RenewAgentLease(currentLease, workerID, token, observedAt, ttl)
|
||||
if renewErr == nil {
|
||||
renewErr = store.RenewAgentLease(executionCtx, nextLease, nextLease.ExpiresAt)
|
||||
}
|
||||
if renewErr == nil {
|
||||
lastSequence++
|
||||
heartbeat = work.AgentCallback{RunID: intent.RunID, WorkerID: workerID, FencingToken: token, Sequence: lastSequence, Kind: work.AgentCallbackHeartbeat, ObservedAt: observedAt}
|
||||
renewErr = domain.ValidateAgentCallback(heartbeat, nextLease, lastSequence-1)
|
||||
}
|
||||
if renewErr == nil {
|
||||
renewErr = store.AcceptAgentCallback(executionCtx, heartbeat)
|
||||
}
|
||||
if renewErr == nil {
|
||||
currentLease = nextLease
|
||||
}
|
||||
stateMu.Unlock()
|
||||
if renewErr != nil {
|
||||
select {
|
||||
case heartbeatFailure <- renewErr:
|
||||
default:
|
||||
}
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
result, err := executor.Execute(executionCtx, work.AgentExecutionRequest{Intent: intent, WorkerID: workerID, FencingToken: token, LeaseExpiresAt: lease.ExpiresAt})
|
||||
close(heartbeatDone)
|
||||
<-heartbeatStopped
|
||||
select {
|
||||
case heartbeatErr := <-heartbeatFailure:
|
||||
return work.AgentExecutionResult{}, heartbeatErr
|
||||
default:
|
||||
}
|
||||
if err != nil {
|
||||
return work.AgentExecutionResult{}, err
|
||||
}
|
||||
stateMu.Lock()
|
||||
lease = currentLease
|
||||
stateMu.Unlock()
|
||||
observedAt := time.UnixMilli(result.ObservedAtUnixMillis)
|
||||
callback := work.AgentCallback{RunID: intent.RunID, WorkerID: workerID, FencingToken: token, Sequence: 2, Kind: work.AgentCallbackResult, ObservedAt: observedAt}
|
||||
callback := work.AgentCallback{RunID: intent.RunID, WorkerID: workerID, FencingToken: token, Sequence: lastSequence + 1, Kind: work.AgentCallbackResult, ObservedAt: observedAt}
|
||||
if result.State == work.AgentExecutionWaiting {
|
||||
callback.Kind = work.AgentCallbackCheckpoint
|
||||
}
|
||||
if err := domain.ValidateAgentCallback(callback, lease, heartbeat.Sequence); err != nil {
|
||||
if err := domain.ValidateAgentCallback(callback, lease, lastSequence); err != nil {
|
||||
return work.AgentExecutionResult{}, err
|
||||
}
|
||||
if err := store.AcceptAgentCallback(ctx, callback); err != nil {
|
||||
|
||||
@@ -18,6 +18,10 @@ func (s *bridgeStore) SaveAgentLease(_ context.Context, lease work.AgentLease) e
|
||||
s.lease = lease
|
||||
return nil
|
||||
}
|
||||
func (s *bridgeStore) RenewAgentLease(_ context.Context, lease work.AgentLease, _ time.Time) error {
|
||||
s.lease = lease
|
||||
return nil
|
||||
}
|
||||
func (s *bridgeStore) AcceptAgentCallback(_ context.Context, callback work.AgentCallback) error {
|
||||
s.callbacks = append(s.callbacks, callback)
|
||||
return nil
|
||||
@@ -29,6 +33,17 @@ func (bridgeExecutor) Execute(context.Context, work.AgentExecutionRequest) (work
|
||||
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "done", ObservedAtUnixMillis: 2}, nil
|
||||
}
|
||||
|
||||
type slowBridgeExecutor struct{}
|
||||
|
||||
func (slowBridgeExecutor) Execute(ctx context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
|
||||
select {
|
||||
case <-time.After(45 * time.Millisecond):
|
||||
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "done", ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
|
||||
case <-ctx.Done():
|
||||
return work.AgentExecutionResult{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteAgentDispatchPersistsLeaseAndBridgeCallbacks(t *testing.T) {
|
||||
store := &bridgeStore{}
|
||||
now := time.UnixMilli(1)
|
||||
@@ -40,3 +55,20 @@ func TestExecuteAgentDispatchPersistsLeaseAndBridgeCallbacks(t *testing.T) {
|
||||
t.Fatalf("callbacks=%+v", store.callbacks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteAgentDispatchRenewsLeaseAndOrdersHeartbeats(t *testing.T) {
|
||||
store := &bridgeStore{}
|
||||
now := time.Now().UTC()
|
||||
_, err := ExecuteAgentDispatch(context.Background(), store, slowBridgeExecutor{}, work.AgentDispatchIntent{DispatchID: "d-slow", IdempotencyKey: "i-slow", DelegationID: "del-slow", RunID: "run-slow", ImplementationID: "impl", ImplementationRevision: 1, SourceRevision: "source", WorktreeID: "tree", VerificationPlanRef: "verify", CreatedAtUnixMillis: now.UnixMilli()}, "worker", 1, now, 30*time.Millisecond)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(store.callbacks) < 3 {
|
||||
t.Fatalf("expected heartbeat callbacks, got %d", len(store.callbacks))
|
||||
}
|
||||
for i, callback := range store.callbacks {
|
||||
if callback.Sequence != uint64(i+1) {
|
||||
t.Fatalf("callback sequence is not ordered: %#v", store.callbacks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,16 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/modules/work/domain"
|
||||
"git.cin.su/m/Assistant/modules/work/ports"
|
||||
)
|
||||
|
||||
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 +22,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 +46,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 +140,39 @@ 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...)
|
||||
if node.TaskProfile != nil {
|
||||
profile := *node.TaskProfile
|
||||
profile.RequiredCaseIDs = append([]string(nil), node.TaskProfile.RequiredCaseIDs...)
|
||||
intents[i].TaskEvaluationProfile = &profile
|
||||
}
|
||||
intents[i].TaskEvaluationCases = append([]capability.TaskEvaluationCase(nil), node.TaskCases...)
|
||||
intents[i].TaskEvaluationExecution = &work.TaskEvaluationDispatchContext{GraphID: graph.ID, GraphRevision: graph.Revision, NodeID: node.ID, BindingRevision: request.Plan.BindingRevision, ActorID: identity.ActorID(request.Plan.ActorID), PrincipalID: identity.PrincipalID(request.Plan.PrincipalID), ScopeID: authority.ScopeID(request.Plan.ScopeID)}
|
||||
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,47 @@
|
||||
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},
|
||||
Cases: []capability.TaskEvaluationCase{{CaseID: "case-a", Prompt: "one", EvidenceArtifactRef: "artifact-a"}, {CaseID: "case-b", Prompt: "two", EvidenceArtifactRef: "artifact-b"}},
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,12 @@ type WorkerCycleResult struct {
|
||||
Idle bool
|
||||
}
|
||||
|
||||
const transientDispatchRetryDelay = time.Second
|
||||
|
||||
func retryClaimedDispatch(ctx context.Context, store ports.WorkerStore, dispatchID, workerID string, now time.Time) error {
|
||||
return store.RetryDispatch(ctx, dispatchID, workerID, now.UnixMilli(), transientDispatchRetryDelay)
|
||||
}
|
||||
|
||||
// RecoverOnlyWorkerCycle keeps durable recovery alive while the real executor
|
||||
// capability is absent. It intentionally does not claim a dispatch: a missing
|
||||
// executor must never be represented as successful work.
|
||||
@@ -37,11 +43,11 @@ func ExecuteWorkerCycle(ctx context.Context, store ports.WorkerStore, workerID s
|
||||
}
|
||||
intent, err := store.LoadDispatchIntent(ctx, item.DispatchID)
|
||||
if err != nil {
|
||||
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
|
||||
_ = retryClaimedDispatch(ctx, store, item.DispatchID, workerID, now)
|
||||
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
|
||||
}
|
||||
if _, err := ExecuteDryRunDispatch(ctx, store, intent, workerID, token, now, leaseTTL); err != nil {
|
||||
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
|
||||
_ = retryClaimedDispatch(ctx, store, item.DispatchID, workerID, now)
|
||||
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
|
||||
}
|
||||
if err := store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchCompleted, now.UnixMilli()); err != nil {
|
||||
@@ -64,15 +70,18 @@ func ExecuteWorkerCycleWithExecutor(ctx context.Context, store ports.WorkerStore
|
||||
}
|
||||
intent, err := store.LoadDispatchIntent(ctx, item.DispatchID)
|
||||
if err != nil {
|
||||
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
|
||||
_ = retryClaimedDispatch(ctx, store, item.DispatchID, workerID, now)
|
||||
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
|
||||
}
|
||||
result, err := ExecuteAgentDispatch(ctx, store, executor, intent, workerID, token, now, leaseTTL)
|
||||
if err != nil {
|
||||
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
|
||||
_ = retryClaimedDispatch(ctx, store, item.DispatchID, workerID, now)
|
||||
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
|
||||
}
|
||||
if result.State != work.AgentExecutionCompleted {
|
||||
if err := retryClaimedDispatch(ctx, store, item.DispatchID, workerID, now); err != nil {
|
||||
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
|
||||
}
|
||||
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, nil
|
||||
}
|
||||
if err := store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchCompleted, now.UnixMilli()); err != nil {
|
||||
|
||||
@@ -26,10 +26,16 @@ func (s *recoveryOnlyStore) LoadDispatchIntent(context.Context, string) (work.Ag
|
||||
func (s *recoveryOnlyStore) CompleteDispatch(context.Context, string, string, work.AgentDispatchQueueState, int64) error {
|
||||
return nil
|
||||
}
|
||||
func (s *recoveryOnlyStore) RetryDispatch(context.Context, string, string, int64, time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
func (s *recoveryOnlyStore) SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error {
|
||||
return nil
|
||||
}
|
||||
func (s *recoveryOnlyStore) SaveAgentLease(context.Context, work.AgentLease) error { return nil }
|
||||
func (s *recoveryOnlyStore) RenewAgentLease(context.Context, work.AgentLease, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
func (s *recoveryOnlyStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
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 len(request.Cases) != len(request.Profile.RequiredCaseIDs) {
|
||||
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||
}
|
||||
seenCases := make(map[string]struct{}, len(request.Cases))
|
||||
for _, item := range request.Cases {
|
||||
if item.CaseID == "" || item.Prompt == "" || item.EvidenceArtifactRef == "" {
|
||||
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||
}
|
||||
if _, exists := seenCases[item.CaseID]; exists {
|
||||
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||
}
|
||||
seenCases[item.CaseID] = struct{}{}
|
||||
}
|
||||
for _, requiredID := range request.Profile.RequiredCaseIDs {
|
||||
if _, exists := seenCases[requiredID]; !exists {
|
||||
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...),
|
||||
TaskProfile: cloneTaskEvaluationProfile(request.Profile),
|
||||
TaskCases: append([]capability.TaskEvaluationCase(nil), request.Cases...),
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func cloneTaskEvaluationProfile(value capability.TaskEvaluationProfile) *capability.TaskEvaluationProfile {
|
||||
copyValue := value
|
||||
copyValue.RequiredCaseIDs = append([]string(nil), value.RequiredCaseIDs...)
|
||||
return ©Value
|
||||
}
|
||||
@@ -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}, Cases: []capability.TaskEvaluationCase{{CaseID: "case-a", Prompt: "short answer", EvidenceArtifactRef: "artifact-a"}, {CaseID: "case-b", Prompt: "long answer", EvidenceArtifactRef: "artifact-b"}}}
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package ports
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
@@ -13,5 +14,6 @@ type AgentExecutor interface {
|
||||
type ExecutionStore interface {
|
||||
SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error
|
||||
SaveAgentLease(context.Context, work.AgentLease) error
|
||||
RenewAgentLease(context.Context, work.AgentLease, time.Time) error
|
||||
AcceptAgentCallback(context.Context, work.AgentCallback) error
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package ports
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
@@ -14,5 +15,6 @@ type WorkerStore interface {
|
||||
LoadDispatchIntent(context.Context, string) (work.AgentDispatchIntent, error)
|
||||
ClaimNextDispatch(context.Context, string, int64) (work.AgentDispatchQueueItem, error)
|
||||
CompleteDispatch(context.Context, string, string, work.AgentDispatchQueueState, int64) error
|
||||
RetryDispatch(context.Context, string, string, int64, time.Duration) error
|
||||
RecoverStaleClaims(context.Context, int64, int64) (int64, error)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ func (s *canaryStore) SaveDispatchIntent(context.Context, work.AgentDispatchInte
|
||||
s.writes++
|
||||
return nil
|
||||
}
|
||||
func (s *canaryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
|
||||
func (s *canaryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
|
||||
func (s *canaryStore) RenewAgentLease(context.Context, work.AgentLease, time.Time) error { return nil }
|
||||
func (s *canaryStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
|
||||
s.writes++
|
||||
return nil
|
||||
|
||||
@@ -21,6 +21,9 @@ func (s *recordingExecutionStore) SaveAgentLease(context.Context, work.AgentLeas
|
||||
s.operations = append(s.operations, "lease")
|
||||
return nil
|
||||
}
|
||||
func (s *recordingExecutionStore) RenewAgentLease(context.Context, work.AgentLease, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
func (s *recordingExecutionStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
|
||||
s.operations = append(s.operations, "callback")
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package agent_execution_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRecoveryMigrationDefinesBoundedRetryState(t *testing.T) {
|
||||
payload, err := os.ReadFile("../../database/migrations/042_agent_execution_recovery.sql")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source := strings.ToLower(string(payload))
|
||||
for _, fragment := range []string{"set role work_role", "retry_limit integer not null default 3", "retry_limit > 0"} {
|
||||
if !strings.Contains(source, fragment) {
|
||||
t.Fatalf("migration is missing %q", fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package semantic_evolution_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGovernedDevelopmentCandidateMigrationIsWorkOwnedAndSandboxed(t *testing.T) {
|
||||
payload, err := os.ReadFile("../../database/migrations/043_governed_development_candidates.sql")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source := strings.ToLower(string(payload))
|
||||
for _, fragment := range []string{"set role work_role", "create table if not exists work.extension_proposals", "create table if not exists work.development_scaffolds", "stage = 'sandboxed'", "protected_main_access = 'denied'", "production_access = 'denied'", "secret_access = 'denied'"} {
|
||||
if !strings.Contains(source, fragment) {
|
||||
t.Fatalf("migration is missing %q", fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user