Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43556e1603 | ||
|
|
dd00dabf32 | ||
|
|
33365ce7ee | ||
|
|
5017a854ee | ||
|
|
839b35f310 |
@@ -0,0 +1,18 @@
|
|||||||
|
package multimodal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"git.cin.su/m/Assistant/contracts/artifact"
|
||||||
|
"git.cin.su/m/Assistant/contracts/capability"
|
||||||
|
"git.cin.su/m/Assistant/contracts/interaction"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NormalizeIngress(id string, modality interaction.IngressModality, sourceRef, deviceRef, locationRef, principalID, contentType string, data []byte, response capability.MultimodalResponse, at int64) (interaction.IngressEnvelope, artifact.MediaArtifact) {
|
||||||
|
hash := sha256.Sum256(data)
|
||||||
|
artifactValue := artifact.MediaArtifact{ID: artifact.ArtifactID("artifact:media:" + hex.EncodeToString(hash[:8])), ContentType: contentType, SizeBytes: int64(len(data)), ContentDigest: hex.EncodeToString(hash[:]), SourceRef: sourceRef, RecordedAtUnixMillis: at}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return interaction.IngressEnvelope{ID: id, Modality: modality, SourceRef: sourceRef, DeviceRef: deviceRef, LocationRef: locationRef, PrincipalID: principalID, Degraded: true, RecordedAtUnixMillis: at}, artifact.MediaArtifact{}
|
||||||
|
}
|
||||||
|
return interaction.IngressEnvelope{ID: id, Modality: modality, SourceRef: sourceRef, DeviceRef: deviceRef, LocationRef: locationRef, PrincipalID: principalID, ArtifactRef: string(artifactValue.ID), ObservationRef: response.EvidenceRef, ProviderRef: response.Provider, ModelRef: response.Model, ModelVersion: response.ModelVersion, ConfidenceMillis: response.ConfidenceMillis, RecordedAtUnixMillis: at}, artifactValue
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package multimodal_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.cin.su/m/Assistant/adapters/multimodal"
|
||||||
|
"git.cin.su/m/Assistant/contracts/capability"
|
||||||
|
"git.cin.su/m/Assistant/contracts/interaction"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIngressNormalizesMediaArtifactAndObservationProvenance(t *testing.T) {
|
||||||
|
e, a := multimodal.NormalizeIngress("ingress-1", interaction.IngressImage, "camera:street", "device:cam", "world:street", "principal", "image/png", []byte("image"), capability.MultimodalResponse{Provider: "local", Model: "vision", ModelVersion: "1", EvidenceRef: "observation-1", ConfidenceMillis: 800}, 1)
|
||||||
|
if !e.Valid() || !a.Valid() || e.ArtifactRef == "" || e.ObservationRef != "observation-1" || e.DeviceRef == "" || e.LocationRef == "" {
|
||||||
|
t.Fatalf("envelope=%+v artifact=%+v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnavailableIngressDegradesWithoutFabricatingObservation(t *testing.T) {
|
||||||
|
e, a := multimodal.NormalizeIngress("ingress-1", interaction.IngressAudio, "mic", "device:mic", "", "principal", "audio/wav", nil, capability.MultimodalResponse{}, 1)
|
||||||
|
if !e.Degraded || e.ObservationRef != "" || a.Valid() {
|
||||||
|
t.Fatalf("envelope=%+v artifact=%+v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,121 @@ var (
|
|||||||
ErrDevelopmentCandidateConflict = errors.New("development candidate conflict")
|
ErrDevelopmentCandidateConflict = errors.New("development candidate conflict")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (r *WorkRepository) SaveInvestigation(ctx context.Context, value work.Investigation, expectedRevision int64) error {
|
||||||
|
if !value.Valid() || expectedRevision < 0 {
|
||||||
|
return errors.New("invalid investigation revision")
|
||||||
|
}
|
||||||
|
attempts := make([]string, 0, len(value.AttemptRefs))
|
||||||
|
for _, item := range value.AttemptRefs {
|
||||||
|
attempts = append(attempts, string(item))
|
||||||
|
}
|
||||||
|
hypotheses := make([]string, 0, len(value.HypothesisRefs))
|
||||||
|
for _, item := range value.HypothesisRefs {
|
||||||
|
hypotheses = append(hypotheses, string(item))
|
||||||
|
}
|
||||||
|
findings := make([]string, 0, len(value.FindingRefs))
|
||||||
|
for _, item := range value.FindingRefs {
|
||||||
|
findings = append(findings, string(item))
|
||||||
|
}
|
||||||
|
blockers := append([]string(nil), value.Blockers...)
|
||||||
|
checkpoints := make([]string, 0, len(value.CheckpointRefs))
|
||||||
|
for _, item := range value.CheckpointRefs {
|
||||||
|
checkpoints = append(checkpoints, string(item))
|
||||||
|
}
|
||||||
|
if expectedRevision == 0 {
|
||||||
|
result, err := r.pool.Exec(ctx, `INSERT INTO work.long_running_work(investigation_id,objective,hypothesis_refs,attempt_refs,finding_refs,blockers,checkpoint_refs,next_step,state,revision,time_budget_millis,cost_budget_micros,max_attempts,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT(investigation_id) DO NOTHING`, value.ID, value.Objective, hypotheses, attempts, findings, blockers, checkpoints, value.NextStep, value.State, value.Revision, value.TimeBudgetMillis, value.CostBudgetMicros, value.MaxAttempts, value.RecordedAtUnixMillis)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if result.RowsAffected() != 1 {
|
||||||
|
return errors.New("investigation already exists")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result, err := r.pool.Exec(ctx, `UPDATE work.long_running_work SET objective=$2,hypothesis_refs=$3,attempt_refs=$4,finding_refs=$5,blockers=$6,checkpoint_refs=$7,next_step=$8,state=$9,revision=$10,time_budget_millis=$11,cost_budget_micros=$12,max_attempts=$13,recorded_at_unix_millis=$14 WHERE investigation_id=$1 AND revision=$15`, value.ID, value.Objective, hypotheses, attempts, findings, blockers, checkpoints, value.NextStep, value.State, value.Revision, value.TimeBudgetMillis, value.CostBudgetMicros, value.MaxAttempts, value.RecordedAtUnixMillis, expectedRevision)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if result.RowsAffected() != 1 {
|
||||||
|
return errors.New("investigation revision conflict")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorkRepository) LoadInvestigation(ctx context.Context, id work.LongRunningWorkID) (work.Investigation, error) {
|
||||||
|
var value work.Investigation
|
||||||
|
var hypotheses, attempts, findings, blockers, checkpoints []string
|
||||||
|
err := r.pool.QueryRow(ctx, `SELECT investigation_id,objective,hypothesis_refs,attempt_refs,finding_refs,blockers,checkpoint_refs,next_step,state,revision,time_budget_millis,cost_budget_micros,max_attempts,recorded_at_unix_millis FROM work.long_running_work WHERE investigation_id=$1`, id).Scan(&value.ID, &value.Objective, &hypotheses, &attempts, &findings, &blockers, &checkpoints, &value.NextStep, &value.State, &value.Revision, &value.TimeBudgetMillis, &value.CostBudgetMicros, &value.MaxAttempts, &value.RecordedAtUnixMillis)
|
||||||
|
if err != nil {
|
||||||
|
return work.Investigation{}, fmt.Errorf("load investigation: %w", err)
|
||||||
|
}
|
||||||
|
for _, item := range hypotheses {
|
||||||
|
value.HypothesisRefs = append(value.HypothesisRefs, work.EvidenceRef(item))
|
||||||
|
}
|
||||||
|
for _, item := range attempts {
|
||||||
|
value.AttemptRefs = append(value.AttemptRefs, work.AgentRunID(item))
|
||||||
|
}
|
||||||
|
for _, item := range findings {
|
||||||
|
value.FindingRefs = append(value.FindingRefs, work.EvidenceRef(item))
|
||||||
|
}
|
||||||
|
value.Blockers = append(value.Blockers, blockers...)
|
||||||
|
for _, item := range checkpoints {
|
||||||
|
value.CheckpointRefs = append(value.CheckpointRefs, work.AgentCheckpointID(item))
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveInvestigationCheckpoint atomically records the checkpoint and marks its
|
||||||
|
// attempts verified. The unique key makes retries idempotent.
|
||||||
|
func (r *WorkRepository) SaveInvestigationCheckpoint(ctx context.Context, checkpoint work.InvestigationCheckpoint) error {
|
||||||
|
if !checkpoint.Valid() {
|
||||||
|
return errors.New("invalid investigation checkpoint")
|
||||||
|
}
|
||||||
|
tx, err := r.pool.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
if _, err = tx.Exec(ctx, `INSERT INTO work.investigation_checkpoints(checkpoint_id,investigation_id,completed_attempt_refs,completed_finding_refs,next_step,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(checkpoint_id) DO NOTHING`, checkpoint.ID, checkpoint.InvestigationID, stringIDs(checkpoint.CompletedAttemptRefs), stringEvidence(checkpoint.CompletedFindingRefs), checkpoint.NextStep, checkpoint.RecordedAtUnixMillis); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, attempt := range checkpoint.CompletedAttemptRefs {
|
||||||
|
if _, err = tx.Exec(ctx, `INSERT INTO work.investigation_attempts(investigation_id,attempt_ref,verified_at_unix_millis) VALUES($1,$2,$3) ON CONFLICT(investigation_id,attempt_ref) DO NOTHING`, checkpoint.InvestigationID, attempt, checkpoint.RecordedAtUnixMillis); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx.Commit(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WorkRepository) UnverifiedInvestigationAttempts(ctx context.Context, id work.LongRunningWorkID, candidates []work.AgentRunID) ([]work.AgentRunID, error) {
|
||||||
|
result := make([]work.AgentRunID, 0, len(candidates))
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
var exists bool
|
||||||
|
if err := r.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM work.investigation_attempts WHERE investigation_id=$1 AND attempt_ref=$2)`, id, candidate).Scan(&exists); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
result = append(result, candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringIDs(values []work.AgentRunID) []string {
|
||||||
|
result := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
result = append(result, string(value))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
func stringEvidence(values []work.EvidenceRef) []string {
|
||||||
|
result := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
result = append(result, string(value))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
|
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
|
||||||
pool, err := openPool(ctx, config)
|
pool, err := openPool(ctx, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -533,7 +533,7 @@ func main() {
|
|||||||
log.Printf("interaction principal history loaded: %d utterances", len(recentUserHistory))
|
log.Printf("interaction principal history loaded: %d utterances", len(recentUserHistory))
|
||||||
}
|
}
|
||||||
conversationHistory = append(conversationHistory, recentUserHistory...)
|
conversationHistory = append(conversationHistory, recentUserHistory...)
|
||||||
gen2Context := interactiondomain.BuildGen2Context(conversationHistory)
|
gen2Context := interactiondomain.BuildScopedGen2Context(conversationHistory, string(session.PrincipalID))
|
||||||
if !creativeUserRequest && locationReply == "" {
|
if !creativeUserRequest && locationReply == "" {
|
||||||
if continuation := interactiondomain.ContinueLocalSearch(effectiveTaskMessage, conversationHistory); continuation != "" {
|
if continuation := interactiondomain.ContinueLocalSearch(effectiveTaskMessage, conversationHistory); continuation != "" {
|
||||||
continuationMessage = continuation
|
continuationMessage = continuation
|
||||||
|
|||||||
@@ -5,6 +5,18 @@ import "git.cin.su/m/Assistant/platform/ids"
|
|||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
type ArtifactID ids.ID
|
type ArtifactID ids.ID
|
||||||
|
type MediaArtifact struct {
|
||||||
|
ID ArtifactID
|
||||||
|
ContentType string
|
||||||
|
SizeBytes int64
|
||||||
|
ContentDigest string
|
||||||
|
SourceRef string
|
||||||
|
RecordedAtUnixMillis int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a MediaArtifact) Valid() bool {
|
||||||
|
return a.ID != "" && a.ContentType != "" && a.SizeBytes > 0 && a.ContentDigest != "" && a.SourceRef != "" && a.RecordedAtUnixMillis > 0
|
||||||
|
}
|
||||||
|
|
||||||
type AgentArtifactAttestation struct {
|
type AgentArtifactAttestation struct {
|
||||||
ArtifactID ArtifactID
|
ArtifactID ArtifactID
|
||||||
|
|||||||
@@ -15,11 +15,15 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MultimodalResponse struct {
|
type MultimodalResponse struct {
|
||||||
Operation MultimodalOperation `json:"operation"`
|
Operation MultimodalOperation `json:"operation"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Provider string `json:"provider,omitempty"`
|
Provider string `json:"provider,omitempty"`
|
||||||
ObservedAt int64 `json:"observed_at_unix_millis"`
|
ObservedAt int64 `json:"observed_at_unix_millis"`
|
||||||
ConfidenceNote string `json:"confidence_note,omitempty"`
|
ConfidenceNote string `json:"confidence_note,omitempty"`
|
||||||
ContentType string `json:"content_type,omitempty"`
|
ContentType string `json:"content_type,omitempty"`
|
||||||
DataBase64 string `json:"data_base64,omitempty"`
|
DataBase64 string `json:"data_base64,omitempty"`
|
||||||
|
Model string `json:"model,omitempty"`
|
||||||
|
ModelVersion string `json:"model_version,omitempty"`
|
||||||
|
ConfidenceMillis int64 `json:"confidence_millis,omitempty"`
|
||||||
|
EvidenceRef string `json:"evidence_ref,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ type BootstrapConfig struct {
|
|||||||
RecoveryLocator string
|
RecoveryLocator string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConversationalConfigurationCommand is an intent, not an assignment. Only
|
||||||
|
// Configuration may turn a SET command into durable state.
|
||||||
|
|
||||||
func (d SettingDefinition) Validate() error {
|
func (d SettingDefinition) Validate() error {
|
||||||
if d.ID == "" || d.OwnerNamespace == "" || d.StableKey == "" || !ids.Revision(d.Revision).Valid() || d.ValueType == "" || d.Lifecycle == "" || d.RecordedAtUnixMillis < 1 {
|
if d.ID == "" || d.OwnerNamespace == "" || d.StableKey == "" || !ids.Revision(d.Revision).Valid() || d.ValueType == "" || d.Lifecycle == "" || d.RecordedAtUnixMillis < 1 {
|
||||||
return fmt.Errorf("invalid setting definition")
|
return fmt.Errorf("invalid setting definition")
|
||||||
|
|||||||
@@ -17,23 +17,27 @@ type CoverageState string
|
|||||||
type ConflictState string
|
type ConflictState string
|
||||||
type EvidenceKind string
|
type EvidenceKind string
|
||||||
type ResearchFindingState string
|
type ResearchFindingState string
|
||||||
|
type ReliabilityState string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FreshnessCurrent FreshnessState = "CURRENT"
|
FreshnessCurrent FreshnessState = "CURRENT"
|
||||||
FreshnessStale FreshnessState = "STALE"
|
FreshnessStale FreshnessState = "STALE"
|
||||||
FreshnessUnknown FreshnessState = "UNKNOWN"
|
FreshnessUnknown FreshnessState = "UNKNOWN"
|
||||||
CoverageComplete CoverageState = "COMPLETE"
|
CoverageComplete CoverageState = "COMPLETE"
|
||||||
CoveragePartial CoverageState = "PARTIAL"
|
CoveragePartial CoverageState = "PARTIAL"
|
||||||
CoverageUnknown CoverageState = "UNKNOWN"
|
CoverageUnknown CoverageState = "UNKNOWN"
|
||||||
ConflictNone ConflictState = "NONE"
|
ConflictNone ConflictState = "NONE"
|
||||||
ConflictPresent ConflictState = "PRESENT"
|
ConflictPresent ConflictState = "PRESENT"
|
||||||
ConflictUnknown ConflictState = "UNKNOWN"
|
ConflictUnknown ConflictState = "UNKNOWN"
|
||||||
EvidenceObserved EvidenceKind = "OBSERVED"
|
EvidenceObserved EvidenceKind = "OBSERVED"
|
||||||
EvidenceAssumed EvidenceKind = "ASSUMED"
|
EvidenceAssumed EvidenceKind = "ASSUMED"
|
||||||
FindingSupported ResearchFindingState = "SUPPORTED"
|
FindingSupported ResearchFindingState = "SUPPORTED"
|
||||||
FindingStale ResearchFindingState = "STALE"
|
FindingStale ResearchFindingState = "STALE"
|
||||||
FindingConflicted ResearchFindingState = "CONFLICTED"
|
FindingConflicted ResearchFindingState = "CONFLICTED"
|
||||||
FindingUnknown ResearchFindingState = "UNKNOWN"
|
FindingUnknown ResearchFindingState = "UNKNOWN"
|
||||||
|
ReliabilityCurrent ReliabilityState = "CURRENT"
|
||||||
|
ReliabilityAging ReliabilityState = "AGING"
|
||||||
|
ReliabilityUnknown ReliabilityState = "UNKNOWN"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResearchPlan struct {
|
type ResearchPlan struct {
|
||||||
@@ -52,6 +56,21 @@ type Source struct {
|
|||||||
Publisher string
|
Publisher string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SourceReliabilityAssessment struct {
|
||||||
|
SourceRef string
|
||||||
|
DomainRef string
|
||||||
|
Freshness FreshnessState
|
||||||
|
HistoryScoreMillis int64
|
||||||
|
ReliabilityScoreMillis int64
|
||||||
|
State ReliabilityState
|
||||||
|
EvidenceRefs []string
|
||||||
|
AssessedAtUnixMillis int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SourceReliabilityAssessment) Valid() bool {
|
||||||
|
return a.SourceRef != "" && a.DomainRef != "" && a.Freshness != "" && a.HistoryScoreMillis >= 0 && a.HistoryScoreMillis <= 1000 && a.ReliabilityScoreMillis >= 0 && a.ReliabilityScoreMillis <= 1000 && a.State != "" && a.AssessedAtUnixMillis > 0
|
||||||
|
}
|
||||||
|
|
||||||
type SourceSnapshot struct {
|
type SourceSnapshot struct {
|
||||||
ID SourceSnapshotID
|
ID SourceSnapshotID
|
||||||
SourceID SourceID
|
SourceID SourceID
|
||||||
|
|||||||
@@ -6,6 +6,30 @@ type ActorID ids.ID
|
|||||||
type PrincipalID ids.ID
|
type PrincipalID ids.ID
|
||||||
type IdentityID ids.ID
|
type IdentityID ids.ID
|
||||||
|
|
||||||
|
type DataRealmKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DataRealmPersonal DataRealmKind = "PERSONAL"
|
||||||
|
DataRealmShared DataRealmKind = "SHARED"
|
||||||
|
DataRealmPublic DataRealmKind = "PUBLIC"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrincipalDataRealm is an identity-owned access boundary. It does not make
|
||||||
|
// a speaker or World person referent an authenticated principal.
|
||||||
|
type PrincipalDataRealm struct {
|
||||||
|
ID string
|
||||||
|
OwnerPrincipalID PrincipalID
|
||||||
|
Kind DataRealmKind
|
||||||
|
MemberPrincipalIDs []PrincipalID
|
||||||
|
Shared bool
|
||||||
|
Revision int64
|
||||||
|
RecordedAtUnixMillis int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r PrincipalDataRealm) Valid() bool {
|
||||||
|
return r.ID != "" && r.OwnerPrincipalID != "" && (r.Kind == DataRealmPersonal || r.Kind == DataRealmShared || r.Kind == DataRealmPublic) && r.Revision > 0 && r.RecordedAtUnixMillis > 0 && (r.Kind != DataRealmShared || r.Shared)
|
||||||
|
}
|
||||||
|
|
||||||
// PrincipalBinding keeps the operational actor separate from the interest on
|
// PrincipalBinding keeps the operational actor separate from the interest on
|
||||||
// whose behalf it acts. A Creator is not exempt from this distinction.
|
// whose behalf it acts. A Creator is not exempt from this distinction.
|
||||||
type PrincipalBinding struct {
|
type PrincipalBinding struct {
|
||||||
|
|||||||
@@ -25,6 +25,44 @@ type SemanticApplicability string
|
|||||||
type SemanticCandidateSource string
|
type SemanticCandidateSource string
|
||||||
type SemanticCandidateSelectionState string
|
type SemanticCandidateSelectionState string
|
||||||
type InformationRequestKind string
|
type InformationRequestKind string
|
||||||
|
type ConfigurationCommandKind string
|
||||||
|
type SpeakerAttribution struct {
|
||||||
|
SourceRef string
|
||||||
|
PrincipalCandidate string
|
||||||
|
ConfidenceMillis int64
|
||||||
|
Authenticated bool
|
||||||
|
ObservationRef string
|
||||||
|
}
|
||||||
|
type IngressModality string
|
||||||
|
|
||||||
|
const (
|
||||||
|
IngressText IngressModality = "TEXT"
|
||||||
|
IngressAudio IngressModality = "AUDIO"
|
||||||
|
IngressImage IngressModality = "IMAGE"
|
||||||
|
IngressVideo IngressModality = "VIDEO"
|
||||||
|
IngressDevice IngressModality = "DEVICE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IngressEnvelope struct {
|
||||||
|
ID string
|
||||||
|
Modality IngressModality
|
||||||
|
SourceRef string
|
||||||
|
DeviceRef string
|
||||||
|
LocationRef string
|
||||||
|
PrincipalID string
|
||||||
|
ArtifactRef string
|
||||||
|
ObservationRef string
|
||||||
|
ProviderRef string
|
||||||
|
ModelRef string
|
||||||
|
ModelVersion string
|
||||||
|
ConfidenceMillis int64
|
||||||
|
Degraded bool
|
||||||
|
RecordedAtUnixMillis int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e IngressEnvelope) Valid() bool {
|
||||||
|
return e.ID != "" && e.Modality != "" && e.SourceRef != "" && e.ConfidenceMillis >= 0 && e.ConfidenceMillis <= 1000 && e.RecordedAtUnixMillis > 0 && (!e.Degraded || e.ObservationRef == "")
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ChannelWebChat Channel = "WEB_CHAT"
|
ChannelWebChat Channel = "WEB_CHAT"
|
||||||
@@ -75,6 +113,8 @@ const (
|
|||||||
InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY"
|
InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY"
|
||||||
InformationRequestTimeDifference InformationRequestKind = "TIME_DIFFERENCE"
|
InformationRequestTimeDifference InformationRequestKind = "TIME_DIFFERENCE"
|
||||||
InformationRequestResearchFailureExplanation InformationRequestKind = "RESEARCH_FAILURE_EXPLANATION"
|
InformationRequestResearchFailureExplanation InformationRequestKind = "RESEARCH_FAILURE_EXPLANATION"
|
||||||
|
ConfigurationCommandSet ConfigurationCommandKind = "SET"
|
||||||
|
ConfigurationCommandQuery ConfigurationCommandKind = "QUERY"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Conversation struct {
|
type Conversation struct {
|
||||||
@@ -106,6 +146,18 @@ type OriginalHumanIntent struct {
|
|||||||
RecordedAtUnixMillis int64
|
RecordedAtUnixMillis int64
|
||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfigurationCommand is Interaction-owned intent. Configuration remains
|
||||||
|
// the only owner allowed to materialise a SettingAssignment.
|
||||||
|
type ConfigurationCommand struct {
|
||||||
|
Kind ConfigurationCommandKind
|
||||||
|
PrincipalID string
|
||||||
|
DefinitionRef string
|
||||||
|
TypedValue string
|
||||||
|
TemporaryOverride bool
|
||||||
|
SessionRef string
|
||||||
|
SourceRef string
|
||||||
|
}
|
||||||
type InteractionRelation struct {
|
type InteractionRelation struct {
|
||||||
ID InteractionRelationID
|
ID InteractionRelationID
|
||||||
FromUtteranceID HumanUtteranceID
|
FromUtteranceID HumanUtteranceID
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package work
|
||||||
|
|
||||||
|
type EvolutionRunStage string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EvolutionNeedRecorded EvolutionRunStage = "IMPROVEMENT_NEED"
|
||||||
|
EvolutionCandidateBuilt EvolutionRunStage = "CANDIDATE"
|
||||||
|
EvolutionIndependentlyEvaluated EvolutionRunStage = "INDEPENDENT_EVALUATION"
|
||||||
|
EvolutionSandboxed EvolutionRunStage = "SANDBOX_BUILD"
|
||||||
|
EvolutionBenchmarked EvolutionRunStage = "BENCHMARK"
|
||||||
|
EvolutionCanaried EvolutionRunStage = "CANARY"
|
||||||
|
EvolutionPromoted EvolutionRunStage = "PROMOTION"
|
||||||
|
EvolutionRolledBack EvolutionRunStage = "ROLLBACK"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GovernedEvolutionResult struct {
|
||||||
|
NeedRef string
|
||||||
|
CandidateRef string
|
||||||
|
EvaluationRef string
|
||||||
|
Stages []EvolutionRunStage
|
||||||
|
EvidenceRefs []EvidenceRef
|
||||||
|
LearningLedgerOutcomeRef string
|
||||||
|
Promoted bool
|
||||||
|
RollbackRef string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GovernedEvolutionOps are explicit gates. The caller supplies existing
|
||||||
|
// development executor/supervisor operations; this interface grants no new
|
||||||
|
// authority and has no autonomous promotion implementation.
|
||||||
|
type GovernedEvolutionOps struct {
|
||||||
|
CreateCandidate func() (EvolutionCandidate, error)
|
||||||
|
Evaluate func(EvolutionCandidate) (EvolutionEvaluation, error)
|
||||||
|
SandboxBuild func(EvolutionCandidate) (EvidenceRef, error)
|
||||||
|
Benchmark func(EvolutionCandidate) (EvidenceRef, bool, error)
|
||||||
|
Canary func(EvolutionCandidate) (EvidenceRef, bool, error)
|
||||||
|
Promote func(EvolutionCandidate, EvolutionEvaluation) (string, error)
|
||||||
|
Rollback func(EvolutionCandidate) (string, error)
|
||||||
|
LearningLedgerOutcome func(GovernedEvolutionResult) (string, error)
|
||||||
|
}
|
||||||
@@ -30,6 +30,22 @@ type Investigation struct {
|
|||||||
RecordedAtUnixMillis int64
|
RecordedAtUnixMillis int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InvestigationCheckpoint is the durable boundary between attempts. A
|
||||||
|
// checkpoint is written only after an attempt has been verified, so a worker
|
||||||
|
// restart can resume at NextStep without replaying that attempt.
|
||||||
|
type InvestigationCheckpoint struct {
|
||||||
|
ID AgentCheckpointID
|
||||||
|
InvestigationID LongRunningWorkID
|
||||||
|
CompletedAttemptRefs []AgentRunID
|
||||||
|
CompletedFindingRefs []EvidenceRef
|
||||||
|
NextStep string
|
||||||
|
RecordedAtUnixMillis int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c InvestigationCheckpoint) Valid() bool {
|
||||||
|
return c.ID != "" && c.InvestigationID != "" && c.NextStep != "" && c.RecordedAtUnixMillis > 0
|
||||||
|
}
|
||||||
|
|
||||||
func (w Investigation) Valid() bool {
|
func (w Investigation) Valid() bool {
|
||||||
return w.ID != "" && w.Objective != "" && w.State != "" && w.Revision > 0 && w.TimeBudgetMillis >= 0 && w.CostBudgetMicros >= 0 && w.MaxAttempts >= 0 && w.RecordedAtUnixMillis > 0
|
return w.ID != "" && w.Objective != "" && w.State != "" && w.Revision > 0 && w.TimeBudgetMillis >= 0 && w.CostBudgetMicros >= 0 && w.MaxAttempts >= 0 && w.RecordedAtUnixMillis > 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ type PerspectiveID string
|
|||||||
type PerspectiveSpec struct {
|
type PerspectiveSpec struct {
|
||||||
ID PerspectiveID
|
ID PerspectiveID
|
||||||
ObjectiveLens string
|
ObjectiveLens string
|
||||||
|
FocusRef string
|
||||||
RetrievalProfileRef string
|
RetrievalProfileRef string
|
||||||
|
SkillRefs []string
|
||||||
|
ToolRefs []string
|
||||||
EvaluationCriteria []string
|
EvaluationCriteria []string
|
||||||
CapabilityRequirements []string
|
CapabilityRequirements []string
|
||||||
ExpectedOutputShape string
|
ExpectedOutputShape string
|
||||||
@@ -13,6 +16,9 @@ type PerspectiveSpec struct {
|
|||||||
type SpecialistAssignment struct {
|
type SpecialistAssignment struct {
|
||||||
Perspective PerspectiveSpec
|
Perspective PerspectiveSpec
|
||||||
SubtaskRef string
|
SubtaskRef string
|
||||||
|
FocusRef string
|
||||||
|
SkillRefs []string
|
||||||
|
ToolRefs []string
|
||||||
EvidenceRefs []EvidenceRef
|
EvidenceRefs []EvidenceRef
|
||||||
}
|
}
|
||||||
type TeamDecision struct {
|
type TeamDecision struct {
|
||||||
@@ -22,5 +28,5 @@ type TeamDecision struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p PerspectiveSpec) Valid() bool {
|
func (p PerspectiveSpec) Valid() bool {
|
||||||
return p.ID != "" && p.ObjectiveLens != "" && len(p.EvaluationCriteria) > 0 && p.ExpectedOutputShape != ""
|
return p.ID != "" && p.ObjectiveLens != "" && p.FocusRef != "" && len(p.SkillRefs) > 0 && len(p.ToolRefs) > 0 && len(p.EvaluationCriteria) > 0 && p.ExpectedOutputShape != ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,16 @@ type ValueTypeSpec struct {
|
|||||||
type WorldObjectID ids.StableID
|
type WorldObjectID ids.StableID
|
||||||
type KindDefinitionID ids.StableID
|
type KindDefinitionID ids.StableID
|
||||||
type WorldAliasID ids.StableID
|
type WorldAliasID ids.StableID
|
||||||
|
type PersonReferent struct {
|
||||||
|
WorldObjectRef SemanticReference
|
||||||
|
DisplayName string
|
||||||
|
ConfidenceMillis int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PersonReferent) Valid() bool {
|
||||||
|
return p.WorldObjectRef.Valid() && p.DisplayName != "" && p.ConfidenceMillis >= 0 && p.ConfidenceMillis <= 1000
|
||||||
|
}
|
||||||
|
|
||||||
type FacetDefinitionID ids.StableID
|
type FacetDefinitionID ids.StableID
|
||||||
type RelationDefinitionID ids.StableID
|
type RelationDefinitionID ids.StableID
|
||||||
type ResolutionDecisionID ids.StableID
|
type ResolutionDecisionID ids.StableID
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
BEGIN;
|
||||||
|
SET ROLE work_role;
|
||||||
|
CREATE TABLE IF NOT EXISTS work.long_running_work (
|
||||||
|
investigation_id TEXT PRIMARY KEY, objective TEXT NOT NULL,
|
||||||
|
hypothesis_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], attempt_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||||
|
finding_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], blockers TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||||
|
checkpoint_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], next_step TEXT NOT NULL, state TEXT NOT NULL,
|
||||||
|
revision BIGINT NOT NULL, time_budget_millis BIGINT NOT NULL, cost_budget_micros BIGINT NOT NULL,
|
||||||
|
max_attempts INTEGER NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
|
||||||
|
CHECK (investigation_id <> ''), CHECK (objective <> ''), CHECK (next_step <> ''), CHECK (revision > 0), CHECK (time_budget_millis >= 0), CHECK (cost_budget_micros >= 0), CHECK (max_attempts >= 0), CHECK (recorded_at_unix_millis > 0)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS work.investigation_checkpoints (
|
||||||
|
checkpoint_id TEXT PRIMARY KEY, investigation_id TEXT NOT NULL, completed_attempt_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||||
|
completed_finding_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], next_step TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
|
||||||
|
CHECK (checkpoint_id <> ''), CHECK (investigation_id <> ''), CHECK (next_step <> ''), CHECK (recorded_at_unix_millis > 0)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS work.investigation_attempts (
|
||||||
|
investigation_id TEXT NOT NULL, attempt_ref TEXT NOT NULL, verified_at_unix_millis BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (investigation_id, attempt_ref), CHECK (investigation_id <> ''), CHECK (attempt_ref <> ''), CHECK (verified_at_unix_millis > 0)
|
||||||
|
);
|
||||||
|
RESET ROLE;
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
BEGIN;
|
||||||
|
SET ROLE identity_role;
|
||||||
|
CREATE TABLE IF NOT EXISTS identity.principal_data_realms (
|
||||||
|
realm_id TEXT PRIMARY KEY, owner_principal_id TEXT NOT NULL, realm_kind TEXT NOT NULL,
|
||||||
|
member_principal_ids TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], shared BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
revision BIGINT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
|
||||||
|
CHECK (realm_id <> ''), CHECK (owner_principal_id <> ''), CHECK (realm_kind IN ('PERSONAL','SHARED','PUBLIC')),
|
||||||
|
CHECK (realm_kind <> 'SHARED' OR shared), CHECK (revision > 0), CHECK (recorded_at_unix_millis > 0)
|
||||||
|
);
|
||||||
|
RESET ROLE;
|
||||||
|
COMMIT;
|
||||||
@@ -1 +1 @@
|
|||||||
{"task_file":"dev/tasks/GEN2-022-wire-governed-self-evolution-loop-using-existing-development-gates.json","task_id":"GEN2-022","task_revision":1}
|
{"task_file":"dev/tasks/GEN2-026-migrate-household-behavior-to-gen2-context-memory-skills-with-local-degr.json","task_id":"GEN2-026","task_revision":1}
|
||||||
|
|||||||
@@ -2,10 +2,40 @@ package domain
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"git.cin.su/m/Assistant/contracts/configuration"
|
"git.cin.su/m/Assistant/contracts/configuration"
|
||||||
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Resolve(definition configuration.SettingDefinition, assignments []configuration.SettingAssignment, selector configuration.ScopeSelector) (configuration.EffectiveSetting,error){if err:=definition.Validate();err!=nil{return configuration.EffectiveSetting{},err}; var matches []configuration.SettingAssignment;for _,a:=range assignments{if err:=a.Validate();err!=nil{return configuration.EffectiveSetting{},err};if a.DefinitionRef==string(definition.ID)&&a.Lifecycle==configuration.SettingActive&&a.Selector.PrincipalRef==selector.PrincipalRef&&a.Selector.SkillRef==selector.SkillRef{matches=append(matches,a)}};if len(matches)==0{return configuration.EffectiveSetting{DefinitionRef:string(definition.ID),Value:definition.DefaultValue,ResolutionTrace:[]string{"system-default"}},nil};sort.SliceStable(matches,func(i,j int)bool{return specificity(matches[i].Selector)>specificity(matches[j].Selector)});if len(matches)>1&&specificity(matches[0].Selector)==specificity(matches[1].Selector){return configuration.EffectiveSetting{},fmt.Errorf("configuration conflict")};return configuration.EffectiveSetting{DefinitionRef:string(definition.ID),Value:matches[0].TypedValue,WinningAssignmentRef:string(matches[0].ID),ResolutionTrace:[]string{"specificity-selected"}},nil}
|
var ErrInvalidConfigurationCommand = fmt.Errorf("invalid conversational configuration command")
|
||||||
func specificity(s configuration.ScopeSelector)int{return len(s.RequiredContextBindings)+boolInt(s.SkillRef!="")+boolInt(s.TaskRef!="")+boolInt(s.SessionRef!="")+boolInt(s.TemporaryOverride)}
|
|
||||||
func boolInt(v bool)int{if v{return 1};return 0}
|
func Resolve(definition configuration.SettingDefinition, assignments []configuration.SettingAssignment, selector configuration.ScopeSelector) (configuration.EffectiveSetting, error) {
|
||||||
|
if err := definition.Validate(); err != nil {
|
||||||
|
return configuration.EffectiveSetting{}, err
|
||||||
|
}
|
||||||
|
var matches []configuration.SettingAssignment
|
||||||
|
for _, a := range assignments {
|
||||||
|
if err := a.Validate(); err != nil {
|
||||||
|
return configuration.EffectiveSetting{}, err
|
||||||
|
}
|
||||||
|
if a.DefinitionRef == string(definition.ID) && a.Lifecycle == configuration.SettingActive && a.Selector.PrincipalRef == selector.PrincipalRef && a.Selector.SkillRef == selector.SkillRef {
|
||||||
|
matches = append(matches, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return configuration.EffectiveSetting{DefinitionRef: string(definition.ID), Value: definition.DefaultValue, ResolutionTrace: []string{"system-default"}}, nil
|
||||||
|
}
|
||||||
|
sort.SliceStable(matches, func(i, j int) bool { return specificity(matches[i].Selector) > specificity(matches[j].Selector) })
|
||||||
|
if len(matches) > 1 && specificity(matches[0].Selector) == specificity(matches[1].Selector) {
|
||||||
|
return configuration.EffectiveSetting{}, fmt.Errorf("configuration conflict")
|
||||||
|
}
|
||||||
|
return configuration.EffectiveSetting{DefinitionRef: string(definition.ID), Value: matches[0].TypedValue, WinningAssignmentRef: string(matches[0].ID), ResolutionTrace: []string{"specificity-selected"}}, nil
|
||||||
|
}
|
||||||
|
func specificity(s configuration.ScopeSelector) int {
|
||||||
|
return len(s.RequiredContextBindings) + boolInt(s.SkillRef != "") + boolInt(s.TaskRef != "") + boolInt(s.SessionRef != "") + boolInt(s.TemporaryOverride)
|
||||||
|
}
|
||||||
|
func boolInt(v bool) int {
|
||||||
|
if v {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.cin.su/m/Assistant/contracts/configuration"
|
||||||
|
"git.cin.su/m/Assistant/platform/ids"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrConfigurationAuthorityRequired = fmt.Errorf("configuration change requires Authority and Commit")
|
||||||
|
|
||||||
|
func AssignmentFromConversation(kind, principalID, definitionRef, typedValue, sessionRef, sourceRef string, temporaryOverride bool, definition configuration.SettingDefinition, now time.Time) (configuration.SettingAssignment, error) {
|
||||||
|
if kind != "SET" || principalID == "" || definitionRef != string(definition.ID) || strings.TrimSpace(typedValue) == "" || now.IsZero() || definition.ValueType == configuration.ValueSecretRef {
|
||||||
|
return configuration.SettingAssignment{}, ErrInvalidConfigurationCommand
|
||||||
|
}
|
||||||
|
if sourceRef == "" {
|
||||||
|
return configuration.SettingAssignment{}, ErrInvalidConfigurationCommand
|
||||||
|
}
|
||||||
|
id, err := ids.New()
|
||||||
|
if err != nil {
|
||||||
|
return configuration.SettingAssignment{}, err
|
||||||
|
}
|
||||||
|
return configuration.SettingAssignment{ID: configuration.SettingAssignmentID(id), PrincipalID: principalID, DefinitionRef: string(definition.ID), DefinitionRevision: definition.Revision, Revision: 1, Selector: configuration.ScopeSelector{PrincipalRef: principalID, SessionRef: sessionRef, TemporaryOverride: temporaryOverride}, TypedValue: strings.TrimSpace(typedValue), Source: configuration.SourceHumanDirective, SourceRef: sourceRef, Lifecycle: configuration.SettingActive, RecordedAtUnixMillis: now.UnixMilli()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExplainEffectiveSetting(definition configuration.SettingDefinition, effective configuration.EffectiveSetting) string {
|
||||||
|
if effective.WinningAssignmentRef == "" {
|
||||||
|
return fmt.Sprintf("Настройка %s сейчас имеет системное значение %q.", definition.StableKey, effective.Value)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Настройка %s сейчас имеет значение %q; применено scoped-правило %s.", definition.StableKey, effective.Value, effective.WinningAssignmentRef)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.cin.su/m/Assistant/contracts/configuration"
|
||||||
|
"git.cin.su/m/Assistant/modules/configuration/domain"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConversationCreatesScopedAssignmentWithoutBypassingAuthority(t *testing.T) {
|
||||||
|
d := configuration.SettingDefinition{ID: "setting:language", OwnerNamespace: "interaction", StableKey: "language", Revision: 2, ValueType: configuration.ValueString, Lifecycle: configuration.SettingActive, RecordedAtUnixMillis: 1}
|
||||||
|
a, err := domain.AssignmentFromConversation("SET", "principal-1", "setting:language", "ru", "session-1", "utterance-1", true, d, time.UnixMilli(2))
|
||||||
|
if err != nil || a.Selector.PrincipalRef != "principal-1" || !a.Selector.TemporaryOverride || a.Source != configuration.SourceHumanDirective {
|
||||||
|
t.Fatalf("assignment=%+v err=%v", a, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSecretConfigurationCannotReceiveConversationalValue(t *testing.T) {
|
||||||
|
d := configuration.SettingDefinition{ID: "setting:key", OwnerNamespace: "capability", StableKey: "key", Revision: 1, ValueType: configuration.ValueSecretRef, Lifecycle: configuration.SettingActive, RecordedAtUnixMillis: 1}
|
||||||
|
if _, err := domain.AssignmentFromConversation("SET", "p", string(d.ID), "secret", "", "u", false, d, time.UnixMilli(2)); err == nil {
|
||||||
|
t.Fatal("secret value accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||||
|
"git.cin.su/m/Assistant/platform/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidOutcomeAssessment = errors.New("invalid world outcome assessment")
|
||||||
|
|
||||||
|
// AssessWorldOutcome keeps Commit's execution fact separate from Epistemics'
|
||||||
|
// observation. No observation means UNKNOWN; a transport receipt alone never
|
||||||
|
// becomes success.
|
||||||
|
func AssessWorldOutcome(principalID, attemptRef, expectedEffect string, receiptRefs []string, observationRefs []epistemics.ObservationID, claimRefs []epistemics.ClaimID, evidenceRefs []epistemics.EvidenceRef, observedState epistemics.WorldOutcomeState, confidence, assessedAt int64) (epistemics.WorldOutcomeAssessment, error) {
|
||||||
|
if principalID == "" || attemptRef == "" || expectedEffect == "" || assessedAt < 1 || confidence < 0 || confidence > 1000 {
|
||||||
|
return epistemics.WorldOutcomeAssessment{}, ErrInvalidOutcomeAssessment
|
||||||
|
}
|
||||||
|
state := epistemics.OutcomeUnknownState
|
||||||
|
if len(observationRefs) == 0 {
|
||||||
|
state = epistemics.OutcomeUnknownState
|
||||||
|
} else if observedState == epistemics.OutcomeVerifiedSuccess || observedState == epistemics.OutcomeVerifiedPartial || observedState == epistemics.OutcomeVerifiedFailure || observedState == epistemics.OutcomeConflicted {
|
||||||
|
state = observedState
|
||||||
|
} else {
|
||||||
|
state = epistemics.OutcomeVerifiedPartial
|
||||||
|
}
|
||||||
|
id, err := ids.New()
|
||||||
|
if err != nil {
|
||||||
|
return epistemics.WorldOutcomeAssessment{}, err
|
||||||
|
}
|
||||||
|
value := epistemics.WorldOutcomeAssessment{ID: string(id), PrincipalID: principalID, ActionAttemptRef: attemptRef, ExecutionReceiptRefs: append([]string(nil), receiptRefs...), ExpectedEffectRef: expectedEffect, ObservationRefs: append([]epistemics.ObservationID(nil), observationRefs...), ClaimRefs: append([]epistemics.ClaimID(nil), claimRefs...), State: state, ConfidenceMillis: confidence, EvidenceRefs: append([]epistemics.EvidenceRef(nil), evidenceRefs...), AssessedAt: assessedAt}
|
||||||
|
if !value.Valid() {
|
||||||
|
return epistemics.WorldOutcomeAssessment{}, ErrInvalidOutcomeAssessment
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||||
|
"git.cin.su/m/Assistant/modules/epistemics/domain"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExecutionReceiptWithoutObservationRemainsUnknown(t *testing.T) {
|
||||||
|
value, err := domain.AssessWorldOutcome("principal", "attempt", "effect", []string{"receipt"}, nil, nil, nil, epistemics.OutcomeVerifiedSuccess, 1000, 1)
|
||||||
|
if err != nil || value.State != epistemics.OutcomeUnknownState {
|
||||||
|
t.Fatalf("value=%+v err=%v", value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObservedOutcomeCanBeVerifiedSeparately(t *testing.T) {
|
||||||
|
value, err := domain.AssessWorldOutcome("principal", "attempt", "effect", []string{"receipt"}, []epistemics.ObservationID{"observation"}, []epistemics.ClaimID{"claim"}, []epistemics.EvidenceRef{"evidence"}, epistemics.OutcomeVerifiedSuccess, 900, 1)
|
||||||
|
if err != nil || value.State != epistemics.OutcomeVerifiedSuccess || len(value.EvidenceRefs) != 1 {
|
||||||
|
t.Fatalf("value=%+v err=%v", value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "git.cin.su/m/Assistant/contracts/epistemics"
|
||||||
|
|
||||||
|
func AssessSourceReliability(sourceRef, domainRef string, freshness epistemics.FreshnessState, historyScore, priorScore int64, evidence []string, at int64) epistemics.SourceReliabilityAssessment {
|
||||||
|
score := (historyScore + priorScore) / 2
|
||||||
|
state := epistemics.ReliabilityCurrent
|
||||||
|
if freshness == epistemics.FreshnessUnknown {
|
||||||
|
state = epistemics.ReliabilityUnknown
|
||||||
|
} else if freshness == epistemics.FreshnessStale {
|
||||||
|
state = epistemics.ReliabilityAging
|
||||||
|
}
|
||||||
|
return epistemics.SourceReliabilityAssessment{SourceRef: sourceRef, DomainRef: domainRef, Freshness: freshness, HistoryScoreMillis: historyScore, ReliabilityScoreMillis: score, State: state, EvidenceRefs: append([]string(nil), evidence...), AssessedAtUnixMillis: at}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||||
|
"git.cin.su/m/Assistant/modules/epistemics/domain"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSourceReliabilityIncludesDomainFreshnessAndHistory(t *testing.T) {
|
||||||
|
a := domain.AssessSourceReliability("source", "weather", epistemics.FreshnessStale, 900, 700, []string{"history"}, 1)
|
||||||
|
if !a.Valid() || a.State != epistemics.ReliabilityAging || a.ReliabilityScoreMillis != 800 {
|
||||||
|
t.Fatalf("assessment=%+v", a)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"git.cin.su/m/Assistant/contracts/identity"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidRealm = errors.New("invalid principal data realm")
|
||||||
|
|
||||||
|
func ValidatePrincipalDataRealm(value identity.PrincipalDataRealm) error {
|
||||||
|
if !value.Valid() {
|
||||||
|
return ErrInvalidRealm
|
||||||
|
}
|
||||||
|
if value.Kind == identity.DataRealmPersonal && len(value.MemberPrincipalIDs) != 0 {
|
||||||
|
return ErrInvalidRealm
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CanReadRealm(value identity.PrincipalDataRealm, principalID identity.PrincipalID) bool {
|
||||||
|
if ValidatePrincipalDataRealm(value) != nil || principalID == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if value.Kind == identity.DataRealmPublic {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if principalID == value.OwnerPrincipalID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, member := range value.MemberPrincipalIDs {
|
||||||
|
if member == principalID {
|
||||||
|
return value.Kind == identity.DataRealmShared && value.Shared
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.cin.su/m/Assistant/contracts/identity"
|
||||||
|
"git.cin.su/m/Assistant/modules/identity/domain"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRealmSeparatesOwnerSharedAndPublicAccess(t *testing.T) {
|
||||||
|
r := identity.PrincipalDataRealm{ID: "realm", OwnerPrincipalID: "owner", Kind: identity.DataRealmShared, Shared: true, MemberPrincipalIDs: []identity.PrincipalID{"member"}, Revision: 1, RecordedAtUnixMillis: 1}
|
||||||
|
if !domain.CanReadRealm(r, "owner") || !domain.CanReadRealm(r, "member") || domain.CanReadRealm(r, "guest") {
|
||||||
|
t.Fatalf("realm access leaked: %+v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.cin.su/m/Assistant/contracts/interaction"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseConfigurationCommand recognises bounded human intent and never writes
|
||||||
|
// configuration or invokes Authority/Commit.
|
||||||
|
func ParseConfigurationCommand(message, principalID, sourceRef string) interaction.ConfigurationCommand {
|
||||||
|
text := strings.TrimSpace(strings.ToLower(message))
|
||||||
|
if principalID == "" || sourceRef == "" {
|
||||||
|
return interaction.ConfigurationCommand{}
|
||||||
|
}
|
||||||
|
if strings.Contains(text, "какая настройка") || strings.HasPrefix(text, "какое значение") {
|
||||||
|
return interaction.ConfigurationCommand{Kind: interaction.ConfigurationCommandQuery, PrincipalID: principalID, SourceRef: sourceRef}
|
||||||
|
}
|
||||||
|
temporary := strings.Contains(text, "только сейчас") || strings.Contains(text, "на этот раз") || strings.Contains(text, "временно")
|
||||||
|
if strings.Contains(text, "настрой") || strings.Contains(text, "установи") || strings.Contains(text, "сделай") {
|
||||||
|
parts := strings.SplitN(message, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
key := strings.TrimSpace(strings.TrimPrefix(strings.ToLower(parts[0]), "настройка"))
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
for _, marker := range []string{"только сейчас", "на этот раз", "временно"} {
|
||||||
|
value = strings.TrimSpace(strings.ReplaceAll(strings.ToLower(value), marker, ""))
|
||||||
|
}
|
||||||
|
return interaction.ConfigurationCommand{Kind: interaction.ConfigurationCommandSet, PrincipalID: principalID, DefinitionRef: key, TypedValue: value, TemporaryOverride: temporary, SourceRef: sourceRef}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return interaction.ConfigurationCommand{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.cin.su/m/Assistant/contracts/interaction"
|
||||||
|
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConversationParserProducesIntentOnly(t *testing.T) {
|
||||||
|
value := interactiondomain.ParseConfigurationCommand("Настройка language = ru только сейчас", "principal-1", "utterance-1")
|
||||||
|
if value.Kind != interaction.ConfigurationCommandSet || value.DefinitionRef != "language" || value.TypedValue != "ru" || !value.TemporaryOverride {
|
||||||
|
t.Fatalf("value=%+v", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,23 @@ type ContextProjection struct {
|
|||||||
// BuildGen2Context is a read-side projection over canonical utterance history.
|
// BuildGen2Context is a read-side projection over canonical utterance history.
|
||||||
// PersonalMemory remains a compatibility fallback and is never written here.
|
// PersonalMemory remains a compatibility fallback and is never written here.
|
||||||
func BuildGen2Context(history []interaction.HumanUtterance) ContextProjection {
|
func BuildGen2Context(history []interaction.HumanUtterance) ContextProjection {
|
||||||
|
return BuildScopedGen2Context(history, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildScopedGen2Context is the Gen2 retrieval boundary for household
|
||||||
|
// context. The caller supplies already-authorized history; this defensive
|
||||||
|
// filter prevents accidental cross-principal projection when a mixed source
|
||||||
|
// is passed during degraded operation.
|
||||||
|
func BuildScopedGen2Context(history []interaction.HumanUtterance, principalID string) ContextProjection {
|
||||||
|
if principalID != "" {
|
||||||
|
scoped := make([]interaction.HumanUtterance, 0, len(history))
|
||||||
|
for _, item := range history {
|
||||||
|
if item.PrincipalID == "" || string(item.PrincipalID) == principalID {
|
||||||
|
scoped = append(scoped, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
history = scoped
|
||||||
|
}
|
||||||
text := CompilePersonalMemory(history).ContextText()
|
text := CompilePersonalMemory(history).ContextText()
|
||||||
if text != "" {
|
if text != "" {
|
||||||
return ContextProjection{Text: text, Source: "retrieval:interaction-utterances"}
|
return ContextProjection{Text: text, Source: "retrieval:interaction-utterances"}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package domain_test
|
|||||||
import (
|
import (
|
||||||
"git.cin.su/m/Assistant/contracts/interaction"
|
"git.cin.su/m/Assistant/contracts/interaction"
|
||||||
"git.cin.su/m/Assistant/modules/interaction/domain"
|
"git.cin.su/m/Assistant/modules/interaction/domain"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,3 +17,13 @@ func TestGen2ContextUsesRetrievalProjectionAndKeepsV1Fallback(t *testing.T) {
|
|||||||
t.Fatalf("fallback missing: %#v", empty)
|
t.Fatalf("fallback missing: %#v", empty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGen2ContextDoesNotProjectAnotherPrincipal(t *testing.T) {
|
||||||
|
got := domain.BuildScopedGen2Context([]interaction.HumanUtterance{
|
||||||
|
{ID: "mine", PrincipalID: "principal-1", Content: "Я сейчас в Москве", ReceivedAtUnixMillis: 2},
|
||||||
|
{ID: "other", PrincipalID: "principal-2", Content: "Я сейчас в Казани", ReceivedAtUnixMillis: 3},
|
||||||
|
}, "principal-1")
|
||||||
|
if got.Text == "" || strings.Contains(got.Text, "Казани") || !strings.Contains(got.Text, "Москве") {
|
||||||
|
t.Fatalf("scoped context=%#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "git.cin.su/m/Assistant/contracts/interaction"
|
||||||
|
|
||||||
|
// AttributeSpeaker is an observational hint only. It deliberately cannot
|
||||||
|
// manufacture an authenticated principal binding from voice recognition.
|
||||||
|
func AttributeSpeaker(sourceRef, principalCandidate, observationRef string, confidence int64) interaction.SpeakerAttribution {
|
||||||
|
if sourceRef == "" || observationRef == "" || confidence < 0 || confidence > 1000 {
|
||||||
|
return interaction.SpeakerAttribution{}
|
||||||
|
}
|
||||||
|
return interaction.SpeakerAttribution{SourceRef: sourceRef, PrincipalCandidate: principalCandidate, ConfidenceMillis: confidence, ObservationRef: observationRef, Authenticated: false}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.cin.su/m/Assistant/modules/interaction/domain"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSpeakerRecognitionRemainsObservationNotAuthentication(t *testing.T) {
|
||||||
|
a := domain.AttributeSpeaker("mic-1", "principal-member", "observation-1", 700)
|
||||||
|
if a.Authenticated || a.PrincipalCandidate == "" || a.ObservationRef == "" {
|
||||||
|
t.Fatalf("attribution=%+v", a)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"git.cin.su/m/Assistant/contracts/work"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrGovernedEvolution = errors.New("governed evolution gate failed")
|
||||||
|
|
||||||
|
func RunGovernedEvolution(needRef string, ops work.GovernedEvolutionOps) (work.GovernedEvolutionResult, error) {
|
||||||
|
result := work.GovernedEvolutionResult{NeedRef: needRef, Stages: []work.EvolutionRunStage{work.EvolutionNeedRecorded}}
|
||||||
|
if needRef == "" || ops.CreateCandidate == nil || ops.Evaluate == nil || ops.SandboxBuild == nil || ops.Benchmark == nil || ops.Promote == nil || ops.Rollback == nil || ops.LearningLedgerOutcome == nil {
|
||||||
|
return result, ErrGovernedEvolution
|
||||||
|
}
|
||||||
|
candidate, err := ops.CreateCandidate()
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
if candidate.ID == "" || candidate.ProposalRef == "" {
|
||||||
|
return result, ErrGovernedEvolution
|
||||||
|
}
|
||||||
|
result.CandidateRef = candidate.ID
|
||||||
|
result.Stages = append(result.Stages, work.EvolutionCandidateBuilt)
|
||||||
|
evaluation, err := ops.Evaluate(candidate)
|
||||||
|
if err != nil || !evaluation.ValidFor(candidate) {
|
||||||
|
return result, ErrGovernedEvolution
|
||||||
|
}
|
||||||
|
result.EvaluationRef = evaluation.CandidateRef
|
||||||
|
result.Stages = append(result.Stages, work.EvolutionIndependentlyEvaluated)
|
||||||
|
sandboxEvidence, err := ops.SandboxBuild(candidate)
|
||||||
|
if err != nil || sandboxEvidence == "" {
|
||||||
|
return result, ErrGovernedEvolution
|
||||||
|
}
|
||||||
|
candidate.SandboxEvidenceRefs = append(candidate.SandboxEvidenceRefs, sandboxEvidence)
|
||||||
|
result.EvidenceRefs = append(result.EvidenceRefs, sandboxEvidence)
|
||||||
|
result.Stages = append(result.Stages, work.EvolutionSandboxed)
|
||||||
|
benchmarkEvidence, passed, err := ops.Benchmark(candidate)
|
||||||
|
if err != nil || !passed || benchmarkEvidence == "" {
|
||||||
|
result = rollbackEvolution(result, candidate, ops)
|
||||||
|
return result, ErrGovernedEvolution
|
||||||
|
}
|
||||||
|
candidate.BenchmarkEvidenceRefs = append(candidate.BenchmarkEvidenceRefs, benchmarkEvidence)
|
||||||
|
result.EvidenceRefs = append(result.EvidenceRefs, benchmarkEvidence)
|
||||||
|
result.Stages = append(result.Stages, work.EvolutionBenchmarked)
|
||||||
|
if ops.Canary != nil {
|
||||||
|
canaryEvidence, passed, canaryErr := ops.Canary(candidate)
|
||||||
|
if canaryErr != nil || !passed || canaryEvidence == "" {
|
||||||
|
result = rollbackEvolution(result, candidate, ops)
|
||||||
|
return result, ErrGovernedEvolution
|
||||||
|
}
|
||||||
|
result.EvidenceRefs = append(result.EvidenceRefs, canaryEvidence)
|
||||||
|
result.Stages = append(result.Stages, work.EvolutionCanaried)
|
||||||
|
}
|
||||||
|
promotionRef, err := ops.Promote(candidate, evaluation)
|
||||||
|
if err != nil || promotionRef == "" {
|
||||||
|
result = rollbackEvolution(result, candidate, ops)
|
||||||
|
return result, ErrGovernedEvolution
|
||||||
|
}
|
||||||
|
result.Promoted = true
|
||||||
|
result.Stages = append(result.Stages, work.EvolutionPromoted)
|
||||||
|
result.LearningLedgerOutcomeRef, err = ops.LearningLedgerOutcome(result)
|
||||||
|
if err != nil || result.LearningLedgerOutcomeRef == "" {
|
||||||
|
return result, ErrGovernedEvolution
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rollbackEvolution(result work.GovernedEvolutionResult, candidate work.EvolutionCandidate, ops work.GovernedEvolutionOps) work.GovernedEvolutionResult {
|
||||||
|
ref, err := ops.Rollback(candidate)
|
||||||
|
if err == nil && ref != "" {
|
||||||
|
result.RollbackRef = ref
|
||||||
|
result.Stages = append(result.Stages, work.EvolutionRolledBack)
|
||||||
|
result.LearningLedgerOutcomeRef, _ = ops.LearningLedgerOutcome(result)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.cin.su/m/Assistant/contracts/work"
|
||||||
|
"git.cin.su/m/Assistant/modules/work/domain"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGovernedEvolutionExecutesGatesAndLedgerOutcome(t *testing.T) {
|
||||||
|
calls := []string{}
|
||||||
|
result, err := domain.RunGovernedEvolution("need-1", work.GovernedEvolutionOps{
|
||||||
|
CreateCandidate: func() (work.EvolutionCandidate, error) {
|
||||||
|
calls = append(calls, "candidate")
|
||||||
|
return work.EvolutionCandidate{ID: "candidate-1", ProposalRef: "proposal-1", CreatorIdentity: "creator", RollbackRef: "rollback-plan"}, nil
|
||||||
|
},
|
||||||
|
Evaluate: func(c work.EvolutionCandidate) (work.EvolutionEvaluation, error) {
|
||||||
|
calls = append(calls, "evaluate")
|
||||||
|
return work.EvolutionEvaluation{CandidateRef: c.ID, EvaluatorIdentity: "independent", Approved: true, MustNotWorsenPassed: true, EvidenceRefs: []work.EvidenceRef{"eval"}, RecordedAtUnixMillis: 1}, nil
|
||||||
|
},
|
||||||
|
SandboxBuild: func(work.EvolutionCandidate) (work.EvidenceRef, error) {
|
||||||
|
calls = append(calls, "sandbox-build")
|
||||||
|
return "sandbox", nil
|
||||||
|
},
|
||||||
|
Benchmark: func(work.EvolutionCandidate) (work.EvidenceRef, bool, error) {
|
||||||
|
calls = append(calls, "benchmark")
|
||||||
|
return "benchmark", true, nil
|
||||||
|
},
|
||||||
|
Canary: func(work.EvolutionCandidate) (work.EvidenceRef, bool, error) {
|
||||||
|
calls = append(calls, "canary")
|
||||||
|
return "canary", true, nil
|
||||||
|
},
|
||||||
|
Promote: func(work.EvolutionCandidate, work.EvolutionEvaluation) (string, error) {
|
||||||
|
calls = append(calls, "promote")
|
||||||
|
return "promotion", nil
|
||||||
|
},
|
||||||
|
Rollback: func(work.EvolutionCandidate) (string, error) {
|
||||||
|
calls = append(calls, "rollback")
|
||||||
|
return "rollback", nil
|
||||||
|
},
|
||||||
|
LearningLedgerOutcome: func(work.GovernedEvolutionResult) (string, error) {
|
||||||
|
calls = append(calls, "ledger")
|
||||||
|
return "ledger-outcome", nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil || !result.Promoted || result.LearningLedgerOutcomeRef != "ledger-outcome" {
|
||||||
|
t.Fatalf("result=%+v err=%v", result, err)
|
||||||
|
}
|
||||||
|
want := []string{"candidate", "evaluate", "sandbox-build", "benchmark", "canary", "promote", "ledger"}
|
||||||
|
if len(calls) != len(want) {
|
||||||
|
t.Fatalf("calls=%v", calls)
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if calls[i] != want[i] {
|
||||||
|
t.Fatalf("calls=%v", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGovernedEvolutionRollsBackOnMustNotWorsenFailure(t *testing.T) {
|
||||||
|
rolledBack := false
|
||||||
|
result, err := domain.RunGovernedEvolution("need-1", work.GovernedEvolutionOps{
|
||||||
|
CreateCandidate: func() (work.EvolutionCandidate, error) {
|
||||||
|
return work.EvolutionCandidate{ID: "candidate-1", ProposalRef: "proposal-1", CreatorIdentity: "creator", RollbackRef: "rollback-plan"}, nil
|
||||||
|
},
|
||||||
|
Evaluate: func(c work.EvolutionCandidate) (work.EvolutionEvaluation, error) {
|
||||||
|
return work.EvolutionEvaluation{CandidateRef: c.ID, EvaluatorIdentity: "independent", Approved: true, MustNotWorsenPassed: true, EvidenceRefs: []work.EvidenceRef{"eval"}, RecordedAtUnixMillis: 1}, nil
|
||||||
|
},
|
||||||
|
SandboxBuild: func(work.EvolutionCandidate) (work.EvidenceRef, error) { return "sandbox", nil }, Benchmark: func(work.EvolutionCandidate) (work.EvidenceRef, bool, error) { return "benchmark", false, nil },
|
||||||
|
Promote: func(work.EvolutionCandidate, work.EvolutionEvaluation) (string, error) {
|
||||||
|
t.Fatal("promotion bypassed failed benchmark")
|
||||||
|
return "", nil
|
||||||
|
}, Rollback: func(work.EvolutionCandidate) (string, error) { rolledBack = true; return "rollback-evidence", nil }, LearningLedgerOutcome: func(r work.GovernedEvolutionResult) (string, error) { return "ledger-rollback", nil },
|
||||||
|
})
|
||||||
|
if err == nil || !rolledBack || result.RollbackRef != "rollback-evidence" || result.Promoted {
|
||||||
|
t.Fatalf("result=%+v err=%v", result, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ func AssembleSpecialists(p work.PerspectiveSpec, subtasks []string) work.TeamDec
|
|||||||
}
|
}
|
||||||
assignments := make([]work.SpecialistAssignment, 0, len(subtasks))
|
assignments := make([]work.SpecialistAssignment, 0, len(subtasks))
|
||||||
for _, s := range subtasks {
|
for _, s := range subtasks {
|
||||||
assignments = append(assignments, work.SpecialistAssignment{Perspective: p, SubtaskRef: s})
|
assignments = append(assignments, work.SpecialistAssignment{Perspective: p, SubtaskRef: s, FocusRef: p.FocusRef, SkillRefs: append([]string(nil), p.SkillRefs...), ToolRefs: append([]string(nil), p.ToolRefs...)})
|
||||||
}
|
}
|
||||||
return work.TeamDecision{UseTeam: true, Reason: "independent subtasks justify bounded perspectives", Assignments: assignments}
|
return work.TeamDecision{UseTeam: true, Reason: "independent subtasks justify bounded perspectives", Assignments: assignments}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestPerspectiveTeamIsBoundedByIndependentSubtasks(t *testing.T) {
|
func TestPerspectiveTeamIsBoundedByIndependentSubtasks(t *testing.T) {
|
||||||
p := work.PerspectiveSpec{ID: "architect", ObjectiveLens: "architecture", EvaluationCriteria: []string{"correctness"}, ExpectedOutputShape: "finding"}
|
p := work.PerspectiveSpec{ID: "architect", ObjectiveLens: "architecture", FocusRef: "focus-1", SkillRefs: []string{"skill-1"}, ToolRefs: []string{"tool-1"}, EvaluationCriteria: []string{"correctness"}, ExpectedOutputShape: "finding"}
|
||||||
if d := domain.AssembleSpecialists(p, []string{"a"}); d.UseTeam {
|
if d := domain.AssembleSpecialists(p, []string{"a"}); d.UseTeam {
|
||||||
t.Fatal("team for one subtask")
|
t.Fatal("team for one subtask")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package supervisor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DurableStore is supervisor-owned external control-plane state. It is not a
|
||||||
|
// candidate Assistant process-local recovery cache.
|
||||||
|
type DurableStore struct{ path string }
|
||||||
|
|
||||||
|
func NewDurableStore(path string) (*DurableStore, error) {
|
||||||
|
if path == "" || !filepath.IsAbs(path) {
|
||||||
|
return nil, errors.New("supervisor state path must be absolute")
|
||||||
|
}
|
||||||
|
return &DurableStore{path: path}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DurableStore) Save(value Lifeboat) error {
|
||||||
|
if s == nil || s.path == "" {
|
||||||
|
return errors.New("supervisor state store is unavailable")
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(s.path), 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
temporary, err := os.CreateTemp(filepath.Dir(s.path), ".lifeboat-*.tmp")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
temporaryName := temporary.Name()
|
||||||
|
defer os.Remove(temporaryName)
|
||||||
|
if err = temporary.Chmod(0600); err == nil {
|
||||||
|
_, err = temporary.Write(payload)
|
||||||
|
}
|
||||||
|
if closeErr := temporary.Close(); err == nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(temporaryName, s.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DurableStore) Load() (Lifeboat, error) {
|
||||||
|
if s == nil || s.path == "" {
|
||||||
|
return Lifeboat{}, errors.New("supervisor state store is unavailable")
|
||||||
|
}
|
||||||
|
payload, err := os.ReadFile(s.path)
|
||||||
|
if err != nil {
|
||||||
|
return Lifeboat{}, err
|
||||||
|
}
|
||||||
|
var value Lifeboat
|
||||||
|
if err := json.Unmarshal(payload, &value); err != nil {
|
||||||
|
return Lifeboat{}, err
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PersistentLifeboat reloads before every transition and commits after it.
|
||||||
|
type PersistentLifeboat struct{ store *DurableStore }
|
||||||
|
|
||||||
|
func NewPersistentLifeboat(store *DurableStore) *PersistentLifeboat {
|
||||||
|
return &PersistentLifeboat{store: store}
|
||||||
|
}
|
||||||
|
func (p *PersistentLifeboat) transition(fn func(*Lifeboat) bool) (bool, error) {
|
||||||
|
value, err := p.store.Load()
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
ok := fn(&value)
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err := p.store.Save(value); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
func (p *PersistentLifeboat) Start(candidate Release) (bool, error) {
|
||||||
|
return p.transition(func(value *Lifeboat) bool { return value.Start(candidate) })
|
||||||
|
}
|
||||||
|
func (p *PersistentLifeboat) Record(observation Observation) (bool, error) {
|
||||||
|
return p.transition(func(value *Lifeboat) bool { return value.Record(observation) })
|
||||||
|
}
|
||||||
|
func (p *PersistentLifeboat) Rollback() (bool, error) {
|
||||||
|
return p.transition(func(value *Lifeboat) bool { return value.Rollback() })
|
||||||
|
}
|
||||||
|
func (p *PersistentLifeboat) Snapshot() (Lifeboat, error) { return p.store.Load() }
|
||||||
@@ -2,6 +2,7 @@ package supervisor_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.cin.su/m/Assistant/runtime/supervisor"
|
"git.cin.su/m/Assistant/runtime/supervisor"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,3 +16,34 @@ func TestLifeboatRollsBackCandidateToLastKnownGood(t *testing.T) {
|
|||||||
t.Fatalf("%#v", l)
|
t.Fatalf("%#v", l)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPersistentLifeboatRestoresLastKnownGoodAfterCandidateFailure(t *testing.T) {
|
||||||
|
store, err := supervisor.NewDurableStore(filepath.Join(t.TempDir(), "control", "lifeboat.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
first := supervisor.NewPersistentLifeboat(store)
|
||||||
|
if ok, err := first.Start(supervisor.Release{ID: "good", Version: "1"}); err != nil || !ok {
|
||||||
|
t.Fatalf("start good: %v %v", ok, err)
|
||||||
|
}
|
||||||
|
if ok, err := first.Record(supervisor.Observation{ReleaseID: "good", Ready: true, ObservedAtUnixMillis: 1}); err != nil || !ok {
|
||||||
|
t.Fatalf("record good: %v %v", ok, err)
|
||||||
|
}
|
||||||
|
if ok, err := first.Start(supervisor.Release{ID: "candidate", Version: "2"}); err != nil || !ok {
|
||||||
|
t.Fatalf("start candidate: %v %v", ok, err)
|
||||||
|
}
|
||||||
|
if ok, err := first.Record(supervisor.Observation{ReleaseID: "candidate", Ready: false, Reason: "candidate crashed", ObservedAtUnixMillis: 2}); err != nil || !ok {
|
||||||
|
t.Fatalf("record failure: %v %v", ok, err)
|
||||||
|
}
|
||||||
|
second := supervisor.NewPersistentLifeboat(store)
|
||||||
|
if ok, err := second.Rollback(); err != nil || !ok {
|
||||||
|
t.Fatalf("rollback after restart: %v %v", ok, err)
|
||||||
|
}
|
||||||
|
state, err := second.Snapshot()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if state.Current.ID != "good" || state.Current.State != supervisor.ReleaseRolledBack || state.LastKnownGood.ID != "good" {
|
||||||
|
t.Fatalf("state=%+v", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user