Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a563a29ee | ||
|
|
734db5d779 | ||
|
|
36fc8ec8c6 | ||
|
|
b8c001be4c | ||
|
|
eeeba6b7c0 | ||
|
|
fce3ec569c | ||
|
|
a925d787dc | ||
|
|
43556e1603 | ||
|
|
dd00dabf32 | ||
|
|
33365ce7ee | ||
|
|
5017a854ee | ||
|
|
839b35f310 | ||
|
|
96687140ce | ||
|
|
b282038949 | ||
|
|
4de506a2af | ||
|
|
36272b9036 | ||
|
|
a779568622 | ||
|
|
243a11490b | ||
|
|
26207d93ef | ||
|
|
42871f2e47 | ||
|
|
6ea1d4b729 | ||
|
|
c6d1ebf36f |
@@ -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")
|
||||
)
|
||||
|
||||
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) {
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
|
||||
@@ -533,7 +533,7 @@ func main() {
|
||||
log.Printf("interaction principal history loaded: %d utterances", len(recentUserHistory))
|
||||
}
|
||||
conversationHistory = append(conversationHistory, recentUserHistory...)
|
||||
gen2Context := interactiondomain.BuildGen2Context(conversationHistory)
|
||||
gen2Context := interactiondomain.BuildScopedGen2Context(conversationHistory, string(session.PrincipalID))
|
||||
if !creativeUserRequest && locationReply == "" {
|
||||
if continuation := interactiondomain.ContinueLocalSearch(effectiveTaskMessage, conversationHistory); continuation != "" {
|
||||
continuationMessage = continuation
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/runtime/supervisor"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
lifeboat *supervisor.PersistentLifeboat
|
||||
stableUpstream string
|
||||
candidate string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func main() {
|
||||
statePath := envOr("SUPERVISOR_STATE_PATH", "/var/lib/supervisor/release-state.json")
|
||||
stable := strings.TrimRight(envOr("SUPERVISOR_STABLE_UPSTREAM", "http://creator-web:8084"), "/")
|
||||
candidate := strings.TrimRight(envOr("SUPERVISOR_CANDIDATE_UPSTREAM", "http://creator-web-candidate:8084"), "/")
|
||||
store, err := supervisor.NewDurableStore(statePath)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
lifeboat := supervisor.NewPersistentLifeboat(store)
|
||||
if _, err := lifeboat.Snapshot(); errors.Is(err, os.ErrNotExist) {
|
||||
baseline := supervisor.Release{ID: envOr("SUPERVISOR_BASELINE_RELEASE", "bootstrap"), Version: envOr("SUPERVISOR_BASELINE_RELEASE", "bootstrap")}
|
||||
if ok, startErr := lifeboat.Start(baseline); startErr != nil || !ok {
|
||||
fatal(fmt.Errorf("initialize baseline: %v", startErr))
|
||||
}
|
||||
if ok, observeErr := lifeboat.Record(supervisor.Observation{ReleaseID: baseline.ID, Ready: true, Reason: "supervisor baseline"}); observeErr != nil || !ok {
|
||||
fatal(fmt.Errorf("initialize baseline observation: %v", observeErr))
|
||||
}
|
||||
if ok, trafficErr := lifeboat.SetTraffic(supervisor.TrafficStable, stable, candidate); trafficErr != nil || !ok {
|
||||
fatal(fmt.Errorf("initialize traffic: %v", trafficErr))
|
||||
}
|
||||
} else if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
s := &server{lifeboat: lifeboat, stableUpstream: stable, candidate: candidate, client: &http.Client{Timeout: 5 * time.Second}}
|
||||
public := http.NewServeMux()
|
||||
public.HandleFunc("GET /healthz", s.health)
|
||||
public.HandleFunc("GET /readyz", s.ready)
|
||||
public.HandleFunc("/", s.proxy)
|
||||
control := http.NewServeMux()
|
||||
control.HandleFunc("GET /v1/state", s.state)
|
||||
control.HandleFunc("POST /v1/releases/start", s.start)
|
||||
control.HandleFunc("POST /v1/releases/observe", s.observe)
|
||||
control.HandleFunc("POST /v1/releases/promote", s.promote)
|
||||
control.HandleFunc("POST /v1/releases/rollback", s.rollback)
|
||||
control.HandleFunc("POST /v1/traffic/canary", s.canary)
|
||||
control.HandleFunc("POST /v1/traffic/promote", s.promoteTraffic)
|
||||
control.HandleFunc("POST /v1/traffic/rollback", s.rollbackTraffic)
|
||||
|
||||
go func() {
|
||||
if err := http.ListenAndServe(envOr("SUPERVISOR_CONTROL_ADDR", ":8095"), control); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
}()
|
||||
if err := http.ListenAndServe(envOr("LISTEN_ADDR", ":8080"), public); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) health(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, map[string]string{"runtime": "supervisor", "status": "ok"})
|
||||
}
|
||||
|
||||
func (s *server) ready(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := s.lifeboat.Snapshot()
|
||||
if err != nil || state.StableUpstream == "" {
|
||||
http.Error(w, "supervisor state unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, state.StableUpstream+"/healthz", nil)
|
||||
if err != nil {
|
||||
http.Error(w, "stable upstream unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
response, err := s.client.Do(request)
|
||||
if err != nil || response.StatusCode/100 != 2 {
|
||||
if response != nil {
|
||||
response.Body.Close()
|
||||
}
|
||||
http.Error(w, "stable upstream unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
response.Body.Close()
|
||||
writeJSON(w, map[string]string{"runtime": "supervisor", "status": "ready"})
|
||||
}
|
||||
|
||||
func (s *server) proxy(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := s.lifeboat.Snapshot()
|
||||
if err != nil {
|
||||
http.Error(w, "supervisor state unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
target := state.StableUpstream
|
||||
selected := "stable"
|
||||
if state.TrafficMode == supervisor.TrafficCandidate || (state.TrafficMode == supervisor.TrafficCanaryHeader && strings.EqualFold(r.Header.Get("X-Assistant-Canary"), "true")) {
|
||||
target = state.CandidateUpstream
|
||||
selected = "candidate"
|
||||
}
|
||||
parsed, err := url.Parse(target)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
http.Error(w, "invalid upstream", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
proxy := httputil.NewSingleHostReverseProxy(parsed)
|
||||
proxy.ModifyResponse = func(response *http.Response) error {
|
||||
response.Header.Set("X-Assistant-Traffic-Target", selected)
|
||||
return nil
|
||||
}
|
||||
proxy.ErrorHandler = func(writer http.ResponseWriter, _ *http.Request, _ error) {
|
||||
http.Error(writer, "upstream unavailable", http.StatusBadGateway)
|
||||
}
|
||||
proxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *server) state(w http.ResponseWriter, _ *http.Request) {
|
||||
state, err := s.lifeboat.Snapshot()
|
||||
if err != nil {
|
||||
http.Error(w, "state unavailable", 503)
|
||||
return
|
||||
}
|
||||
writeJSON(w, state)
|
||||
}
|
||||
func (s *server) start(w http.ResponseWriter, r *http.Request) {
|
||||
var release supervisor.Release
|
||||
if !decode(w, r, &release) {
|
||||
return
|
||||
}
|
||||
ok, err := s.lifeboat.Start(release)
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) observe(w http.ResponseWriter, r *http.Request) {
|
||||
var observation supervisor.Observation
|
||||
if !decode(w, r, &observation) {
|
||||
return
|
||||
}
|
||||
ok, err := s.lifeboat.Record(observation)
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) promote(w http.ResponseWriter, _ *http.Request) {
|
||||
ok, err := s.lifeboat.Promote()
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) rollback(w http.ResponseWriter, _ *http.Request) {
|
||||
ok, err := s.lifeboat.Rollback()
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) canary(w http.ResponseWriter, _ *http.Request) {
|
||||
ok, err := s.lifeboat.SetTraffic(supervisor.TrafficCanaryHeader, s.stableUpstream, s.candidate)
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) promoteTraffic(w http.ResponseWriter, _ *http.Request) {
|
||||
ok, err := s.lifeboat.Promote()
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) rollbackTraffic(w http.ResponseWriter, _ *http.Request) {
|
||||
ok, err := s.lifeboat.RollbackTraffic()
|
||||
respond(w, ok, err)
|
||||
}
|
||||
|
||||
func decode(w http.ResponseWriter, r *http.Request, value any) bool {
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(value); err != nil {
|
||||
http.Error(w, "invalid control request", 400)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
func respond(w http.ResponseWriter, ok bool, err error) {
|
||||
if err != nil {
|
||||
http.Error(w, "control state unavailable", 503)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
http.Error(w, "control transition rejected", 409)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]bool{"ok": true})
|
||||
}
|
||||
func writeJSON(w http.ResponseWriter, value any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
func envOr(name, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
func fatal(err error) { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
|
||||
@@ -5,6 +5,18 @@ import "git.cin.su/m/Assistant/platform/ids"
|
||||
import "errors"
|
||||
|
||||
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 {
|
||||
ArtifactID ArtifactID
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package capability
|
||||
|
||||
type IntelligencePath string
|
||||
|
||||
const (
|
||||
PathRule IntelligencePath = "RULE"
|
||||
PathReflex IntelligencePath = "REFLEX"
|
||||
PathSkill IntelligencePath = "SKILL"
|
||||
PathSmallLocal IntelligencePath = "SMALL_LOCAL"
|
||||
PathStrongLocal IntelligencePath = "STRONG_LOCAL"
|
||||
PathExternal IntelligencePath = "EXTERNAL"
|
||||
PathResearch IntelligencePath = "RESEARCH"
|
||||
PathTeam IntelligencePath = "TEAM"
|
||||
)
|
||||
|
||||
type StrategyRequest struct {
|
||||
RequiredQuality int64
|
||||
PreferLowLatency bool
|
||||
CostBudgetMicros int64
|
||||
RiskClass string
|
||||
FreshnessRequired bool
|
||||
CostOfError int64
|
||||
}
|
||||
type StrategyDecision struct {
|
||||
Path IntelligencePath
|
||||
Reason string
|
||||
Escalate bool
|
||||
}
|
||||
|
||||
func (r StrategyRequest) Select() StrategyDecision {
|
||||
if r.RiskClass == "HIGH" || r.CostOfError >= 800 {
|
||||
return StrategyDecision{Path: PathStrongLocal, Reason: "high consequence requires deeper reasoning", Escalate: true}
|
||||
}
|
||||
if r.RequiredQuality <= 500 && r.CostBudgetMicros > 0 {
|
||||
return StrategyDecision{Path: PathRule, Reason: "bounded low-risk path"}
|
||||
}
|
||||
if r.PreferLowLatency {
|
||||
return StrategyDecision{Path: PathReflex, Reason: "latency preference with later escalation"}
|
||||
}
|
||||
return StrategyDecision{Path: PathSkill, Reason: "validated reusable strategy"}
|
||||
}
|
||||
@@ -15,11 +15,15 @@ const (
|
||||
)
|
||||
|
||||
type MultimodalResponse struct {
|
||||
Operation MultimodalOperation `json:"operation"`
|
||||
Text string `json:"text"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
ObservedAt int64 `json:"observed_at_unix_millis"`
|
||||
ConfidenceNote string `json:"confidence_note,omitempty"`
|
||||
ContentType string `json:"content_type,omitempty"`
|
||||
DataBase64 string `json:"data_base64,omitempty"`
|
||||
Operation MultimodalOperation `json:"operation"`
|
||||
Text string `json:"text"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
ObservedAt int64 `json:"observed_at_unix_millis"`
|
||||
ConfidenceNote string `json:"confidence_note,omitempty"`
|
||||
ContentType string `json:"content_type,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"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package capability
|
||||
|
||||
type ReflexID string
|
||||
type ReflexLifecycle string
|
||||
type ReflexKind string
|
||||
|
||||
const (
|
||||
ReflexCandidate ReflexLifecycle = "CANDIDATE"
|
||||
ReflexValidating ReflexLifecycle = "VALIDATING"
|
||||
ReflexActive ReflexLifecycle = "ACTIVE"
|
||||
ReflexDegraded ReflexLifecycle = "DEGRADED"
|
||||
ReflexStale ReflexLifecycle = "STALE"
|
||||
ReflexSuspended ReflexLifecycle = "SUSPENDED"
|
||||
ReflexRetired ReflexLifecycle = "RETIRED"
|
||||
ReflexDeterministicCode ReflexKind = "DETERMINISTIC_CODE"
|
||||
ReflexRule ReflexKind = "RULE"
|
||||
ReflexWorkflow ReflexKind = "WORKFLOW"
|
||||
ReflexQueryTemplate ReflexKind = "QUERY_TEMPLATE"
|
||||
ReflexSmallModelChain ReflexKind = "SMALL_MODEL_CHAIN"
|
||||
ReflexBoundedAdapter ReflexKind = "BOUNDED_ADAPTER_OPERATION"
|
||||
)
|
||||
|
||||
type ShadowPolicy struct {
|
||||
Enabled bool
|
||||
SampleRateMillis int64
|
||||
ComparatorRef string
|
||||
MaxShadowCostMicros *int64
|
||||
EligibleErrorCostClasses []string
|
||||
}
|
||||
type RevalidationPolicy struct {
|
||||
MaxAgeMillis *int64
|
||||
RevalidateOnDependencyRevisionChange bool
|
||||
RevalidateOnWorldVersionChange bool
|
||||
RevalidateAfterFailureCount *int64
|
||||
RevalidateAfterCorrectionCount *int64
|
||||
SampleAuditRateMillis *int64
|
||||
}
|
||||
type Reflex struct {
|
||||
ID ReflexID
|
||||
PrincipalID string
|
||||
SourceSkillRef SkillID
|
||||
CreatedAt int64
|
||||
}
|
||||
type ReflexRevision struct {
|
||||
ReflexID ReflexID
|
||||
Revision int64
|
||||
SourceSkillRevision int64
|
||||
Kind ReflexKind
|
||||
Applicability ApplicabilitySpec
|
||||
ImplementationRef string
|
||||
ImplementationVersion string
|
||||
ArtifactDigest string
|
||||
DependencyRefs []string
|
||||
ValidationEvidenceRefs []SkillEvidenceRef
|
||||
KnownFailureExperienceRefs []SkillEvidenceRef
|
||||
Shadow ShadowPolicy
|
||||
Revalidation RevalidationPolicy
|
||||
LastValidatedAt int64
|
||||
Lifecycle ReflexLifecycle
|
||||
RecordedAt int64
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package capability
|
||||
|
||||
type SkillID string
|
||||
type SkillRevision struct {
|
||||
SkillID SkillID
|
||||
Revision int64
|
||||
Description string
|
||||
Applicability ApplicabilitySpec
|
||||
ImplementationRef string
|
||||
ImplementationVersion string
|
||||
DependencyRefs []string
|
||||
SupportingExperienceRefs []SkillEvidenceRef
|
||||
KnownFailureExperienceRefs []SkillEvidenceRef
|
||||
ValidationEvidenceRefs []SkillEvidenceRef
|
||||
QualityAssessmentRefs []string
|
||||
EnvironmentVersionConstraints []string
|
||||
LastValidatedAt int64
|
||||
Lifecycle SkillLifecycleStatus
|
||||
SupersedesRevision int64
|
||||
RecordedAt int64
|
||||
}
|
||||
type ApplicabilitySpec struct {
|
||||
RequiredContextBindings []string
|
||||
PreferredContextBindings []string
|
||||
ExcludedContextBindings []string
|
||||
RequiredWorldKinds []string
|
||||
VersionConstraints []string
|
||||
InputConditions []string
|
||||
MinimumEvidenceFreshness string
|
||||
}
|
||||
type Skill struct {
|
||||
ID SkillID
|
||||
PrincipalID string
|
||||
Name string
|
||||
CreatedAt int64
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package commit
|
||||
|
||||
type ExecutorResult string
|
||||
type AmbiguityState string
|
||||
|
||||
const (
|
||||
ExecutorAccepted ExecutorResult = "ACCEPTED"
|
||||
ExecutorReportedSuccess ExecutorResult = "REPORTED_SUCCESS"
|
||||
ExecutorReportedFailure ExecutorResult = "REPORTED_FAILURE"
|
||||
ExecutorNoResponse ExecutorResult = "NO_RESPONSE"
|
||||
ExecutorAmbiguous ExecutorResult = "AMBIGUOUS"
|
||||
)
|
||||
|
||||
type ExecutionReceipt struct {
|
||||
ID string
|
||||
ActionAttemptRef ActionAttemptID
|
||||
ExecutorImplementationRef string
|
||||
ExternalOperationRef string
|
||||
ExternalRequestID string
|
||||
RequestDigest string
|
||||
ResponseDigest string
|
||||
DispatchAtUnixMillis int64
|
||||
CompletedAtUnixMillis *int64
|
||||
ExecutorResult ExecutorResult
|
||||
Ambiguity AmbiguityState
|
||||
ReconciliationKey string
|
||||
RawArtifactRef string
|
||||
}
|
||||
|
||||
func (r ExecutionReceipt) Valid() bool {
|
||||
return r.ID != "" && r.ActionAttemptRef != "" && r.ExecutorImplementationRef != "" && r.RequestDigest != "" && r.ExecutorResult != "" && r.ReconciliationKey != "" && r.DispatchAtUnixMillis > 0
|
||||
}
|
||||
@@ -81,6 +81,9 @@ type BootstrapConfig struct {
|
||||
RecoveryLocator string
|
||||
}
|
||||
|
||||
// ConversationalConfigurationCommand is an intent, not an assignment. Only
|
||||
// Configuration may turn a SET command into durable state.
|
||||
|
||||
func (d SettingDefinition) Validate() error {
|
||||
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")
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package epistemics
|
||||
|
||||
type WorldOutcomeState string
|
||||
|
||||
const (
|
||||
OutcomeVerifiedSuccess WorldOutcomeState = "VERIFIED_SUCCESS"
|
||||
OutcomeVerifiedPartial WorldOutcomeState = "VERIFIED_PARTIAL"
|
||||
OutcomeVerifiedFailure WorldOutcomeState = "VERIFIED_FAILURE"
|
||||
OutcomeUnverified WorldOutcomeState = "UNVERIFIED"
|
||||
OutcomeUnknownState WorldOutcomeState = "UNKNOWN"
|
||||
OutcomeConflicted WorldOutcomeState = "CONFLICTED"
|
||||
)
|
||||
|
||||
type WorldOutcomeAssessment struct {
|
||||
ID string
|
||||
PrincipalID string
|
||||
ActionAttemptRef string
|
||||
ExecutionReceiptRefs []string
|
||||
ExpectedEffectRef string
|
||||
ObservationRefs []ObservationID
|
||||
ClaimRefs []ClaimID
|
||||
State WorldOutcomeState
|
||||
ConfidenceMillis int64
|
||||
EvidenceRefs []EvidenceRef
|
||||
AssessedAt int64
|
||||
}
|
||||
|
||||
func (a WorldOutcomeAssessment) Valid() bool {
|
||||
return a.ID != "" && a.PrincipalID != "" && a.ActionAttemptRef != "" && a.State != "" && a.ConfidenceMillis >= 0 && a.ConfidenceMillis <= 1000 && a.AssessedAt > 0
|
||||
}
|
||||
@@ -17,23 +17,27 @@ type CoverageState string
|
||||
type ConflictState string
|
||||
type EvidenceKind string
|
||||
type ResearchFindingState string
|
||||
type ReliabilityState string
|
||||
|
||||
const (
|
||||
FreshnessCurrent FreshnessState = "CURRENT"
|
||||
FreshnessStale FreshnessState = "STALE"
|
||||
FreshnessUnknown FreshnessState = "UNKNOWN"
|
||||
CoverageComplete CoverageState = "COMPLETE"
|
||||
CoveragePartial CoverageState = "PARTIAL"
|
||||
CoverageUnknown CoverageState = "UNKNOWN"
|
||||
ConflictNone ConflictState = "NONE"
|
||||
ConflictPresent ConflictState = "PRESENT"
|
||||
ConflictUnknown ConflictState = "UNKNOWN"
|
||||
EvidenceObserved EvidenceKind = "OBSERVED"
|
||||
EvidenceAssumed EvidenceKind = "ASSUMED"
|
||||
FindingSupported ResearchFindingState = "SUPPORTED"
|
||||
FindingStale ResearchFindingState = "STALE"
|
||||
FindingConflicted ResearchFindingState = "CONFLICTED"
|
||||
FindingUnknown ResearchFindingState = "UNKNOWN"
|
||||
FreshnessCurrent FreshnessState = "CURRENT"
|
||||
FreshnessStale FreshnessState = "STALE"
|
||||
FreshnessUnknown FreshnessState = "UNKNOWN"
|
||||
CoverageComplete CoverageState = "COMPLETE"
|
||||
CoveragePartial CoverageState = "PARTIAL"
|
||||
CoverageUnknown CoverageState = "UNKNOWN"
|
||||
ConflictNone ConflictState = "NONE"
|
||||
ConflictPresent ConflictState = "PRESENT"
|
||||
ConflictUnknown ConflictState = "UNKNOWN"
|
||||
EvidenceObserved EvidenceKind = "OBSERVED"
|
||||
EvidenceAssumed EvidenceKind = "ASSUMED"
|
||||
FindingSupported ResearchFindingState = "SUPPORTED"
|
||||
FindingStale ResearchFindingState = "STALE"
|
||||
FindingConflicted ResearchFindingState = "CONFLICTED"
|
||||
FindingUnknown ResearchFindingState = "UNKNOWN"
|
||||
ReliabilityCurrent ReliabilityState = "CURRENT"
|
||||
ReliabilityAging ReliabilityState = "AGING"
|
||||
ReliabilityUnknown ReliabilityState = "UNKNOWN"
|
||||
)
|
||||
|
||||
type ResearchPlan struct {
|
||||
@@ -52,6 +56,21 @@ type Source struct {
|
||||
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 {
|
||||
ID SourceSnapshotID
|
||||
SourceID SourceID
|
||||
|
||||
@@ -6,6 +6,30 @@ type ActorID ids.ID
|
||||
type PrincipalID 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
|
||||
// whose behalf it acts. A Creator is not exempt from this distinction.
|
||||
type PrincipalBinding struct {
|
||||
|
||||
@@ -25,6 +25,44 @@ type SemanticApplicability string
|
||||
type SemanticCandidateSource string
|
||||
type SemanticCandidateSelectionState 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 (
|
||||
ChannelWebChat Channel = "WEB_CHAT"
|
||||
@@ -75,6 +113,8 @@ const (
|
||||
InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY"
|
||||
InformationRequestTimeDifference InformationRequestKind = "TIME_DIFFERENCE"
|
||||
InformationRequestResearchFailureExplanation InformationRequestKind = "RESEARCH_FAILURE_EXPLANATION"
|
||||
ConfigurationCommandSet ConfigurationCommandKind = "SET"
|
||||
ConfigurationCommandQuery ConfigurationCommandKind = "QUERY"
|
||||
)
|
||||
|
||||
type Conversation struct {
|
||||
@@ -106,6 +146,18 @@ type OriginalHumanIntent struct {
|
||||
RecordedAtUnixMillis int64
|
||||
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 {
|
||||
ID InteractionRelationID
|
||||
FromUtteranceID HumanUtteranceID
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package self
|
||||
|
||||
type BenchmarkCaseRef string
|
||||
type PersonalBenchmark struct {
|
||||
ID string
|
||||
PrincipalID string
|
||||
ApprovedCaseRefs []BenchmarkCaseRef
|
||||
GoalProfile []string
|
||||
PreferenceEvidenceRefs []SemanticReference
|
||||
RecordedAt int64
|
||||
}
|
||||
type BenchmarkEvaluation struct {
|
||||
CaseRef BenchmarkCaseRef
|
||||
Path string
|
||||
QualityMillis int64
|
||||
ReliabilityMillis int64
|
||||
LatencyMillis int64
|
||||
CostMicros int64
|
||||
CorrectionCount int64
|
||||
EvidenceRefs []SemanticReference
|
||||
}
|
||||
|
||||
func (e BenchmarkEvaluation) Valid() bool {
|
||||
return e.CaseRef != "" && len(e.EvidenceRefs) > 0 && e.QualityMillis >= 0 && e.ReliabilityMillis >= 0 && e.CorrectionCount >= 0
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package self
|
||||
|
||||
import "fmt"
|
||||
|
||||
type AssessmentID string
|
||||
type LearningLedgerEntryID string
|
||||
type AssessmentState string
|
||||
|
||||
const (
|
||||
AssessmentCandidate AssessmentState = "CANDIDATE"
|
||||
AssessmentConfirmed AssessmentState = "CONFIRMED"
|
||||
AssessmentMonitoring AssessmentState = "MONITORING"
|
||||
AssessmentResolved AssessmentState = "RESOLVED"
|
||||
)
|
||||
|
||||
type CompetenceAssessment struct {
|
||||
ID AssessmentID
|
||||
PrincipalID string
|
||||
SubjectRef SemanticReference
|
||||
Revision uint64
|
||||
ContextMatcher []string
|
||||
QualityMillis int64
|
||||
ReliabilityMillis int64
|
||||
ConfidenceMillis int64
|
||||
EvidenceRefs []SemanticReference
|
||||
LastValidatedAt int64
|
||||
AssessedAt int64
|
||||
SupersedesRef *SemanticReference
|
||||
}
|
||||
type WeaknessPattern struct {
|
||||
ID AssessmentID
|
||||
PrincipalID string
|
||||
Revision uint64
|
||||
PatternKey string
|
||||
ContextMatcher []string
|
||||
FailureMode string
|
||||
DetectionEvidenceRefs []SemanticReference
|
||||
State AssessmentState
|
||||
RecordedAt int64
|
||||
}
|
||||
type DependencyAssessment struct {
|
||||
ID AssessmentID
|
||||
SubjectRef SemanticReference
|
||||
DependencyRef SemanticReference
|
||||
ImpactClass string
|
||||
CurrentEffect string
|
||||
EvidenceRefs []SemanticReference
|
||||
AssessedAt int64
|
||||
}
|
||||
type ImprovementNeed struct {
|
||||
ID AssessmentID
|
||||
PrincipalID string
|
||||
SubjectRef SemanticReference
|
||||
ProblemSummary string
|
||||
EvidenceRefs []SemanticReference
|
||||
ExpectedBenefit string
|
||||
Priority int
|
||||
State string
|
||||
CreatedAt int64
|
||||
}
|
||||
type LearningLedgerEntry struct {
|
||||
ID LearningLedgerEntryID
|
||||
PrincipalID string
|
||||
ImprovementNeedRef *SemanticReference
|
||||
ObservedProblem string
|
||||
EvidenceRefs []SemanticReference
|
||||
Diagnosis string
|
||||
Hypothesis string
|
||||
ProposedChangeRef SemanticReference
|
||||
ExpectedImprovement string
|
||||
ActualEvaluationRefs []SemanticReference
|
||||
DevelopmentGateRef *SemanticReference
|
||||
BeforeVersionRefs []SemanticReference
|
||||
AfterVersionRefs []SemanticReference
|
||||
Decision string
|
||||
SideEffects []string
|
||||
RollbackRef *SemanticReference
|
||||
RecordedAt int64
|
||||
}
|
||||
|
||||
func (a CompetenceAssessment) Validate() error {
|
||||
if a.ID == "" || a.PrincipalID == "" || !a.SubjectRef.Valid() || a.Revision < 1 || a.ConfidenceMillis < 0 || a.ConfidenceMillis > 1000 || len(a.EvidenceRefs) == 0 || a.AssessedAt < 1 {
|
||||
return fmt.Errorf("competence requires evidence")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (l LearningLedgerEntry) Validate() error {
|
||||
if l.ID == "" || l.PrincipalID == "" || l.ObservedProblem == "" || len(l.EvidenceRefs) == 0 || l.Decision == "" || l.RecordedAt < 1 {
|
||||
return fmt.Errorf("learning ledger entry is incomplete")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package work
|
||||
|
||||
type ConsequenceAssessment struct {
|
||||
ID string
|
||||
TargetRefs []string
|
||||
Impact int64
|
||||
Reversibility int64
|
||||
Uncertainty int64
|
||||
Scope int64
|
||||
Recoverability int64
|
||||
CostOfError int64
|
||||
ExpectedConsequences []string
|
||||
MitigationOptions []string
|
||||
RequiresDryRun bool
|
||||
RequiresBackup bool
|
||||
RecommendedConfirmation string
|
||||
OwnerAccepted bool
|
||||
EvidenceRefs []EvidenceRef
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
func (a ConsequenceAssessment) Valid() bool {
|
||||
return a.ID != "" && len(a.TargetRefs) > 0 && a.Impact >= 0 && a.Impact <= 1000 && a.Reversibility >= 0 && a.Reversibility <= 1000 && a.Uncertainty >= 0 && a.Uncertainty <= 1000 && a.Recoverability >= 0 && a.Recoverability <= 1000 && a.RecordedAtUnixMillis > 0
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package work
|
||||
|
||||
type LongRunningWorkID string
|
||||
type LongRunningState string
|
||||
|
||||
const (
|
||||
LongRunningProposed LongRunningState = "PROPOSED"
|
||||
LongRunningRunning LongRunningState = "RUNNING"
|
||||
LongRunningPaused LongRunningState = "PAUSED"
|
||||
LongRunningBlocked LongRunningState = "BLOCKED"
|
||||
LongRunningCompleted LongRunningState = "COMPLETED"
|
||||
LongRunningExhausted LongRunningState = "EXHAUSTED"
|
||||
LongRunningCancelled LongRunningState = "CANCELLED"
|
||||
)
|
||||
|
||||
type Investigation struct {
|
||||
ID LongRunningWorkID
|
||||
Objective string
|
||||
HypothesisRefs []EvidenceRef
|
||||
AttemptRefs []AgentRunID
|
||||
FindingRefs []EvidenceRef
|
||||
Blockers []string
|
||||
CheckpointRefs []AgentCheckpointID
|
||||
NextStep string
|
||||
State LongRunningState
|
||||
Revision int64
|
||||
TimeBudgetMillis int64
|
||||
CostBudgetMicros int64
|
||||
MaxAttempts int
|
||||
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 {
|
||||
return w.ID != "" && w.Objective != "" && w.State != "" && w.Revision > 0 && w.TimeBudgetMillis >= 0 && w.CostBudgetMicros >= 0 && w.MaxAttempts >= 0 && w.RecordedAtUnixMillis > 0
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package work
|
||||
|
||||
type PerspectiveID string
|
||||
type PerspectiveSpec struct {
|
||||
ID PerspectiveID
|
||||
ObjectiveLens string
|
||||
FocusRef string
|
||||
RetrievalProfileRef string
|
||||
SkillRefs []string
|
||||
ToolRefs []string
|
||||
EvaluationCriteria []string
|
||||
CapabilityRequirements []string
|
||||
ExpectedOutputShape string
|
||||
Critic bool
|
||||
}
|
||||
type SpecialistAssignment struct {
|
||||
Perspective PerspectiveSpec
|
||||
SubtaskRef string
|
||||
FocusRef string
|
||||
SkillRefs []string
|
||||
ToolRefs []string
|
||||
EvidenceRefs []EvidenceRef
|
||||
}
|
||||
type TeamDecision struct {
|
||||
UseTeam bool
|
||||
Reason string
|
||||
Assignments []SpecialistAssignment
|
||||
}
|
||||
|
||||
func (p PerspectiveSpec) Valid() bool {
|
||||
return p.ID != "" && p.ObjectiveLens != "" && p.FocusRef != "" && len(p.SkillRefs) > 0 && len(p.ToolRefs) > 0 && len(p.EvaluationCriteria) > 0 && p.ExpectedOutputShape != ""
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package work
|
||||
|
||||
type EvolutionChangeLevel string
|
||||
|
||||
const (
|
||||
EvolutionKnowledge EvolutionChangeLevel = "KNOWLEDGE"
|
||||
EvolutionBehavior EvolutionChangeLevel = "BEHAVIOR"
|
||||
EvolutionCapability EvolutionChangeLevel = "CAPABILITY"
|
||||
EvolutionCore EvolutionChangeLevel = "CORE"
|
||||
)
|
||||
|
||||
type EvolutionCandidate struct {
|
||||
ID string
|
||||
ProposalRef string
|
||||
ChangeLevel EvolutionChangeLevel
|
||||
CreatorIdentity string
|
||||
BaseVersionRef string
|
||||
CandidateVersionRef string
|
||||
MustNotWorsen []string
|
||||
SandboxEvidenceRefs []EvidenceRef
|
||||
BenchmarkEvidenceRefs []EvidenceRef
|
||||
RollbackRef string
|
||||
State string
|
||||
}
|
||||
type EvolutionEvaluation struct {
|
||||
CandidateRef string
|
||||
EvaluatorIdentity string
|
||||
Approved bool
|
||||
EvidenceRefs []EvidenceRef
|
||||
MustNotWorsenPassed bool
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
func (e EvolutionEvaluation) ValidFor(c EvolutionCandidate) bool {
|
||||
return e.CandidateRef == c.ID && e.EvaluatorIdentity != "" && e.EvaluatorIdentity != c.CreatorIdentity && e.Approved && e.MustNotWorsenPassed && len(e.EvidenceRefs) > 0 && e.RecordedAtUnixMillis > 0
|
||||
}
|
||||
@@ -104,6 +104,16 @@ type ValueTypeSpec struct {
|
||||
type WorldObjectID ids.StableID
|
||||
type KindDefinitionID 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 RelationDefinitionID ids.StableID
|
||||
type ResolutionDecisionID ids.StableID
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
-- Gen2 owner-role provisioning for the dedicated bootstrap/migration principal.
|
||||
-- This is intentionally separate from feature migrations and may be run repeatedly.
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'world_role') THEN
|
||||
CREATE ROLE world_role LOGIN;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'configuration_role') THEN
|
||||
CREATE ROLE configuration_role LOGIN;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'self_role') THEN
|
||||
CREATE ROLE self_role LOGIN;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
role_name TEXT;
|
||||
can_login BOOLEAN;
|
||||
BEGIN
|
||||
FOREACH role_name IN ARRAY ARRAY['world_role', 'configuration_role', 'self_role'] LOOP
|
||||
SELECT rolcanlogin INTO can_login FROM pg_roles WHERE rolname = role_name;
|
||||
IF can_login IS DISTINCT FROM TRUE THEN
|
||||
RAISE EXCEPTION 'Gen2 owner role % exists but is not a V1-compatible LOGIN role', role_name;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS world AUTHORIZATION world_role;
|
||||
CREATE SCHEMA IF NOT EXISTS configuration AUTHORIZATION configuration_role;
|
||||
CREATE SCHEMA IF NOT EXISTS self AUTHORIZATION self_role;
|
||||
|
||||
REVOKE ALL ON SCHEMA world, configuration, self FROM PUBLIC;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
schema_name TEXT;
|
||||
expected_owner TEXT;
|
||||
actual_owner TEXT;
|
||||
BEGIN
|
||||
FOREACH schema_name IN ARRAY ARRAY['world', 'configuration', 'self'] LOOP
|
||||
expected_owner := schema_name || '_role';
|
||||
SELECT pg_get_userbyid(nspowner)
|
||||
INTO actual_owner
|
||||
FROM pg_namespace
|
||||
WHERE nspname = schema_name;
|
||||
IF actual_owner IS DISTINCT FROM expected_owner THEN
|
||||
RAISE EXCEPTION 'Gen2 schema % is owned by %, expected %', schema_name, actual_owner, expected_owner;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END
|
||||
$$;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,7 @@
|
||||
BEGIN;
|
||||
SET ROLE self_role;
|
||||
CREATE TABLE IF NOT EXISTS self.competence_assessments (assessment_id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, subject_ref JSONB NOT NULL, revision BIGINT NOT NULL CHECK(revision>0), context_matcher JSONB NOT NULL, confidence_millis BIGINT NOT NULL, evidence_refs JSONB NOT NULL, assessed_at_unix_millis BIGINT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS self.learning_ledger (entry_id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, observed_problem TEXT NOT NULL, evidence_refs JSONB NOT NULL, diagnosis TEXT NOT NULL, hypothesis TEXT NOT NULL, decision TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL);
|
||||
GRANT SELECT, INSERT, UPDATE ON self.competence_assessments, self.learning_ledger TO self_role;
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -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;
|
||||
@@ -25,6 +25,16 @@ psql_exec() {
|
||||
docker exec -i "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_SUPERUSER" -d "$DATABASE" "$@"
|
||||
}
|
||||
|
||||
# Owner roles and schemas are provisioned by the dedicated bootstrap boundary,
|
||||
# before feature migrations run. This keeps role creation out of migrations and
|
||||
# makes existing PostgreSQL volumes converge idempotently as well.
|
||||
GEN2_BOOTSTRAP="$REPO_DIR/database/bootstrap/002_gen2_roles_and_schemas.sql"
|
||||
if [[ ! -f "$GEN2_BOOTSTRAP" ]]; then
|
||||
echo "migration runner: Gen2 bootstrap is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
psql_exec < "$GEN2_BOOTSTRAP"
|
||||
|
||||
psql_exec -c "CREATE TABLE IF NOT EXISTS public.assistant_migration_history (migration_id TEXT PRIMARY KEY, applied_at_unix_millis BIGINT NOT NULL CHECK (applied_at_unix_millis > 0));"
|
||||
|
||||
if [[ -n "${MIGRATION_BASELINE:-}" ]]; then
|
||||
|
||||
@@ -277,7 +277,7 @@ services:
|
||||
- --port
|
||||
- "8086"
|
||||
- --model
|
||||
- /models/Qwen3-8B-Q4_K_M.gguf
|
||||
- /models/Qwen3.8-27B-UD-Q2_K_XL.gguf
|
||||
- --alias
|
||||
- domovoy-lifeboat
|
||||
- --jinja
|
||||
@@ -299,6 +299,29 @@ services:
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
supervisor:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: supervisor, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LISTEN_ADDR: ":8080"
|
||||
SUPERVISOR_CONTROL_ADDR: ":8095"
|
||||
SUPERVISOR_STATE_PATH: /var/lib/supervisor/release-state.json
|
||||
SUPERVISOR_STABLE_UPSTREAM: http://creator-web:8084
|
||||
SUPERVISOR_CANDIDATE_UPSTREAM: http://creator-web-candidate:8084
|
||||
SUPERVISOR_BASELINE_RELEASE: ${ASSISTANT_SOURCE_REVISION:-unknown}
|
||||
ports:
|
||||
- "${CREATOR_WEB_PORT:-18080}:8080"
|
||||
- "127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}:8095"
|
||||
depends_on:
|
||||
creator-web: { condition: service_healthy }
|
||||
volumes:
|
||||
- assistant_supervisor_state:/var/lib/supervisor
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
creator-web:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: creator-web, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
@@ -310,7 +333,6 @@ services:
|
||||
COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083
|
||||
AGENT_WORKER_URL: http://agent-worker:8090
|
||||
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
|
||||
ports: ["${CREATOR_WEB_PORT:-18080}:8084"]
|
||||
depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
|
||||
@@ -318,6 +340,12 @@ services:
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
creator-web-candidate:
|
||||
profiles: ["assistant-canary"]
|
||||
extends:
|
||||
service: creator-web
|
||||
restart: "no"
|
||||
|
||||
volumes:
|
||||
assistant_pg:
|
||||
assistant_nats:
|
||||
@@ -333,6 +361,7 @@ volumes:
|
||||
external: true
|
||||
assistant_research_sources:
|
||||
name: assistant_research_sources
|
||||
assistant_supervisor_state:
|
||||
|
||||
networks:
|
||||
llm-local:
|
||||
|
||||
@@ -4,5 +4,6 @@ LABEL org.opencontainers.image.revision=$SOURCE_REVISION \
|
||||
org.opencontainers.image.source="https://git.cin.su/m/Assistant.git"
|
||||
|
||||
COPY database/bootstrap/001_roles_and_schemas.sql /docker-entrypoint-initdb.d/001_roles_and_schemas.sql
|
||||
COPY database/bootstrap/002_gen2_roles_and_schemas.sql /docker-entrypoint-initdb.d/002_gen2_roles_and_schemas.sql
|
||||
COPY deploy/images/002_owner_passwords.sh /docker-entrypoint-initdb.d/002_owner_passwords.sh
|
||||
RUN chmod 0555 /docker-entrypoint-initdb.d/002_owner_passwords.sh
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_DIR="${REPO_DIR:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
|
||||
ENV_FILE="${ENV_FILE:-/home/m/assistant-deploy/production-compose.env}"
|
||||
COMPOSE_FILE="$BASE_DIR/deploy/docker-compose.prod.yml"
|
||||
SUPERVISOR_CONTROL_URL="${SUPERVISOR_CONTROL_URL:-http://127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}}"
|
||||
REVISION="${1:?candidate revision is required}"
|
||||
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
|
||||
control() { curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL$1" "${@:2}" >/dev/null; }
|
||||
fail_rollback() {
|
||||
control /v1/traffic/rollback || true
|
||||
control /v1/releases/rollback || true
|
||||
echo "production canary failed; stable traffic rollback requested" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
compose --profile assistant-canary build --build-arg "SOURCE_REVISION=$REVISION" creator-web-candidate
|
||||
compose --profile assistant-canary up -d --no-build creator-web-candidate
|
||||
for attempt in $(seq 1 30); do
|
||||
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' assistant-creator-web-candidate-1 2>/dev/null || true)"
|
||||
[[ "$status" == healthy ]] && break
|
||||
[[ "$attempt" == 30 ]] && fail_rollback
|
||||
sleep 2
|
||||
done
|
||||
|
||||
control /v1/releases/start -H 'Content-Type: application/json' --data "{\"id\":\"candidate-$REVISION\",\"version\":\"$REVISION\"}" || fail_rollback
|
||||
control /v1/releases/observe -H 'Content-Type: application/json' --data "{\"release_id\":\"candidate-$REVISION\",\"ready\":true,\"reason\":\"candidate healthcheck passed\",\"observed_at_unix_millis\":$(date +%s%3N)}" || fail_rollback
|
||||
control /v1/traffic/canary || fail_rollback
|
||||
|
||||
headers="$(mktemp)"
|
||||
trap 'rm -f "$headers"' EXIT
|
||||
if ! curl -fsS --max-time 10 -D "$headers" -H 'X-Assistant-Canary: true' "https://assistant.cin.su/login" -o /dev/null; then fail_rollback; fi
|
||||
grep -qi '^X-Assistant-Traffic-Target: candidate' "$headers" || fail_rollback
|
||||
|
||||
control /v1/releases/promote || fail_rollback
|
||||
echo "production canary promoted revision=$REVISION"
|
||||
@@ -1 +1 @@
|
||||
{"task_file":"dev/tasks/GEN2-012-add-typed-scoped-versioned-configuration-owner-and-bootstrap-boundary.json","task_id":"GEN2-012","task_revision":1}
|
||||
{"task_file":"dev/tasks/GEN2-DEPLOY-001-supervisor-and-production-rollback.json","task_id":"GEN2-DEPLOY-001","task_revision":1}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"task_id": "GEN2-DB-001",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Исправить provisioning Gen2 owner roles/schemas до применения migrations 046-050 без ослабления V1 DB isolation.",
|
||||
"requirement_ids": ["REQ-GEN2-DB-001-ROLES", "REQ-GEN2-DB-001-OWNERSHIP", "REQ-GEN2-DB-001-IDEMPOTENCY", "REQ-GEN2-DB-001-RECOVERY"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["database bootstrap", "deployment migration runner"],
|
||||
"paths": [
|
||||
"database/bootstrap/002_gen2_roles_and_schemas.sql",
|
||||
"deploy/apply-migrations.sh",
|
||||
"deploy/images/postgres.Dockerfile",
|
||||
"dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json"
|
||||
],
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"GEN2-028",
|
||||
"production-canary-before-provisioning",
|
||||
"feature-migration-role-creation",
|
||||
"cross-owner-grants",
|
||||
"new canonical owner",
|
||||
"production canary before provisioning"
|
||||
]
|
||||
},
|
||||
"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": ["V1 database ownership/ACL", "GEN2 canonical owner registry", "GEN2 execution contract"], "may_change": ["Bootstrap provisioning only; feature migration semantics are unchanged."]},
|
||||
"semantic_owner": "Dedicated PostgreSQL bootstrap/migration principal; semantic ownership remains World, Configuration, SELF and existing V1 owners.",
|
||||
"canonical_state": ["world schema owned by world_role", "configuration schema owned by configuration_role", "self schema owned by self_role", "no cross-owner canonical writes"],
|
||||
"state_transitions": ["missing Gen2 roles/schemas -> idempotently provisioned -> migrations 046-050 apply -> restart/recovery preserves schema and migration state"],
|
||||
"side_effects_and_privileges": ["Create only the three declared LOGIN owner roles when absent.", "Revoke PUBLIC schema privileges and fail closed on an unexpected schema owner.", "Do not invent or print credentials; do not grant cross-owner access."],
|
||||
"persistence_and_migrations": ["Provisioning runs in the dedicated bootstrap boundary before feature migrations.", "Migrations 046-050 remain unchanged and are applied by the existing migration history runner."],
|
||||
"failure_retry_restart": ["Provisioning is safe to run repeatedly.", "Unexpected existing role/schema ownership fails closed and requires an architecture decision.", "PostgreSQL restart/recovery must retain owner roles, schemas, ACL and migration history."],
|
||||
"assumptions": ["This is a user-authorized remediation task for the previously stopped GEN2-027 migration decision.", "No Gen2 runtime consumer currently requires a new database password; credentials are not invented by this task.", "GEN2-027 resumes only after provisioning and recovery evidence; GEN2-028 is not started."],
|
||||
"acceptance_scenarios": ["GEN2-DB-001-SCN-01: all roles required by migrations 046-050 are identified", "GEN2-DB-001-SCN-02: provisioning succeeds twice without duplicate objects", "GEN2-DB-001-SCN-03: migrations 046-050 apply in the controlled deployment path", "GEN2-DB-001-SCN-04: schema owners and PUBLIC ACL are confirmed", "GEN2-DB-001-SCN-05: PostgreSQL restart/recovery preserves the result", "GEN2-DB-001-EXIT: migration blocker is removed without weakening V1 isolation"],
|
||||
"required_tests": ["FAST governance: go run ./cmd/devctl preflight -task dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json", "FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "FAST whitespace: git diff --check gen2-integration --", "TARGETED: provisioning repeated twice, migrations 046-050, ACL/ownership verification and PostgreSQL restart/recovery"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"task_id": "GEN2-DEPLOY-001",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Развернуть независимый Supervisor control plane с durable release state, controlled traffic switching и доказуемым rollback перед GEN2-027.",
|
||||
"requirement_ids": ["REQ-GEN2-DEPLOY-001-SUPERVISOR", "REQ-GEN2-DEPLOY-001-TRAFFIC", "REQ-GEN2-DEPLOY-001-ROLLBACK", "REQ-GEN2-DEPLOY-001-LIFEBOAT"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["deployment control", "runtime supervisor", "runtime health"],
|
||||
"paths": [
|
||||
"deploy/**",
|
||||
"runtime/supervisor/**",
|
||||
"runtime/health/**",
|
||||
"cmd/supervisor/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-DEPLOY-001-supervisor-and-production-rollback.json",
|
||||
"docs/evidence/**"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["GEN2-028", "database/**", "security/**", "architecture/**", "Authority/Commit bypass", "cognitive-lifeboat as Supervisor"]
|
||||
},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["GEN2-027 production canary contract", "runtime/supervisor durable state", "existing deployment update controller"], "may_change": ["deployment topology, Supervisor HTTP control contract and traffic routing only"]},
|
||||
"semantic_owner": "Operations/Supervisor; not a semantic owner and not a cognitive provider.",
|
||||
"canonical_state": ["durable release/current/last-known-good evidence owned by Supervisor control plane", "traffic target is Supervisor-owned operational state"],
|
||||
"state_transitions": ["baseline -> candidate -> observed healthy -> header canary -> promoted OR independent rollback to last-known-good"],
|
||||
"side_effects_and_privileges": ["Control API is bound only to the production-internal loopback port.", "Supervisor has no database, Authority or Commit credentials.", "Candidate traffic is explicit and rollback remains available."],
|
||||
"persistence_and_migrations": ["No database changes; durable state is stored in a dedicated named volume outside candidate containers."],
|
||||
"failure_retry_restart": ["Supervisor restart reloads release and traffic state from its volume.", "Candidate failure must not erase last-known-good or block stable traffic rollback.", "Lifeboat model failure remains a separately reported degraded provider condition."],
|
||||
"assumptions": ["GEN2-027 resumes only after this task's production evidence; GEN2-028 is not started.", "The existing public ingress forwards to the configured creator web host port."],
|
||||
"acceptance_scenarios": ["GEN2-DEPLOY-001-SCN-01: Supervisor is a separate healthy production container", "GEN2-DEPLOY-001-SCN-02: release state survives Supervisor restart", "GEN2-DEPLOY-001-SCN-03: canary header reaches candidate while default traffic stays stable", "GEN2-DEPLOY-001-SCN-04: candidate failure rolls traffic back to last-known-good", "GEN2-DEPLOY-001-SCN-05: cognitive lifeboat diagnosis and degraded role are explicit", "GEN2-DEPLOY-001-EXIT: GEN2-027 rollback prerequisites are proven"],
|
||||
"required_tests": ["FAST: go test ./runtime/supervisor/... ./runtime/health/... ./cmd/supervisor/...", "FAST: go vet ./runtime/supervisor/... ./runtime/health/... ./cmd/supervisor/...", "FAST governance: preflight", "FAST scope: check-diff", "FAST independent: independent", "FAST whitespace: git diff --check", "TARGETED recovery: Supervisor restart, canary and rollback on docker.cin.su"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStrategyUsesRiskAndCostSignals(t *testing.T) {
|
||||
if got := (capability.StrategyRequest{RequiredQuality: 400, CostBudgetMicros: 10}).Select().Path; got != capability.PathRule {
|
||||
t.Fatal(got)
|
||||
}
|
||||
if got := (capability.StrategyRequest{RiskClass: "HIGH", CostOfError: 900}).Select(); got.Path != capability.PathStrongLocal || !got.Escalate {
|
||||
t.Fatalf("%#v", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
func ValidateReflexRevision(v capability.ReflexRevision) error {
|
||||
if v.ReflexID == "" || v.SourceSkillRevision < 1 || v.Kind == "" || v.ImplementationRef == "" || v.Lifecycle == "" || v.RecordedAt < 1 {
|
||||
return fmt.Errorf("invalid reflex revision")
|
||||
}
|
||||
if v.Lifecycle == capability.ReflexActive && len(v.ValidationEvidenceRefs) == 0 {
|
||||
return fmt.Errorf("active reflex requires evidence")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func ReflexFallback(d capability.FastPathDecision) string {
|
||||
if d.Decision == capability.FastPathApplied {
|
||||
return "reflex"
|
||||
}
|
||||
return "skill->reasoning"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/modules/capability/domain"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReflexActiveRequiresEvidenceAndFallbacks(t *testing.T) {
|
||||
v := capability.ReflexRevision{ReflexID: "r", SourceSkillRevision: 1, Kind: capability.ReflexRule, ImplementationRef: "rule", Lifecycle: capability.ReflexActive, RecordedAt: 1}
|
||||
if domain.ValidateReflexRevision(v) == nil {
|
||||
t.Fatal("active reflex without evidence")
|
||||
}
|
||||
v.ValidationEvidenceRefs = []capability.SkillEvidenceRef{"eval"}
|
||||
if err := domain.ValidateReflexRevision(v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if domain.ReflexFallback(capability.FastPathDecision{Decision: capability.FastPathEscalate}) != "skill->reasoning" {
|
||||
t.Fatal("missing escalation")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
func ValidateSkillRevision(value capability.SkillRevision) error {
|
||||
if value.SkillID == "" || value.Revision < 1 || value.ImplementationRef == "" || value.Lifecycle == "" || value.RecordedAt < 1 {
|
||||
return fmt.Errorf("invalid skill revision")
|
||||
}
|
||||
if value.Lifecycle == capability.SkillValidated && len(value.ValidationEvidenceRefs) == 0 {
|
||||
return fmt.Errorf("validated skill requires evidence")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,10 +2,40 @@ package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"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}
|
||||
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}
|
||||
var ErrInvalidConfigurationCommand = fmt.Errorf("invalid conversational configuration command")
|
||||
|
||||
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.
|
||||
// PersonalMemory remains a compatibility fallback and is never written here.
|
||||
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()
|
||||
if text != "" {
|
||||
return ContextProjection{Text: text, Source: "retrieval:interaction-utterances"}
|
||||
|
||||
@@ -3,6 +3,7 @@ package domain_test
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -16,3 +17,13 @@ func TestGen2ContextUsesRetrievalProjectionAndKeepsV1Fallback(t *testing.T) {
|
||||
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,17 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
selfcontract "git.cin.su/m/Assistant/contracts/self"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBenchmarkRequiresEvidenceAndDoesNotPromoteAlone(t *testing.T) {
|
||||
e := selfcontract.BenchmarkEvaluation{CaseRef: "case", QualityMillis: 900}
|
||||
if e.Valid() {
|
||||
t.Fatal("benchmark without evidence accepted")
|
||||
}
|
||||
e.EvidenceRefs = []selfcontract.SemanticReference{{OwnerNamespace: "epistemics", SemanticKind: "evaluation", StableID: "e"}}
|
||||
if !e.Valid() {
|
||||
t.Fatal("evidence-backed evaluation rejected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/self"
|
||||
)
|
||||
|
||||
func ValidateCompetence(v self.CompetenceAssessment) error { return v.Validate() }
|
||||
func ValidateLearning(v self.LearningLedgerEntry) error { return v.Validate() }
|
||||
@@ -0,0 +1,18 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
selfcontract "git.cin.su/m/Assistant/contracts/self"
|
||||
"git.cin.su/m/Assistant/modules/self/domain"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompetenceCannotBeSelfDeclaredWithoutEvidence(t *testing.T) {
|
||||
v := selfcontract.CompetenceAssessment{ID: "a", PrincipalID: "p", SubjectRef: selfcontract.SemanticReference{OwnerNamespace: "capability", SemanticKind: "skill", StableID: "s"}, Revision: 1, ConfidenceMillis: 1000, AssessedAt: 1}
|
||||
if domain.ValidateCompetence(v) == nil {
|
||||
t.Fatal("unsupported competence accepted")
|
||||
}
|
||||
v.EvidenceRefs = []selfcontract.SemanticReference{{OwnerNamespace: "epistemics", SemanticKind: "evaluation", StableID: "e"}}
|
||||
if err := domain.ValidateCompetence(v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package domain
|
||||
|
||||
import "git.cin.su/m/Assistant/contracts/work"
|
||||
|
||||
func AssessConsequences(id string, target string, impact, reversibility, uncertainty, recoverability, cost int64, at int64) work.ConsequenceAssessment {
|
||||
a := work.ConsequenceAssessment{ID: id, TargetRefs: []string{target}, Impact: impact, Reversibility: reversibility, Uncertainty: uncertainty, Recoverability: recoverability, CostOfError: cost, RecordedAtUnixMillis: at}
|
||||
a.RequiresDryRun = impact >= 700 || reversibility >= 700
|
||||
a.RequiresBackup = reversibility >= 500
|
||||
a.RecommendedConfirmation = "STANDARD"
|
||||
if uncertainty*cost > 500000 {
|
||||
a.RecommendedConfirmation = "EXPLICIT_OWNER_CONFIRMATION"
|
||||
}
|
||||
return a
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/modules/work/domain"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConsequenceAdvisesMitigationWithoutCapabilityBlacklist(t *testing.T) {
|
||||
a := domain.AssessConsequences("a", "database:a", 800, 800, 900, 300, 900, 1)
|
||||
if !a.Valid() || !a.RequiresDryRun || !a.RequiresBackup || a.RecommendedConfirmation != "EXPLICIT_OWNER_CONFIRMATION" {
|
||||
t.Fatalf("%#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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package domain
|
||||
|
||||
import "git.cin.su/m/Assistant/contracts/work"
|
||||
|
||||
func PauseInvestigation(value work.Investigation) work.Investigation {
|
||||
if value.Valid() {
|
||||
value.State = work.LongRunningPaused
|
||||
value.Revision++
|
||||
}
|
||||
return value
|
||||
}
|
||||
func ResumeInvestigation(value work.Investigation) work.Investigation {
|
||||
if value.Valid() && value.State == work.LongRunningPaused {
|
||||
value.State = work.LongRunningRunning
|
||||
value.Revision++
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/modules/work/domain"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInvestigationPauseResumeIsDurableStateTransition(t *testing.T) {
|
||||
w := work.Investigation{ID: "i", Objective: "diagnose", State: work.LongRunningRunning, Revision: 1, RecordedAtUnixMillis: 1}
|
||||
w = domain.PauseInvestigation(w)
|
||||
w = domain.ResumeInvestigation(w)
|
||||
if w.State != work.LongRunningRunning || w.Revision != 3 {
|
||||
t.Fatalf("%#v", w)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package domain
|
||||
|
||||
import "git.cin.su/m/Assistant/contracts/work"
|
||||
|
||||
func AssembleSpecialists(p work.PerspectiveSpec, subtasks []string) work.TeamDecision {
|
||||
if !p.Valid() {
|
||||
return work.TeamDecision{Reason: "invalid perspective"}
|
||||
}
|
||||
if len(subtasks) < 2 {
|
||||
return work.TeamDecision{Reason: "single reasoning path is sufficient"}
|
||||
}
|
||||
assignments := make([]work.SpecialistAssignment, 0, len(subtasks))
|
||||
for _, s := range subtasks {
|
||||
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}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/modules/work/domain"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPerspectiveTeamIsBoundedByIndependentSubtasks(t *testing.T) {
|
||||
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 {
|
||||
t.Fatal("team for one subtask")
|
||||
}
|
||||
if d := domain.AssembleSpecialists(p, []string{"a", "b"}); !d.UseTeam || len(d.Assignments) != 2 {
|
||||
t.Fatalf("%#v", d)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package domain
|
||||
|
||||
import "git.cin.su/m/Assistant/contracts/work"
|
||||
|
||||
func PromoteEvolution(c work.EvolutionCandidate, e work.EvolutionEvaluation) bool {
|
||||
return e.ValidFor(c) && c.ProposalRef != "" && c.RollbackRef != "" && len(c.SandboxEvidenceRefs) > 0 && len(c.BenchmarkEvidenceRefs) > 0
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/modules/work/domain"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEvolutionRequiresIndependentEvaluatorAndRollbackEvidence(t *testing.T) {
|
||||
c := work.EvolutionCandidate{ID: "c", ProposalRef: "p", ChangeLevel: work.EvolutionBehavior, CreatorIdentity: "creator", RollbackRef: "rollback", SandboxEvidenceRefs: []work.EvidenceRef{"sandbox"}, BenchmarkEvidenceRefs: []work.EvidenceRef{"benchmark"}}
|
||||
e := work.EvolutionEvaluation{CandidateRef: "c", EvaluatorIdentity: "creator", Approved: true, MustNotWorsenPassed: true, EvidenceRefs: []work.EvidenceRef{"eval"}, RecordedAtUnixMillis: 1}
|
||||
if domain.PromoteEvolution(c, e) {
|
||||
t.Fatal("creator promoted own change")
|
||||
}
|
||||
e.EvaluatorIdentity = "independent"
|
||||
if !domain.PromoteEvolution(c, e) {
|
||||
t.Fatal("independently evaluated candidate rejected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 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
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewPersistentLifeboat(store *DurableStore) *PersistentLifeboat {
|
||||
return &PersistentLifeboat{store: store}
|
||||
}
|
||||
func (p *PersistentLifeboat) transition(fn func(*Lifeboat) bool) (bool, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
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) SetTraffic(mode TrafficMode, stableUpstream, candidateUpstream string) (bool, error) {
|
||||
return p.transition(func(value *Lifeboat) bool { return value.SetTraffic(mode, stableUpstream, candidateUpstream) })
|
||||
}
|
||||
|
||||
func (p *PersistentLifeboat) Promote() (bool, error) {
|
||||
return p.transition(func(value *Lifeboat) bool {
|
||||
if value.Current.State != ReleaseHealthy || value.Current.ID == "" {
|
||||
return false
|
||||
}
|
||||
return value.SetTraffic(TrafficCandidate, value.StableUpstream, value.CandidateUpstream)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *PersistentLifeboat) RollbackTraffic() (bool, error) {
|
||||
return p.transition(func(value *Lifeboat) bool {
|
||||
if !value.Rollback() {
|
||||
return false
|
||||
}
|
||||
return value.SetTraffic(TrafficStable, value.StableUpstream, value.CandidateUpstream)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *PersistentLifeboat) Snapshot() (Lifeboat, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.store.Load()
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package supervisor
|
||||
|
||||
type ReleaseState string
|
||||
|
||||
const (
|
||||
ReleaseCandidate ReleaseState = "CANDIDATE"
|
||||
ReleaseHealthy ReleaseState = "HEALTHY"
|
||||
ReleaseFailed ReleaseState = "FAILED"
|
||||
ReleaseRolledBack ReleaseState = "ROLLED_BACK"
|
||||
)
|
||||
|
||||
type TrafficMode string
|
||||
|
||||
const (
|
||||
TrafficStable TrafficMode = "STABLE"
|
||||
TrafficCanaryHeader TrafficMode = "CANARY_HEADER"
|
||||
TrafficCandidate TrafficMode = "CANDIDATE"
|
||||
)
|
||||
|
||||
type Release struct {
|
||||
ID string
|
||||
Version string
|
||||
State ReleaseState
|
||||
LastKnownGood bool
|
||||
}
|
||||
type Observation struct {
|
||||
ReleaseID string
|
||||
Ready bool
|
||||
Reason string
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
type Lifeboat struct {
|
||||
Current Release
|
||||
LastKnownGood Release
|
||||
Observations []Observation
|
||||
TrafficMode TrafficMode
|
||||
StableUpstream string
|
||||
CandidateUpstream string
|
||||
}
|
||||
|
||||
func (l *Lifeboat) Start(candidate Release) bool {
|
||||
if candidate.ID == "" || candidate.Version == "" {
|
||||
return false
|
||||
}
|
||||
candidate.State = ReleaseCandidate
|
||||
l.Current = candidate
|
||||
return true
|
||||
}
|
||||
func (l *Lifeboat) Record(o Observation) bool {
|
||||
if o.ReleaseID == "" {
|
||||
return false
|
||||
}
|
||||
l.Observations = append(l.Observations, o)
|
||||
if o.ReleaseID != l.Current.ID {
|
||||
return true
|
||||
}
|
||||
if o.Ready {
|
||||
l.Current.State = ReleaseHealthy
|
||||
l.Current.LastKnownGood = true
|
||||
l.LastKnownGood = l.Current
|
||||
} else {
|
||||
l.Current.State = ReleaseFailed
|
||||
}
|
||||
return true
|
||||
}
|
||||
func (l *Lifeboat) Rollback() bool {
|
||||
if l.LastKnownGood.ID == "" {
|
||||
return false
|
||||
}
|
||||
l.Current = l.LastKnownGood
|
||||
l.Current.State = ReleaseRolledBack
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *Lifeboat) SetTraffic(mode TrafficMode, stableUpstream, candidateUpstream string) bool {
|
||||
if mode != TrafficStable && mode != TrafficCanaryHeader && mode != TrafficCandidate {
|
||||
return false
|
||||
}
|
||||
if stableUpstream == "" || (mode != TrafficStable && candidateUpstream == "") {
|
||||
return false
|
||||
}
|
||||
l.TrafficMode = mode
|
||||
l.StableUpstream = stableUpstream
|
||||
l.CandidateUpstream = candidateUpstream
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package supervisor_test
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/runtime/supervisor"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLifeboatRollsBackCandidateToLastKnownGood(t *testing.T) {
|
||||
l := supervisor.Lifeboat{}
|
||||
l.Start(supervisor.Release{ID: "good", Version: "1"})
|
||||
l.Record(supervisor.Observation{ReleaseID: "good", Ready: true})
|
||||
l.Start(supervisor.Release{ID: "candidate", Version: "2"})
|
||||
l.Record(supervisor.Observation{ReleaseID: "candidate", Ready: false})
|
||||
if !l.Rollback() || l.Current.ID != "good" || l.Current.State != supervisor.ReleaseRolledBack {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersistentSupervisorRestoresTrafficModeAfterRestart(t *testing.T) {
|
||||
store, err := supervisor.NewDurableStore(filepath.Join(t.TempDir(), "control", "release-state.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := supervisor.NewPersistentLifeboat(store)
|
||||
if ok, err := first.Start(supervisor.Release{ID: "candidate", Version: "2"}); err != nil || !ok {
|
||||
t.Fatalf("start: %v %v", ok, err)
|
||||
}
|
||||
if ok, err := first.Record(supervisor.Observation{ReleaseID: "candidate", Ready: true, ObservedAtUnixMillis: 1}); err != nil || !ok {
|
||||
t.Fatalf("observe: %v %v", ok, err)
|
||||
}
|
||||
if ok, err := first.SetTraffic(supervisor.TrafficCanaryHeader, "http://stable:8084", "http://candidate:8084"); err != nil || !ok {
|
||||
t.Fatalf("canary: %v %v", ok, err)
|
||||
}
|
||||
second := supervisor.NewPersistentLifeboat(store)
|
||||
state, err := second.Snapshot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if state.TrafficMode != supervisor.TrafficCanaryHeader || state.CandidateUpstream != "http://candidate:8084" || state.LastKnownGood.ID != "candidate" {
|
||||
t.Fatalf("state=%+v", state)
|
||||
}
|
||||
if ok, err := second.RollbackTraffic(); err != nil || !ok {
|
||||
t.Fatalf("rollback traffic: %v %v", ok, err)
|
||||
}
|
||||
state, err = second.Snapshot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if state.TrafficMode != supervisor.TrafficStable || state.Current.State != supervisor.ReleaseRolledBack {
|
||||
t.Fatalf("rollback state=%+v", state)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package execution_outcome_test
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/commit"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReportedExecutionSuccessIsNotWorldVerification(t *testing.T) {
|
||||
r := commit.ExecutionReceipt{ID: "r", ActionAttemptRef: "a", ExecutorImplementationRef: "impl", RequestDigest: "d", ExecutorResult: commit.ExecutorReportedSuccess, ReconciliationKey: "k", DispatchAtUnixMillis: 1}
|
||||
if !r.Valid() {
|
||||
t.Fatal("receipt invalid")
|
||||
}
|
||||
o := epistemics.WorldOutcomeAssessment{ID: "o", PrincipalID: "p", ActionAttemptRef: "a", State: epistemics.OutcomeUnverified, ConfidenceMillis: 0, AssessedAt: 1}
|
||||
if !o.Valid() {
|
||||
t.Fatal("unverified assessment invalid")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user