Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
323a67a7d5 | ||
|
|
bab96b7ede | ||
|
|
1b091a50f2 | ||
|
|
790fd7e768 | ||
|
|
875286b9fc | ||
|
|
8e26826ae2 | ||
|
|
c30e0c72ea | ||
|
|
d2338b1485 | ||
|
|
7f82240a8f | ||
|
|
fa62322ed2 | ||
|
|
8081b4de34 | ||
|
|
f792d208ef | ||
|
|
04dad79af1 | ||
|
|
02b472b627 | ||
|
|
a6eb4abc91 | ||
|
|
9766e5396e | ||
|
|
cb92f95550 | ||
|
|
e83cf3582b | ||
|
|
a65ee372a9 | ||
|
|
1f01b7013c | ||
|
|
93bc4a0fdf | ||
|
|
aec63de0c7 | ||
|
|
dc7f7b3b2b | ||
|
|
e3b55c9705 | ||
|
|
879de3bd95 | ||
|
|
3e0be6245e | ||
|
|
08deab8dda | ||
|
|
82ee6e5ba0 | ||
|
|
339f6a6770 | ||
|
|
6fb9184d14 | ||
|
|
3f2b008109 | ||
|
|
470fdf8c03 | ||
|
|
15c9517d0c | ||
|
|
8687c9b792 | ||
|
|
ff33a62edd | ||
|
|
b439a813e5 | ||
|
|
39d2d79c51 | ||
|
|
305a55a948 | ||
|
|
13a3446dd0 | ||
|
|
8c2989f2c6 | ||
|
|
b097269df4 | ||
|
|
39f9081b96 | ||
|
|
3de01f8a94 | ||
|
|
948896cd45 | ||
|
|
545e855298 | ||
|
|
6f8c4e2bc7 | ||
|
|
947d9d78a1 | ||
|
|
8a3b085855 | ||
|
|
cf66cd472c | ||
|
|
01f4fa27c2 | ||
|
|
d286fbb234 | ||
|
|
3b4ce66df0 | ||
|
|
2306c8d6b3 | ||
|
|
2b8887b3b2 | ||
|
|
4750dd859c | ||
|
|
a6059cc9c6 | ||
|
|
498bc885c3 | ||
|
|
487f7b7c9d | ||
|
|
b1b52920ab | ||
|
|
57fb572d5b | ||
|
|
0849e9c4aa | ||
|
|
51fa9ea506 | ||
|
|
2cf1fb5c5e | ||
|
|
4f06720e3f | ||
|
|
cdc8eea1a7 | ||
|
|
9f717d720e | ||
|
|
3b5c0ff56d | ||
|
|
9cbf6c7536 | ||
|
|
9650eefcb4 | ||
|
|
1ed28ef77a | ||
|
|
c56610fa75 | ||
|
|
3d7836d37c | ||
|
|
0482e6ac14 | ||
|
|
526368be7e | ||
|
|
3c2d8a38d7 | ||
|
|
cabf386203 | ||
|
|
f9bd267c04 | ||
|
|
b041a94f71 | ||
|
|
814c9e1d32 | ||
|
|
b93715867d | ||
|
|
009314e930 | ||
|
|
0954c8f4ef | ||
|
|
0a2599cfa6 | ||
|
|
ae9933ee84 | ||
|
|
7a59abc608 | ||
|
|
6e0cf3cd1d | ||
|
|
d7e77ea3dd | ||
|
|
47fd0c4b02 | ||
|
|
df9181ef6a | ||
|
|
b0d31012da | ||
|
|
8d149b2b9d | ||
|
|
0d10e6fd82 | ||
|
|
9aa5771f55 | ||
|
|
75abfb204b | ||
|
|
b0ee54153b | ||
|
|
26cf85108a | ||
|
|
7eb2f5f313 | ||
|
|
39a27102c9 | ||
|
|
7ac9a2697c | ||
|
|
f2e8ec08c5 | ||
|
|
1cfbb4ae66 | ||
|
|
7618a0bcbe | ||
|
|
dcdc9a2e64 | ||
|
|
5783faf9dc | ||
|
|
9a968fe2f6 | ||
|
|
41d4d7202f | ||
|
|
ed3932c57a | ||
|
|
3367361483 | ||
|
|
51638d6e8c | ||
|
|
0a1ad9f7d5 | ||
|
|
4d51e65226 | ||
|
|
11a6a23f6e | ||
|
|
ce666bbc43 | ||
|
|
64870798b9 | ||
|
|
21e995432d | ||
|
|
3e99edd3a8 | ||
|
|
c1aff60443 | ||
|
|
3aa981bc59 | ||
|
|
7759d8257e | ||
|
|
ade274011f | ||
|
|
c232169270 | ||
|
|
4352e00491 | ||
|
|
21830a03e4 | ||
|
|
4428e166be | ||
|
|
a08c187284 | ||
|
|
41b0ac196e | ||
|
|
5cf566dccd | ||
|
|
b3491520bd | ||
|
|
d870e1361d | ||
|
|
8131d6cef3 | ||
|
|
4bfca4fe24 | ||
|
|
21239cf3cc | ||
|
|
75857a2036 | ||
|
|
9a9307af89 | ||
|
|
ff66e8dc5c | ||
|
|
a862a07ae1 | ||
|
|
4d63c1bea4 | ||
|
|
91c38de167 | ||
|
|
926afc82b5 | ||
|
|
4a066d71f4 | ||
|
|
7fc611ed08 | ||
|
|
924d8c3767 | ||
|
|
67ff1f73cd | ||
|
|
0ad247bae3 | ||
|
|
2de2885f2f | ||
|
|
069c3837f3 | ||
|
|
8a88565772 | ||
|
|
e7b32be1e0 | ||
|
|
d4640d7617 | ||
|
|
29b20bc5b0 | ||
|
|
071adb5ecb | ||
|
|
465a90b25d | ||
|
|
95d6ad2d6a | ||
|
|
1e554f6758 | ||
|
|
1c7d512710 | ||
|
|
8c6a7bebbd | ||
|
|
36774b37ae | ||
|
|
c79de1c757 | ||
|
|
acf012720f |
@@ -15,6 +15,8 @@ jobs:
|
||||
go-version: '1.26.6'
|
||||
- name: Active task preflight
|
||||
run: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json
|
||||
- name: Independent governance verifier
|
||||
run: go run ./cmd/devctl/independent
|
||||
- name: Exact diff scope
|
||||
run: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json
|
||||
- name: Format check
|
||||
@@ -25,3 +27,5 @@ jobs:
|
||||
run: go test ./...
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
- name: Diff whitespace check
|
||||
run: git diff --check
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
type AuthorityRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
func OpenAuthority(ctx context.Context, c ContinuityConfig) (*AuthorityRepository, error) {
|
||||
p, e := pgxpool.New(ctx, fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", c.Host, c.Port, c.Database, c.User, c.Password))
|
||||
p, e := openPool(ctx, c)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@ var ErrCapabilityImplementationNotFound = errors.New("capability implementation
|
||||
type CapabilityRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
func OpenCapability(ctx context.Context, config ContinuityConfig) (*CapabilityRepository, error) {
|
||||
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password)
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -218,6 +217,81 @@ func (r *CapabilityRepository) SaveCapabilityUsageObservation(ctx context.Contex
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) SaveSkillProfile(ctx context.Context, value capability.SkillProfile) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.skill_profiles (
|
||||
skill_profile_id,revision,name,description,applicable_context,implementation_ref,implementation_version,status,
|
||||
supporting_experience_refs,known_failure_experience_refs,confidence_millis,last_validated_unix_millis,
|
||||
observed_failure_count,observed_correction_count,recorded_at_unix_millis)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT DO NOTHING`,
|
||||
value.ID, value.Revision, value.Name, value.Description, value.ApplicableContext, value.ImplementationRef,
|
||||
value.ImplementationVersion, value.Status, value.SupportingExperienceRefs, value.KnownFailureExperienceRefs,
|
||||
value.ConfidenceMillis, value.LastValidatedUnixMillis, value.ObservedFailureCount, value.ObservedCorrectionCount,
|
||||
value.RecordedAtUnixMillis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.skill_profile_heads(skill_profile_id,revision,status)
|
||||
VALUES ($1,$2,$3)
|
||||
ON CONFLICT (skill_profile_id) DO UPDATE SET revision=EXCLUDED.revision,status=EXCLUDED.status,updated_at=CURRENT_TIMESTAMP
|
||||
WHERE capability.skill_profile_heads.revision < EXCLUDED.revision`, value.ID, value.Revision, value.Status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) LoadSkillProfile(ctx context.Context, id capability.SkillProfileID) (capability.SkillProfile, error) {
|
||||
var value capability.SkillProfile
|
||||
err := r.pool.QueryRow(ctx, `SELECT
|
||||
p.skill_profile_id,p.revision,p.name,p.description,p.applicable_context,p.implementation_ref,p.implementation_version,p.status,
|
||||
p.supporting_experience_refs,p.known_failure_experience_refs,p.confidence_millis,p.last_validated_unix_millis,
|
||||
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis
|
||||
FROM capability.skill_profile_heads h
|
||||
JOIN capability.skill_profiles p ON p.skill_profile_id=h.skill_profile_id AND p.revision=h.revision
|
||||
WHERE p.skill_profile_id=$1`, id).Scan(&value.ID, &value.Revision, &value.Name, &value.Description, &value.ApplicableContext,
|
||||
&value.ImplementationRef, &value.ImplementationVersion, &value.Status, &value.SupportingExperienceRefs,
|
||||
&value.KnownFailureExperienceRefs, &value.ConfidenceMillis, &value.LastValidatedUnixMillis,
|
||||
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis)
|
||||
if err != nil {
|
||||
return capability.SkillProfile{}, fmt.Errorf("load skill profile: %w", err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit int) ([]capability.SkillProfile, error) {
|
||||
if limit < 1 || limit > 100 {
|
||||
return nil, fmt.Errorf("invalid skill profile limit")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
p.skill_profile_id,p.revision,p.name,p.description,p.applicable_context,p.implementation_ref,p.implementation_version,p.status,
|
||||
p.supporting_experience_refs,p.known_failure_experience_refs,p.confidence_millis,p.last_validated_unix_millis,
|
||||
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis
|
||||
FROM capability.skill_profile_heads h
|
||||
JOIN capability.skill_profiles p ON p.skill_profile_id=h.skill_profile_id AND p.revision=h.revision
|
||||
ORDER BY h.updated_at DESC, p.skill_profile_id LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var result []capability.SkillProfile
|
||||
for rows.Next() {
|
||||
var value capability.SkillProfile
|
||||
if err := rows.Scan(&value.ID, &value.Revision, &value.Name, &value.Description, &value.ApplicableContext,
|
||||
&value.ImplementationRef, &value.ImplementationVersion, &value.Status, &value.SupportingExperienceRefs,
|
||||
&value.KnownFailureExperienceRefs, &value.ConfidenceMillis, &value.LastValidatedUnixMillis,
|
||||
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) LoadBinding(ctx context.Context, id capability.CapabilityBindingID, revision int64) (capability.Binding, error) {
|
||||
var binding capability.Binding
|
||||
err := r.pool.QueryRow(ctx, `SELECT binding_id,capability_id,owner_principal_id,revision,state,https_origin,delivery_path,receipt_path,
|
||||
@@ -248,18 +322,18 @@ func (r *CapabilityRepository) LoadActiveBinding(ctx context.Context, capability
|
||||
|
||||
func (r *CapabilityRepository) SaveProviderInstance(ctx context.Context, value capability.ProviderInstance) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances
|
||||
(provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT DO NOTHING`, value.ID, value.Revision, value.Kind,
|
||||
value.EndpointRef, value.CredentialRef, value.Enabled, value.Qualification, value.Health, value.ObservedAtUnixMillis)
|
||||
(provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis,billing_mode,auth_mode,input_cost_micros,output_cost_micros,request_cost_micros)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, value.ID, value.Revision, value.Kind,
|
||||
value.EndpointRef, value.CredentialRef, value.Enabled, value.Qualification, value.Health, value.ObservedAtUnixMillis, value.BillingMode, value.AuthMode, value.InputCostMicros, value.OutputCostMicros, value.RequestCostMicros)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) SaveModelDescriptor(ctx context.Context, value capability.ModelDescriptor) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors
|
||||
(model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`, value.ID, value.Revision,
|
||||
(model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis,intelligence_score_millis)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT DO NOTHING`, value.ID, value.Revision,
|
||||
value.ProviderInstanceID, value.ProviderModelID, value.Fingerprint, value.MaxContextBytes, value.Modalities,
|
||||
value.Capabilities, value.Qualification, value.Health, value.ObservedAtUnixMillis)
|
||||
value.Capabilities, value.Qualification, value.Health, value.ObservedAtUnixMillis, value.IntelligenceScoreMillis)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -275,8 +349,8 @@ func (r *CapabilityRepository) SaveCognitiveBinding(ctx context.Context, value c
|
||||
func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error) {
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
b.binding_id,b.revision,b.capability_id,b.owner_principal_id,b.provider_instance_id,b.provider_revision,b.model_descriptor_id,b.model_revision,b.data_policy,b.qualification,b.activation,b.health,b.limit_policy_ref,
|
||||
p.provider_instance_id,p.revision,p.provider_kind,p.endpoint_ref,COALESCE(p.credential_ref,''),p.enabled,p.qualification,COALESCE(ph.health,p.health),GREATEST(p.observed_at_unix_millis,COALESCE(ph.observed_at_unix_millis,0)),
|
||||
m.model_descriptor_id,m.revision,m.provider_instance_id,m.provider_model_id,m.fingerprint,m.max_context_bytes,m.modalities,m.capabilities,m.qualification,COALESCE(ma.health,m.health),GREATEST(m.observed_at_unix_millis,COALESCE(ma.observed_at_unix_millis,0))
|
||||
p.provider_instance_id,p.revision,p.provider_kind,p.endpoint_ref,COALESCE(p.credential_ref,''),p.enabled,p.qualification,COALESCE(ph.health,p.health),GREATEST(p.observed_at_unix_millis,COALESCE(ph.observed_at_unix_millis,0)),p.billing_mode,p.auth_mode,p.input_cost_micros,p.output_cost_micros,p.request_cost_micros,
|
||||
m.model_descriptor_id,m.revision,m.provider_instance_id,m.provider_model_id,m.fingerprint,m.max_context_bytes,m.modalities,m.capabilities,m.qualification,COALESCE(ma.health,m.health),GREATEST(m.observed_at_unix_millis,COALESCE(ma.observed_at_unix_millis,0)),m.intelligence_score_millis
|
||||
FROM capability.cognitive_binding_revision_heads bh
|
||||
JOIN capability.cognitive_bindings b ON b.binding_id=bh.binding_id AND b.revision=bh.revision
|
||||
JOIN capability.cognitive_provider_instances p ON p.provider_instance_id=b.provider_instance_id AND p.revision=b.provider_revision
|
||||
@@ -292,8 +366,8 @@ func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capa
|
||||
for rows.Next() {
|
||||
var value capability.ResolvedCognitiveBinding
|
||||
err = rows.Scan(&value.Binding.ID, &value.Binding.Revision, &value.Binding.CapabilityID, &value.Binding.OwnerPrincipalID, &value.Binding.ProviderInstanceID, &value.Binding.ProviderRevision, &value.Binding.ModelDescriptorID, &value.Binding.ModelRevision, &value.Binding.DataPolicy, &value.Binding.Qualification, &value.Binding.Activation, &value.Binding.Health, &value.Binding.LimitPolicyRef,
|
||||
&value.Provider.ID, &value.Provider.Revision, &value.Provider.Kind, &value.Provider.EndpointRef, &value.Provider.CredentialRef, &value.Provider.Enabled, &value.Provider.Qualification, &value.Provider.Health, &value.Provider.ObservedAtUnixMillis,
|
||||
&value.Model.ID, &value.Model.Revision, &value.Model.ProviderInstanceID, &value.Model.ProviderModelID, &value.Model.Fingerprint, &value.Model.MaxContextBytes, &value.Model.Modalities, &value.Model.Capabilities, &value.Model.Qualification, &value.Model.Health, &value.Model.ObservedAtUnixMillis)
|
||||
&value.Provider.ID, &value.Provider.Revision, &value.Provider.Kind, &value.Provider.EndpointRef, &value.Provider.CredentialRef, &value.Provider.Enabled, &value.Provider.Qualification, &value.Provider.Health, &value.Provider.ObservedAtUnixMillis, &value.Provider.BillingMode, &value.Provider.AuthMode, &value.Provider.InputCostMicros, &value.Provider.OutputCostMicros, &value.Provider.RequestCostMicros,
|
||||
&value.Model.ID, &value.Model.Revision, &value.Model.ProviderInstanceID, &value.Model.ProviderModelID, &value.Model.Fingerprint, &value.Model.MaxContextBytes, &value.Model.Modalities, &value.Model.Capabilities, &value.Model.Qualification, &value.Model.Health, &value.Model.ObservedAtUnixMillis, &value.Model.IntelligenceScoreMillis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -340,7 +414,7 @@ func (r *CapabilityRepository) SaveCognitiveDiscovery(ctx context.Context, value
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9) ON CONFLICT DO NOTHING`, value.Provider.ID, value.Provider.Revision, value.Provider.Kind, value.Provider.EndpointRef, value.Provider.CredentialRef, value.Provider.Enabled, value.Provider.Qualification, value.Provider.Health, value.Provider.ObservedAtUnixMillis)
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis,billing_mode,auth_mode,input_cost_micros,output_cost_micros,request_cost_micros) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT (provider_instance_id,revision) DO UPDATE SET health=EXCLUDED.health,observed_at_unix_millis=EXCLUDED.observed_at_unix_millis,billing_mode=EXCLUDED.billing_mode,auth_mode=EXCLUDED.auth_mode,input_cost_micros=EXCLUDED.input_cost_micros,output_cost_micros=EXCLUDED.output_cost_micros,request_cost_micros=EXCLUDED.request_cost_micros WHERE capability.cognitive_provider_instances.observed_at_unix_millis <= EXCLUDED.observed_at_unix_millis`, value.Provider.ID, value.Provider.Revision, value.Provider.Kind, value.Provider.EndpointRef, value.Provider.CredentialRef, value.Provider.Enabled, value.Provider.Qualification, value.Provider.Health, value.Provider.ObservedAtUnixMillis, value.Provider.BillingMode, value.Provider.AuthMode, value.Provider.InputCostMicros, value.Provider.OutputCostMicros, value.Provider.RequestCostMicros)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -349,7 +423,7 @@ func (r *CapabilityRepository) SaveCognitiveDiscovery(ctx context.Context, value
|
||||
return err
|
||||
}
|
||||
for index, model := range value.Models {
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`, model.ID, model.Revision, model.ProviderInstanceID, model.ProviderModelID, model.Fingerprint, model.MaxContextBytes, model.Modalities, model.Capabilities, model.Qualification, model.Health, model.ObservedAtUnixMillis)
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis,intelligence_score_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT (model_descriptor_id,revision) DO UPDATE SET health=EXCLUDED.health,observed_at_unix_millis=EXCLUDED.observed_at_unix_millis,intelligence_score_millis=EXCLUDED.intelligence_score_millis WHERE capability.cognitive_model_descriptors.observed_at_unix_millis <= EXCLUDED.observed_at_unix_millis`, model.ID, model.Revision, model.ProviderInstanceID, model.ProviderModelID, model.Fingerprint, model.MaxContextBytes, model.Modalities, model.Capabilities, model.Qualification, model.Health, model.ObservedAtUnixMillis, model.IntelligenceScoreMillis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -359,7 +433,11 @@ func (r *CapabilityRepository) SaveCognitiveDiscovery(ctx context.Context, value
|
||||
return err
|
||||
}
|
||||
binding := value.Bindings[index]
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision, binding.CapabilityID, binding.OwnerPrincipalID, binding.ProviderInstanceID, binding.ProviderRevision, binding.ModelDescriptorID, binding.ModelRevision, binding.DataPolicy, binding.Qualification, binding.Activation, binding.Health, binding.LimitPolicyRef)
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT (binding_id,revision) DO UPDATE SET health=EXCLUDED.health WHERE capability.cognitive_bindings.health <> EXCLUDED.health`, binding.ID, binding.Revision, binding.CapabilityID, binding.OwnerPrincipalID, binding.ProviderInstanceID, binding.ProviderRevision, binding.ModelDescriptorID, binding.ModelRevision, binding.DataPolicy, binding.Qualification, binding.Activation, binding.Health, binding.LimitPolicyRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -379,10 +457,10 @@ func (r *CapabilityRepository) SaveCognitiveCanaryPromotion(ctx context.Context,
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
p, m, b, e := value.Provider, value.Model, value.Binding, value.Evaluation
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9) ON CONFLICT DO NOTHING`, p.ID, p.Revision, p.Kind, p.EndpointRef, p.CredentialRef, p.Enabled, p.Qualification, p.Health, p.ObservedAtUnixMillis); err != nil {
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis,billing_mode,auth_mode,input_cost_micros,output_cost_micros,request_cost_micros) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, p.ID, p.Revision, p.Kind, p.EndpointRef, p.CredentialRef, p.Enabled, p.Qualification, p.Health, p.ObservedAtUnixMillis, p.BillingMode, p.AuthMode, p.InputCostMicros, p.OutputCostMicros, p.RequestCostMicros); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`, m.ID, m.Revision, m.ProviderInstanceID, m.ProviderModelID, m.Fingerprint, m.MaxContextBytes, m.Modalities, m.Capabilities, m.Qualification, m.Health, m.ObservedAtUnixMillis); err != nil {
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis,intelligence_score_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT DO NOTHING`, m.ID, m.Revision, m.ProviderInstanceID, m.ProviderModelID, m.Fingerprint, m.MaxContextBytes, m.Modalities, m.Capabilities, m.Qualification, m.Health, m.ObservedAtUnixMillis, m.IntelligenceScoreMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT DO NOTHING`, b.ID, b.Revision, b.CapabilityID, b.OwnerPrincipalID, b.ProviderInstanceID, b.ProviderRevision, b.ModelDescriptorID, b.ModelRevision, b.DataPolicy, b.Qualification, b.Activation, b.Health, b.LimitPolicyRef); err != nil {
|
||||
@@ -401,6 +479,32 @@ func (r *CapabilityRepository) SaveCognitiveCanaryPromotion(ctx context.Context,
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) SaveCognitiveActivationPromotion(ctx context.Context, value capability.CognitiveActivationPromotion) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
b, e := value.Binding, value.Evaluation
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT DO NOTHING`, b.ID, b.Revision, b.CapabilityID, b.OwnerPrincipalID, b.ProviderInstanceID, b.ProviderRevision, b.ModelDescriptorID, b.ModelRevision, b.DataPolicy, b.Qualification, b.Activation, b.Health, b.LimitPolicyRef); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_evaluated_capabilities (evaluation_id,binding_id,binding_revision,capability_id,result,quality_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT DO NOTHING`, e.ID, e.BindingID, e.BindingRevision, e.CapabilityID, e.Result, e.QualityScoreMillis, e.CriticalFailureMillis, e.BenchmarkVersion, e.ArtifactID, e.ObservedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `UPDATE capability.cognitive_bindings SET activation='DEPRECATED' WHERE capability_id=$1 AND owner_principal_id=$2 AND activation='ACTIVE' AND NOT (binding_id=$3 AND revision=$4)`, b.CapabilityID, b.OwnerPrincipalID, b.ID, b.Revision); err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := tx.Exec(ctx, `UPDATE capability.cognitive_binding_revision_heads SET revision=$1,updated_at=CURRENT_TIMESTAMP WHERE binding_id=$2 AND revision=$3`, b.Revision, b.ID, value.ExpectedBindingRevision)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() != 1 {
|
||||
return ErrCapabilityBindingConflict
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) SavePerformanceEvidence(ctx context.Context, value capability.PerformanceEvidence) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_performance_evidence (evidence_id,binding_id,binding_revision,model_descriptor_id,model_revision,hardware_profile,runtime,quantization,context_units,thinking_enabled,generation_tps_millis,prompt_tps_millis,ttft_micros,latency_micros,memory_bytes,reliability_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) ON CONFLICT DO NOTHING`, value.ID, value.BindingID, value.BindingRevision, value.ModelDescriptorID, value.ModelRevision, value.HardwareProfile, value.Runtime, value.Quantization, value.ContextUnits, value.ThinkingEnabled, value.GenerationTPSMillis, value.PromptTPSMillis, value.TTFTMicros, value.LatencyMicros, value.MemoryBytes, value.ReliabilityMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
|
||||
return err
|
||||
|
||||
@@ -23,7 +23,7 @@ var (
|
||||
type CommitRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
func OpenCommit(ctx context.Context, c ContinuityConfig) (*CommitRepository, error) {
|
||||
p, e := pgxpool.New(ctx, fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", c.Host, c.Port, c.Database, c.User, c.Password))
|
||||
p, e := openPool(ctx, c)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
continuitydomain "git.cin.su/m/Assistant/modules/continuity/domain"
|
||||
@@ -28,9 +30,25 @@ var (
|
||||
type ContinuityConfig struct{ Host, Port, Database, User, Password string }
|
||||
type ContinuityRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
func openPool(ctx context.Context, config ContinuityConfig) (*pgxpool.Pool, error) {
|
||||
poolConfig, err := pgxpool.ParseConfig("")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
port, err := strconv.ParseUint(config.Port, 10, 16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid postgres port: %w", err)
|
||||
}
|
||||
poolConfig.ConnConfig.Host = config.Host
|
||||
poolConfig.ConnConfig.Port = uint16(port)
|
||||
poolConfig.ConnConfig.Database = config.Database
|
||||
poolConfig.ConnConfig.User = config.User
|
||||
poolConfig.ConnConfig.Password = config.Password
|
||||
return pgxpool.NewWithConfig(ctx, poolConfig)
|
||||
}
|
||||
|
||||
func OpenContinuity(ctx context.Context, config ContinuityConfig) (*ContinuityRepository, error) {
|
||||
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password)
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -144,10 +162,10 @@ func (r *ContinuityRepository) ClaimDueReminder(ctx context.Context, id continui
|
||||
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, err
|
||||
}
|
||||
err = tx.QueryRow(ctx, `INSERT INTO continuity.attention_items (
|
||||
attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
|
||||
attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,
|
||||
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,
|
||||
action_attempt_id,delivery_outcome,response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,0,'','','','','','','','',0,1)
|
||||
VALUES ($1,$2,$3,$4,$5,'',$6,$7,$8,$9,$10,0,'','','','','','','','',0,1)
|
||||
ON CONFLICT (source_kind, source_prospective_item_id) DO UPDATE SET
|
||||
state=EXCLUDED.state,
|
||||
due_at_unix_millis=EXCLUDED.due_at_unix_millis,
|
||||
@@ -163,12 +181,12 @@ func (r *ContinuityRepository) ClaimDueReminder(ctx context.Context, id continui
|
||||
response_outcome='',
|
||||
responded_at_unix_millis=0,
|
||||
revision=continuity.attention_items.revision+1
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis,
|
||||
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
|
||||
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`,
|
||||
attention.ID, attention.OwnerPrincipalID, attention.ScopeID, attention.SourceKind, attention.SourceProspectiveID,
|
||||
attention.Level, attention.State, attention.Summary, attention.DueAtUnixMillis, attention.CreatedAtUnixMillis).Scan(
|
||||
&attention.ID, &attention.OwnerPrincipalID, &attention.ScopeID, &attention.SourceKind, &attention.SourceProspectiveID,
|
||||
&attention.ID, &attention.OwnerPrincipalID, &attention.ScopeID, &attention.SourceKind, &attention.SourceProspectiveID, &attention.SourceExternalRef,
|
||||
&attention.Level, &attention.State, &attention.Summary, &attention.DueAtUnixMillis, &attention.CreatedAtUnixMillis,
|
||||
&attention.DeliveredAtUnixMillis, &attention.ActionIntentID, &attention.AuthorizedActionID, &attention.ActionAttemptID,
|
||||
&attention.DeliveryOutcome, &attention.ResponseID, &attention.ResponseUtteranceID, &attention.ResponseOperation,
|
||||
@@ -186,7 +204,7 @@ func (r *ContinuityRepository) PendingAttention(ctx context.Context, limit int)
|
||||
if limit < 1 || limit > 100 {
|
||||
return nil, fmt.Errorf("invalid attention limit")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
|
||||
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,
|
||||
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
|
||||
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision
|
||||
FROM continuity.attention_items WHERE state=$1 ORDER BY due_at_unix_millis, attention_item_id LIMIT $2`, continuity.AttentionPending, limit)
|
||||
@@ -201,7 +219,7 @@ func (r *ContinuityRepository) AttentionByOwner(ctx context.Context, owner ident
|
||||
if owner == "" || limit < 1 || limit > 100 {
|
||||
return nil, fmt.Errorf("invalid attention owner query")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
|
||||
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,
|
||||
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
|
||||
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision
|
||||
FROM continuity.attention_items WHERE owner_principal_id=$1 ORDER BY due_at_unix_millis DESC, attention_item_id DESC LIMIT $2`, owner, limit)
|
||||
@@ -213,45 +231,98 @@ func (r *ContinuityRepository) AttentionByOwner(ctx context.Context, owner ident
|
||||
}
|
||||
|
||||
func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner identity.PrincipalID, limit int) ([]continuitydomain.ResponseCandidate, error) {
|
||||
if owner == "" || limit < 1 || limit > 10 {
|
||||
if owner == "" || limit < 1 || limit > 50 {
|
||||
return nil, fmt.Errorf("invalid response candidate query")
|
||||
}
|
||||
candidates := make([]continuitydomain.ResponseCandidate, 0, limit)
|
||||
attentionRows, err := r.pool.Query(ctx, `SELECT
|
||||
attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,
|
||||
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
|
||||
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision
|
||||
FROM continuity.attention_items
|
||||
WHERE owner_principal_id=$1
|
||||
AND source_kind=$2
|
||||
AND state IN ($3,$4,$5)
|
||||
ORDER BY created_at_unix_millis DESC, attention_item_id DESC
|
||||
LIMIT $6`,
|
||||
owner, continuity.AttentionSourceAgentDelegation, continuity.AttentionPending, continuity.AttentionDelivered, continuity.AttentionFailed, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list delegation response candidates: %w", err)
|
||||
}
|
||||
defer attentionRows.Close()
|
||||
for attentionRows.Next() {
|
||||
item, err := scanAttentionItem(attentionRows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan delegation response candidate: %w", err)
|
||||
}
|
||||
candidates = append(candidates, continuitydomain.ResponseCandidate{Attention: item})
|
||||
}
|
||||
if err := attentionRows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("list delegation response candidates: %w", err)
|
||||
}
|
||||
if len(candidates) >= limit {
|
||||
return candidates[:limit], nil
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
a.attention_item_id,a.owner_principal_id,a.scope_id,a.source_kind,a.source_prospective_item_id,a.level,a.state,a.summary,
|
||||
a.due_at_unix_millis,a.created_at_unix_millis,a.delivered_at_unix_millis,a.action_intent_id,a.authorized_action_id,a.action_attempt_id,a.delivery_outcome,
|
||||
a.response_id,a.response_utterance_id,a.response_operation,a.response_outcome,a.responded_at_unix_millis,a.revision,
|
||||
COALESCE(a.attention_item_id,''),COALESCE(a.owner_principal_id,''),COALESCE(a.scope_id,''),COALESCE(a.source_kind,''),COALESCE(a.source_prospective_item_id,''),COALESCE(a.source_external_ref,''),COALESCE(a.level,''),COALESCE(a.state,''),COALESCE(a.summary,''),
|
||||
COALESCE(a.due_at_unix_millis,0),COALESCE(a.created_at_unix_millis,0),COALESCE(a.delivered_at_unix_millis,0),COALESCE(a.action_intent_id,''),COALESCE(a.authorized_action_id,''),COALESCE(a.action_attempt_id,''),COALESCE(a.delivery_outcome,''),
|
||||
COALESCE(a.response_id,''),COALESCE(a.response_utterance_id,''),COALESCE(a.response_operation,''),COALESCE(a.response_outcome,''),COALESCE(a.responded_at_unix_millis,0),COALESCE(a.revision,0),
|
||||
p.prospective_item_id,p.commitment_id,p.owner_principal_id,p.scope_id,p.goal_id,p.source_intent_id,p.source_utterance_id,p.summary,p.trigger_at_unix_millis,p.state,p.revision
|
||||
FROM continuity.attention_items a
|
||||
JOIN continuity.prospective_items p ON p.prospective_item_id=a.source_prospective_item_id
|
||||
WHERE a.owner_principal_id=$1
|
||||
AND a.source_kind=$2
|
||||
AND a.state IN ($3,$4,$5)
|
||||
FROM continuity.prospective_items p
|
||||
LEFT JOIN continuity.attention_items a ON a.source_kind=$2 AND a.source_prospective_item_id=p.prospective_item_id AND a.owner_principal_id=p.owner_principal_id
|
||||
WHERE p.owner_principal_id=$1
|
||||
AND (a.attention_item_id IS NULL OR a.state IN ($3,$4,$5))
|
||||
AND p.state IN ($6,$7)
|
||||
ORDER BY a.delivered_at_unix_millis DESC, a.due_at_unix_millis DESC, a.attention_item_id DESC
|
||||
ORDER BY p.recorded_at DESC, COALESCE(a.delivered_at_unix_millis,0) DESC, p.prospective_item_id DESC
|
||||
LIMIT $8`,
|
||||
owner, continuity.AttentionSourceProspectiveReminder, continuity.AttentionDelivered, continuity.AttentionPending,
|
||||
continuity.AttentionFailed, continuity.ProspectiveDelivered, continuity.ProspectiveArmed, limit)
|
||||
continuity.AttentionFailed, continuity.ProspectiveDelivered, continuity.ProspectiveArmed, limit-len(candidates))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list human response candidates: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var candidates []continuitydomain.ResponseCandidate
|
||||
for rows.Next() {
|
||||
var candidate continuitydomain.ResponseCandidate
|
||||
var attentionID, attentionOwner, attentionScope, sourceKind, sourceProspective, sourceExternalRef, level, attentionState, attentionSummary string
|
||||
var actionIntent, authorizedAction, actionAttempt, deliveryOutcome, responseID, responseUtterance, responseOperation, responseOutcome string
|
||||
var dueAt, createdAt, deliveredAt, respondedAt int64
|
||||
var attentionRevision uint64
|
||||
if err := rows.Scan(
|
||||
&candidate.Attention.ID, &candidate.Attention.OwnerPrincipalID, &candidate.Attention.ScopeID, &candidate.Attention.SourceKind,
|
||||
&candidate.Attention.SourceProspectiveID, &candidate.Attention.Level, &candidate.Attention.State, &candidate.Attention.Summary,
|
||||
&candidate.Attention.DueAtUnixMillis, &candidate.Attention.CreatedAtUnixMillis, &candidate.Attention.DeliveredAtUnixMillis,
|
||||
&candidate.Attention.ActionIntentID, &candidate.Attention.AuthorizedActionID, &candidate.Attention.ActionAttemptID,
|
||||
&candidate.Attention.DeliveryOutcome, &candidate.Attention.ResponseID, &candidate.Attention.ResponseUtteranceID,
|
||||
&candidate.Attention.ResponseOperation, &candidate.Attention.ResponseOutcome, &candidate.Attention.RespondedAtUnixMillis,
|
||||
&candidate.Attention.Revision,
|
||||
&attentionID, &attentionOwner, &attentionScope, &sourceKind, &sourceProspective, &sourceExternalRef, &level, &attentionState, &attentionSummary,
|
||||
&dueAt, &createdAt, &deliveredAt, &actionIntent, &authorizedAction, &actionAttempt, &deliveryOutcome,
|
||||
&responseID, &responseUtterance, &responseOperation, &responseOutcome, &respondedAt, &attentionRevision,
|
||||
&candidate.Prospective.ID, &candidate.Prospective.CommitmentID, &candidate.Prospective.OwnerPrincipalID,
|
||||
&candidate.Prospective.ScopeID, &candidate.Prospective.GoalID, &candidate.Prospective.SourceIntentID,
|
||||
&candidate.Prospective.SourceUtteranceID, &candidate.Prospective.Summary, &candidate.Prospective.TriggerAtUnixMillis,
|
||||
&candidate.Prospective.State, &candidate.Prospective.Revision); err != nil {
|
||||
return nil, fmt.Errorf("scan human response candidate: %w", err)
|
||||
}
|
||||
if attentionID != "" {
|
||||
candidate.Attention = continuity.AttentionItem{
|
||||
ID: continuity.AttentionItemID(attentionID),
|
||||
OwnerPrincipalID: identity.PrincipalID(attentionOwner),
|
||||
ScopeID: authority.ScopeID(attentionScope),
|
||||
SourceKind: continuity.AttentionSourceKind(sourceKind),
|
||||
SourceProspectiveID: continuity.ProspectiveItemID(sourceProspective),
|
||||
SourceExternalRef: sourceExternalRef,
|
||||
Level: continuity.AttentionLevel(level),
|
||||
State: continuity.AttentionState(attentionState),
|
||||
Summary: attentionSummary,
|
||||
DueAtUnixMillis: dueAt,
|
||||
CreatedAtUnixMillis: createdAt,
|
||||
DeliveredAtUnixMillis: deliveredAt,
|
||||
ActionIntentID: continuity.CommittedActionIntentID(actionIntent),
|
||||
AuthorizedActionID: continuity.CommittedAuthorizedActionID(authorizedAction),
|
||||
ActionAttemptID: continuity.CommittedActionAttemptID(actionAttempt),
|
||||
DeliveryOutcome: deliveryOutcome,
|
||||
ResponseID: continuity.HumanResponseID(responseID),
|
||||
ResponseUtteranceID: responseUtterance,
|
||||
ResponseOperation: continuity.HumanResponseOperation(responseOperation),
|
||||
ResponseOutcome: continuity.ResponseOutcome(responseOutcome),
|
||||
RespondedAtUnixMillis: respondedAt,
|
||||
Revision: continuity.AttentionRevision(attentionRevision),
|
||||
}
|
||||
}
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -260,6 +331,47 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
func scanAttentionItem(scanner interface {
|
||||
Scan(...any) error
|
||||
}) (continuity.AttentionItem, error) {
|
||||
var item continuity.AttentionItem
|
||||
var attentionID, attentionOwner, attentionScope, sourceKind, sourceProspective, sourceExternalRef, level, attentionState, attentionSummary string
|
||||
var actionIntent, authorizedAction, actionAttempt, deliveryOutcome, responseID, responseUtterance, responseOperation, responseOutcome string
|
||||
var dueAt, createdAt, deliveredAt, respondedAt int64
|
||||
var attentionRevision uint64
|
||||
if err := scanner.Scan(
|
||||
&attentionID, &attentionOwner, &attentionScope, &sourceKind, &sourceProspective, &sourceExternalRef, &level, &attentionState, &attentionSummary,
|
||||
&dueAt, &createdAt, &deliveredAt, &actionIntent, &authorizedAction, &actionAttempt, &deliveryOutcome,
|
||||
&responseID, &responseUtterance, &responseOperation, &responseOutcome, &respondedAt, &attentionRevision); err != nil {
|
||||
return continuity.AttentionItem{}, err
|
||||
}
|
||||
item = continuity.AttentionItem{
|
||||
ID: continuity.AttentionItemID(attentionID),
|
||||
OwnerPrincipalID: identity.PrincipalID(attentionOwner),
|
||||
ScopeID: authority.ScopeID(attentionScope),
|
||||
SourceKind: continuity.AttentionSourceKind(sourceKind),
|
||||
SourceProspectiveID: continuity.ProspectiveItemID(sourceProspective),
|
||||
SourceExternalRef: sourceExternalRef,
|
||||
Level: continuity.AttentionLevel(level),
|
||||
State: continuity.AttentionState(attentionState),
|
||||
Summary: attentionSummary,
|
||||
DueAtUnixMillis: dueAt,
|
||||
CreatedAtUnixMillis: createdAt,
|
||||
DeliveredAtUnixMillis: deliveredAt,
|
||||
ActionIntentID: continuity.CommittedActionIntentID(actionIntent),
|
||||
AuthorizedActionID: continuity.CommittedAuthorizedActionID(authorizedAction),
|
||||
ActionAttemptID: continuity.CommittedActionAttemptID(actionAttempt),
|
||||
DeliveryOutcome: deliveryOutcome,
|
||||
ResponseID: continuity.HumanResponseID(responseID),
|
||||
ResponseUtteranceID: responseUtterance,
|
||||
ResponseOperation: continuity.HumanResponseOperation(responseOperation),
|
||||
ResponseOutcome: continuity.ResponseOutcome(responseOutcome),
|
||||
RespondedAtUnixMillis: respondedAt,
|
||||
Revision: continuity.AttentionRevision(attentionRevision),
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response continuity.HumanResponse) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
@@ -292,8 +404,10 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response
|
||||
if tag.RowsAffected() != 1 {
|
||||
return ErrProspectiveStateConflict
|
||||
}
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
|
||||
return err
|
||||
if response.TargetAttentionID != "" {
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case continuity.ResponseSnooze:
|
||||
tag, err := tx.Exec(ctx, `UPDATE continuity.prospective_items SET state=$1, trigger_at_unix_millis=$2, revision=revision+1
|
||||
@@ -306,8 +420,10 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response
|
||||
if tag.RowsAffected() != 1 {
|
||||
return ErrProspectiveStateConflict
|
||||
}
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
|
||||
return err
|
||||
if response.TargetAttentionID != "" {
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case continuity.ResponseCancel:
|
||||
tag, err := tx.Exec(ctx, `UPDATE continuity.prospective_items SET state=$1, revision=revision+1
|
||||
@@ -320,10 +436,22 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response
|
||||
if tag.RowsAffected() != 1 {
|
||||
return ErrProspectiveStateConflict
|
||||
}
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionSuppressed); err != nil {
|
||||
return err
|
||||
if response.TargetAttentionID != "" {
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionSuppressed); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case continuity.ResponseAcknowledge, continuity.ResponseDismiss:
|
||||
if response.TargetAttentionID == "" {
|
||||
return ErrAttentionNotFound
|
||||
}
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
|
||||
return err
|
||||
}
|
||||
case continuity.ResponseContinue, continuity.ResponseReject:
|
||||
if response.TargetAttentionID == "" {
|
||||
return ErrAttentionNotFound
|
||||
}
|
||||
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -331,15 +459,55 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *ContinuityRepository) CreateAttentionItem(ctx context.Context, item continuity.AttentionItem) (continuity.AttentionItem, error) {
|
||||
err := r.pool.QueryRow(ctx, `INSERT INTO continuity.attention_items(
|
||||
attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,
|
||||
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
|
||||
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22)
|
||||
ON CONFLICT (source_kind, source_external_ref) WHERE source_external_ref <> '' DO UPDATE SET
|
||||
level=EXCLUDED.level,
|
||||
state=EXCLUDED.state,
|
||||
summary=EXCLUDED.summary,
|
||||
due_at_unix_millis=EXCLUDED.due_at_unix_millis,
|
||||
created_at_unix_millis=EXCLUDED.created_at_unix_millis,
|
||||
delivered_at_unix_millis=0,
|
||||
action_intent_id='',
|
||||
authorized_action_id='',
|
||||
action_attempt_id='',
|
||||
delivery_outcome='',
|
||||
response_id='',
|
||||
response_utterance_id='',
|
||||
response_operation='',
|
||||
response_outcome='',
|
||||
responded_at_unix_millis=0,
|
||||
revision=continuity.attention_items.revision+1
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis,
|
||||
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
|
||||
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`,
|
||||
item.ID, item.OwnerPrincipalID, item.ScopeID, item.SourceKind, item.SourceProspectiveID, item.SourceExternalRef, item.Level, item.State,
|
||||
item.Summary, item.DueAtUnixMillis, item.CreatedAtUnixMillis, item.DeliveredAtUnixMillis, item.ActionIntentID, item.AuthorizedActionID,
|
||||
item.ActionAttemptID, item.DeliveryOutcome, item.ResponseID, item.ResponseUtteranceID, item.ResponseOperation, item.ResponseOutcome,
|
||||
item.RespondedAtUnixMillis, item.Revision).Scan(
|
||||
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State,
|
||||
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID, &item.AuthorizedActionID,
|
||||
&item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID, &item.ResponseOperation, &item.ResponseOutcome,
|
||||
&item.RespondedAtUnixMillis, &item.Revision)
|
||||
if err != nil {
|
||||
return continuity.AttentionItem{}, fmt.Errorf("create attention item: %w", err)
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (r *ContinuityRepository) MarkAttentionDelivering(ctx context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision) (continuity.AttentionItem, error) {
|
||||
var item continuity.AttentionItem
|
||||
err := r.pool.QueryRow(ctx, `UPDATE continuity.attention_items SET state=$1, revision=revision+1
|
||||
WHERE attention_item_id=$2 AND state=$3 AND revision=$4
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis,
|
||||
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
|
||||
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`,
|
||||
continuity.AttentionDelivering, id, continuity.AttentionPending, revision).Scan(
|
||||
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
|
||||
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State,
|
||||
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
|
||||
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID,
|
||||
&item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision)
|
||||
@@ -355,12 +523,12 @@ func (r *ContinuityRepository) MarkAttentionDelivered(ctx context.Context, id co
|
||||
SET state=$1, revision=revision+1, delivered_at_unix_millis=$2, action_intent_id=$3, authorized_action_id=$4,
|
||||
action_attempt_id=$5, delivery_outcome=$6
|
||||
WHERE attention_item_id=$7 AND state=$8 AND revision=$9
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis,
|
||||
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
|
||||
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`,
|
||||
continuity.AttentionDelivered, deliveredAtUnixMillis, intentID, authorizedID, attemptID, outcome,
|
||||
id, continuity.AttentionDelivering, revision).Scan(
|
||||
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
|
||||
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State,
|
||||
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
|
||||
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID,
|
||||
&item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision)
|
||||
@@ -376,12 +544,12 @@ func (r *ContinuityRepository) MarkAttentionFailed(ctx context.Context, id conti
|
||||
SET state=$1, revision=revision+1, action_intent_id=$2, authorized_action_id=$3,
|
||||
action_attempt_id=$4, delivery_outcome=$5
|
||||
WHERE attention_item_id=$6 AND state=$7 AND revision=$8
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis,
|
||||
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
|
||||
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`,
|
||||
continuity.AttentionFailed, intentID, authorizedID, attemptID, outcome,
|
||||
id, continuity.AttentionDelivering, revision).Scan(
|
||||
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
|
||||
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State,
|
||||
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
|
||||
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID,
|
||||
&item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision)
|
||||
@@ -490,7 +658,7 @@ func scanAttentionRows(rows continuityRows) ([]continuity.AttentionItem, error)
|
||||
items := make([]continuity.AttentionItem, 0)
|
||||
for rows.Next() {
|
||||
var item continuity.AttentionItem
|
||||
if err := rows.Scan(&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID,
|
||||
if err := rows.Scan(&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef,
|
||||
&item.Level, &item.State, &item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis,
|
||||
&item.DeliveredAtUnixMillis, &item.ActionIntentID, &item.AuthorizedActionID, &item.ActionAttemptID,
|
||||
&item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID, &item.ResponseOperation,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type EpistemicsRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
func OpenEpistemics(ctx context.Context, config ContinuityConfig) (*EpistemicsRepository, error) {
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EpistemicsRepository{pool: pool}, nil
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) Close() { r.pool.Close() }
|
||||
|
||||
func (r *EpistemicsRepository) SaveSystemExpectation(ctx context.Context, value epistemics.SystemExpectation) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.system_expectations (
|
||||
expectation_id, original_intent_id, source_utterance_id, expected_outcome, expected_effect,
|
||||
confidence_millis, evidence_basis, recorded_at_unix_millis)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT DO NOTHING`,
|
||||
value.ID, value.OriginalIntentID, value.SourceUtteranceID, value.ExpectedOutcome, value.ExpectedEffect,
|
||||
value.ConfidenceMillis, value.EvidenceBasis, value.RecordedAtUnixMillis)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limit int) ([]epistemics.ExperienceRecord, error) {
|
||||
if limit < 1 || limit > 100 {
|
||||
return nil, fmt.Errorf("invalid experience limit")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
experience_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
|
||||
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
|
||||
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
|
||||
verification_evidence,confidence_millis,recorded_at_unix_millis
|
||||
FROM epistemics.experience_records ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var result []epistemics.ExperienceRecord
|
||||
for rows.Next() {
|
||||
var value epistemics.ExperienceRecord
|
||||
if err := rows.Scan(&value.ID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
|
||||
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
|
||||
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
|
||||
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, limit int) ([]epistemics.ReflectionCandidate, error) {
|
||||
if limit < 1 || limit > 100 {
|
||||
return nil, fmt.Errorf("invalid reflection limit")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
reflection_candidate_id,trigger,state,source_experience_id,original_intent_id,source_utterance_id,
|
||||
summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision
|
||||
FROM epistemics.reflection_candidates ORDER BY recorded_at_unix_millis DESC, reflection_candidate_id DESC LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var result []epistemics.ReflectionCandidate
|
||||
for rows.Next() {
|
||||
var value epistemics.ReflectionCandidate
|
||||
if err := rows.Scan(&value.ID, &value.Trigger, &value.State, &value.SourceExperienceID, &value.OriginalIntentID, &value.SourceUtteranceID,
|
||||
&value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) SaveExperienceRecord(ctx context.Context, value epistemics.ExperienceRecord) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.experience_records (
|
||||
experience_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
|
||||
capability_implementation_ref, capability_usage_observation_ref, capability_gap_ref, skill_ref, expectation_id,
|
||||
expected_outcome, actual_outcome, outcome_ref, outcome_detail, human_response_ref, correction_utterance_id,
|
||||
verification_evidence, confidence_millis, recorded_at_unix_millis)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21) ON CONFLICT DO NOTHING`,
|
||||
value.ID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
|
||||
value.CapabilityImplementationRef, value.CapabilityUsageObservationRef, value.CapabilityGapRef, value.SkillRef, value.ExpectationID,
|
||||
value.ExpectedOutcome, value.ActualOutcome, value.OutcomeRef, value.OutcomeDetail, value.HumanResponseRef, value.CorrectionUtteranceID,
|
||||
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) SaveReflectionCandidate(ctx context.Context, value epistemics.ReflectionCandidate) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.reflection_candidates (
|
||||
reflection_candidate_id, trigger, state, source_experience_id, original_intent_id, source_utterance_id,
|
||||
summary, evidence_refs, requires_cognition, recorded_at_unix_millis, revision)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`,
|
||||
value.ID, value.Trigger, value.State, value.SourceExperienceID, value.OriginalIntentID, value.SourceUtteranceID,
|
||||
value.Summary, value.EvidenceRefs, value.RequiresCognition, value.RecordedAtUnixMillis, value.Revision)
|
||||
return err
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
type IdentityRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
func OpenIdentity(ctx context.Context, config ContinuityConfig) (*IdentityRepository, error) {
|
||||
pool, err := pgxpool.New(ctx, fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password))
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
@@ -11,7 +10,7 @@ import (
|
||||
type InteractionRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
func OpenInteraction(ctx context.Context, c ContinuityConfig) (*InteractionRepository, error) {
|
||||
p, e := pgxpool.New(ctx, fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", c.Host, c.Port, c.Database, c.User, c.Password))
|
||||
p, e := openPool(ctx, c)
|
||||
return &InteractionRepository{p}, e
|
||||
}
|
||||
func (r *InteractionRepository) Close() { r.pool.Close() }
|
||||
@@ -44,15 +43,25 @@ func (r *InteractionRepository) AppendUtterance(ctx context.Context, c interacti
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *InteractionRepository) SaveSemanticResult(ctx context.Context, interpretation interaction.UtteranceInterpretation, projects []interaction.ProjectReference, relations []interaction.InteractionRelation) error {
|
||||
func (r *InteractionRepository) SaveSemanticResult(ctx context.Context, interpretation interaction.UtteranceInterpretation, candidates []interaction.SemanticCandidate, projects []interaction.ProjectReference, relations []interaction.InteractionRelation) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO interaction.utterance_interpretations(interpretation_id,utterance_id,speech_act,confidence_millis,source,assertion_verification,requested_outcome,clarification,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)`, interpretation.ID, interpretation.UtteranceID, interpretation.SpeechAct, interpretation.ConfidenceMillis, interpretation.Source, interpretation.AssertionVerification, interpretation.RequestedOutcome, interpretation.Clarification, interpretation.RecordedAtUnixMillis); err != nil {
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO interaction.utterance_interpretations(interpretation_id,utterance_id,speech_act,secondary_speech_act,confidence_millis,source,assertion_verification,requested_outcome,clarification,needs_cognition,applicability,selection_reason,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`, interpretation.ID, interpretation.UtteranceID, interpretation.SpeechAct, interpretation.SecondarySpeechAct, interpretation.ConfidenceMillis, interpretation.Source, interpretation.AssertionVerification, interpretation.RequestedOutcome, interpretation.Clarification, interpretation.NeedsCognition, interpretation.Applicability, interpretation.SelectionReason, interpretation.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO interaction.semantic_candidates(candidate_id,utterance_id,source,selection_state,primary_speech_act,secondary_speech_act,confidence_millis,applicability,requested_outcome,clarification,needs_cognition,selection_reason,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT(candidate_id) DO UPDATE SET selection_state=EXCLUDED.selection_state,primary_speech_act=EXCLUDED.primary_speech_act,secondary_speech_act=EXCLUDED.secondary_speech_act,confidence_millis=EXCLUDED.confidence_millis,applicability=EXCLUDED.applicability,requested_outcome=EXCLUDED.requested_outcome,clarification=EXCLUDED.clarification,needs_cognition=EXCLUDED.needs_cognition,selection_reason=EXCLUDED.selection_reason,recorded_at_unix_millis=EXCLUDED.recorded_at_unix_millis`, candidate.ID, candidate.UtteranceID, candidate.Source, candidate.SelectionState, candidate.PrimarySpeechAct, candidate.SecondarySpeechAct, candidate.ConfidenceMillis, candidate.Applicability, candidate.RequestedOutcome, candidate.Clarification, candidate.NeedsCognition, candidate.SelectionReason, candidate.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, hint := range candidate.CapabilityHints {
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO interaction.semantic_candidate_capability_hints(candidate_id,capability_kind) VALUES($1,$2) ON CONFLICT DO NOTHING`, candidate.ID, hint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, project := range projects {
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO interaction.project_references(project_reference_id,utterance_id,project_id,project_display_name,role,provenance,confidence_millis,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8)`, project.ID, project.UtteranceID, project.ProjectID, project.ProjectDisplayName, project.Role, project.Provenance, project.ConfidenceMillis, project.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
|
||||
@@ -2,7 +2,6 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
@@ -13,8 +12,7 @@ import (
|
||||
type SituationRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
func OpenSituation(ctx context.Context, config ContinuityConfig) (*SituationRepository, error) {
|
||||
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password)
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
@@ -10,9 +12,14 @@ import (
|
||||
|
||||
type WorkRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
var (
|
||||
ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict")
|
||||
ErrAgentLeaseUnavailable = errors.New("agent lease unavailable")
|
||||
ErrAgentCallbackRejected = errors.New("agent callback rejected")
|
||||
)
|
||||
|
||||
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
|
||||
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password)
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,6 +47,105 @@ func (r *WorkRepository) Load(ctx context.Context, id work.TaskID) (work.TaskTar
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveDispatchIntent(ctx context.Context, intent work.AgentDispatchIntent) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
_, err = tx.Exec(ctx, `INSERT INTO work.agent_dispatch_intents(dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(idempotency_key) DO NOTHING`, intent.DispatchID, intent.IdempotencyKey, intent.DelegationID, intent.RunID, intent.ImplementationID, intent.ImplementationRevision, intent.SourceRevision, intent.WorktreeID, intent.VerificationPlanRef, intent.CreatedAtUnixMillis)
|
||||
if err != nil {
|
||||
return fmt.Errorf("save dispatch intent: %w", err)
|
||||
}
|
||||
var existing work.AgentDispatchIntent
|
||||
err = tx.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE idempotency_key=$1`, intent.IdempotencyKey).Scan(&existing.DispatchID, &existing.IdempotencyKey, &existing.DelegationID, &existing.RunID, &existing.ImplementationID, &existing.ImplementationRevision, &existing.SourceRevision, &existing.WorktreeID, &existing.VerificationPlanRef, &existing.CreatedAtUnixMillis)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read dispatch intent: %w", err)
|
||||
}
|
||||
if existing != intent {
|
||||
return ErrDispatchIntentConflict
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) EnqueueDispatch(ctx context.Context, item work.AgentDispatchQueueItem) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO work.agent_dispatch_queue(dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(dispatch_id) DO NOTHING`, item.DispatchID, item.State, item.AvailableAtUnixMillis, item.Attempts, item.ClaimedBy, item.UpdatedAtUnixMillis)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *WorkRepository) ClaimNextDispatch(ctx context.Context, workerID string, nowUnixMillis int64) (work.AgentDispatchQueueItem, error) {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return work.AgentDispatchQueueItem{}, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
var item work.AgentDispatchQueueItem
|
||||
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
|
||||
if err != nil {
|
||||
return work.AgentDispatchQueueItem{}, err
|
||||
}
|
||||
item.State = work.AgentDispatchClaimed
|
||||
item.Attempts++
|
||||
item.ClaimedBy = workerID
|
||||
item.UpdatedAtUnixMillis = nowUnixMillis
|
||||
_, err = tx.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=$1,attempts=$2,claimed_by=$3,updated_at_unix_millis=$4 WHERE dispatch_id=$5 AND state='QUEUED'`, item.State, item.Attempts, item.ClaimedBy, item.UpdatedAtUnixMillis, item.DispatchID)
|
||||
if err != nil {
|
||||
return work.AgentDispatchQueueItem{}, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return work.AgentDispatchQueueItem{}, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) CompleteDispatch(ctx context.Context, dispatchID, workerID string, state work.AgentDispatchQueueState, nowUnixMillis int64) error {
|
||||
if state != work.AgentDispatchCompleted && state != work.AgentDispatchFailed {
|
||||
return fmt.Errorf("invalid terminal dispatch state: %s", state)
|
||||
}
|
||||
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=$1,updated_at_unix_millis=$2 WHERE dispatch_id=$3 AND state='CLAIMED' AND claimed_by=$4`, state, nowUnixMillis, dispatchID, workerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if command.RowsAffected() != 1 {
|
||||
return ErrAgentCallbackRejected
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveAgentLease(ctx context.Context, lease work.AgentLease) error {
|
||||
err := r.pool.QueryRow(ctx, `INSERT INTO work.agent_leases(lease_id,run_id,worker_id,fencing_token,state,acquired_at_unix_millis,expires_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(run_id) DO UPDATE SET lease_id=EXCLUDED.lease_id,worker_id=EXCLUDED.worker_id,fencing_token=EXCLUDED.fencing_token,state=EXCLUDED.state,acquired_at_unix_millis=EXCLUDED.acquired_at_unix_millis,expires_at_unix_millis=EXCLUDED.expires_at_unix_millis WHERE work.agent_leases.state='EXPIRED' OR work.agent_leases.expires_at_unix_millis <= EXCLUDED.acquired_at_unix_millis RETURNING run_id`, lease.ID, lease.RunID, lease.WorkerID, lease.FencingToken, lease.State, lease.AcquiredAt.UnixMilli(), lease.ExpiresAt.UnixMilli()).Scan(new(string))
|
||||
if err != nil {
|
||||
return ErrAgentLeaseUnavailable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) RenewAgentLease(ctx context.Context, lease work.AgentLease, expiresAt time.Time) error {
|
||||
command, err := r.pool.Exec(ctx, `UPDATE work.agent_leases SET expires_at_unix_millis=$1 WHERE run_id=$2 AND worker_id=$3 AND fencing_token=$4 AND state='ACTIVE' AND expires_at_unix_millis > $5`, expiresAt.UnixMilli(), lease.RunID, lease.WorkerID, lease.FencingToken, time.Now().UnixMilli())
|
||||
if err != nil || command.RowsAffected() != 1 {
|
||||
return ErrAgentLeaseUnavailable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) AcceptAgentCallback(ctx context.Context, callback work.AgentCallback) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
var runID string
|
||||
err = tx.QueryRow(ctx, `SELECT run_id FROM work.agent_leases WHERE run_id=$1 AND worker_id=$2 AND fencing_token=$3 AND state='ACTIVE' AND expires_at_unix_millis > $4 FOR UPDATE`, callback.RunID, callback.WorkerID, callback.FencingToken, callback.ObservedAt.UnixMilli()).Scan(&runID)
|
||||
if err != nil {
|
||||
return ErrAgentCallbackRejected
|
||||
}
|
||||
command, err := tx.Exec(ctx, `INSERT INTO work.agent_callback_heads(run_id,last_sequence,updated_at_unix_millis) VALUES($1,$2,$3) ON CONFLICT(run_id) DO UPDATE SET last_sequence=EXCLUDED.last_sequence,updated_at_unix_millis=EXCLUDED.updated_at_unix_millis WHERE work.agent_callback_heads.last_sequence < EXCLUDED.last_sequence`, runID, callback.Sequence, callback.ObservedAt.UnixMilli())
|
||||
if err != nil || command.RowsAffected() != 1 {
|
||||
return ErrAgentCallbackRejected
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SavePlanningResult(ctx context.Context, result work.PlanningResult) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
@@ -75,3 +181,221 @@ func (r *WorkRepository) SavePlanningResult(ctx context.Context, result work.Pla
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveDelegationBundle(ctx context.Context, delegation work.Delegation, run work.AgentRun) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.delegations(
|
||||
delegation_id,goal_id,work_plan_id,plan_candidate_id,capability_requirement_id,capability_kind,
|
||||
selected_implementation_id,selected_implementation_revision,selected_implementation_kind,protocol,
|
||||
source_intent_id,source_utterance_id,conversation_id,project_id,project_display_name,desired_outcome,
|
||||
constraint_summary,verification_summary,context_summary,context_refs,state,latest_run_id,latest_result_id,
|
||||
created_at_unix_millis,updated_at_unix_millis)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25)
|
||||
ON CONFLICT(delegation_id) DO NOTHING`,
|
||||
delegation.ID, delegation.GoalID, delegation.PlanID, delegation.PlanCandidateID, delegation.CapabilityRequirementID,
|
||||
delegation.CapabilityKind, delegation.SelectedImplementationID, delegation.SelectedImplementationRev,
|
||||
delegation.SelectedImplementationKind, delegation.Protocol, delegation.SourceIntentID, delegation.SourceUtteranceID,
|
||||
delegation.ConversationID, delegation.ProjectID, delegation.ProjectDisplayName, delegation.DesiredOutcome,
|
||||
delegation.ConstraintSummary, delegation.VerificationSummary, delegation.ContextSummary, delegation.ContextRefs,
|
||||
delegation.State, delegation.LatestRunID, delegation.LatestResultID, delegation.CreatedAtUnixMillis, delegation.UpdatedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.agent_runs(
|
||||
run_id,delegation_id,implementation_id,implementation_revision,protocol,state,derived_instruction_summary,
|
||||
started_at_unix_millis,updated_at_unix_millis,completed_at_unix_millis)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(run_id) DO NOTHING`,
|
||||
run.ID, run.DelegationID, run.ImplementationID, run.ImplementationRevision, run.Protocol, run.State,
|
||||
run.DerivedInstructionSummary, run.StartedAtUnixMillis, run.UpdatedAtUnixMillis, run.CompletedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveAgentCheckpoint(ctx context.Context, run work.AgentRun, checkpoint work.AgentCheckpoint) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err = tx.Exec(ctx, `UPDATE work.agent_runs
|
||||
SET state=$2,updated_at_unix_millis=$3,completed_at_unix_millis=$4
|
||||
WHERE run_id=$1`, run.ID, run.State, run.UpdatedAtUnixMillis, run.CompletedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `UPDATE work.delegations
|
||||
SET state=$2,updated_at_unix_millis=$3,latest_run_id=$4
|
||||
WHERE delegation_id=$1`, checkpoint.DelegationID, delegationStateForRun(run.State), checkpoint.RecordedAtUnixMillis, run.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.agent_checkpoints(
|
||||
checkpoint_id,delegation_id,run_id,state,blocker_class,completed_summary,remaining_summary,test_summary,
|
||||
branch_name,commit_hash,next_step,artifact_refs,recorded_at_unix_millis)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT(checkpoint_id) DO NOTHING`,
|
||||
checkpoint.ID, checkpoint.DelegationID, checkpoint.RunID, checkpoint.State, checkpoint.BlockerClass,
|
||||
checkpoint.CompletedSummary, checkpoint.RemainingSummary, checkpoint.TestSummary, checkpoint.BranchName,
|
||||
checkpoint.CommitHash, checkpoint.NextStep, checkpoint.ArtifactRefs, checkpoint.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveAgentResult(ctx context.Context, run work.AgentRun, result work.AgentResult) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err = tx.Exec(ctx, `UPDATE work.agent_runs
|
||||
SET state=$2,updated_at_unix_millis=$3,completed_at_unix_millis=$4
|
||||
WHERE run_id=$1`, run.ID, run.State, run.UpdatedAtUnixMillis, run.CompletedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `UPDATE work.delegations
|
||||
SET state=$2,updated_at_unix_millis=$3,latest_run_id=$4,latest_result_id=$5
|
||||
WHERE delegation_id=$1`, result.DelegationID, work.DelegationVerifying, result.RecordedAtUnixMillis, run.ID, result.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.agent_results(
|
||||
result_id,delegation_id,run_id,state,summary,outcome_ref,verification_required,verification_summary,artifact_refs,recorded_at_unix_millis)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(result_id) DO NOTHING`,
|
||||
result.ID, result.DelegationID, result.RunID, result.State, result.Summary, result.OutcomeRef, result.VerificationRequired,
|
||||
result.VerificationSummary, result.ArtifactRefs, result.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveResumedRun(ctx context.Context, delegation work.Delegation, run work.AgentRun) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err = tx.Exec(ctx, `UPDATE work.delegations
|
||||
SET state=$2,updated_at_unix_millis=$3,latest_run_id=$4
|
||||
WHERE delegation_id=$1`, delegation.ID, delegation.State, delegation.UpdatedAtUnixMillis, delegation.LatestRunID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.agent_runs(
|
||||
run_id,delegation_id,implementation_id,implementation_revision,protocol,state,derived_instruction_summary,
|
||||
started_at_unix_millis,updated_at_unix_millis,completed_at_unix_millis)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(run_id) DO NOTHING`,
|
||||
run.ID, run.DelegationID, run.ImplementationID, run.ImplementationRevision, run.Protocol, run.State,
|
||||
run.DerivedInstructionSummary, run.StartedAtUnixMillis, run.UpdatedAtUnixMillis, run.CompletedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveDelegationVerification(ctx context.Context, delegation work.Delegation, verification work.DelegationVerification) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err = tx.Exec(ctx, `UPDATE work.delegations
|
||||
SET state=$2,updated_at_unix_millis=$3
|
||||
WHERE delegation_id=$1`, delegation.ID, delegation.State, delegation.UpdatedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.delegation_verifications(
|
||||
verification_id,delegation_id,run_id,result_id,verified,summary,evidence_refs,recorded_at_unix_millis)
|
||||
VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(verification_id) DO NOTHING`,
|
||||
verification.ID, verification.DelegationID, verification.RunID, verification.ResultID, verification.Verified,
|
||||
verification.Summary, verification.EvidenceRefs, verification.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) LatestAgentResult(ctx context.Context, delegationID work.DelegationID) (work.AgentResult, error) {
|
||||
var value work.AgentResult
|
||||
err := r.pool.QueryRow(ctx, `SELECT result_id,delegation_id,run_id,state,summary,outcome_ref,verification_required,verification_summary,artifact_refs,recorded_at_unix_millis
|
||||
FROM work.agent_results WHERE delegation_id=$1 ORDER BY recorded_at_unix_millis DESC, result_id DESC LIMIT 1`, delegationID).
|
||||
Scan(&value.ID, &value.DelegationID, &value.RunID, &value.State, &value.Summary, &value.OutcomeRef, &value.VerificationRequired, &value.VerificationSummary, &value.ArtifactRefs, &value.RecordedAtUnixMillis)
|
||||
if err != nil {
|
||||
return work.AgentResult{}, fmt.Errorf("load latest agent result: %w", err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) CountAgentRuns(ctx context.Context, delegationID work.DelegationID) (int, error) {
|
||||
var count int
|
||||
err := r.pool.QueryRow(ctx, `SELECT COUNT(*) FROM work.agent_runs WHERE delegation_id=$1`, delegationID).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("count agent runs: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) LatestAgentCheckpoint(ctx context.Context, delegationID work.DelegationID) (work.AgentCheckpoint, error) {
|
||||
var value work.AgentCheckpoint
|
||||
err := r.pool.QueryRow(ctx, `SELECT checkpoint_id,delegation_id,run_id,state,blocker_class,completed_summary,remaining_summary,test_summary,branch_name,commit_hash,next_step,artifact_refs,recorded_at_unix_millis
|
||||
FROM work.agent_checkpoints WHERE delegation_id=$1 ORDER BY recorded_at_unix_millis DESC, checkpoint_id DESC LIMIT 1`, delegationID).
|
||||
Scan(&value.ID, &value.DelegationID, &value.RunID, &value.State, &value.BlockerClass, &value.CompletedSummary, &value.RemainingSummary,
|
||||
&value.TestSummary, &value.BranchName, &value.CommitHash, &value.NextStep, &value.ArtifactRefs, &value.RecordedAtUnixMillis)
|
||||
if err != nil {
|
||||
return work.AgentCheckpoint{}, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) LoadDelegation(ctx context.Context, id work.DelegationID) (work.Delegation, error) {
|
||||
var value work.Delegation
|
||||
err := r.pool.QueryRow(ctx, `SELECT delegation_id,goal_id,work_plan_id,plan_candidate_id,capability_requirement_id,capability_kind,
|
||||
selected_implementation_id,selected_implementation_revision,selected_implementation_kind,protocol,
|
||||
source_intent_id,source_utterance_id,conversation_id,project_id,project_display_name,desired_outcome,constraint_summary,
|
||||
verification_summary,context_summary,context_refs,state,latest_run_id,latest_result_id,created_at_unix_millis,updated_at_unix_millis
|
||||
FROM work.delegations WHERE delegation_id=$1`, id).
|
||||
Scan(&value.ID, &value.GoalID, &value.PlanID, &value.PlanCandidateID, &value.CapabilityRequirementID, &value.CapabilityKind,
|
||||
&value.SelectedImplementationID, &value.SelectedImplementationRev, &value.SelectedImplementationKind, &value.Protocol,
|
||||
&value.SourceIntentID, &value.SourceUtteranceID, &value.ConversationID, &value.ProjectID, &value.ProjectDisplayName,
|
||||
&value.DesiredOutcome, &value.ConstraintSummary, &value.VerificationSummary, &value.ContextSummary, &value.ContextRefs,
|
||||
&value.State, &value.LatestRunID, &value.LatestResultID, &value.CreatedAtUnixMillis, &value.UpdatedAtUnixMillis)
|
||||
if err != nil {
|
||||
return work.Delegation{}, fmt.Errorf("load delegation: %w", err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) LoadAgentRun(ctx context.Context, id work.AgentRunID) (work.AgentRun, error) {
|
||||
var value work.AgentRun
|
||||
err := r.pool.QueryRow(ctx, `SELECT run_id,delegation_id,implementation_id,implementation_revision,protocol,state,derived_instruction_summary,started_at_unix_millis,updated_at_unix_millis,completed_at_unix_millis
|
||||
FROM work.agent_runs WHERE run_id=$1`, id).
|
||||
Scan(&value.ID, &value.DelegationID, &value.ImplementationID, &value.ImplementationRevision, &value.Protocol, &value.State, &value.DerivedInstructionSummary,
|
||||
&value.StartedAtUnixMillis, &value.UpdatedAtUnixMillis, &value.CompletedAtUnixMillis)
|
||||
if err != nil {
|
||||
return work.AgentRun{}, fmt.Errorf("load agent run: %w", err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func delegationStateForRun(state work.AgentRunState) work.DelegationState {
|
||||
switch state {
|
||||
case work.AgentRunStarted, work.AgentRunRunning:
|
||||
return work.DelegationRunning
|
||||
case work.AgentRunWaiting, work.AgentRunCheckpointed, work.AgentRunExecutionWindowExhausted:
|
||||
return work.DelegationWaiting
|
||||
case work.AgentRunBlocked:
|
||||
return work.DelegationBlocked
|
||||
case work.AgentRunCompleted:
|
||||
return work.DelegationVerifying
|
||||
case work.AgentRunCancelled:
|
||||
return work.DelegationCancelled
|
||||
case work.AgentRunFailed:
|
||||
return work.DelegationFailed
|
||||
default:
|
||||
return work.DelegationPending
|
||||
}
|
||||
}
|
||||
|
||||
func latestRowErr(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# ADR-0016 — Experience, Reflection and Skill Evolution Foundation V1
|
||||
|
||||
Status: Accepted
|
||||
|
||||
Task: DEV-000067
|
||||
|
||||
## Context
|
||||
|
||||
Domovoy can now receive human utterances, interpret intent, optionally plan work, resolve capabilities, perform narrow actions through existing owner boundaries, observe outcomes and accept human responses. The next architecture step is to preserve what happened and why, so repeated successful understanding may become reusable skill while failures, corrections and drift can return the system to broader thinking.
|
||||
|
||||
This must not introduce a giant learning manager, self-modifying loop, prompt optimizer or permanent hand-coded user profile. Learning evidence must remain historical and revisable.
|
||||
|
||||
## Decision
|
||||
|
||||
1. Do not create a new canonical owner for DEV-000067.
|
||||
2. `epistemics` owns provider-neutral experience, expectation, outcome and reflection-candidate semantics because these are evidence/provenance about what happened and why. It stores typed references to Interaction, Continuity, Work and Capability records rather than copying their canonical state.
|
||||
3. `capability` owns skill-profile and fast-path lifecycle semantics because Skill V1 is a reusable capability/implementation pattern derived from evidence, not a new authority or profession category.
|
||||
4. A `SystemExpectation` records what the system expected from meaningful help before outcome is known. "Model returned text" and "HTTP 200" are not sufficient success semantics.
|
||||
5. `ExperienceRecord` links original human intent/utterance, context refs, selected help method, capability/implementation refs, expectation, outcome class, human response/correction refs and verification evidence.
|
||||
6. `ReflectionCandidate` is append-only evidence that broader reflection may be warranted. It may be deterministic or routed through the Cognitive Gateway, but model output cannot directly mutate canonical beliefs, skills, authority, Go source or deployment.
|
||||
7. Skill lifecycle is explicit: `CANDIDATE`, `EVALUATING`, `VALIDATED`, `DEGRADED`, `SUSPENDED`, `RETIRED`. Replacing or suspending a skill never deletes source observations, experiences, corrections, outcomes, beliefs or original human intent history.
|
||||
8. Fast paths are optimizations, not truth. Every optimized path must be able to return `NOT_APPLICABLE`, `LOW_CONFIDENCE`, `OUT_OF_DISTRIBUTION`, `CONFLICT` or `STALE` and escalate to broader reasoning/clarification.
|
||||
9. User corrections are strong learning evidence but are not automatically permanent rules. They create experience/reflection evidence and may later support revisable beliefs or skill changes.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Interaction remains canonical owner of conversations, utterances, original human intent and corrections.
|
||||
- Continuity remains canonical owner of HumanResponse, Attention and ProspectiveItem continuation state.
|
||||
- Capability remains canonical owner of capability implementations, gaps, usage observations and skill profiles.
|
||||
- Epistemics records evidence/reflection using typed refs only; it does not mutate other owners.
|
||||
- No external mutation, new authority, new provider adapter, deployment authority expansion or self-modifying code path is introduced.
|
||||
- Cognitive reflection, if later used, must go through Cognitive Gateway/Router and structured validation.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No LearningManager, reinforcement-learning platform, autonomous curriculum, skill dashboard, universal user profile, plugin marketplace or self-modifying development loop.
|
||||
- No automatic activation/promotion of cognitive models or capability implementations.
|
||||
- No new canonical owner.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Production evidence such as reminder acceptance, human correction, capability gaps, cognitive advisory outcomes and repeated capability failures can be represented consistently.
|
||||
- Validated fast paths can stay cheap, while stale/low-confidence/out-of-distribution cases can return to thinking instead of being forced into old rules.
|
||||
- Knowledge and experience survive skill implementation replacement.
|
||||
@@ -0,0 +1,54 @@
|
||||
# ADR-0017 — General Agent Delegation and Codex Bridge V1
|
||||
|
||||
Status: Accepted
|
||||
|
||||
Task: DEV-000069
|
||||
|
||||
## Context
|
||||
|
||||
Domovoy already has Human Interaction, Situation/context, optional durable Work/Plan, Capability registry/resolution, Continuity/Attention, HumanResponse, Experience/Reflection, Skill evolution, Secret Resolver and Cognitive Gateway/Router. The next step is not "make Codex the center", but to let Domovoy delegate suitable work to a general external intelligent agent while preserving canonical human intent, work semantics, authority boundaries and future portability to non-Codex agents.
|
||||
|
||||
The first real implementation is Codex for software-engineering work. However, a model is not an external agent. Cognitive models provide advice/candidates, while an external agent may inspect state, use tools, modify files, run tests, maintain a long-running task, checkpoint, resume and return evidence-bearing results.
|
||||
|
||||
## Decision
|
||||
|
||||
1. Do not create a new canonical owner for DEV-000069.
|
||||
2. `capability` remains the canonical owner for agent implementation registration by reusing `CapabilityImplementation` with `Kind=AGENT`. A Codex bridge is one concrete implementation, not a new canonical capability identity.
|
||||
3. `work` remains the canonical owner for durable delegation lifecycle. DEV-000069 extends Work semantics from planning-only `DelegationCandidate` to real delegation execution state such as `Delegation`, `AgentRun`, `AgentCheckpoint` and `AgentResult`.
|
||||
4. `continuity` remains the canonical owner for human waiting/attention semantics. Human-dependent blockers or completed long-running delegations surface through Attention rather than a Codex-specific notification path.
|
||||
5. `epistemics` remains the canonical owner for delegation outcome evidence through `ExperienceRecord` and later reflection, using typed refs to Work, Capability, Artifact and Interaction state.
|
||||
6. `artifact` remains the place for durable artifact metadata/refs when an agent run produces file, diff, test, log, PR or deployment evidence. Work stores typed refs, not payload copies.
|
||||
7. `OriginalHumanIntent`, `Goal`, `Work`, `CapabilityRequirement` and `Delegation` must remain canonical semantic records. Agent-specific instruction or prompt text is only a derived adapter artifact with provenance that distinguishes:
|
||||
- what the human asked;
|
||||
- what Domovoy inferred;
|
||||
- what project/runtime context was added;
|
||||
- how completion will be verified.
|
||||
8. Execution-window exhaustion is a run lifecycle state, not automatic task failure. The same higher-level delegation may continue through later runs while preserving checkpoint/history.
|
||||
9. Codex transport/UI wording is adapter-local. Canonical state must stay valid if the coding agent is replaced by another future agent.
|
||||
|
||||
## Ownership
|
||||
|
||||
- `interaction` owns the append-only human request/correction history and original intent traceability.
|
||||
- `situation` resolves project/repository context and active focus used for delegation context compilation.
|
||||
- `work` owns durable delegation requests, selected implementation refs, run lifecycle, checkpoint/result refs and verification gating before Work completion.
|
||||
- `capability` owns executor-neutral capability identity and concrete eligible implementations, including `AGENT` implementations such as Codex.
|
||||
- `continuity` owns waiting/attention/human-response continuation for delegation progress and genuine human blockers.
|
||||
- `epistemics` owns experience/reflection evidence derived from delegation outcomes.
|
||||
- `artifact` owns durable metadata for diffs/logs/checkpoints/results when they must survive outside a transient adapter response.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- `SOFTWARE_ENGINEERING`, `MODIFY_CODE`, `INSPECT_REPOSITORY` and `RUN_TESTS` remain executor-neutral capability semantics.
|
||||
- `MODIFY_CODE != CODEX`.
|
||||
- Canonical Work/Delegation does not become a Codex prompt.
|
||||
- Secret access remains least-privilege through CredentialRef/Secret Resolver. Agent capability availability does not imply credential access or authority.
|
||||
- A completed agent run does not automatically mark higher-level Work complete. Verification must inspect available evidence.
|
||||
- Technical failures, execution-window exhaustion and human-dependent blockers are distinct lifecycle states.
|
||||
- `LLM-free` startup must continue to pass when Codex is unavailable.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Domovoy can delegate suitable software-engineering work without forcing the human to author a giant Codex prompt.
|
||||
- The first production bridge can target Codex while leaving canonical semantics portable to future scientific, accounting, research or alternate coding agents.
|
||||
- Long-running agent work can checkpoint and continue across bounded execution windows.
|
||||
- Human chat remains simple, while operator/debug tooling can inspect the durable delegation/run/checkpoint/result state.
|
||||
@@ -0,0 +1,52 @@
|
||||
# ADR-0018 — External audit and exploration mode
|
||||
|
||||
Status: Accepted
|
||||
Date: 2026-08-22
|
||||
Accepted under: ARCH-000012
|
||||
|
||||
## Decision
|
||||
|
||||
1. Domovoy may operate in a test-only `Exploration` profile when an ordinary
|
||||
local conversation or cognitive-provider recovery request is received.
|
||||
Exploration permits contextual reasoning, allowlisted read-only diagnostics,
|
||||
hypothesis generation, alternatives and a reversible next-step proposal.
|
||||
2. Exploration is not an authority grant. It does not grant access to raw
|
||||
secrets, executor credentials, a provider's unapproved data-egress path, or
|
||||
direct mutation of canonical or external state.
|
||||
3. Audit is an independently deployed, rebuildable projection of compact,
|
||||
redacted action/proposal events. It is not a canonical owner and never
|
||||
executes actions.
|
||||
4. An attributed external audit verdict is an Observation. A later Commit
|
||||
contract may require a current `ALLOW` Observation as an additional
|
||||
precondition for explicitly configured action classes. `REVIEW_REQUIRED`,
|
||||
`STOP`, missing or stale review do not authorize execution.
|
||||
5. Loss of the audit deployment does not remove exploration or approved
|
||||
read-only diagnostics. A review-governed external action remains a pending
|
||||
proposal; it is neither executed nor silently converted to failure.
|
||||
6. Audit payloads exclude raw credentials, secrets and private content. They
|
||||
carry stable identifiers/references, classifications, hashes and redacted
|
||||
summaries only.
|
||||
7. Initial delivery is staged: first semantic local dialogue and provider
|
||||
recovery, then capability-use telemetry, then audit-only projection. No
|
||||
existing executor action is review-governed until a later task names its
|
||||
class and recovery behavior.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Local cognition remains useful during provider loss and can seek a recovery
|
||||
plan instead of returning canned availability text.
|
||||
- Higher-level external scrutiny is possible without creating a competing
|
||||
executor or a second source of semantic truth.
|
||||
- Commit/Executor, semantic ownership, append-only evidence and unknown
|
||||
outcome semantics remain invariants, so external review has attributable
|
||||
evidence to inspect.
|
||||
- The first implementation task may change local semantic routing and
|
||||
capability telemetry only; audit projection and Commit gating need their
|
||||
own explicit contracts and failure/rebuild tests.
|
||||
|
||||
## Revisit triggers
|
||||
|
||||
- The first external audit projection is deployed.
|
||||
- A concrete external action class is proposed for review gating.
|
||||
- Audit needs a data class beyond redacted references.
|
||||
- The Exploration profile needs a new privilege or canonical owner.
|
||||
@@ -4,4 +4,4 @@ Accepted architecture decisions live here. Normal implementation tasks may read
|
||||
|
||||
Use `ADR-XXXX-title.md` with: problem, evidence, alternatives, decision, rationale, consequences, validity conditions, revisit triggers, migration/rollback, status.
|
||||
|
||||
Current decisions include `ADR-0006-creator-advisory-boundary.md`, the fail-closed provider-neutral advisory boundary, and `ADR-0007-openai-advisory-binding.md`, the accepted but not yet production-enabled OpenAI binding class.
|
||||
Current decisions include `ADR-0006-creator-advisory-boundary.md`, the fail-closed provider-neutral advisory boundary; `ADR-0007-openai-advisory-binding.md`, the accepted but not yet production-enabled OpenAI binding class; and `ADR-0018-external-audit-exploration-mode.md`, the staged external-audit and test Exploration profile.
|
||||
|
||||
@@ -65,7 +65,12 @@
|
||||
"WorkPlan",
|
||||
"PlanStep",
|
||||
"CapabilityRequirement",
|
||||
"DelegationCandidate"
|
||||
"DelegationCandidate",
|
||||
"Delegation",
|
||||
"AgentRun",
|
||||
"AgentCheckpoint",
|
||||
"AgentResult",
|
||||
"DelegationVerification"
|
||||
],
|
||||
"database_schema": "work",
|
||||
"boundary_class": "remote_capable",
|
||||
@@ -84,7 +89,10 @@
|
||||
"Observation",
|
||||
"Claim",
|
||||
"Belief",
|
||||
"Provenance"
|
||||
"Provenance",
|
||||
"SystemExpectation",
|
||||
"ExperienceRecord",
|
||||
"ReflectionCandidate"
|
||||
],
|
||||
"database_schema": "epistemics",
|
||||
"boundary_class": "remote_capable",
|
||||
@@ -122,7 +130,10 @@
|
||||
"CapabilitySelection",
|
||||
"CapabilityGap",
|
||||
"AcquisitionStrategyCandidate",
|
||||
"CapabilityUsageObservation"
|
||||
"CapabilityUsageObservation",
|
||||
"SkillProfile",
|
||||
"SkillEvidence",
|
||||
"FastPathDecision"
|
||||
],
|
||||
"database_schema": "capability",
|
||||
"boundary_class": "remote_capable",
|
||||
|
||||
@@ -25,17 +25,17 @@ modules:
|
||||
|
||||
situation:
|
||||
id: mod.situation
|
||||
owns: [Situation, FocusBinding]
|
||||
owns: [Situation, FocusBinding, ProjectRegistry, ActiveProjectFocus]
|
||||
database_schema: situation
|
||||
boundary_class: remote_capable
|
||||
may_depend_on: [contracts.identity, contracts.work, contracts.capability]
|
||||
may_depend_on: [contracts.identity, contracts.authority, contracts.work, contracts.capability]
|
||||
|
||||
work:
|
||||
id: mod.work
|
||||
owns: [Goal, Task, Decision]
|
||||
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate, Delegation, AgentRun, AgentCheckpoint, AgentResult, DelegationVerification]
|
||||
database_schema: work
|
||||
boundary_class: remote_capable
|
||||
may_depend_on: [contracts.identity, contracts.authority, platform.events]
|
||||
may_depend_on: [contracts.identity, contracts.authority, contracts.epistemics, contracts.capability, platform.ids, platform.events]
|
||||
|
||||
epistemics:
|
||||
id: mod.epistemics
|
||||
@@ -53,7 +53,7 @@ modules:
|
||||
|
||||
capability:
|
||||
id: mod.capability
|
||||
owns: [Capability, CapabilityBinding, CapabilityHealth]
|
||||
owns: [Capability, CapabilityBinding, CapabilityHealth, CapabilityImplementation, CapabilityResolution, CapabilitySelection, CapabilityGap, AcquisitionStrategyCandidate, CapabilityUsageObservation, SkillProfile, SkillEvidence, FastPathDecision]
|
||||
database_schema: capability
|
||||
boundary_class: remote_capable
|
||||
may_depend_on: [contracts.identity, contracts.authority, contracts.artifact]
|
||||
@@ -81,11 +81,12 @@ platform:
|
||||
- platform.inbox
|
||||
- platform.telemetry
|
||||
- platform.errors
|
||||
- platform.secrets
|
||||
rule: "platform owns mechanisms, never domain meaning"
|
||||
|
||||
runtime_topology_v1:
|
||||
core_runtime:
|
||||
modules: [identity, situation, work, epistemics, capability, artifact]
|
||||
modules: [identity, situation, work, epistemics, capability, artifact, interaction]
|
||||
continuity_runtime:
|
||||
modules: [continuity]
|
||||
commit_runtime:
|
||||
@@ -107,6 +108,7 @@ storage_topology_v1:
|
||||
capability: capability_role
|
||||
commit: commit_role
|
||||
artifact: artifact_role
|
||||
interaction: interaction_role
|
||||
cross_owner_fk: forbidden_by_default
|
||||
cross_owner_write: forbidden
|
||||
cross_owner_application_join: forbidden
|
||||
@@ -142,6 +144,10 @@ protected_paths:
|
||||
- architecture/architecture-registry.yaml
|
||||
- architecture/adrs/**
|
||||
- ci/architecture/**
|
||||
- .gitea/workflows/ci.yml
|
||||
- cmd/devctl/**
|
||||
- dev/ACTIVE_TASK.json
|
||||
- ci/architecture/**
|
||||
- security/**
|
||||
|
||||
default_feature_task_authority:
|
||||
|
||||
@@ -27,14 +27,14 @@ modules:
|
||||
|
||||
work:
|
||||
id: mod.work
|
||||
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate]
|
||||
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate, Delegation, AgentRun, AgentCheckpoint, AgentResult, DelegationVerification]
|
||||
database_schema: work
|
||||
boundary_class: remote_capable
|
||||
may_depend_on: [contracts.identity, contracts.authority, contracts.epistemics, contracts.capability, platform.ids, platform.events]
|
||||
|
||||
epistemics:
|
||||
id: mod.epistemics
|
||||
owns: [Observation, Claim, Belief, Provenance]
|
||||
owns: [Observation, Claim, Belief, Provenance, SystemExpectation, ExperienceRecord, ReflectionCandidate]
|
||||
database_schema: epistemics
|
||||
boundary_class: remote_capable
|
||||
may_depend_on: [contracts.identity, contracts.artifact, platform.events]
|
||||
@@ -48,7 +48,7 @@ modules:
|
||||
|
||||
capability:
|
||||
id: mod.capability
|
||||
owns: [Capability, CapabilityBinding, CapabilityHealth, CapabilityImplementation, CapabilityResolution, CapabilitySelection, CapabilityGap, AcquisitionStrategyCandidate, CapabilityUsageObservation]
|
||||
owns: [Capability, CapabilityBinding, CapabilityHealth, CapabilityImplementation, CapabilityResolution, CapabilitySelection, CapabilityGap, AcquisitionStrategyCandidate, CapabilityUsageObservation, SkillProfile, SkillEvidence, FastPathDecision]
|
||||
database_schema: capability
|
||||
boundary_class: remote_capable
|
||||
may_depend_on: [contracts.identity, contracts.authority, contracts.artifact]
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/modules/work/application"
|
||||
"git.cin.su/m/Assistant/modules/work/ports"
|
||||
)
|
||||
|
||||
type memoryStore struct{ writes int }
|
||||
|
||||
func (s *memoryStore) SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error {
|
||||
s.writes++
|
||||
return nil
|
||||
}
|
||||
func (s *memoryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
|
||||
func (s *memoryStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
|
||||
s.writes++
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
enabled := flag.Bool("enable-canary", false, "explicitly enable the manual canary")
|
||||
implementation := flag.String("implementation", "fake-worker", "allowlisted deterministic implementation")
|
||||
postgresMode := flag.Bool("postgres", false, "use the Work PostgreSQL repository instead of in-memory storage")
|
||||
flag.Parse()
|
||||
|
||||
now := time.UnixMilli(1_700_000_000_000)
|
||||
intent := work.AgentDispatchIntent{DispatchID: "manual-dispatch-1", IdempotencyKey: "manual-idempotency-1", DelegationID: "manual-delegation-1", RunID: "manual-run-1", ImplementationID: *implementation, ImplementationRevision: 1, SourceRevision: "manual-source-1", WorktreeID: "manual-worktree-1", VerificationPlanRef: "manual-verification-1", CreatedAtUnixMillis: now.UnixMilli()}
|
||||
var store ports.ExecutionStore = &memoryStore{}
|
||||
var closeStore func()
|
||||
if *postgresMode {
|
||||
repository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: os.Getenv("WORK_POSTGRES_PASSWORD")})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
store = repository
|
||||
closeStore = repository.Close
|
||||
}
|
||||
if closeStore != nil {
|
||||
defer closeStore()
|
||||
}
|
||||
result, err := application.ExecuteCanaryDryRun(context.Background(), store, application.CanaryConfig{Enabled: *enabled, ImplementationID: "fake-worker", MaxDispatches: 1}, 0, intent, "manual-worker", 1, now, 15*time.Minute)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
output := struct {
|
||||
DispatchID string `json:"dispatch_id"`
|
||||
RunID string `json:"run_id"`
|
||||
CallbackCount int `json:"callback_count"`
|
||||
StoreWrites int `json:"store_writes"`
|
||||
}{result.Intent.DispatchID, string(result.Intent.RunID), result.CallbackCount(), 0}
|
||||
if memory, ok := store.(*memoryStore); ok {
|
||||
output.StoreWrites = memory.writes
|
||||
}
|
||||
if err := json.NewEncoder(os.Stdout).Encode(output); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/models"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
realityevaluation "git.cin.su/m/Assistant/tests/reality_evaluation"
|
||||
)
|
||||
|
||||
type manifest struct {
|
||||
ArtifactID string `json:"artifact_id"`
|
||||
Providers []providerCandidate `json:"providers"`
|
||||
}
|
||||
|
||||
type providerCandidate struct {
|
||||
ProviderInstanceID string `json:"provider_instance_id"`
|
||||
ProviderKind string `json:"provider_kind"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Candidates []modelCandidate `json:"candidates"`
|
||||
}
|
||||
|
||||
type modelCandidate struct {
|
||||
ProviderModelID string `json:"provider_model_id"`
|
||||
ModelDescriptorID string `json:"model_descriptor_id"`
|
||||
ModelRevision int64 `json:"model_revision"`
|
||||
BindingID string `json:"binding_id"`
|
||||
BindingRevision int64 `json:"binding_revision"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
manifestPath := flag.String("manifest", "", "benchmark manifest JSON")
|
||||
suitePath := flag.String("suite", "", "cognitive evaluation suite JSON")
|
||||
outputPath := flag.String("output", "", "output artifact JSON")
|
||||
observedAt := flag.String("observed-at", "2026-08-17T12:00:00Z", "RFC3339 timestamp for reproducible artifact generation")
|
||||
timeout := flag.Duration("case-timeout", 30*time.Second, "deadline for each case")
|
||||
flag.Parse()
|
||||
if *manifestPath == "" || *suitePath == "" || *outputPath == "" || *timeout <= 0 {
|
||||
fatal(errors.New("manifest, suite, output and positive case-timeout are required"))
|
||||
}
|
||||
at, err := time.Parse(time.RFC3339, *observedAt)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
input, err := os.ReadFile(*manifestPath)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
var plan manifest
|
||||
if err := json.Unmarshal(input, &plan); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if len(plan.Providers) == 0 || len(plan.Providers[0].Candidates) == 0 {
|
||||
fatal(errors.New("manifest has no candidates"))
|
||||
}
|
||||
suite, err := realityevaluation.LoadCognitiveSuite(*suitePath)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
provider := plan.Providers[0]
|
||||
candidate := provider.Candidates[0]
|
||||
adapter, err := newProvider(provider)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
responses := map[string]string{}
|
||||
for index, item := range suite.Cases {
|
||||
caseCtx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||
response, err := adapter.Advise(caseCtx, cognitive.AdvisoryRequest{
|
||||
Version: cognitive.CreatorAdvisoryV1,
|
||||
RequestID: cognitive.RequestID(fmt.Sprintf("cognitive-eval-%d", index)),
|
||||
ActorID: "benchmark-actor",
|
||||
PrincipalID: "domovoy-creator-principal",
|
||||
ScopeID: "benchmark-public-synthetic",
|
||||
Message: item.Prompt,
|
||||
Locale: "ru-RU",
|
||||
MaxOutputBytes: 4096,
|
||||
ProviderBindingID: capability.CapabilityBindingID(candidate.BindingID),
|
||||
ProviderBindingRevision: candidate.BindingRevision,
|
||||
ProviderInstanceID: capability.ProviderInstanceID(provider.ProviderInstanceID),
|
||||
ModelDescriptorID: capability.ModelDescriptorID(candidate.ModelDescriptorID),
|
||||
ProviderModelID: capability.ProviderModelID(candidate.ProviderModelID),
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
responses[item.ID] = "ADAPTER_ERROR: " + err.Error()
|
||||
continue
|
||||
}
|
||||
responses[item.ID] = response.AdvisoryText
|
||||
}
|
||||
artifact, err := realityevaluation.EvaluateCognitiveSuite(
|
||||
suite,
|
||||
provider.ProviderInstanceID,
|
||||
candidate.ProviderModelID,
|
||||
candidate.ModelDescriptorID,
|
||||
candidate.BindingID,
|
||||
candidate.BindingRevision,
|
||||
responses,
|
||||
at,
|
||||
)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
payload, err := json.MarshalIndent(artifact, "", " ")
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
payload = append(payload, '\n')
|
||||
if err := os.WriteFile(*outputPath, payload, 0o600); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
fmt.Printf("artifact=%s total_cases=%d output=%s\n", artifact.ArtifactID, artifact.TotalCases, *outputPath)
|
||||
}
|
||||
|
||||
func newProvider(candidate providerCandidate) (models.LocalProvider, error) {
|
||||
switch candidate.ProviderKind {
|
||||
case "openai-compatible-chat-v1":
|
||||
return models.NewOpenAICompatibleLocalAdapter(candidate.Endpoint, nil)
|
||||
case "ollama-v1":
|
||||
return models.NewOllamaAdapter(candidate.Endpoint, nil)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported provider kind %q", candidate.ProviderKind)
|
||||
}
|
||||
}
|
||||
|
||||
func fatal(err error) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
realityevaluation "git.cin.su/m/Assistant/tests/reality_evaluation"
|
||||
)
|
||||
|
||||
func main() {
|
||||
suitePath := flag.String("suite", "", "path to versioned reality-evaluation suite JSON")
|
||||
outputPath := flag.String("output", "", "path to JSON artifact output")
|
||||
observedAt := flag.String("observed-at", "2026-08-17T12:00:00Z", "RFC3339 timestamp for reproducible artifact generation")
|
||||
flag.Parse()
|
||||
if *suitePath == "" || *outputPath == "" {
|
||||
fatal(fmt.Errorf("suite and output are required"))
|
||||
}
|
||||
at, err := time.Parse(time.RFC3339, *observedAt)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
suite, err := realityevaluation.LoadSuite(*suitePath)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
artifact, err := realityevaluation.RunSuite(suite, at)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
payload, err := json.MarshalIndent(artifact, "", " ")
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
payload = append(payload, '\n')
|
||||
if err := os.WriteFile(*outputPath, payload, 0o600); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
reportPath := strings.TrimSuffix(*outputPath, filepath.Ext(*outputPath)) + ".md"
|
||||
if err := os.WriteFile(reportPath, []byte(realityevaluation.MarkdownReport(artifact)), 0o600); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
fmt.Printf("artifact=%s total_cases=%d output=%s report=%s\n", artifact.ArtifactID, artifact.TotalCases, *outputPath, reportPath)
|
||||
}
|
||||
|
||||
func fatal(err error) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
discovered, probe := registration.Discovery.Discover(r.Context())
|
||||
snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: capability.DataPolicyLocalOnly, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()}
|
||||
snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, AuthMode: registration.AuthMode, BillingMode: registration.BillingMode, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: registration.DataPolicy, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()}
|
||||
for _, model := range discovered {
|
||||
snapshot.Models = append(snapshot.Models, capability.DiscoveredModelSnapshot{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, MaxContextBytes: model.MaxContextBytes, Modalities: model.Modalities, Capabilities: model.Capabilities, Installed: capability.OperationalYes, Loaded: capability.OperationalNotObserved, Invokable: capability.OperationalNotObserved})
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/models"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
@@ -29,6 +31,9 @@ type providerRegistration struct {
|
||||
Kind capability.ProviderKind
|
||||
EndpointRef capability.EndpointBindingRef
|
||||
CredentialRef capability.CredentialRef
|
||||
AuthMode capability.AuthMode
|
||||
BillingMode capability.BillingMode
|
||||
DataPolicy capability.DataPolicy
|
||||
Provider advisoryProvider
|
||||
Discovery models.LocalProvider
|
||||
}
|
||||
@@ -36,6 +41,8 @@ type providerRegistration struct {
|
||||
type cognitiveGateway struct {
|
||||
resolver bindingResolver
|
||||
providers map[capability.ProviderKind][]providerRegistration
|
||||
builtinLocal *providerRegistration
|
||||
externalRecovery []providerRegistration
|
||||
now func() time.Time
|
||||
allowCanary bool
|
||||
canaryMaxOutputBytes int
|
||||
@@ -71,10 +78,10 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
|
||||
if g.allowCanary && g.canaryMaxOutputBytes > 0 && maxOutputBytes > g.canaryMaxOutputBytes {
|
||||
maxOutputBytes = g.canaryMaxOutputBytes
|
||||
}
|
||||
requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(maxOutputBytes)}
|
||||
requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(maxOutputBytes), Objective: selectionObjectiveForMeaning(request.Meaning, request.Message)}
|
||||
selected, err := g.resolver.Resolve(r.Context(), requirement)
|
||||
if err != nil {
|
||||
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
|
||||
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
if selected.Binding.DataPolicy != capability.DataPolicyLocalOnly && !request.EgressAcknowledged {
|
||||
@@ -83,7 +90,7 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
provider := g.provider(selected)
|
||||
if provider == nil {
|
||||
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
|
||||
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
|
||||
@@ -110,9 +117,118 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
|
||||
response = fallbackResponse
|
||||
}
|
||||
}
|
||||
if response.FinishReason != cognitive.FinishCompleted && fallbackEligible(response.FinishReason) {
|
||||
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage})
|
||||
}
|
||||
|
||||
func selectionObjectiveForMessage(message string) capability.SelectionObjective {
|
||||
lower := strings.ToLower(message)
|
||||
if strings.Contains(lower, "дешев") || strings.Contains(lower, "бесплат") || strings.Contains(lower, "эконом") || strings.Contains(lower, "cost") || strings.Contains(lower, "cheap") {
|
||||
return capability.SelectionEconomy
|
||||
}
|
||||
if strings.Contains(lower, "быстр") || strings.Contains(lower, "срочно") || strings.Contains(lower, "статус") || strings.Contains(lower, "quick") || strings.Contains(lower, "fast") {
|
||||
return capability.SelectionSpeed
|
||||
}
|
||||
if strings.Contains(lower, "сложн") || strings.Contains(lower, "анализ") || strings.Contains(lower, "код") || strings.Contains(lower, "план") || strings.Contains(lower, "глубок") || strings.Contains(lower, "reason") || strings.Contains(lower, "code") {
|
||||
return capability.SelectionQuality
|
||||
}
|
||||
return capability.SelectionBalanced
|
||||
}
|
||||
|
||||
func selectionObjectiveForMeaning(meaning cognitive.TaskMeaning, message string) capability.SelectionObjective {
|
||||
if meaning.CapabilityKind != "" || len(meaning.CapabilityHints) > 0 || meaning.SpeechAct != "" || meaning.RequestedOutcome != "" {
|
||||
kinds := append([]capability.CapabilityKind{meaning.CapabilityKind}, meaning.CapabilityHints...)
|
||||
for _, kind := range kinds {
|
||||
switch kind {
|
||||
case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindVerifyFact, capability.CapabilityKindInspectProject, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs:
|
||||
return capability.SelectionQuality
|
||||
case capability.CapabilityKindScheduleReminder:
|
||||
return capability.SelectionSpeed
|
||||
}
|
||||
}
|
||||
if meaning.RequestedOutcome != "" || meaning.SpeechAct == "REQUEST_TO_INVESTIGATE" || meaning.SpeechAct == "DIRECTIVE" {
|
||||
return capability.SelectionQuality
|
||||
}
|
||||
if meaning.ConfidenceMillis > 0 {
|
||||
return capability.SelectionBalanced
|
||||
}
|
||||
}
|
||||
return selectionObjectiveForMessage(message)
|
||||
}
|
||||
|
||||
func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.GatewayAdvisoryResponse {
|
||||
text := "Я рядом и продолжаю работу во встроенном режиме Домового. Внешний когнитивный маршрут сейчас недоступен, поэтому опираюсь на уже установленный смысл сообщения и локальные возможности."
|
||||
switch request.Meaning.InformationRequestKind {
|
||||
case interaction.InformationRequestCognitiveModelInventory:
|
||||
text = "Запрос понят как проверка моделей. Встроенный режим Домового доступен; я могу показать подключённые модели по назначению, качеству, скорости и стоимости. Ключи и пароли в разговор не нужны."
|
||||
case interaction.InformationRequestCapabilityInventory:
|
||||
text = "Запрос понят как проверка возможностей. Я могу объяснить доступные capabilities и отличить то, что уже подключено, от того, что пока является только планом или capability gap."
|
||||
case interaction.InformationRequestCognitiveChannelStatus:
|
||||
text = "Запрос понят как проверка когнитивного канала. Локальный режим остаётся доступен; я объясню наблюдаемое состояние и что можно сделать без ожидания внешней модели."
|
||||
case interaction.InformationRequestCognitiveRecovery:
|
||||
text = "Запрос понят как восстановление ИИ. Начинаю с локального маршрута: фиксирую, что недоступно, проверяю безопасные варианты восстановления и сформулирую следующий обратимый шаг."
|
||||
default:
|
||||
switch request.Meaning.SpeechAct {
|
||||
case "REQUEST_TO_INVESTIGATE":
|
||||
text = "Я принял это как исследование. Даже без внешней модели могу сформулировать гипотезы, отделить известное от неизвестного и предложить следующий проверяемый шаг."
|
||||
case "QUESTION":
|
||||
text = "Я отвечаю во встроенном режиме Домового. Если для полного ответа не хватает внешней модели, объясню это прямо и продолжу с тем, что можно проверить локально."
|
||||
}
|
||||
}
|
||||
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted}
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) invokeBuiltinAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) cognitive.GatewayAdvisoryResponse {
|
||||
if g.builtinLocal != nil && g.builtinLocal.Provider != nil {
|
||||
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
|
||||
started := g.now()
|
||||
response, err := g.builtinLocal.Provider.Advise(ctx, invocation)
|
||||
if response.RequestID == "" {
|
||||
response.RequestID = request.RequestID
|
||||
}
|
||||
if response.Version == "" {
|
||||
response.Version = request.Version
|
||||
}
|
||||
if err != nil {
|
||||
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, FinishReason: cognitive.FinishInvalidProviderResponse}
|
||||
}
|
||||
g.observeBuiltin(ctx, g.builtinLocal, response, started)
|
||||
if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" {
|
||||
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}
|
||||
}
|
||||
}
|
||||
if request.EgressAcknowledged {
|
||||
if response, ok := g.recoverExternalAdvisory(ctx, request, maxOutputBytes); ok {
|
||||
return response
|
||||
}
|
||||
}
|
||||
return builtinAdvisory(request)
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) recoverExternalAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) (cognitive.GatewayAdvisoryResponse, bool) {
|
||||
for _, candidate := range g.externalRecovery {
|
||||
if candidate.Provider == nil || candidate.Discovery == nil {
|
||||
continue
|
||||
}
|
||||
probeCtx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
||||
modelsFound, probe := candidate.Discovery.Discover(probeCtx)
|
||||
cancel()
|
||||
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
|
||||
continue
|
||||
}
|
||||
model := modelsFound[0]
|
||||
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "recovery-external-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: capability.ProviderInstanceID("recovery-" + string(candidate.ProviderInstanceID)), ModelDescriptorID: capability.ModelDescriptorID("recovery-model-" + string(model.ProviderModelID)), ProviderModelID: model.ProviderModelID}
|
||||
response, err := candidate.Provider.Advise(ctx, invocation)
|
||||
if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" {
|
||||
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}, true
|
||||
}
|
||||
}
|
||||
return cognitive.GatewayAdvisoryResponse{}, false
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) observe(ctx context.Context, selected capability.ResolvedCognitiveBinding, response cognitive.AdvisoryResponse, started time.Time) {
|
||||
usage := response.Usage
|
||||
observationID, idErr := ids.New()
|
||||
@@ -132,6 +248,21 @@ func (g cognitiveGateway) observe(ctx context.Context, selected capability.Resol
|
||||
}
|
||||
}
|
||||
|
||||
// observeBuiltin gives the always-available local provider the same evidence
|
||||
// path as a resolved binding. The identity is stable and contains references
|
||||
// only; no endpoint, credential, prompt, or response text is persisted here.
|
||||
func (g cognitiveGateway) observeBuiltin(ctx context.Context, registration *providerRegistration, response cognitive.AdvisoryResponse, started time.Time) {
|
||||
if registration == nil {
|
||||
return
|
||||
}
|
||||
selected := capability.ResolvedCognitiveBinding{
|
||||
Binding: capability.CognitiveBinding{ID: "builtin-local-binding-v1", Revision: 1},
|
||||
Provider: capability.ProviderInstance{ID: "builtin-local-lifeboat-v1", Revision: 1, Kind: registration.Kind},
|
||||
Model: capability.ModelDescriptor{ID: "builtin-local-model-v1", Revision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ProviderModelID: "domovoy-lifeboat"},
|
||||
}
|
||||
g.observe(ctx, selected, response, started)
|
||||
}
|
||||
|
||||
func fallbackEligible(reason cognitive.FinishReason) bool {
|
||||
switch reason {
|
||||
case cognitive.FinishProviderUnavailable, cognitive.FinishModelUnavailable, cognitive.FinishTimeout, cognitive.FinishDegraded:
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
)
|
||||
|
||||
type registryStub struct {
|
||||
@@ -114,7 +115,45 @@ func TestCanaryOutputLimitIsClamped(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoActiveBindingKeepsGatewayAvailableWithoutInvocation(t *testing.T) {
|
||||
func TestSelectionObjectiveForMessage(t *testing.T) {
|
||||
cases := []struct {
|
||||
message string
|
||||
objective capability.SelectionObjective
|
||||
}{
|
||||
{"Сделай глубокий анализ кода", capability.SelectionQuality},
|
||||
{"Нужен быстрый статус", capability.SelectionSpeed},
|
||||
{"Выбери бесплатную модель", capability.SelectionEconomy},
|
||||
{"Помоги сформулировать ответ", capability.SelectionBalanced},
|
||||
}
|
||||
for _, test := range cases {
|
||||
if actual := selectionObjectiveForMessage(test.message); actual != test.objective {
|
||||
t.Errorf("message=%q objective=%q, want %q", test.message, actual, test.objective)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectionObjectiveUsesTrustedMeaningBeforeText(t *testing.T) {
|
||||
meaning := cognitive.TaskMeaning{CapabilityKind: capability.CapabilityKindModifyCode, SpeechAct: "DIRECTIVE"}
|
||||
if actual := selectionObjectiveForMeaning(meaning, "сделай быстро и дёшево"); actual != capability.SelectionQuality {
|
||||
t.Fatalf("objective=%q, want quality", actual)
|
||||
}
|
||||
if actual := selectionObjectiveForMeaning(cognitive.TaskMeaning{}, "сделай быстро"); actual != capability.SelectionSpeed {
|
||||
t.Fatalf("fallback objective=%q, want speed", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectionObjectiveForInterpretationUsesSemanticSignals(t *testing.T) {
|
||||
request := cognitive.GatewayInterpretationRequest{RequestedOutcome: "сравнить варианты", DeterministicApplicability: cognitive.InterpretationApplicable, DeterministicConfidence: 900, DeterministicSpeechAct: "QUESTION"}
|
||||
if actual := selectionObjectiveForInterpretation(request); actual != capability.SelectionQuality {
|
||||
t.Fatalf("objective=%q, want quality", actual)
|
||||
}
|
||||
request = cognitive.GatewayInterpretationRequest{DeterministicApplicability: cognitive.InterpretationApplicable, DeterministicConfidence: 900, DeterministicSpeechAct: "ASSERTION"}
|
||||
if actual := selectionObjectiveForInterpretation(request); actual != capability.SelectionSpeed {
|
||||
t.Fatalf("objective=%q, want speed", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoActiveBindingUsesBuiltInProviderWithoutInvocation(t *testing.T) {
|
||||
registry := ®istryStub{err: errors.New("no active binding")}
|
||||
provider := &stubAdvisoryProvider{}
|
||||
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"unused": {{Provider: provider}}}, now: time.Now}
|
||||
@@ -125,11 +164,63 @@ func TestNoActiveBindingKeepsGatewayAvailableWithoutInvocation(t *testing.T) {
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.Code != http.StatusOK || result.FinishReason != cognitive.FinishProviderUnavailable || provider.called {
|
||||
if response.Code != http.StatusOK || result.FinishReason != cognitive.FinishCompleted || result.AdvisoryText == "" || provider.called {
|
||||
t.Fatalf("status=%d result=%+v provider_called=%t", response.Code, result, provider.called)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltInAdvisoryUsesTrustedInformationIntentInsteadOfMessageKeywords(t *testing.T) {
|
||||
request := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", Message: "arbitrary text", Meaning: cognitive.TaskMeaning{InformationRequestKind: interaction.InformationRequestCognitiveRecovery}}
|
||||
response := builtinAdvisory(request)
|
||||
if !strings.Contains(response.AdvisoryText, "восстановление ИИ") {
|
||||
t.Fatalf("unexpected recovery response: %q", response.AdvisoryText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoActiveBindingInvokesBuiltInLocalProvider(t *testing.T) {
|
||||
registry := ®istryStub{err: errors.New("no active binding")}
|
||||
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "локальный ответ", FinishReason: cognitive.FinishCompleted}}
|
||||
builtin := providerRegistration{ProviderInstanceID: "local-lifeboat", Kind: "openai-compatible-chat-v1", EndpointRef: "local-lifeboat-v1", Provider: provider}
|
||||
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
|
||||
response := httptest.NewRecorder()
|
||||
gateway.advisoryHandler(response, request)
|
||||
if response.Code != http.StatusOK || !provider.called || !strings.Contains(response.Body.String(), "локальный ответ") {
|
||||
t.Fatalf("status=%d provider_called=%t body=%s", response.Code, provider.called, response.Body.String())
|
||||
}
|
||||
if registry.observation == nil || registry.observation.BindingID != "builtin-local-binding-v1" || registry.observation.ProviderInstanceID != "builtin-local-lifeboat-v1" || registry.observation.ModelDescriptorID != "builtin-local-model-v1" || registry.observation.ResultClass != capability.CognitiveResultClass(cognitive.FinishCompleted) {
|
||||
t.Fatalf("built-in usage observation=%+v", registry.observation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltInProviderFailureIsObservedWithoutBlockingFallback(t *testing.T) {
|
||||
registry := ®istryStub{err: errors.New("no active binding")}
|
||||
provider := &stubAdvisoryProvider{err: errors.New("local timeout")}
|
||||
builtin := providerRegistration{Kind: "ollama-v1", Provider: provider}
|
||||
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
|
||||
response := httptest.NewRecorder()
|
||||
gateway.advisoryHandler(response, request)
|
||||
if response.Code != http.StatusOK || registry.observation == nil {
|
||||
t.Fatalf("status=%d observation=%+v body=%s", response.Code, registry.observation, response.Body.String())
|
||||
}
|
||||
if registry.observation.ResultClass != capability.CognitiveResultClass(cognitive.FinishInvalidProviderResponse) || registry.observation.FailureType != capability.CognitiveFailureType(cognitive.FinishInvalidProviderResponse) {
|
||||
t.Fatalf("failure observation=%+v", registry.observation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltInInterpretationIsObserved(t *testing.T) {
|
||||
registry := ®istryStub{err: errors.New("no active binding")}
|
||||
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{FinishReason: cognitive.FinishCompleted, AdvisoryText: `{"primary_speech_act":"QUESTION","secondary_speech_act":"","confidence_millis":900,"applicability":"APPLICABLE","requested_outcome":"","needs_cognition":false,"clarification":"NOT_REQUIRED","selection_reason":"local","capability_hints":[]}`}}
|
||||
builtin := providerRegistration{Kind: "openai-compatible-chat-v1", Provider: provider}
|
||||
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
|
||||
request := cognitive.GatewayInterpretationRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "interpretation-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", Message: "что доступно", MaxOutputBytes: 4096, DeterministicSpeechAct: "QUESTION", DeterministicApplicability: cognitive.InterpretationApplicable}
|
||||
result := gateway.interpretWithBuiltin(context.Background(), request, request.MaxOutputBytes)
|
||||
if result.FinishReason != cognitive.FinishCompleted || registry.observation == nil || registry.observation.RequestID != "interpretation-1" {
|
||||
t.Fatalf("result=%+v observation=%+v", result, registry.observation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderLossBetweenRoutingAndInvocationFallsBackOnce(t *testing.T) {
|
||||
primary := gatewaySelection()
|
||||
primary.Binding.ID = "gpu-binding"
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
)
|
||||
|
||||
func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var request cognitive.GatewayInterpretationRequest
|
||||
if !decodeJSON(w, r, 80<<10, &request) {
|
||||
return
|
||||
}
|
||||
if request.Version != cognitive.CreatorAdvisoryV1 || request.RequestID == "" || request.ActorID == "" || request.PrincipalID == "" || request.ScopeID == "" || strings.TrimSpace(request.Message) == "" || request.MaxOutputBytes < 1 {
|
||||
http.Error(w, "invalid interpretation request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
maxOutputBytes := request.MaxOutputBytes
|
||||
if g.allowCanary && g.canaryMaxOutputBytes > 0 && maxOutputBytes > g.canaryMaxOutputBytes {
|
||||
maxOutputBytes = g.canaryMaxOutputBytes
|
||||
}
|
||||
requirement := capability.CognitiveRequirement{
|
||||
CapabilityID: creatorAdvisoryCapabilityID,
|
||||
OwnerPrincipalID: request.PrincipalID,
|
||||
DataPolicy: request.DataPolicy,
|
||||
RequiredModalities: []capability.ModelModality{capability.ModalityText},
|
||||
RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory},
|
||||
AllowCanary: g.allowCanary,
|
||||
InputBytes: int64(len(request.Message)),
|
||||
MaxOutputBytes: int64(maxOutputBytes),
|
||||
Objective: selectionObjectiveForInterpretation(request),
|
||||
}
|
||||
selected, err := g.resolver.Resolve(r.Context(), requirement)
|
||||
if err != nil {
|
||||
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
provider := g.provider(selected)
|
||||
if provider == nil {
|
||||
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
invocation := cognitive.AdvisoryRequest{
|
||||
Version: request.Version,
|
||||
RequestID: request.RequestID,
|
||||
ActorID: request.ActorID,
|
||||
PrincipalID: request.PrincipalID,
|
||||
ScopeID: request.ScopeID,
|
||||
Message: interpretationPrompt(request),
|
||||
Locale: request.Locale,
|
||||
MaxOutputBytes: maxOutputBytes,
|
||||
ProviderBindingID: selected.Binding.ID,
|
||||
ProviderBindingRevision: selected.Binding.Revision,
|
||||
ProviderInstanceID: selected.Provider.ID,
|
||||
ModelDescriptorID: selected.Model.ID,
|
||||
ProviderModelID: selected.Model.ProviderModelID,
|
||||
}
|
||||
started := g.now()
|
||||
response, invokeErr := provider.Advise(r.Context(), invocation)
|
||||
if invokeErr != nil {
|
||||
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishInvalidProviderResponse}
|
||||
}
|
||||
g.observe(r.Context(), selected, response, started)
|
||||
if response.FinishReason != cognitive.FinishCompleted {
|
||||
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: response.FinishReason, Usage: response.Usage})
|
||||
return
|
||||
}
|
||||
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
|
||||
if !ok {
|
||||
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishInvalidStructuredOutput, Usage: response.Usage})
|
||||
return
|
||||
}
|
||||
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage})
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) interpretWithBuiltin(ctx context.Context, request cognitive.GatewayInterpretationRequest, maxOutputBytes int) cognitive.GatewayInterpretationResponse {
|
||||
if g.builtinLocal == nil || g.builtinLocal.Provider == nil {
|
||||
return deterministicInterpretationResponse(request)
|
||||
}
|
||||
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: interpretationPrompt(request), Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
|
||||
started := g.now()
|
||||
response, err := g.builtinLocal.Provider.Advise(ctx, invocation)
|
||||
if response.RequestID == "" {
|
||||
response.RequestID = request.RequestID
|
||||
}
|
||||
if response.Version == "" {
|
||||
response.Version = request.Version
|
||||
}
|
||||
if err != nil {
|
||||
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, FinishReason: cognitive.FinishInvalidProviderResponse}
|
||||
}
|
||||
g.observeBuiltin(ctx, g.builtinLocal, response, started)
|
||||
if err != nil || response.FinishReason != cognitive.FinishCompleted {
|
||||
return deterministicInterpretationResponse(request)
|
||||
}
|
||||
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
|
||||
if !ok {
|
||||
return deterministicInterpretationResponse(request)
|
||||
}
|
||||
return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage}
|
||||
}
|
||||
|
||||
// deterministicInterpretationResponse keeps the semantic route alive when the
|
||||
// local model cannot produce strict JSON in time. It does not invent meaning:
|
||||
// it carries forward the already-recorded deterministic interpretation and
|
||||
// leaves the advisory answer to the built-in gateway path.
|
||||
func deterministicInterpretationResponse(request cognitive.GatewayInterpretationRequest) cognitive.GatewayInterpretationResponse {
|
||||
speechAct := request.DeterministicSpeechAct
|
||||
if !validSpeechAct(speechAct) {
|
||||
speechAct = "UNKNOWN"
|
||||
}
|
||||
applicability := request.DeterministicApplicability
|
||||
switch applicability {
|
||||
case cognitive.InterpretationApplicable, cognitive.InterpretationLowConfidence, cognitive.InterpretationMixedIntent, cognitive.InterpretationNeedsBroader, cognitive.InterpretationClarificationRequired:
|
||||
default:
|
||||
applicability = cognitive.InterpretationLowConfidence
|
||||
}
|
||||
confidence := request.DeterministicConfidence
|
||||
if confidence < 0 {
|
||||
confidence = 0
|
||||
}
|
||||
if confidence > 1000 {
|
||||
confidence = 1000
|
||||
}
|
||||
candidate := cognitive.GatewayInterpretationCandidate{
|
||||
PrimarySpeechAct: speechAct,
|
||||
SecondarySpeechAct: request.DeterministicSecondary,
|
||||
ConfidenceMillis: confidence,
|
||||
Applicability: applicability,
|
||||
RequestedOutcome: request.RequestedOutcome,
|
||||
NeedsCognition: true,
|
||||
Clarification: "NOT_REQUIRED",
|
||||
SelectionReason: "deterministic interpretation retained; built-in structured interpretation was unavailable",
|
||||
}
|
||||
if applicability == cognitive.InterpretationClarificationRequired {
|
||||
candidate.NeedsCognition = false
|
||||
candidate.Clarification = "REQUIRED"
|
||||
}
|
||||
return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted}
|
||||
}
|
||||
|
||||
func selectionObjectiveForInterpretation(request cognitive.GatewayInterpretationRequest) capability.SelectionObjective {
|
||||
if request.RequestedOutcome != "" || request.DeterministicApplicability == cognitive.InterpretationNeedsBroader || request.DeterministicApplicability == cognitive.InterpretationMixedIntent || request.DeterministicApplicability == cognitive.InterpretationLowConfidence {
|
||||
return capability.SelectionQuality
|
||||
}
|
||||
if request.DeterministicConfidence >= 800 && request.DeterministicSpeechAct != "QUESTION" {
|
||||
return capability.SelectionSpeed
|
||||
}
|
||||
return capability.SelectionBalanced
|
||||
}
|
||||
|
||||
func interpretationPrompt(request cognitive.GatewayInterpretationRequest) string {
|
||||
projects := "none"
|
||||
if len(request.ProjectNames) > 0 {
|
||||
projects = strings.Join(request.ProjectNames, ", ")
|
||||
}
|
||||
return fmt.Sprintf(`Return only strict JSON with keys primary_speech_act, secondary_speech_act, confidence_millis, applicability, requested_outcome, needs_cognition, clarification, selection_reason, capability_hints.
|
||||
Allowed speech acts: DIRECTIVE, ASSERTION, DECISION, PREFERENCE, QUESTION, HYPOTHESIS, CORRECTION, REQUEST_TO_INVESTIGATE, CONFIRMATION, REJECTION, CANCELLATION, CONVERSATIONAL, UNKNOWN.
|
||||
Allowed applicability: APPLICABLE, LOW_CONFIDENCE, MIXED_INTENT, NEEDS_BROADER_INTERPRETATION, CLARIFICATION_REQUIRED.
|
||||
Allowed clarification: NOT_REQUIRED or REQUIRED.
|
||||
Capability hints may only contain known capability kinds like COGNITIVE_REASONING, MODIFY_CODE, RUN_TESTS, SEND_EMAIL, SCHEDULE_REMINDER, VERIFY_FACT, INSPECT_PROJECT, READ_DATABASE, QUERY_LOGS.
|
||||
Interpret the human utterance semantically. Do not claim action execution. Do not treat punctuation alone as decisive. If uncertain, set applicability accordingly.
|
||||
Message: %q
|
||||
Deterministic primary: %s
|
||||
Deterministic secondary: %s
|
||||
Deterministic confidence: %d
|
||||
Deterministic applicability: %s
|
||||
Deterministic requested outcome: %q
|
||||
Relevant project names: %s`, request.Message, request.DeterministicSpeechAct, request.DeterministicSecondary, request.DeterministicConfidence, request.DeterministicApplicability, request.RequestedOutcome, projects)
|
||||
}
|
||||
|
||||
func parseInterpretationCandidate(text string) (cognitive.GatewayInterpretationCandidate, bool) {
|
||||
start := strings.Index(text, "{")
|
||||
end := strings.LastIndex(text, "}")
|
||||
if start < 0 || end <= start {
|
||||
return cognitive.GatewayInterpretationCandidate{}, false
|
||||
}
|
||||
var candidate cognitive.GatewayInterpretationCandidate
|
||||
if err := json.Unmarshal([]byte(text[start:end+1]), &candidate); err != nil {
|
||||
return cognitive.GatewayInterpretationCandidate{}, false
|
||||
}
|
||||
if !validSpeechAct(candidate.PrimarySpeechAct) || candidate.ConfidenceMillis < 0 || candidate.ConfidenceMillis > 1000 {
|
||||
return cognitive.GatewayInterpretationCandidate{}, false
|
||||
}
|
||||
if candidate.SecondarySpeechAct != "" && !validSpeechAct(candidate.SecondarySpeechAct) {
|
||||
return cognitive.GatewayInterpretationCandidate{}, false
|
||||
}
|
||||
switch candidate.Applicability {
|
||||
case cognitive.InterpretationApplicable, cognitive.InterpretationLowConfidence, cognitive.InterpretationMixedIntent, cognitive.InterpretationNeedsBroader, cognitive.InterpretationClarificationRequired:
|
||||
default:
|
||||
return cognitive.GatewayInterpretationCandidate{}, false
|
||||
}
|
||||
if candidate.Clarification != "NOT_REQUIRED" && candidate.Clarification != "REQUIRED" {
|
||||
return cognitive.GatewayInterpretationCandidate{}, false
|
||||
}
|
||||
return candidate, true
|
||||
}
|
||||
|
||||
func validSpeechAct(value string) bool {
|
||||
switch value {
|
||||
case "DIRECTIVE", "ASSERTION", "DECISION", "PREFERENCE", "QUESTION", "HYPOTHESIS", "CORRECTION", "REQUEST_TO_INVESTIGATE", "CONFIRMATION", "REJECTION", "CANCELLATION", "CONVERSATIONAL", "UNKNOWN":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func writeInterpretationResponse(w http.ResponseWriter, response cognitive.GatewayInterpretationResponse) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/models"
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"git.cin.su/m/Assistant/contracts/situation"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
)
|
||||
|
||||
func TestLiveInterpretationTrace(t *testing.T) {
|
||||
if os.Getenv("ASSISTANT_LIVE_SEMANTIC_TRACE") != "1" {
|
||||
t.Skip("set ASSISTANT_LIVE_SEMANTIC_TRACE=1 for live semantic/gateway/raw trace")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rawBase := envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1")
|
||||
rawModelID, err := firstRawModel(ctx, rawBase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cpuAdapter, err := models.NewOpenAICompatibleLocalAdapter(rawBase, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
discovered, probe := cpuAdapter.Discover(ctx)
|
||||
if probe.Health != capability.HealthAvailable || len(discovered) == 0 {
|
||||
t.Fatalf("cpu discovery unavailable: probe=%+v models=%+v", probe, discovered)
|
||||
}
|
||||
selected := capability.ResolvedCognitiveBinding{
|
||||
Binding: capability.CognitiveBinding{
|
||||
ID: "trace-binding",
|
||||
Revision: 1,
|
||||
CapabilityID: "creator-advisory",
|
||||
OwnerPrincipalID: "domovoy-creator-principal",
|
||||
DataPolicy: capability.DataPolicyLocalOnly,
|
||||
},
|
||||
Provider: capability.ProviderInstance{
|
||||
ID: "local-cpu-primary",
|
||||
Revision: 1,
|
||||
Kind: "openai-compatible-chat-v1",
|
||||
EndpointRef: "local-cpu-primary-v1",
|
||||
},
|
||||
Model: capability.ModelDescriptor{
|
||||
ID: capability.ModelDescriptorID("trace-model"),
|
||||
Revision: 1,
|
||||
ProviderModelID: discovered[0].ProviderModelID,
|
||||
},
|
||||
}
|
||||
gateway := cognitiveGateway{
|
||||
resolver: traceResolver{selected: selected},
|
||||
providers: map[capability.ProviderKind][]providerRegistration{
|
||||
"openai-compatible-chat-v1": {{
|
||||
ProviderInstanceID: "local-cpu-primary",
|
||||
Kind: "openai-compatible-chat-v1",
|
||||
EndpointRef: "local-cpu-primary-v1",
|
||||
Provider: cpuAdapter,
|
||||
}},
|
||||
},
|
||||
now: time.Now,
|
||||
allowCanary: true,
|
||||
}
|
||||
for _, message := range []string{"ты кто", "можешь расказать об утре"} {
|
||||
conversation := interaction.Conversation{ID: "trace-conversation", ActorID: "trace-actor", PrincipalID: "domovoy-creator-principal", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
|
||||
utterance, _, _, err := interactiondomain.NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
semantic, err := interactiondomain.InterpretUtterance(utterance, []situation.ProjectDescriptor{{ID: "assistant", DisplayName: "Assistant"}}, nil, time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rawResponse, err := rawCompletion(ctx, rawBase, rawModelID, message)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := cognitive.GatewayInterpretationRequest{
|
||||
Version: cognitive.CreatorAdvisoryV1,
|
||||
RequestID: "trace-request",
|
||||
ActorID: identity.ActorID("trace-actor"),
|
||||
PrincipalID: identity.PrincipalID("domovoy-creator-principal"),
|
||||
ScopeID: authority.ScopeID("domovoy-global-scope"),
|
||||
Message: message,
|
||||
Locale: "ru",
|
||||
MaxOutputBytes: 4096,
|
||||
DataPolicy: capability.DataPolicyLocalOnly,
|
||||
DeterministicSpeechAct: string(semantic.Interpretation.SpeechAct),
|
||||
DeterministicSecondary: string(semantic.Interpretation.SecondarySpeechAct),
|
||||
DeterministicConfidence: semantic.Interpretation.ConfidenceMillis,
|
||||
DeterministicApplicability: cognitive.InterpretationApplicability(semantic.Interpretation.Applicability),
|
||||
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
|
||||
}
|
||||
payload, _ := json.Marshal(request)
|
||||
recorder := httptest.NewRecorder()
|
||||
httpRequest := httptest.NewRequest(http.MethodPost, "/v1/interpretations", bytes.NewReader(payload))
|
||||
gateway.interpretationHandler(recorder, httpRequest.WithContext(ctx))
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("message=%q gateway status=%d body=%s", message, recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var gatewayResponse cognitive.GatewayInterpretationResponse
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &gatewayResponse); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gatewayResponse.FinishReason != cognitive.FinishCompleted {
|
||||
t.Fatalf("message=%q gateway finish=%s body=%s", message, gatewayResponse.FinishReason, recorder.Body.String())
|
||||
}
|
||||
if rawResponse == "" {
|
||||
t.Fatalf("message=%q raw response is empty", message)
|
||||
}
|
||||
if message == "ты кто" {
|
||||
if semantic.Interpretation.SpeechAct != interaction.SpeechActQuestion || !semantic.NeedsCognition {
|
||||
t.Fatalf("semantic path still fails punctuation-free identity question: %+v", semantic.Interpretation)
|
||||
}
|
||||
if gatewayResponse.Candidate.PrimarySpeechAct != "QUESTION" {
|
||||
t.Fatalf("gateway did not preserve question semantics: %+v", gatewayResponse.Candidate)
|
||||
}
|
||||
}
|
||||
t.Logf("TRACE message=%q deterministic=%s/%s needs_cognition=%t raw=%q gateway_candidate=%+v", message, semantic.Interpretation.SpeechAct, semantic.Interpretation.Applicability, semantic.NeedsCognition, truncate(rawResponse, 180), gatewayResponse.Candidate)
|
||||
}
|
||||
}
|
||||
|
||||
type traceResolver struct {
|
||||
selected capability.ResolvedCognitiveBinding
|
||||
}
|
||||
|
||||
func (r traceResolver) Resolve(context.Context, capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
|
||||
return r.selected, nil
|
||||
}
|
||||
|
||||
func (r traceResolver) Observe(context.Context, capability.CognitiveUsageObservation) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r traceResolver) RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error) {
|
||||
return capability.CognitiveDiscoveryRegistration{}, nil
|
||||
}
|
||||
|
||||
func firstRawModel(ctx context.Context, base string) (string, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, base+"/models", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var payload struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 64<<10)).Decode(&payload); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(payload.Data) == 0 || payload.Data[0].ID == "" {
|
||||
return "", io.EOF
|
||||
}
|
||||
return payload.Data[0].ID, nil
|
||||
}
|
||||
|
||||
func rawCompletion(ctx context.Context, base, model, message string) (string, error) {
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"model": model,
|
||||
"max_tokens": 128,
|
||||
"messages": []map[string]string{
|
||||
{"role": "user", "content": message},
|
||||
},
|
||||
"stream": false,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, base+"/chat/completions", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var result struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(result.Choices) == 0 {
|
||||
return "", io.EOF
|
||||
}
|
||||
return result.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
||||
func truncate(value string, max int) string {
|
||||
if len(value) <= max {
|
||||
return value
|
||||
}
|
||||
return value[:max] + "..."
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
)
|
||||
|
||||
func TestBuiltinInterpretationTimeoutRetainsDeterministicCandidate(t *testing.T) {
|
||||
request := cognitive.GatewayInterpretationRequest{
|
||||
Version: cognitive.CreatorAdvisoryV1,
|
||||
RequestID: "request-1",
|
||||
DeterministicSpeechAct: "QUESTION",
|
||||
DeterministicConfidence: 720,
|
||||
DeterministicApplicability: cognitive.InterpretationNeedsBroader,
|
||||
RequestedOutcome: "вывести список настроек",
|
||||
}
|
||||
response := (cognitiveGateway{}).interpretWithBuiltin(context.Background(), request, 4096)
|
||||
if response.FinishReason != cognitive.FinishCompleted {
|
||||
t.Fatalf("finish reason=%q", response.FinishReason)
|
||||
}
|
||||
if response.Candidate.PrimarySpeechAct != "QUESTION" || response.Candidate.ConfidenceMillis != 720 || response.Candidate.RequestedOutcome != request.RequestedOutcome {
|
||||
t.Fatalf("candidate did not retain deterministic meaning: %+v", response.Candidate)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -40,37 +41,102 @@ func main() {
|
||||
}
|
||||
providers := map[capability.ProviderKind][]providerRegistration{}
|
||||
discoveries := map[string]providerRegistration{}
|
||||
var builtinLocal *providerRegistration
|
||||
externalRecovery := []providerRegistration{}
|
||||
cpuAdapter, err := models.NewOpenAICompatibleLocalAdapter(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", Provider: cpuAdapter, Discovery: cpuAdapter}
|
||||
cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyLocalOnly, Provider: cpuAdapter, Discovery: cpuAdapter}
|
||||
providers[cpu.Kind] = append(providers[cpu.Kind], cpu)
|
||||
discoveries["cpu"] = cpu
|
||||
if lifeboat, enabled, lifeboatErr := newLifeboatProvider(os.Getenv("LOCAL_LIFEBOAT_PROVIDER_URL")); lifeboatErr != nil {
|
||||
log.Fatal(lifeboatErr)
|
||||
} else if enabled {
|
||||
lifeboat.DataPolicy = capability.DataPolicyLocalOnly
|
||||
providers[lifeboat.Kind] = append(providers[lifeboat.Kind], lifeboat)
|
||||
discoveries["lifeboat"] = lifeboat
|
||||
builtinLocal = &lifeboat
|
||||
}
|
||||
gpuAdapter, err := models.NewOllamaAdapter(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "ollama-v1", EndpointRef: "local-gpu-opportunistic-v1", Provider: gpuAdapter, Discovery: gpuAdapter}
|
||||
gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "ollama-v1", EndpointRef: "local-gpu-opportunistic-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: gpuAdapter, Discovery: gpuAdapter}
|
||||
providers[gpu.Kind] = append(providers[gpu.Kind], gpu)
|
||||
discoveries["gpu"] = gpu
|
||||
externalRecovery = append(externalRecovery, gpu)
|
||||
if openAIProvider != nil {
|
||||
kind := capability.ProviderKind(envOr("OPENAI_PROVIDER_KIND", "openai-responses-v1"))
|
||||
providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), Provider: openAIProvider})
|
||||
providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), AuthMode: capability.AuthKey, BillingMode: capability.BillingPerInput, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: openAIProvider})
|
||||
}
|
||||
gateway := cognitiveGateway{resolver: resolver, providers: providers, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096}
|
||||
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096}
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
readinessClient := &http.Client{Timeout: 2 * time.Second}
|
||||
lifeboatURL := strings.TrimSuffix(strings.TrimRight(envOr("LOCAL_LIFEBOAT_PROVIDER_URL", "http://cognitive-lifeboat:8086/v1"), "/"), "/v1") + "/health"
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "cognitive-worker", Probes: []health.Probe{
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/")+"/models"),
|
||||
health.HTTPProbe(readinessClient, lifeboatURL),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"),
|
||||
}})
|
||||
mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal)
|
||||
mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler)
|
||||
mux.HandleFunc("POST /v1/advisories", gateway.advisoryHandler)
|
||||
mux.HandleFunc("POST /v1/interpretations", gateway.interpretationHandler)
|
||||
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
|
||||
mux.HandleFunc("POST /v1/provider-discovery/{provider}", discovery.handler)
|
||||
refreshProviderDiscoveries(resolver, discovery)
|
||||
log.Printf("cognitive-worker listening on %s", addr)
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshProviderDiscoveries(resolver bindingResolver, service discoveryService) {
|
||||
for name := range service.providers {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
||||
registration, ok := service.providers[name]
|
||||
if !ok || registration.Discovery == nil {
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
discovered, probe := registration.Discovery.Discover(ctx)
|
||||
cancel()
|
||||
snapshot := capability.CognitiveDiscoverySnapshot{
|
||||
ProviderInstanceID: registration.ProviderInstanceID,
|
||||
ProviderKind: registration.Kind,
|
||||
EndpointRef: registration.EndpointRef,
|
||||
CredentialRef: registration.CredentialRef,
|
||||
AuthMode: registration.AuthMode,
|
||||
BillingMode: registration.BillingMode,
|
||||
Enabled: true,
|
||||
OwnerPrincipalID: service.owner,
|
||||
CapabilityID: creatorAdvisoryCapabilityID,
|
||||
DataPolicy: registration.DataPolicy,
|
||||
LimitPolicyRef: "creator-advisory-local-v1",
|
||||
Health: probe.Health,
|
||||
FailureClass: capability.CognitiveFailureType(probe.Failure),
|
||||
ObservedAtUnixMillis: time.Now().UnixMilli(),
|
||||
}
|
||||
for _, model := range discovered {
|
||||
snapshot.Models = append(snapshot.Models, capability.DiscoveredModelSnapshot{
|
||||
ProviderModelID: model.ProviderModelID,
|
||||
Fingerprint: model.Fingerprint,
|
||||
MaxContextBytes: model.MaxContextBytes,
|
||||
Modalities: model.Modalities,
|
||||
Capabilities: model.Capabilities,
|
||||
Installed: capability.OperationalYes,
|
||||
Loaded: capability.OperationalNotObserved,
|
||||
Invokable: capability.OperationalNotObserved,
|
||||
})
|
||||
}
|
||||
if _, err := resolver.RegisterDiscovery(context.Background(), snapshot); err != nil {
|
||||
log.Printf("startup cognitive discovery refresh failed for %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deterministicAdvisory(w http.ResponseWriter, r *http.Request) {
|
||||
request, ok := decodeAdvisoryRequest(w, r)
|
||||
if !ok {
|
||||
@@ -173,3 +239,22 @@ func envOr(name, fallback string) string {
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func newLifeboatProvider(rawURL string) (providerRegistration, bool, error) {
|
||||
if rawURL == "" {
|
||||
return providerRegistration{}, false, nil
|
||||
}
|
||||
adapter, err := models.NewOpenAICompatibleLocalAdapter(rawURL, nil)
|
||||
if err != nil {
|
||||
return providerRegistration{}, false, err
|
||||
}
|
||||
return providerRegistration{
|
||||
ProviderInstanceID: "local-lifeboat",
|
||||
Kind: "openai-compatible-chat-v1",
|
||||
EndpointRef: "local-lifeboat-v1",
|
||||
AuthMode: capability.AuthNone,
|
||||
BillingMode: capability.BillingFree,
|
||||
Provider: adapter,
|
||||
Discovery: adapter,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLifeboatProviderIsOptionalAndLocalAdapterBacked(t *testing.T) {
|
||||
if _, enabled, err := newLifeboatProvider(""); err != nil || enabled {
|
||||
t.Fatalf("empty lifeboat URL should disable provider: enabled=%v err=%v", enabled, err)
|
||||
}
|
||||
provider, enabled, err := newLifeboatProvider("http://lifeboat:8080/v1")
|
||||
if err != nil || !enabled {
|
||||
t.Fatalf("configured lifeboat should be enabled: enabled=%v err=%v", enabled, err)
|
||||
}
|
||||
if provider.ProviderInstanceID != "local-lifeboat" || provider.EndpointRef != "local-lifeboat-v1" || provider.Provider == nil {
|
||||
t.Fatalf("unexpected lifeboat registration: %+v", provider)
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,13 @@ func main() {
|
||||
dispatcher := operationDispatcher{continuityExecutor: continuityExecutor, continuityReconciler: continuityExecutor, bindings: creatorCore, secretRef: notificationSecretRef, secret: notificationSecret, client: creatorHTTPClient}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "commit-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
readinessClient := &http.Client{Timeout: 2 * time.Second}
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "commit-runtime", Probes: []health.Probe{
|
||||
health.TCPProbe("commit-postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
|
||||
health.TCPProbe("authority-postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(coreURL, "/")+"/healthz"),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
|
||||
}})
|
||||
registerExecutionRoutes(mux, repository, dispatcher, dispatcher, executorToken)
|
||||
registerCreatorCommandRoutes(mux, creatorCommandHandler{
|
||||
core: creatorCore, continuity: creatorContinuity, authority: authorityRepository, repository: repository,
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
type attentionRepository interface {
|
||||
AttentionByOwner(context.Context, identity.PrincipalID, int) ([]continuity.AttentionItem, error)
|
||||
CreateAttentionItem(context.Context, continuity.AttentionItem) (continuity.AttentionItem, error)
|
||||
}
|
||||
|
||||
func registerCreatorAttentionRoutes(mux *http.ServeMux, identityClient creatorIdentityClient, repository attentionRepository) {
|
||||
@@ -61,6 +62,53 @@ func registerCreatorAttentionRoutes(mux *http.ServeMux, identityClient creatorId
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/attention", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := creatorSessionForAttention(w, r, identityClient)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request continuity.CreateAttentionRequest
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid attention request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if request.OwnerPrincipalID == "" {
|
||||
request.OwnerPrincipalID = session.PrincipalID
|
||||
}
|
||||
if request.OwnerPrincipalID != session.PrincipalID || request.ScopeID == "" || request.SourceKind == "" || request.Level == "" || request.Summary == "" || request.CreatedAtUnixMillis < 1 || request.DueAtUnixMillis < 1 {
|
||||
http.Error(w, "invalid attention request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if request.SourceKind == continuity.AttentionSourceAgentDelegation && request.SourceExternalRef == "" {
|
||||
http.Error(w, "delegation attention requires source external ref", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
item := continuity.AttentionItem{
|
||||
ID: continuity.AttentionItemID(request.SourceExternalRef),
|
||||
OwnerPrincipalID: request.OwnerPrincipalID,
|
||||
ScopeID: request.ScopeID,
|
||||
SourceKind: request.SourceKind,
|
||||
SourceProspectiveID: request.SourceProspectiveID,
|
||||
SourceExternalRef: request.SourceExternalRef,
|
||||
Level: request.Level,
|
||||
State: continuity.AttentionPending,
|
||||
Summary: request.Summary,
|
||||
DueAtUnixMillis: request.DueAtUnixMillis,
|
||||
CreatedAtUnixMillis: request.CreatedAtUnixMillis,
|
||||
Revision: 1,
|
||||
}
|
||||
saved, err := repository.CreateAttentionItem(r.Context(), item)
|
||||
if err != nil {
|
||||
http.Error(w, "attention unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(saved)
|
||||
})
|
||||
}
|
||||
|
||||
func creatorSessionForAttention(w http.ResponseWriter, r *http.Request, identityClient creatorIdentityClient) (identity.CreatorSession, bool) {
|
||||
@@ -98,6 +146,15 @@ func humanAttentionState(state continuity.AttentionState) string {
|
||||
}
|
||||
|
||||
func humanResponseHint(item continuity.AttentionItem) string {
|
||||
if item.SourceKind == continuity.AttentionSourceAgentDelegation {
|
||||
if item.State == continuity.AttentionDelivered || item.State == continuity.AttentionPending || item.State == continuity.AttentionFailed {
|
||||
return "Можно ответить: «Продолжай» или «Нет, нужен другой вариант»."
|
||||
}
|
||||
if item.ResponseOutcome != "" {
|
||||
return "Ответ записан: " + string(item.ResponseOutcome)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if item.State == continuity.AttentionDelivered || item.State == continuity.AttentionPending || item.State == continuity.AttentionFailed {
|
||||
return "Можно ответить: «Готово», «Отложи на 10 минут» или «Отмени»."
|
||||
}
|
||||
@@ -121,5 +178,8 @@ func humanAttentionSource(kind continuity.AttentionSourceKind) string {
|
||||
if kind == continuity.AttentionSourceProspectiveReminder {
|
||||
return "напоминание"
|
||||
}
|
||||
if kind == continuity.AttentionSourceAgentDelegation {
|
||||
return "задача coding-agent"
|
||||
}
|
||||
return "событие Домового"
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func registerCreatorResponseRoute(mux *http.ServeMux, identityClient creatorIden
|
||||
http.Error(w, "human response unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
candidates, err := repository.ResponseCandidates(r.Context(), session.PrincipalID, 3)
|
||||
candidates, err := repository.ResponseCandidates(r.Context(), session.PrincipalID, 20)
|
||||
if err != nil {
|
||||
http.Error(w, "response candidates unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -65,6 +65,39 @@ func TestCreatorResponseEndpointRecordsAmbiguousWithoutSilentTargetMutation(t *t
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatorResponseEndpointContinuesDelegationAttentionViaNaturalMessage(t *testing.T) {
|
||||
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
|
||||
}))
|
||||
defer core.Close()
|
||||
identityClient, err := newCreatorIdentityClient(core.URL, core.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repository := &fakeResponseRepository{candidates: []continuitydomain.ResponseCandidate{
|
||||
{Attention: continuity.AttentionItem{
|
||||
ID: "work:delegation:delegation-1",
|
||||
OwnerPrincipalID: "principal-1",
|
||||
ScopeID: "scope-1",
|
||||
SourceKind: continuity.AttentionSourceAgentDelegation,
|
||||
SourceExternalRef: "work:delegation:delegation-1",
|
||||
State: continuity.AttentionPending,
|
||||
Summary: "Нужен твой выбор по задаче coding-agent: docs/PROJECT_STATUS.md",
|
||||
Revision: 1,
|
||||
}},
|
||||
{Attention: continuity.AttentionItem{ID: "attention-1", SourceProspectiveID: "prospective-1", State: continuity.AttentionDelivered, Summary: "старое напоминание"}},
|
||||
}}
|
||||
mux := http.NewServeMux()
|
||||
registerCreatorResponseRoute(mux, identityClient, repository, func() time.Time { return time.Unix(100, 0) })
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/creator/responses", strings.NewReader(`{"SourceUtteranceID":"utterance-1","SourceIntentID":"intent-1","Message":"Продолжай задачу coding-agent по docs/PROJECT_STATUS.md","RequestedAtUnixMillis":100000}`))
|
||||
request.Header.Set("Authorization", "Bearer ok")
|
||||
recorder := httptest.NewRecorder()
|
||||
mux.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusCreated || repository.applied.Operation != continuity.ResponseContinue || repository.applied.TargetExternalRef != "work:delegation:delegation-1" || repository.applied.Outcome != continuity.ResponseOutcomeContinued {
|
||||
t.Fatalf("status=%d applied=%#v body=%s", recorder.Code, repository.applied, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
type fakeResponseRepository struct {
|
||||
candidates []continuitydomain.ResponseCandidate
|
||||
applied continuity.HumanResponse
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
@@ -31,6 +32,12 @@ func main() {
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "continuity-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
readinessClient := &http.Client{Timeout: 2 * time.Second}
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "continuity-runtime", Probes: []health.Probe{
|
||||
health.TCPProbe("postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"), "/")+"/healthz"),
|
||||
}})
|
||||
executorToken := os.Getenv("COMMIT_EXECUTOR_TOKEN")
|
||||
if executorToken == "" {
|
||||
log.Fatal("COMMIT_EXECUTOR_TOKEN is required")
|
||||
|
||||
@@ -19,7 +19,9 @@ import (
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
|
||||
workdomain "git.cin.su/m/Assistant/modules/work/domain"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
@@ -33,6 +35,11 @@ type continuityResponseClient struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type continuityAttentionClient struct {
|
||||
baseURL *url.URL
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newContinuityReminderClient(rawURL string, client *http.Client) (continuityReminderClient, error) {
|
||||
base, err := url.Parse(rawURL)
|
||||
if err != nil || base.Scheme == "" || base.Host == "" {
|
||||
@@ -55,6 +62,17 @@ func newContinuityResponseClient(rawURL string, client *http.Client) (continuity
|
||||
return continuityResponseClient{baseURL: base, client: client}, nil
|
||||
}
|
||||
|
||||
func newContinuityAttentionClient(rawURL string, client *http.Client) (continuityAttentionClient, error) {
|
||||
base, err := url.Parse(rawURL)
|
||||
if err != nil || base.Scheme == "" || base.Host == "" {
|
||||
return continuityAttentionClient{}, fmt.Errorf("invalid Continuity Runtime URL")
|
||||
}
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return continuityAttentionClient{baseURL: base, client: client}, nil
|
||||
}
|
||||
|
||||
func (c continuityReminderClient) Schedule(ctx context.Context, authorization string, request continuity.ScheduleReminderRequest) (continuity.ScheduleReminderResult, error) {
|
||||
target := *c.baseURL
|
||||
target.Path = "/v1/creator/reminders"
|
||||
@@ -114,6 +132,37 @@ func (c continuityResponseClient) Respond(ctx context.Context, authorization str
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c continuityAttentionClient) Create(ctx context.Context, authorization string, request continuity.CreateAttentionRequest) (continuity.AttentionItem, error) {
|
||||
target := *c.baseURL
|
||||
target.Path = "/v1/creator/attention"
|
||||
payload, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return continuity.AttentionItem{}, err
|
||||
}
|
||||
upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return continuity.AttentionItem{}, err
|
||||
}
|
||||
upstream.Header.Set("Content-Type", "application/json")
|
||||
upstream.Header.Set("Authorization", authorization)
|
||||
response, err := c.client.Do(upstream)
|
||||
if err != nil {
|
||||
return continuity.AttentionItem{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusCreated {
|
||||
return continuity.AttentionItem{}, fmt.Errorf("continuity attention status %d", response.StatusCode)
|
||||
}
|
||||
var result continuity.AttentionItem
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return continuity.AttentionItem{}, err
|
||||
}
|
||||
if result.ID == "" || result.SourceKind == "" {
|
||||
return continuity.AttentionItem{}, fmt.Errorf("invalid attention result")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func resolveChatCapability(ctx context.Context, repository *postgres.CapabilityRepository, kind capability.CapabilityKind, session identity.CreatorSession, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, now time.Time) (capability.CapabilityResolution, error) {
|
||||
requirementID, err := ids.New()
|
||||
if err != nil {
|
||||
@@ -127,6 +176,7 @@ func resolveChatCapability(ctx context.Context, repository *postgres.CapabilityR
|
||||
OriginalIntentID: capability.OriginalIntentRef(intent.ID),
|
||||
SourceUtteranceID: capability.SourceUtteranceRef(utterance.ID),
|
||||
RequiredConstraints: []capability.DataConstraint{capability.DataConstraintLocalOnly},
|
||||
AllowCanary: allowCanaryForChatCapability(kind),
|
||||
AuthorityContextRef: capability.AuthorityPolicyCreatorDirectiveV1,
|
||||
ObservedAtUnixMillis: now.UnixMilli(),
|
||||
}
|
||||
@@ -144,10 +194,86 @@ func resolveChatCapability(ctx context.Context, repository *postgres.CapabilityR
|
||||
return resolution, err
|
||||
}
|
||||
|
||||
func observeCapabilityUsage(ctx context.Context, repository *postgres.CapabilityRepository, selected capability.ResolvedCapabilityImplementation, requirement capability.CapabilityResolutionRequirement, outcome capability.UsageOutcomeClass, outcomeRef string, started time.Time, now time.Time) {
|
||||
func allowCanaryForChatCapability(kind capability.CapabilityKind) bool {
|
||||
switch kind {
|
||||
case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type createdDelegation struct {
|
||||
Delegation work.Delegation
|
||||
Run work.AgentRun
|
||||
}
|
||||
|
||||
func maybeCreateAgentDelegation(ctx context.Context, workRepository *postgres.WorkRepository, capabilityRepository *postgres.CapabilityRepository, session identity.CreatorSession, conversation interaction.Conversation, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, planning work.PlanningResult, now time.Time) (*createdDelegation, capability.CapabilityResolution, error) {
|
||||
requirement, ok := primarySoftwareRequirement(planning.Candidate.Requirements)
|
||||
if !ok {
|
||||
return nil, capability.CapabilityResolution{}, nil
|
||||
}
|
||||
resolution, err := resolveChatCapability(ctx, capabilityRepository, requirement.CapabilityKind, session, intent, utterance, now)
|
||||
if err != nil {
|
||||
return nil, resolution, err
|
||||
}
|
||||
if len(resolution.Eligible) == 0 || resolution.Eligible[0].Implementation.Kind != capability.ImplementationAgent {
|
||||
return nil, resolution, capabilitydomain.ErrNoCapabilityImplementation
|
||||
}
|
||||
started, err := workdomain.StartDelegation(workdomain.DelegationRequest{
|
||||
Goal: planning.Goal,
|
||||
Plan: planning.Plan,
|
||||
Candidate: planning.Candidate,
|
||||
Requirement: requirement,
|
||||
Implementation: resolution.Eligible[0].Implementation,
|
||||
ConversationID: work.ConversationRef(conversation.ID),
|
||||
OriginalIntentText: intent.Text,
|
||||
ContextSummary: planning.Candidate.Summary,
|
||||
ContextRefs: []string{"interaction:intent:" + string(intent.ID), "interaction:utterance:" + string(utterance.ID), "work:plan:" + string(planning.Plan.ID)},
|
||||
VerificationSummary: "verify through changed files, relevant tests, and explicit run evidence before work completion",
|
||||
DerivedInstruction: buildSoftwareAgentInstruction(planning, intent, utterance),
|
||||
StartedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, capability.CapabilityResolution{}, err
|
||||
}
|
||||
if err := workRepository.SaveDelegationBundle(ctx, started.Delegation, started.Run); err != nil {
|
||||
return nil, capability.CapabilityResolution{}, err
|
||||
}
|
||||
return &createdDelegation{Delegation: started.Delegation, Run: started.Run}, resolution, nil
|
||||
}
|
||||
|
||||
func primarySoftwareRequirement(values []work.CapabilityRequirement) (work.CapabilityRequirement, bool) {
|
||||
for _, requirement := range values {
|
||||
switch requirement.CapabilityKind {
|
||||
case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject:
|
||||
return requirement, true
|
||||
}
|
||||
}
|
||||
return work.CapabilityRequirement{}, false
|
||||
}
|
||||
|
||||
func buildSoftwareAgentInstruction(planning work.PlanningResult, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance) string {
|
||||
steps := make([]string, 0, len(planning.Candidate.Steps))
|
||||
for _, step := range planning.Candidate.Steps {
|
||||
if step.Description != "" {
|
||||
steps = append(steps, step.Description)
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(strings.Join([]string{
|
||||
"Human asked: " + strings.TrimSpace(intent.Text),
|
||||
"Original utterance: " + strings.TrimSpace(utterance.Content),
|
||||
"Desired outcome: " + planning.Goal.DesiredOutcome,
|
||||
"Plan summary: " + planning.Candidate.Summary,
|
||||
"Verification: run relevant tests and keep explicit evidence before claiming completion.",
|
||||
"Steps: " + strings.Join(steps, " | "),
|
||||
}, "\n"))
|
||||
}
|
||||
|
||||
func observeCapabilityUsage(ctx context.Context, repository *postgres.CapabilityRepository, selected capability.ResolvedCapabilityImplementation, requirement capability.CapabilityResolutionRequirement, outcome capability.UsageOutcomeClass, outcomeRef string, started time.Time, now time.Time) (capability.CapabilityUsageObservationID, error) {
|
||||
id, err := ids.New()
|
||||
if err != nil {
|
||||
return
|
||||
return "", err
|
||||
}
|
||||
latency := now.Sub(started).Milliseconds()
|
||||
observation := capability.CapabilityUsageObservation{
|
||||
@@ -164,7 +290,10 @@ func observeCapabilityUsage(ctx context.Context, repository *postgres.Capability
|
||||
LatencyMillis: &latency,
|
||||
ObservedAtUnixMillis: now.UnixMilli(),
|
||||
}
|
||||
_ = repository.SaveCapabilityUsageObservation(ctx, observation)
|
||||
if err := repository.SaveCapabilityUsageObservation(ctx, observation); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return observation.ID, nil
|
||||
}
|
||||
|
||||
func buildReminderRequest(message string, session identity.CreatorSession, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, now time.Time) (continuity.ScheduleReminderRequest, error) {
|
||||
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
func TestBuildReminderRequestParsesTomorrowAtTen(t *testing.T) {
|
||||
@@ -72,3 +74,29 @@ func TestReminderSummaryRemovesSchedulingWords(t *testing.T) {
|
||||
t.Fatalf("summary = %q", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSoftwareAgentInstructionKeepsHumanIntentSeparateFromDerivedContext(t *testing.T) {
|
||||
instruction := buildSoftwareAgentInstruction(
|
||||
work.PlanningResult{
|
||||
Goal: work.Goal{DesiredOutcome: "fix chat wording"},
|
||||
Candidate: work.PlanCandidate{Summary: "accepted deterministic software change plan", Steps: []work.PlanStep{{Description: "Inspect chat surface"}, {Description: "Change copy"}, {Description: "Run tests"}}},
|
||||
},
|
||||
interaction.OriginalHumanIntent{ID: "intent-1", Text: "Исправь текст в чате Assistant."},
|
||||
interaction.HumanUtterance{ID: "utterance-1", Content: "Исправь текст в чате Assistant."},
|
||||
)
|
||||
if !strings.Contains(instruction, "Human asked: Исправь текст в чате Assistant.") {
|
||||
t.Fatalf("instruction must preserve human intent: %q", instruction)
|
||||
}
|
||||
if !strings.Contains(instruction, "Verification: run relevant tests") || !strings.Contains(instruction, "Plan summary: accepted deterministic software change plan") {
|
||||
t.Fatalf("instruction must include derived execution context: %q", instruction)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowCanaryForChatCapabilityIsLimitedToSoftwareEngineeringPath(t *testing.T) {
|
||||
if !allowCanaryForChatCapability(capability.CapabilityKindModifyCode) || !allowCanaryForChatCapability(capability.CapabilityKindRunTests) || !allowCanaryForChatCapability(capability.CapabilityKindInspectProject) {
|
||||
t.Fatal("software-engineering capabilities must allow canary implementations")
|
||||
}
|
||||
if allowCanaryForChatCapability(capability.CapabilityKindScheduleReminder) {
|
||||
t.Fatal("non software-engineering capability must not implicitly allow canary")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
type cognitiveInterpretationClient struct {
|
||||
baseURL *url.URL
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cognitiveInterpretationClient, error) {
|
||||
base, err := url.Parse(rawURL)
|
||||
if err != nil || base.Scheme == "" || base.Host == "" {
|
||||
return cognitiveInterpretationClient{}, fmt.Errorf("invalid Cognitive Runtime URL")
|
||||
}
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return cognitiveInterpretationClient{baseURL: base, client: client}, nil
|
||||
}
|
||||
|
||||
func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request cognitive.GatewayInterpretationRequest) (cognitive.GatewayInterpretationResponse, error) {
|
||||
target := *c.baseURL
|
||||
target.Path = "/v1/interpretations"
|
||||
payload, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return cognitive.GatewayInterpretationResponse{}, err
|
||||
}
|
||||
upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return cognitive.GatewayInterpretationResponse{}, err
|
||||
}
|
||||
upstream.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client.Do(upstream)
|
||||
if err != nil {
|
||||
return cognitive.GatewayInterpretationResponse{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return cognitive.GatewayInterpretationResponse{}, fmt.Errorf("cognitive interpretation status %d", response.StatusCode)
|
||||
}
|
||||
var result cognitive.GatewayInterpretationResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return cognitive.GatewayInterpretationResponse{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool {
|
||||
// The built-in advisory channel is the default conversational path. A
|
||||
// simple question must not first wait for the slower, strict JSON
|
||||
// interpretation route: that route exists for ambiguity and work routing,
|
||||
// not as a prerequisite for talking to Domovoy.
|
||||
if isSimpleLocalQuestion(result) {
|
||||
return false
|
||||
}
|
||||
switch result.Interpretation.Applicability {
|
||||
case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isSimpleLocalQuestion(result interactiondomain.SemanticResult) bool {
|
||||
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion || result.Interpretation.Clarification == interaction.ClarificationRequired {
|
||||
return false
|
||||
}
|
||||
for _, hint := range result.CapabilityHints {
|
||||
switch hint {
|
||||
case "MODIFY_CODE", "RUN_TESTS", "SEND_EMAIL", "SCHEDULE_REMINDER", "READ_DATABASE", "QUERY_LOGS":
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func buildGatewayInterpretationRequest(session identity.CreatorSession, utterance interaction.HumanUtterance, semantic interactiondomain.SemanticResult, projectNames []string) cognitive.GatewayInterpretationRequest {
|
||||
requestID, _ := ids.New()
|
||||
return cognitive.GatewayInterpretationRequest{
|
||||
Version: cognitive.CreatorAdvisoryV1,
|
||||
RequestID: cognitive.RequestID(requestID),
|
||||
ActorID: session.ActorID,
|
||||
PrincipalID: session.PrincipalID,
|
||||
ScopeID: authority.ScopeID("domovoy-global-scope"),
|
||||
Message: utterance.Content,
|
||||
Locale: "ru",
|
||||
MaxOutputBytes: 4096,
|
||||
DataPolicy: capability.DataPolicyLocalOnly,
|
||||
DeterministicSpeechAct: string(semantic.Interpretation.SpeechAct),
|
||||
DeterministicSecondary: string(semantic.Interpretation.SecondarySpeechAct),
|
||||
DeterministicConfidence: semantic.Interpretation.ConfidenceMillis,
|
||||
DeterministicApplicability: cognitive.InterpretationApplicability(semantic.Interpretation.Applicability),
|
||||
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
|
||||
ProjectNames: projectNames,
|
||||
}
|
||||
}
|
||||
|
||||
func interactionCandidateFromGateway(utteranceID interaction.HumanUtteranceID, candidate cognitive.GatewayInterpretationCandidate, now interface{ UnixMilli() int64 }) (interaction.SemanticCandidate, error) {
|
||||
id, err := ids.New()
|
||||
if err != nil {
|
||||
return interaction.SemanticCandidate{}, err
|
||||
}
|
||||
return interaction.SemanticCandidate{
|
||||
ID: interaction.SemanticCandidateID(id),
|
||||
UtteranceID: utteranceID,
|
||||
Source: interaction.CandidateSourceCognitive,
|
||||
SelectionState: interaction.CandidateSelected,
|
||||
PrimarySpeechAct: interaction.SpeechAct(candidate.PrimarySpeechAct),
|
||||
SecondarySpeechAct: interaction.SpeechAct(candidate.SecondarySpeechAct),
|
||||
ConfidenceMillis: candidate.ConfidenceMillis,
|
||||
Applicability: interaction.SemanticApplicability(candidate.Applicability),
|
||||
RequestedOutcome: candidate.RequestedOutcome,
|
||||
Clarification: interaction.ClarificationState(candidate.Clarification),
|
||||
NeedsCognition: candidate.NeedsCognition,
|
||||
SelectionReason: candidate.SelectionReason,
|
||||
CapabilityHints: capabilityHintStrings(candidate.CapabilityHints),
|
||||
RecordedAtUnixMillis: now.UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func capabilityHintStrings(values []capability.CapabilityKind) []string {
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, string(value))
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
)
|
||||
|
||||
func TestSimpleQuestionUsesBuiltInAdvisoryWithoutStrictInterpretation(t *testing.T) {
|
||||
result := interactiondomain.SemanticResult{
|
||||
Interpretation: interaction.UtteranceInterpretation{
|
||||
SpeechAct: interaction.SpeechActQuestion,
|
||||
Applicability: interaction.SemanticNeedsBroaderInterpret,
|
||||
Clarification: interaction.ClarificationNotRequired,
|
||||
},
|
||||
CapabilityHints: []string{"COGNITIVE_REASONING", "VERIFY_FACT"},
|
||||
}
|
||||
if shouldRequestBroaderInterpretation(result) {
|
||||
t.Fatal("simple question must go directly to the built-in advisory path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperationalQuestionStillUsesStrictInterpretation(t *testing.T) {
|
||||
result := interactiondomain.SemanticResult{
|
||||
Interpretation: interaction.UtteranceInterpretation{
|
||||
SpeechAct: interaction.SpeechActQuestion,
|
||||
Applicability: interaction.SemanticNeedsBroaderInterpret,
|
||||
Clarification: interaction.ClarificationNotRequired,
|
||||
},
|
||||
CapabilityHints: []string{"MODIFY_CODE", "RUN_TESTS"},
|
||||
}
|
||||
if !shouldRequestBroaderInterpretation(result) {
|
||||
t.Fatal("operational question must retain strict interpretation routing")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
|
||||
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
|
||||
)
|
||||
|
||||
const reminderResponseSkillID = capability.SkillProfileID("skill-obvious-reminder-response-v1")
|
||||
|
||||
type learningEpistemicsStore interface {
|
||||
SaveSystemExpectation(context.Context, epistemics.SystemExpectation) error
|
||||
SaveExperienceRecord(context.Context, epistemics.ExperienceRecord) error
|
||||
SaveReflectionCandidate(context.Context, epistemics.ReflectionCandidate) error
|
||||
}
|
||||
|
||||
type learningCapabilityStore interface {
|
||||
LoadSkillProfile(context.Context, capability.SkillProfileID) (capability.SkillProfile, error)
|
||||
SaveSkillProfile(context.Context, capability.SkillProfile) error
|
||||
}
|
||||
|
||||
type learningRecorder struct {
|
||||
epistemics learningEpistemicsStore
|
||||
capability capabilityLearningStore
|
||||
}
|
||||
|
||||
type capabilityLearningStore interface {
|
||||
learningCapabilityStore
|
||||
}
|
||||
|
||||
type reminderResponseLearningInput struct {
|
||||
Utterance interaction.HumanUtterance
|
||||
Intent interaction.OriginalHumanIntent
|
||||
Result continuity.HumanResponseResult
|
||||
At time.Time
|
||||
}
|
||||
|
||||
type correctionLearningInput struct {
|
||||
Utterance interaction.HumanUtterance
|
||||
Intent interaction.OriginalHumanIntent
|
||||
At time.Time
|
||||
}
|
||||
|
||||
type fastPathDeclineLearningInput struct {
|
||||
Utterance interaction.HumanUtterance
|
||||
Intent interaction.OriginalHumanIntent
|
||||
Result continuity.HumanResponseResult
|
||||
Reason capability.FastPathDeclineReason
|
||||
At time.Time
|
||||
}
|
||||
|
||||
type capabilityLearningInput struct {
|
||||
Utterance interaction.HumanUtterance
|
||||
Intent interaction.OriginalHumanIntent
|
||||
ImplementationID capability.CapabilityImplementationID
|
||||
UsageObservation capability.CapabilityUsageObservationID
|
||||
GapID capability.CapabilityGapID
|
||||
Outcome capability.UsageOutcomeClass
|
||||
OutcomeRef string
|
||||
At time.Time
|
||||
}
|
||||
|
||||
type delegationLearningInput struct {
|
||||
Intent interaction.OriginalHumanIntent
|
||||
Utterance interaction.HumanUtterance
|
||||
DelegationID string
|
||||
RunID string
|
||||
ImplementationID capability.CapabilityImplementationID
|
||||
OutcomeRef string
|
||||
VerificationRefs []epistemics.EvidenceRef
|
||||
Verified bool
|
||||
ExecutionWindows int
|
||||
HumanBlocked bool
|
||||
At time.Time
|
||||
}
|
||||
|
||||
func (r learningRecorder) recordReminderResponse(ctx context.Context, input reminderResponseLearningInput) {
|
||||
if r.epistemics == nil || r.capability == nil || input.Intent.ID == "" || input.Utterance.ID == "" || input.Result.Response.ID == "" {
|
||||
return
|
||||
}
|
||||
expectedEffect := "resolve obvious reminder continuation using deterministic target resolver"
|
||||
if input.Result.Response.TargetProspectiveID != "" {
|
||||
expectedEffect += " for prospective item " + string(input.Result.Response.TargetProspectiveID)
|
||||
}
|
||||
expectation := epistemics.SystemExpectation{
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
ExpectedOutcome: epistemics.OutcomeSuccess,
|
||||
ExpectedEffect: expectedEffect,
|
||||
ConfidenceMillis: 850,
|
||||
EvidenceBasis: []epistemics.EvidenceRef{"continuity:response-target-resolution"},
|
||||
RecordedAtUnixMillis: input.At.UnixMilli(),
|
||||
}
|
||||
expectation, err := epistemicsdomain.NewSystemExpectation(expectation)
|
||||
if err != nil {
|
||||
log.Printf("learning reminder expectation invalid: %v", err)
|
||||
return
|
||||
}
|
||||
if err := r.epistemics.SaveSystemExpectation(ctx, expectation); err != nil {
|
||||
log.Printf("learning reminder expectation unavailable: %v", err)
|
||||
return
|
||||
}
|
||||
actual, confidence := outcomeForHumanResponse(input.Result.Response)
|
||||
experience := epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceContinuity,
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
ContextRefs: contextRefs(
|
||||
"continuity:human-response:"+string(input.Result.Response.ID),
|
||||
"continuity:prospective:"+string(input.Result.Response.TargetProspectiveID),
|
||||
"continuity:attention:"+string(input.Result.Response.TargetAttentionID),
|
||||
),
|
||||
HelpMethod: epistemics.HelpMethodDeterministicFastPath,
|
||||
HelpMethodRef: "continuity:human-response-target-resolver-v1",
|
||||
SkillRef: epistemics.SkillRef(reminderResponseSkillID),
|
||||
ExpectationID: expectation.ID,
|
||||
ExpectedOutcome: expectation.ExpectedOutcome,
|
||||
ActualOutcome: actual,
|
||||
OutcomeRef: "continuity:human-response:" + string(input.Result.Response.ID),
|
||||
OutcomeDetail: string(input.Result.Response.Operation) + " -> " + string(input.Result.Response.Outcome),
|
||||
HumanResponseRef: epistemics.HumanResponseRef(input.Result.Response.ID),
|
||||
VerificationEvidence: []epistemics.EvidenceRef{epistemics.EvidenceRef("human-response:" + string(input.Result.Response.Outcome))},
|
||||
ConfidenceMillis: confidence,
|
||||
RecordedAtUnixMillis: input.At.UnixMilli(),
|
||||
}
|
||||
r.saveExperienceAndReflection(ctx, experience)
|
||||
r.updateReminderSkill(ctx, experience, skillOutcomeForExperience(experience), false)
|
||||
}
|
||||
|
||||
func (r learningRecorder) recordCorrection(ctx context.Context, input correctionLearningInput) {
|
||||
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
|
||||
return
|
||||
}
|
||||
experience := epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceInteraction,
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
HelpMethod: epistemics.HelpMethodClarification,
|
||||
ExpectedOutcome: epistemics.OutcomeSuccess,
|
||||
ActualOutcome: epistemics.OutcomeUserCorrected,
|
||||
OutcomeDetail: "human correction indicates prior interpretation or target resolution may be wrong",
|
||||
CorrectionUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
VerificationEvidence: []epistemics.EvidenceRef{"interaction:human-correction"},
|
||||
ConfidenceMillis: 1000,
|
||||
RecordedAtUnixMillis: input.At.UnixMilli(),
|
||||
}
|
||||
r.saveExperienceAndReflection(ctx, experience)
|
||||
}
|
||||
|
||||
func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastPathDeclineLearningInput) {
|
||||
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
|
||||
return
|
||||
}
|
||||
experience, err := epistemicsdomain.FastPathDeclineExperience(
|
||||
epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
epistemics.SkillRef(reminderResponseSkillID),
|
||||
string(outcomeForDecline(input.Reason)),
|
||||
input.At.UnixMilli(),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if input.Result.Response.ID != "" {
|
||||
experience.ContextRefs = contextRefs("continuity:response-candidate:" + string(input.Result.Response.Operation))
|
||||
}
|
||||
saved, ok := r.saveExperienceAndReflection(ctx, experience)
|
||||
if ok {
|
||||
r.updateReminderSkill(ctx, saved, capability.UsageSkipped, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabilityLearningInput) {
|
||||
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
|
||||
return
|
||||
}
|
||||
actual := epistemics.OutcomeSuccess
|
||||
switch input.Outcome {
|
||||
case capability.UsageFailed:
|
||||
actual = epistemics.OutcomeFailed
|
||||
case capability.UsageSkipped:
|
||||
actual = epistemics.OutcomeNotApplicable
|
||||
case capability.UsageUnknown:
|
||||
actual = epistemics.OutcomeUnknown
|
||||
}
|
||||
experience := epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceCapability,
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
HelpMethod: epistemics.HelpMethodCapabilityExecution,
|
||||
CapabilityImplementationRef: epistemics.CapabilityImplementationRef(input.ImplementationID),
|
||||
CapabilityUsageObservationRef: epistemics.CapabilityUsageObservationRef(input.UsageObservation),
|
||||
CapabilityGapRef: epistemics.CapabilityGapRef(input.GapID),
|
||||
ExpectedOutcome: epistemics.OutcomeSuccess,
|
||||
ActualOutcome: actual,
|
||||
OutcomeRef: input.OutcomeRef,
|
||||
OutcomeDetail: "capability outcome " + string(input.Outcome),
|
||||
VerificationEvidence: []epistemics.EvidenceRef{"capability:usage-observation"},
|
||||
ConfidenceMillis: 900,
|
||||
RecordedAtUnixMillis: input.At.UnixMilli(),
|
||||
}
|
||||
r.saveExperienceAndReflection(ctx, experience)
|
||||
}
|
||||
|
||||
func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input delegationLearningInput) {
|
||||
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" || input.ImplementationID == "" {
|
||||
return
|
||||
}
|
||||
actual := epistemics.OutcomeSuccess
|
||||
confidence := int64(950)
|
||||
if !input.Verified {
|
||||
actual = epistemics.OutcomeFailed
|
||||
confidence = 850
|
||||
}
|
||||
detailParts := []string{"delegation outcome recorded"}
|
||||
if input.ExecutionWindows > 1 {
|
||||
detailParts = append(detailParts, "execution-window-continuations="+strconv.Itoa(input.ExecutionWindows-1))
|
||||
}
|
||||
if input.HumanBlocked {
|
||||
detailParts = append(detailParts, "human-blocker-occurred")
|
||||
}
|
||||
experience := epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceCapability,
|
||||
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
|
||||
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
|
||||
ContextRefs: contextRefs("work:delegation:"+input.DelegationID, "work:run:"+input.RunID),
|
||||
HelpMethod: epistemics.HelpMethodCapabilityExecution,
|
||||
HelpMethodRef: "work:agent-delegation-v1",
|
||||
CapabilityImplementationRef: epistemics.CapabilityImplementationRef(input.ImplementationID),
|
||||
ExpectedOutcome: epistemics.OutcomeSuccess,
|
||||
ActualOutcome: actual,
|
||||
OutcomeRef: input.OutcomeRef,
|
||||
OutcomeDetail: strings.Join(detailParts, "; "),
|
||||
VerificationEvidence: input.VerificationRefs,
|
||||
ConfidenceMillis: confidence,
|
||||
RecordedAtUnixMillis: input.At.UnixMilli(),
|
||||
}
|
||||
r.saveExperienceAndReflection(ctx, experience)
|
||||
}
|
||||
|
||||
func (r learningRecorder) saveExperienceAndReflection(ctx context.Context, value epistemics.ExperienceRecord) (epistemics.ExperienceRecord, bool) {
|
||||
value, err := epistemicsdomain.NewExperienceRecord(value)
|
||||
if err != nil {
|
||||
log.Printf("learning experience invalid: %v", err)
|
||||
return epistemics.ExperienceRecord{}, false
|
||||
}
|
||||
if err := r.epistemics.SaveExperienceRecord(ctx, value); err != nil {
|
||||
log.Printf("learning experience unavailable: %v", err)
|
||||
return epistemics.ExperienceRecord{}, false
|
||||
}
|
||||
if candidate, ok, err := epistemicsdomain.ReflectionForExperience(value); err == nil && ok {
|
||||
if err := r.epistemics.SaveReflectionCandidate(ctx, candidate); err != nil {
|
||||
log.Printf("learning reflection unavailable: %v", err)
|
||||
}
|
||||
} else if err != nil {
|
||||
log.Printf("learning reflection invalid: %v", err)
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
func (r learningRecorder) updateReminderSkill(ctx context.Context, experience epistemics.ExperienceRecord, outcome capability.UsageOutcomeClass, corrected bool) {
|
||||
if r.capability == nil || experience.ID == "" {
|
||||
return
|
||||
}
|
||||
profile, err := r.capability.LoadSkillProfile(ctx, reminderResponseSkillID)
|
||||
if err != nil {
|
||||
log.Printf("learning skill profile unavailable: %v", err)
|
||||
return
|
||||
}
|
||||
next, err := capabilitydomain.ObserveSkillOutcome(profile, capability.SkillOutcomeObservation{
|
||||
ExperienceRef: capability.SkillEvidenceRef("epistemics:experience:" + string(experience.ID)),
|
||||
Outcome: outcome,
|
||||
UserCorrected: corrected,
|
||||
ObservedAtUnixMillis: experience.RecordedAtUnixMillis,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("learning skill observation invalid: %v", err)
|
||||
return
|
||||
}
|
||||
if err := r.capability.SaveSkillProfile(ctx, next); err != nil {
|
||||
log.Printf("learning skill profile save unavailable: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func outcomeForHumanResponse(response continuity.HumanResponse) (epistemics.OutcomeClass, int64) {
|
||||
if response.ResolutionState != continuity.ResponseResolved {
|
||||
switch response.ResolutionState {
|
||||
case continuity.ResponseAmbiguous:
|
||||
return epistemics.OutcomeConflict, 850
|
||||
case continuity.ResponseNoTarget:
|
||||
return epistemics.OutcomeOutOfDistribution, 850
|
||||
default:
|
||||
return epistemics.OutcomeUnexpected, 700
|
||||
}
|
||||
}
|
||||
switch response.Outcome {
|
||||
case continuity.ResponseOutcomeUserReportedComplete, continuity.ResponseOutcomeSnoozed, continuity.ResponseOutcomeCancelled, continuity.ResponseOutcomeDismissed:
|
||||
return epistemics.OutcomeSuccess, 950
|
||||
case continuity.ResponseOutcomeClarificationNeeded:
|
||||
return epistemics.OutcomeConflict, 850
|
||||
default:
|
||||
return epistemics.OutcomeUnknown, 500
|
||||
}
|
||||
}
|
||||
|
||||
func skillOutcomeForExperience(experience epistemics.ExperienceRecord) capability.UsageOutcomeClass {
|
||||
switch experience.ActualOutcome {
|
||||
case epistemics.OutcomeSuccess, epistemics.OutcomePartialSuccess:
|
||||
return capability.UsageSucceeded
|
||||
case epistemics.OutcomeFailed, epistemics.OutcomeUnexpected, epistemics.OutcomeUserCorrected:
|
||||
return capability.UsageFailed
|
||||
case epistemics.OutcomeUnknown:
|
||||
return capability.UsageUnknown
|
||||
default:
|
||||
return capability.UsageSkipped
|
||||
}
|
||||
}
|
||||
|
||||
func outcomeForDecline(reason capability.FastPathDeclineReason) epistemics.OutcomeClass {
|
||||
switch reason {
|
||||
case capability.FastPathLowConfidence:
|
||||
return epistemics.OutcomeLowConfidence
|
||||
case capability.FastPathOutOfDistribution:
|
||||
return epistemics.OutcomeOutOfDistribution
|
||||
case capability.FastPathConflict:
|
||||
return epistemics.OutcomeConflict
|
||||
case capability.FastPathStale:
|
||||
return epistemics.OutcomeStale
|
||||
default:
|
||||
return epistemics.OutcomeNotApplicable
|
||||
}
|
||||
}
|
||||
|
||||
func contextRefs(values ...string) []epistemics.ContextRef {
|
||||
refs := make([]epistemics.ContextRef, 0, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || strings.HasSuffix(value, ":") {
|
||||
continue
|
||||
}
|
||||
refs = append(refs, epistemics.ContextRef(value))
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
func looksLikeUnsupportedContinuation(message string, result continuity.HumanResponseResult) bool {
|
||||
lower := strings.ToLower(strings.TrimSpace(message))
|
||||
if result.Handled {
|
||||
return false
|
||||
}
|
||||
if result.Response.Operation == continuity.ResponseContinue || result.Response.Operation == continuity.ResponseReject {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(lower, "это") && (strings.Contains(lower, "напомин") || strings.Contains(lower, "перенеси") || strings.Contains(lower, "закрой") || strings.Contains(lower, "передвинь"))
|
||||
}
|
||||
|
||||
func declineReasonForResponse(result continuity.HumanResponseResult) capability.FastPathDeclineReason {
|
||||
switch result.Response.Operation {
|
||||
case continuity.ResponseReject:
|
||||
return capability.FastPathConflict
|
||||
case continuity.ResponseContinue:
|
||||
return capability.FastPathNotApplicable
|
||||
default:
|
||||
return capability.FastPathOutOfDistribution
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
)
|
||||
|
||||
type learningEpistemicsStub struct {
|
||||
experiences []epistemics.ExperienceRecord
|
||||
}
|
||||
|
||||
func (s *learningEpistemicsStub) SaveSystemExpectation(context.Context, epistemics.SystemExpectation) error {
|
||||
return nil
|
||||
}
|
||||
func (s *learningEpistemicsStub) SaveExperienceRecord(_ context.Context, value epistemics.ExperienceRecord) error {
|
||||
s.experiences = append(s.experiences, value)
|
||||
return nil
|
||||
}
|
||||
func (s *learningEpistemicsStub) SaveReflectionCandidate(context.Context, epistemics.ReflectionCandidate) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type learningCapabilityStub struct {
|
||||
loadCalls int
|
||||
saveCalls int
|
||||
}
|
||||
|
||||
func (s *learningCapabilityStub) LoadSkillProfile(context.Context, capability.SkillProfileID) (capability.SkillProfile, error) {
|
||||
s.loadCalls++
|
||||
return capability.SkillProfile{}, nil
|
||||
}
|
||||
func (s *learningCapabilityStub) SaveSkillProfile(context.Context, capability.SkillProfile) error {
|
||||
s.saveCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestGenericCorrectionDoesNotDegradeReminderSkill(t *testing.T) {
|
||||
epistemicsStore := &learningEpistemicsStub{}
|
||||
capabilityStore := &learningCapabilityStub{}
|
||||
recorder := learningRecorder{epistemics: epistemicsStore, capability: capabilityStore}
|
||||
recorder.recordCorrection(context.Background(), correctionLearningInput{
|
||||
Intent: interaction.OriginalHumanIntent{ID: "intent-1"},
|
||||
Utterance: interaction.HumanUtterance{ID: "utterance-1"},
|
||||
At: time.UnixMilli(1000),
|
||||
})
|
||||
if capabilityStore.loadCalls != 0 || capabilityStore.saveCalls != 0 {
|
||||
t.Fatalf("generic correction must not mutate reminder skill: load=%d save=%d", capabilityStore.loadCalls, capabilityStore.saveCalls)
|
||||
}
|
||||
if len(epistemicsStore.experiences) != 1 || epistemicsStore.experiences[0].SkillRef != "" {
|
||||
t.Fatalf("generic correction must remain evidence without a reminder skill reference: %#v", epistemicsStore.experiences)
|
||||
}
|
||||
}
|
||||
+578
-23
@@ -8,6 +8,7 @@ import (
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
@@ -68,6 +69,19 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer capabilityRepository.Close()
|
||||
epistemicsPassword, err := readCredentialRefSecret("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
epistemicsUser := os.Getenv("EPISTEMICS_POSTGRES_USER")
|
||||
if epistemicsUser == "" {
|
||||
epistemicsUser = "epistemics_role"
|
||||
}
|
||||
epistemicsRepository, err := postgres.OpenEpistemics(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: epistemicsUser, Password: epistemicsPassword})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer epistemicsRepository.Close()
|
||||
identityRepository, err := postgres.OpenIdentity(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("IDENTITY_POSTGRES_USER"), Password: os.Getenv("IDENTITY_POSTGRES_PASSWORD")})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -94,6 +108,15 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
attentionClient, err := newContinuityAttentionClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cognitiveClient, err := newCognitiveInterpretationClient(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), &http.Client{Timeout: 20 * time.Second})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
|
||||
mux := http.NewServeMux()
|
||||
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
|
||||
@@ -112,6 +135,12 @@ func main() {
|
||||
return situationdomain.NewResolver(bindings).Resolve(runtime)
|
||||
}
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "core-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
readinessClient := &http.Client{Timeout: 2 * time.Second}
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "core-runtime", Probes: []health.Probe{
|
||||
health.TCPProbe("postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/healthz"),
|
||||
}})
|
||||
mux.HandleFunc("POST /v1/creator/sessions", creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword, sessionKey, time.Now))
|
||||
mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
@@ -172,6 +201,45 @@ func main() {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(status)
|
||||
})
|
||||
mux.HandleFunc("GET /v1/creator/learning/recent", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
limit := 10
|
||||
if raw := r.URL.Query().Get("limit"); raw != "" {
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil || parsed < 1 || parsed > 50 {
|
||||
http.Error(w, "invalid learning limit", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
limit = parsed
|
||||
}
|
||||
experiences, err := epistemicsRepository.RecentExperienceRecords(r.Context(), limit)
|
||||
if err != nil {
|
||||
log.Printf("learning recent experiences unavailable: %v", err)
|
||||
http.Error(w, "learning experience unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
reflections, err := epistemicsRepository.RecentReflectionCandidates(r.Context(), limit)
|
||||
if err != nil {
|
||||
log.Printf("learning recent reflections unavailable: %v", err)
|
||||
http.Error(w, "learning reflection unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
skills, err := capabilityRepository.RecentSkillProfiles(r.Context(), limit)
|
||||
if err != nil {
|
||||
log.Printf("learning recent skills unavailable: %v", err)
|
||||
http.Error(w, "learning skills unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_ = json.NewEncoder(w).Encode(struct {
|
||||
Experiences []epistemics.ExperienceRecord
|
||||
Reflections []epistemics.ReflectionCandidate
|
||||
Skills []capability.SkillProfile
|
||||
}{Experiences: experiences, Reflections: reflections, Skills: skills})
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/chat/messages", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
@@ -231,11 +299,31 @@ func main() {
|
||||
http.Error(w, "interpret chat message", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
cognitiveAttempted := false
|
||||
cognitiveAvailable := true
|
||||
if shouldRequestBroaderInterpretation(semantic) {
|
||||
cognitiveAttempted = true
|
||||
projectNames := make([]string, 0, len(semantic.Projects))
|
||||
for _, ref := range semantic.Projects {
|
||||
if ref.ProjectDisplayName != "" {
|
||||
projectNames = append(projectNames, ref.ProjectDisplayName)
|
||||
}
|
||||
}
|
||||
if response, interpretationErr := cognitiveClient.Interpret(r.Context(), buildGatewayInterpretationRequest(session, utterance, semantic, projectNames)); interpretationErr == nil && response.FinishReason == cognitive.FinishCompleted {
|
||||
if candidate, candidateErr := interactionCandidateFromGateway(utterance.ID, response.Candidate, now); candidateErr == nil {
|
||||
semantic = interactiondomain.ApplyCognitiveCandidate(semantic, utterance.Content, candidate, previousFocus)
|
||||
} else {
|
||||
cognitiveAvailable = false
|
||||
}
|
||||
} else {
|
||||
cognitiveAvailable = false
|
||||
}
|
||||
}
|
||||
if err := interactionRepository.AppendUtterance(r.Context(), conversation, utterance, intent, relations); err != nil {
|
||||
http.Error(w, "save chat message", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := interactionRepository.SaveSemanticResult(r.Context(), semantic.Interpretation, semantic.Projects, semantic.Relations); err != nil {
|
||||
if err := interactionRepository.SaveSemanticResult(r.Context(), semantic.Interpretation, semantic.Candidates, semantic.Projects, semantic.Relations); err != nil {
|
||||
http.Error(w, "save chat semantics", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -245,6 +333,9 @@ func main() {
|
||||
Message: utterance.Content,
|
||||
RequestedAtUnixMillis: now.UnixMilli(),
|
||||
})
|
||||
if semantic.Interpretation.SpeechAct == interaction.SpeechActCorrection {
|
||||
learning.recordCorrection(r.Context(), correctionLearningInput{Utterance: utterance, Intent: intent, At: now})
|
||||
}
|
||||
activeFocusName := ""
|
||||
activeFocusProvenance := situation.ProjectResolutionProvenance("")
|
||||
if semantic.ActiveFocus != nil {
|
||||
@@ -270,6 +361,12 @@ func main() {
|
||||
if responseErr == nil && responseResult.Handled {
|
||||
planningReply = responseResult.Reply
|
||||
needsCognition = false
|
||||
learning.recordReminderResponse(r.Context(), reminderResponseLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, At: now})
|
||||
if resumed, err := continueDelegationFromHumanResponse(r.Context(), workRepository, responseResult.Response, now); err == nil && resumed {
|
||||
planningReply = "Принял. Продолжаю ту же задачу coding-agent без потери контекста."
|
||||
}
|
||||
} else if responseErr == nil && looksLikeUnsupportedContinuation(utterance.Content, responseResult) {
|
||||
learning.recordFastPathDecline(r.Context(), fastPathDeclineLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, Reason: declineReasonForResponse(responseResult), At: now})
|
||||
}
|
||||
planningInput := work.PlanningInput{
|
||||
ConversationID: work.ConversationRef(conversation.ID),
|
||||
@@ -283,14 +380,19 @@ func main() {
|
||||
ProjectProvenance: work.ProjectProvenanceRef(activeFocusProvenance),
|
||||
Message: utterance.Content,
|
||||
SpeechAct: work.SpeechActRef(semantic.Interpretation.SpeechAct),
|
||||
SecondarySpeechAct: work.SpeechActRef(semantic.Interpretation.SecondarySpeechAct),
|
||||
AssertionVerification: epistemics.VerificationState(semantic.Interpretation.AssertionVerification),
|
||||
CapabilityHints: toCapabilityKinds(semantic.CapabilityHints),
|
||||
NeedsCognition: semantic.NeedsCognition,
|
||||
RequiresClarification: semantic.Interpretation.Clarification == interaction.ClarificationRequired,
|
||||
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
|
||||
}
|
||||
helpPath := workdomain.DetermineHelpPath(planningInput)
|
||||
if responseErr == nil && responseResult.Handled {
|
||||
helpPath = work.HelpPath{}
|
||||
}
|
||||
if isCapabilityQuestion(utterance.Content) {
|
||||
switch semantic.InformationRequestKind {
|
||||
case interaction.InformationRequestCapabilityInventory:
|
||||
kinds, err := capabilityRepository.AvailableCapabilityKinds(r.Context(), session.PrincipalID)
|
||||
if err == nil && len(kinds) > 0 {
|
||||
planningReply = "Сейчас у меня подключены реальные capabilities: " + humanCapabilityList(kinds) + ". Для рассуждений я использую отдельный Cognitive Gateway/Router, если локальный CANARY доступен. Возможности вроде отправки почты пока будут фиксироваться как capability gap, а не как выполненное действие."
|
||||
@@ -298,14 +400,28 @@ func main() {
|
||||
planningReply = "Я умею записывать диалог и намерения, работать с текущим контекстом, строить планы и обращаться к Cognitive Gateway при доступности локального CANARY. Реальные action-capabilities подключаются через registry; если capability нет, я фиксирую gap."
|
||||
}
|
||||
needsCognition = false
|
||||
case interaction.InformationRequestCognitiveModelInventory:
|
||||
candidates, inventoryErr := capabilityRepository.LoadCognitiveCandidates(r.Context(), capability.CapabilityID("creator-advisory"), session.PrincipalID)
|
||||
planningReply = humanCognitiveModelInventory(candidates, inventoryErr)
|
||||
needsCognition = false
|
||||
case interaction.InformationRequestCognitiveChannelStatus, interaction.InformationRequestCognitiveRecovery:
|
||||
health, healthErr := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID("cognitive-proposal"))
|
||||
planningReply = humanCognitiveChannelReply(semantic.InformationRequestKind, semantic.Interpretation.SpeechAct, health, healthErr)
|
||||
needsCognition = false
|
||||
}
|
||||
if helpPath.Reply != "" {
|
||||
planningReply = helpPath.Reply
|
||||
needsCognition = false
|
||||
}
|
||||
if planningReply == "" && needsCognition && cognitiveAttempted && !cognitiveAvailable {
|
||||
planningReply = humanCognitiveDegradedReply(utterance.Content)
|
||||
needsCognition = false
|
||||
}
|
||||
var capabilityStatus capability.CapabilityResolutionStatus
|
||||
var capabilityKind capability.CapabilityKind
|
||||
var capabilityGapID capability.CapabilityGapID
|
||||
var delegationID work.DelegationID
|
||||
var delegationState work.DelegationState
|
||||
var reminder *continuity.ProspectiveItem
|
||||
if helpPath.Kind == work.HelpPathDirectCapability && len(helpPath.SuggestedCapabilityKinds) > 0 {
|
||||
capabilityKind = helpPath.SuggestedCapabilityKinds[0]
|
||||
@@ -320,10 +436,12 @@ func main() {
|
||||
if scheduleErr == nil {
|
||||
reminder = &result.Item
|
||||
planningReply = fmt.Sprintf("Готово. Я создал напоминание: %s.", result.Item.Summary)
|
||||
observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageSucceeded, string(result.Item.ID), started, finished)
|
||||
usageID, _ := observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageSucceeded, string(result.Item.ID), started, finished)
|
||||
learning.recordCapabilityUse(r.Context(), capabilityLearningInput{Utterance: utterance, Intent: intent, ImplementationID: resolution.Eligible[0].Implementation.ID, UsageObservation: usageID, Outcome: capability.UsageSucceeded, OutcomeRef: "continuity:prospective:" + string(result.Item.ID), At: finished})
|
||||
} else {
|
||||
planningReply = "Scheduling capability сейчас недоступна: напоминание не создано. Я не буду притворяться, что действие выполнено."
|
||||
observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageFailed, "continuity-reminder-unavailable", started, finished)
|
||||
usageID, _ := observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageFailed, "continuity-reminder-unavailable", started, finished)
|
||||
learning.recordCapabilityUse(r.Context(), capabilityLearningInput{Utterance: utterance, Intent: intent, ImplementationID: resolution.Eligible[0].Implementation.ID, UsageObservation: usageID, Outcome: capability.UsageFailed, OutcomeRef: "continuity-reminder-unavailable", At: finished})
|
||||
}
|
||||
} else {
|
||||
planningReply = "Мне нужно уточнить время напоминания. Например: «Напомни завтра в 10 позвонить врачу»."
|
||||
@@ -332,6 +450,7 @@ func main() {
|
||||
} else if resolution.Gap != nil {
|
||||
capabilityGapID = resolution.Gap.ID
|
||||
planningReply = "Сейчас scheduling capability недоступна, поэтому напоминание не создано. Я зафиксировал capability gap."
|
||||
learning.recordCapabilityUse(r.Context(), capabilityLearningInput{Utterance: utterance, Intent: intent, GapID: resolution.Gap.ID, Outcome: capability.UsageSkipped, OutcomeRef: "capability:gap:" + string(resolution.Gap.ID), At: now})
|
||||
}
|
||||
needsCognition = false
|
||||
}
|
||||
@@ -344,6 +463,7 @@ func main() {
|
||||
} else if resolution.Gap != nil {
|
||||
capabilityGapID = resolution.Gap.ID
|
||||
planningReply = "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксировал capability gap, без ложного выполнения."
|
||||
learning.recordCapabilityUse(r.Context(), capabilityLearningInput{Utterance: utterance, Intent: intent, GapID: resolution.Gap.ID, Outcome: capability.UsageSkipped, OutcomeRef: "capability:gap:" + string(resolution.Gap.ID), At: now})
|
||||
}
|
||||
needsCognition = false
|
||||
}
|
||||
@@ -360,30 +480,252 @@ func main() {
|
||||
planningReply = planning.Reply
|
||||
workPlanned = true
|
||||
needsCognition = false
|
||||
if created, resolution, createErr := maybeCreateAgentDelegation(r.Context(), workRepository, capabilityRepository, session, conversation, intent, utterance, planning, now); createErr == nil && created != nil {
|
||||
delegationID = created.Delegation.ID
|
||||
delegationState = created.Delegation.State
|
||||
capabilityKind = created.Delegation.CapabilityKind
|
||||
capabilityStatus = resolution.Status
|
||||
planningReply = "Понял задачу. Я собрал work item, выбрал coding-agent в CANARY-режиме и создал delegation. Буду вести её как отдельный run с checkpoint/resume, без потери исходного human intent."
|
||||
} else if resolution.Gap != nil {
|
||||
capabilityKind = resolution.Requirement.CapabilityKind
|
||||
capabilityStatus = resolution.Status
|
||||
capabilityGapID = resolution.Gap.ID
|
||||
planningReply = planningReply + " Для coding delegation подходящий agent implementation сейчас недоступен, поэтому я сохранил план без ложного запуска."
|
||||
}
|
||||
}
|
||||
response := interaction.ChatMessageResponse{
|
||||
ConversationID: conversation.ID,
|
||||
UtteranceID: utterance.ID,
|
||||
OriginalIntentID: intent.ID,
|
||||
SpeechAct: semantic.Interpretation.SpeechAct,
|
||||
InterpretationState: utterance.InterpretationState,
|
||||
NeedsCognition: needsCognition,
|
||||
FallbackText: semantic.Reply,
|
||||
ActiveProjectName: activeFocusName,
|
||||
ProjectProvenance: activeFocusProvenance,
|
||||
Clarification: semantic.Interpretation.Clarification,
|
||||
WorkPlanned: workPlanned,
|
||||
PlanningReply: planningReply,
|
||||
CapabilityStatus: capabilityStatus,
|
||||
CapabilityKind: capabilityKind,
|
||||
CapabilityGapID: capabilityGapID,
|
||||
Reminder: reminder,
|
||||
ConversationID: conversation.ID,
|
||||
UtteranceID: utterance.ID,
|
||||
OriginalIntentID: intent.ID,
|
||||
SpeechAct: semantic.Interpretation.SpeechAct,
|
||||
InterpretationState: utterance.InterpretationState,
|
||||
NeedsCognition: needsCognition,
|
||||
FallbackText: semantic.Reply,
|
||||
ActiveProjectName: activeFocusName,
|
||||
ProjectProvenance: activeFocusProvenance,
|
||||
Clarification: semantic.Interpretation.Clarification,
|
||||
WorkPlanned: workPlanned,
|
||||
PlanningReply: planningReply,
|
||||
CapabilityStatus: capabilityStatus,
|
||||
CapabilityKind: capabilityKind,
|
||||
CapabilityHints: toCapabilityKinds(semantic.CapabilityHints),
|
||||
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
|
||||
InformationRequestKind: semantic.InformationRequestKind,
|
||||
InterpretationConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
|
||||
CapabilityGapID: capabilityGapID,
|
||||
DelegationID: string(delegationID),
|
||||
DelegationState: string(delegationState),
|
||||
Reminder: reminder,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
})
|
||||
mux.HandleFunc("GET /v1/creator/delegations/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
value, err := workRepository.LoadDelegation(r.Context(), work.DelegationID(r.PathValue("id")))
|
||||
if err != nil {
|
||||
http.Error(w, "delegation not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
})
|
||||
mux.HandleFunc("GET /v1/creator/delegations/{id}/latest-checkpoint", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
value, err := workRepository.LatestAgentCheckpoint(r.Context(), work.DelegationID(r.PathValue("id")))
|
||||
if err != nil {
|
||||
http.Error(w, "checkpoint not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/delegations/{id}/runs/{run}/checkpoints", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
BlockerClass work.AgentBlockerClass
|
||||
CompletedSummary string
|
||||
RemainingSummary string
|
||||
TestSummary string
|
||||
BranchName string
|
||||
CommitHash string
|
||||
NextStep string
|
||||
ArtifactRefs []work.EvidenceRef
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid checkpoint", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
run, err := workRepository.LoadAgentRun(r.Context(), work.AgentRunID(r.PathValue("run")))
|
||||
if err != nil || run.DelegationID != work.DelegationID(r.PathValue("id")) {
|
||||
http.Error(w, "run not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
updatedRun, checkpoint, err := workdomain.CheckpointRun(run, request.BlockerClass, request.CompletedSummary, request.RemainingSummary, request.TestSummary, request.BranchName, request.CommitHash, request.NextStep, request.ArtifactRefs, time.UnixMilli(request.RecordedAtUnixMillis))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid checkpoint", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := workRepository.SaveAgentCheckpoint(r.Context(), updatedRun, checkpoint); err != nil {
|
||||
http.Error(w, "save checkpoint", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if request.BlockerClass == work.AgentBlockerHumanDecisionRequired {
|
||||
delegation, err := workRepository.LoadDelegation(r.Context(), run.DelegationID)
|
||||
if err == nil {
|
||||
_, _ = attentionClient.Create(r.Context(), r.Header.Get("Authorization"), continuity.CreateAttentionRequest{
|
||||
OwnerPrincipalID: session.PrincipalID,
|
||||
ScopeID: authority.ScopeID("domovoy-global-scope"),
|
||||
SourceKind: continuity.AttentionSourceAgentDelegation,
|
||||
SourceExternalRef: delegationAttentionRef(delegation.ID),
|
||||
Level: continuity.AttentionActionRequired,
|
||||
Summary: "Нужен твой выбор по задаче coding-agent: " + firstNonEmpty(request.RemainingSummary, delegation.DesiredOutcome),
|
||||
DueAtUnixMillis: checkpoint.RecordedAtUnixMillis,
|
||||
CreatedAtUnixMillis: checkpoint.RecordedAtUnixMillis,
|
||||
})
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(checkpoint)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/delegations/{id}/runs/{run}/results", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Summary string
|
||||
OutcomeRef string
|
||||
VerificationSummary string
|
||||
ArtifactRefs []work.EvidenceRef
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid result", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
run, err := workRepository.LoadAgentRun(r.Context(), work.AgentRunID(r.PathValue("run")))
|
||||
if err != nil || run.DelegationID != work.DelegationID(r.PathValue("id")) {
|
||||
http.Error(w, "run not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
updatedRun, result, err := workdomain.CompleteRun(run, request.Summary, request.OutcomeRef, request.VerificationSummary, request.ArtifactRefs, time.UnixMilli(request.RecordedAtUnixMillis))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid result", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := workRepository.SaveAgentResult(r.Context(), updatedRun, result); err != nil {
|
||||
http.Error(w, "save result", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/delegations/{id}/resume", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
delegation, err := workRepository.LoadDelegation(r.Context(), work.DelegationID(r.PathValue("id")))
|
||||
if err != nil {
|
||||
http.Error(w, "delegation not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
run, err := workRepository.LoadAgentRun(r.Context(), delegation.LatestRunID)
|
||||
if err != nil {
|
||||
http.Error(w, "run not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
checkpoint, err := workRepository.LatestAgentCheckpoint(r.Context(), delegation.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "checkpoint not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
updatedDelegation, resumedRun, err := workdomain.ResumeDelegationRun(delegation, run, checkpoint, time.Now())
|
||||
if err != nil {
|
||||
http.Error(w, "resume not allowed", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if err := workRepository.SaveResumedRun(r.Context(), updatedDelegation, resumedRun); err != nil {
|
||||
http.Error(w, "save resumed run", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(resumedRun)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/delegations/{id}/verify", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Verified bool
|
||||
Summary string
|
||||
EvidenceRefs []work.EvidenceRef
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil || request.RecordedAtUnixMillis < 1 {
|
||||
http.Error(w, "invalid verification", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
delegation, err := workRepository.LoadDelegation(r.Context(), work.DelegationID(r.PathValue("id")))
|
||||
if err != nil {
|
||||
http.Error(w, "delegation not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
run, err := workRepository.LoadAgentRun(r.Context(), delegation.LatestRunID)
|
||||
if err != nil {
|
||||
http.Error(w, "run not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
result, err := workRepository.LatestAgentResult(r.Context(), delegation.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "result not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
updatedDelegation, verification, err := workdomain.VerifyDelegationResult(delegation, run, result, request.Verified, request.Summary, request.EvidenceRefs, time.UnixMilli(request.RecordedAtUnixMillis))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid verification", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := workRepository.SaveDelegationVerification(r.Context(), updatedDelegation, verification); err != nil {
|
||||
http.Error(w, "save verification", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
runCount, _ := workRepository.CountAgentRuns(r.Context(), delegation.ID)
|
||||
learning.recordDelegationOutcome(r.Context(), delegationLearningInput{
|
||||
Intent: interaction.OriginalHumanIntent{ID: interaction.OriginalHumanIntentID(delegation.SourceIntentID), Text: delegation.DesiredOutcome},
|
||||
Utterance: interaction.HumanUtterance{ID: interaction.HumanUtteranceID(delegation.SourceUtteranceID), Content: delegation.DesiredOutcome},
|
||||
DelegationID: string(delegation.ID),
|
||||
RunID: string(run.ID),
|
||||
ImplementationID: run.ImplementationID,
|
||||
OutcomeRef: result.OutcomeRef,
|
||||
VerificationRefs: evidenceRefsToEpistemics(request.EvidenceRefs),
|
||||
Verified: request.Verified,
|
||||
ExecutionWindows: runCount,
|
||||
HumanBlocked: false,
|
||||
At: time.UnixMilli(request.RecordedAtUnixMillis),
|
||||
})
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(verification)
|
||||
})
|
||||
mux.HandleFunc("GET /v1/principal-bindings/{actor}/{principal}", func(w http.ResponseWriter, r *http.Request) {
|
||||
binding, err := identityRepository.LoadBinding(r.Context(), identity.ActorID(r.PathValue("actor")), identity.PrincipalID(r.PathValue("principal")))
|
||||
if err != nil {
|
||||
@@ -499,6 +841,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
if err := capabilityRepository.SaveCognitiveDiscovery(r.Context(), registration); err != nil {
|
||||
log.Printf("cognitive discovery persistence failed: %v", err)
|
||||
http.Error(w, "save cognitive discovery", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -559,6 +902,45 @@ func main() {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(promotion)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/cognitive-activate", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request capability.CognitiveActivationPromotionRequest
|
||||
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid cognitive activation", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
request.RequestedByPrincipalID = session.PrincipalID
|
||||
request.CapabilityID = "creator-advisory"
|
||||
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID)
|
||||
if err != nil {
|
||||
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
var current capability.ResolvedCognitiveBinding
|
||||
for _, candidate := range candidates {
|
||||
if candidate.Binding.ID == request.BindingID && candidate.Binding.Revision == request.ExpectedBindingRevision {
|
||||
current = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
promotion, err := capabilitydomain.BuildActivationPromotion(request, current)
|
||||
if err != nil {
|
||||
http.Error(w, "cognitive activation precondition failed", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if err = capabilityRepository.SaveCognitiveActivationPromotion(r.Context(), promotion); err != nil {
|
||||
http.Error(w, "cognitive activation conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(promotion)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/cognitive-registry/performance-evidence", func(w http.ResponseWriter, r *http.Request) {
|
||||
var value capability.PerformanceEvidence
|
||||
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
|
||||
@@ -777,9 +1159,119 @@ func envOr(name, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
func isCapabilityQuestion(message string) bool {
|
||||
lower := strings.ToLower(message)
|
||||
return strings.Contains(lower, "что ты умеешь") || strings.Contains(lower, "что умеешь") || strings.Contains(lower, "какие возможности")
|
||||
func humanCognitiveModelInventory(candidates []capability.ResolvedCognitiveBinding, inventoryErr error) string {
|
||||
if inventoryErr != nil {
|
||||
return "Я не смогла прочитать реестр подключённых моделей. Встроенный канал Домового остаётся доступен; настройки можно проверить повторно после восстановления реестра. Ключи и пароли не выводятся в чат."
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return "В реестре пока нет подтверждённых подключённых моделей. Встроенный локальный канал Домового доступен всегда; внешние модели добавляются через настройки подключения. Ключи и пароли в чат не выводятся."
|
||||
}
|
||||
lines := make([]string, 0, len(candidates)+1)
|
||||
lines = append(lines, "Подключённые модели из реестра:")
|
||||
for _, candidate := range candidates {
|
||||
modelName := string(candidate.Model.ProviderModelID)
|
||||
if modelName == "" {
|
||||
modelName = "имя модели не подтверждено"
|
||||
}
|
||||
status := cognitiveInventoryStatus(candidate)
|
||||
pricing := cognitiveInventoryPricing(candidate.Provider)
|
||||
details := []string{status, "тип " + string(candidate.Provider.Kind), "политика " + string(candidate.Binding.DataPolicy)}
|
||||
if pricing != "" {
|
||||
details = append(details, pricing)
|
||||
}
|
||||
if candidate.Provider.AuthMode != "" && candidate.Provider.AuthMode != capability.AuthNone {
|
||||
details = append(details, "доступ "+humanAuthMode(candidate.Provider.AuthMode))
|
||||
}
|
||||
lines = append(lines, "- "+modelName+" ("+strings.Join(details, "; ")+")")
|
||||
}
|
||||
lines = append(lines, "Секреты, ключи и пароли намеренно не показываются.")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func cognitiveInventoryStatus(candidate capability.ResolvedCognitiveBinding) string {
|
||||
if candidate.Provider.Health == capability.HealthAvailable && candidate.Model.Health == capability.HealthAvailable {
|
||||
if candidate.Binding.Activation == capability.ActivationActive {
|
||||
return "активна"
|
||||
}
|
||||
return "доступна, не активирована для автоматического выбора"
|
||||
}
|
||||
if candidate.Provider.Health == capability.HealthDegraded || candidate.Model.Health == capability.HealthDegraded {
|
||||
return "работает с ограничениями"
|
||||
}
|
||||
return "недоступна или не подтверждена"
|
||||
}
|
||||
|
||||
func cognitiveInventoryPricing(provider capability.ProviderInstance) string {
|
||||
switch provider.BillingMode {
|
||||
case capability.BillingFree:
|
||||
return "бесплатная"
|
||||
case capability.BillingPerInput:
|
||||
return "платная: стоимость входа указана в реестре"
|
||||
case capability.BillingPerOutput:
|
||||
return "платная: стоимость выхода указана в реестре"
|
||||
case capability.BillingPerRequest:
|
||||
return "платная: стоимость запроса указана в реестре"
|
||||
default:
|
||||
return "стоимость не подтверждена"
|
||||
}
|
||||
}
|
||||
|
||||
func humanAuthMode(mode capability.AuthMode) string {
|
||||
switch mode {
|
||||
case capability.AuthKey:
|
||||
return "API-ключ"
|
||||
case capability.AuthBearer:
|
||||
return "Bearer-токен"
|
||||
case capability.AuthUsernamePassword:
|
||||
return "логин и пароль"
|
||||
case capability.AuthOAuth:
|
||||
return "OAuth"
|
||||
case capability.AuthSecretFile:
|
||||
return "защищённый файл секрета"
|
||||
default:
|
||||
return "защищённый способ"
|
||||
}
|
||||
}
|
||||
|
||||
func humanCognitiveChannelReply(kind interaction.InformationRequestKind, act interaction.SpeechAct, health capability.Health, healthErr error) string {
|
||||
recoveryRequested := kind == interaction.InformationRequestCognitiveRecovery || act == interaction.SpeechActRequestToInvestigate || act == interaction.SpeechActDirective
|
||||
if healthErr != nil {
|
||||
if recoveryRequested {
|
||||
return "Я не вижу подтверждённого состояния локального канала. Домовой всё равно рядом: опишите, что изменилось, и я помогу подготовить аварийную настройку."
|
||||
}
|
||||
return "Я не вижу подтверждённого состояния локального канала, но Домовой работает. Можно продолжать разговор и проверить канал ещё раз."
|
||||
}
|
||||
var observation string
|
||||
switch health.State {
|
||||
case capability.HealthAvailable:
|
||||
observation = "Основной локальный канал сейчас доступен."
|
||||
case capability.HealthDegraded:
|
||||
observation = "Основной локальный канал сейчас работает с ограничениями."
|
||||
case capability.HealthUnavailable:
|
||||
observation = "Основной локальный канал сейчас недоступен."
|
||||
default:
|
||||
observation = "Состояние основного локального канала пока не подтверждено."
|
||||
}
|
||||
if recoveryRequested {
|
||||
if health.State == capability.HealthUnavailable {
|
||||
return observation + " Домовой жив: начну с проверки встроенного маршрута, затем зафиксирую варианты аварийного восстановления без передачи секретов."
|
||||
}
|
||||
return observation + " Я не меняю модель прямо из разговора: сначала покажу наблюдаемое состояние и подготовлю следующий проверяемый шаг восстановления."
|
||||
}
|
||||
if health.State == capability.HealthUnavailable {
|
||||
if recoveryRequested {
|
||||
return observation + " Домовой не остановился: начну с проверки встроенного маршрута, затем зафиксирую доступные варианты восстановления без передачи секретов."
|
||||
}
|
||||
return observation + " Но Домовой не остановился: продолжаю принимать сообщения и помогу восстановить локальный канал."
|
||||
}
|
||||
return observation + " Это наблюдение о готовности capability, а не прямой доступ к модели."
|
||||
}
|
||||
|
||||
func humanCognitiveDegradedReply(message string) string {
|
||||
if strings.TrimSpace(message) == "" {
|
||||
return "Основной канал мышления сейчас недоступен. Я рядом и продолжаю работать в локальном аварийном режиме."
|
||||
}
|
||||
return "Основной канал мышления сейчас недоступен. Домовой жив: сообщение принято, а я могу помочь разобраться с локальным каналом и настройкой восстановления."
|
||||
}
|
||||
|
||||
func humanCapabilityList(kinds []capability.CapabilityKind) string {
|
||||
@@ -796,3 +1288,66 @@ func humanCapabilityList(kinds []capability.CapabilityKind) string {
|
||||
}
|
||||
return strings.Join(labels, ", ")
|
||||
}
|
||||
|
||||
func toCapabilityKinds(values []string) []capability.CapabilityKind {
|
||||
result := make([]capability.CapabilityKind, 0, len(values))
|
||||
for _, value := range values {
|
||||
kind := capability.CapabilityKind(strings.TrimSpace(value))
|
||||
if kind == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, kind)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func delegationAttentionRef(id work.DelegationID) string {
|
||||
return "work:delegation:" + string(id)
|
||||
}
|
||||
|
||||
func continueDelegationFromHumanResponse(ctx context.Context, repository *postgres.WorkRepository, response continuity.HumanResponse, now time.Time) (bool, error) {
|
||||
if response.Operation != continuity.ResponseContinue || !strings.HasPrefix(response.TargetExternalRef, "work:delegation:") {
|
||||
return false, nil
|
||||
}
|
||||
delegationID := work.DelegationID(strings.TrimPrefix(response.TargetExternalRef, "work:delegation:"))
|
||||
delegation, err := repository.LoadDelegation(ctx, delegationID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
run, err := repository.LoadAgentRun(ctx, delegation.LatestRunID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
checkpoint, err := repository.LatestAgentCheckpoint(ctx, delegationID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
updatedDelegation, resumedRun, err := workdomain.ResumeDelegationRun(delegation, run, checkpoint, now)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := repository.SaveResumedRun(ctx, updatedDelegation, resumedRun); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func evidenceRefsToEpistemics(values []work.EvidenceRef) []epistemics.EvidenceRef {
|
||||
result := make([]epistemics.EvidenceRef, 0, len(values))
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(string(value)) == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, epistemics.EvidenceRef(value))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
)
|
||||
|
||||
func TestBearerTokenRequiresBearerScheme(t *testing.T) {
|
||||
@@ -23,6 +27,58 @@ func TestBearerTokenRequiresBearerScheme(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) {
|
||||
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveChannelStatus, interaction.SpeechActQuestion, capability.Health{State: capability.HealthAvailable, CapabilityID: "provider-secret-model"}, nil)
|
||||
if !strings.Contains(reply, "доступен") || strings.Contains(reply, "provider-secret-model") {
|
||||
t.Fatalf("unexpected safe reply: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCognitiveConfigurationRequestDoesNotPretendToMutate(t *testing.T) {
|
||||
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveRecovery, interaction.SpeechActDirective, capability.Health{State: capability.HealthUnavailable}, nil)
|
||||
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "аварийн") {
|
||||
t.Fatalf("unexpected configuration reply: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCognitiveDegradedReplyKeepsDomovoyAlive(t *testing.T) {
|
||||
reply := humanCognitiveDegradedReply("Помоги настроить локальную модель")
|
||||
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "принято") {
|
||||
t.Fatalf("unexpected degraded reply: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelInventoryQuestionUsesReadOnlyFastPath(t *testing.T) {
|
||||
for _, message := range []string{"какие модели доступны?", "напиши список настроек ИИ", "какие есть подключения ИИ"} {
|
||||
result, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: message}, nil, nil, time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("interpretation failed for %q: %v", message, err)
|
||||
}
|
||||
if result.InformationRequestKind != interaction.InformationRequestCognitiveModelInventory {
|
||||
t.Fatalf("semantic inventory request %q was not recognized: kind=%q act=%q hints=%v", message, result.InformationRequestKind, result.Interpretation.SpeechAct, result.CapabilityHints)
|
||||
}
|
||||
}
|
||||
ordinary, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: "настрой локальную модель"}, nil, nil, time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ordinary.InformationRequestKind == interaction.InformationRequestCognitiveModelInventory {
|
||||
t.Fatal("configuration mutation must not use read-only model inventory path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHumanCognitiveModelInventoryDoesNotExposeSecrets(t *testing.T) {
|
||||
cost := int64(25)
|
||||
text := humanCognitiveModelInventory([]capability.ResolvedCognitiveBinding{{
|
||||
Binding: capability.CognitiveBinding{DataPolicy: capability.DataPolicyExternalMessageOnly, Activation: capability.ActivationActive},
|
||||
Provider: capability.ProviderInstance{Kind: "openai-compatible-chat-v1", CredentialRef: "secret-ref", AuthMode: capability.AuthKey, BillingMode: capability.BillingPerRequest, RequestCostMicros: &cost, Health: capability.HealthAvailable},
|
||||
Model: capability.ModelDescriptor{ProviderModelID: "example-model", Health: capability.HealthAvailable},
|
||||
}}, nil)
|
||||
if strings.Contains(text, "secret-ref") || strings.Contains(text, "25") || !strings.Contains(text, "example-model") || !strings.Contains(text, "API-ключ") {
|
||||
t.Fatalf("unsafe or incomplete inventory: %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatorSessionHandlerAcceptsConfiguredCredentials(t *testing.T) {
|
||||
handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), func() time.Time { return time.Unix(1000, 0) })
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(`{"username":"m","password":"password"}`))
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
)
|
||||
|
||||
func TestChatPageServedWithoutInternalIDs(t *testing.T) {
|
||||
t.Setenv("ASSISTANT_BUILD_REVISION", "0123456789abcdef")
|
||||
t.Setenv("ASSISTANT_BUILD_TIME", "2026-08-22T03:04:05Z")
|
||||
handler, err := newHandler("http://core", "http://continuity", "http://commit", http.DefaultClient, "http://cognitive")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -25,11 +27,62 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) {
|
||||
t.Fatalf("expected /chat status 200, got %d", recorder.Code)
|
||||
}
|
||||
body := recorder.Body.String()
|
||||
for _, forbidden := range []string{"provider_id", "model_id", "binding_id", "workspace-id", "session-id"} {
|
||||
for _, required := range []string{"cognitive-status", "aria-live=\"polite\"", "cognitive-console-link", "href=\"/admin\"", "Проверь канал", "Изменить настройку", "ai-data-error", "Не вставляйте пароли", "build-meta", "версия ", "обновлено ", "datetime=\""} {
|
||||
if !strings.Contains(body, required) {
|
||||
t.Fatalf("/chat is missing human status marker %q", required)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{"provider_id", "model_id", "binding_id", "workspace-id", "session-id", "__ASSISTANT_BUILD_REVISION__", "__ASSISTANT_BUILD_TIME__", "__ASSISTANT_BUILD_DATETIME__"} {
|
||||
if strings.Contains(body, forbidden) {
|
||||
t.Fatalf("/chat leaks internal field %q", forbidden)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(body, "версия 0123456789ab") || !strings.Contains(body, "обновлено 22.08.2026 03:04 UTC") || !strings.Contains(body, "datetime=\"2026-08-22T03:04:05Z\"") {
|
||||
t.Fatalf("/chat does not render build metadata: %q", body)
|
||||
}
|
||||
for _, path := range []string{"/", "/chat.html"} {
|
||||
alternate := httptest.NewRecorder()
|
||||
handler.ServeHTTP(alternate, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
if alternate.Code != http.StatusOK || strings.Contains(alternate.Body.String(), "__ASSISTANT_BUILD_REVISION__") {
|
||||
t.Fatalf("%s did not render build metadata: status=%d", path, alternate.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatScriptExplainsSessionAndRuntimeFailures(t *testing.T) {
|
||||
body, err := staticFiles.ReadFile("static/assets/chat.js")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
script := string(body)
|
||||
for _, required := range []string{"class ChatApiError", "Сессия устарела", "основной runtime сейчас недоступен", "Проверьте текст и повторите попытку"} {
|
||||
if !strings.Contains(script, required) {
|
||||
t.Fatalf("chat script is missing recovery message %q", required)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicSurfacesSeparateChatAndConsole(t *testing.T) {
|
||||
handler, err := newHandler("http://core", "http://continuity", "http://commit", http.DefaultClient, "http://cognitive")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
path string
|
||||
must string
|
||||
mustNot string
|
||||
}{
|
||||
{path: "/", must: "Напишите Домовому обычное сообщение", mustNot: "context-preset"},
|
||||
{path: "/admin", must: "Чат с Домовым", mustNot: "Напишите Домовому обычное сообщение"},
|
||||
{path: "/chat", must: "Напишите Домовому обычное сообщение", mustNot: "context-preset"},
|
||||
} {
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, test.path, nil))
|
||||
body := recorder.Body.String()
|
||||
if recorder.Code != http.StatusOK || !strings.Contains(body, test.must) || strings.Contains(body, test.mustNot) {
|
||||
t.Fatalf("%s response=%d contains expected=%t forbidden=%t", test.path, recorder.Code, strings.Contains(body, test.must), strings.Contains(body, test.mustNot))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
|
||||
@@ -47,7 +100,7 @@ func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
|
||||
t.Fatalf("unexpected core chat request: %#v", request)
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, InterpretationState: interaction.InterpretationInterpreted, NeedsCognition: true, FallbackText: "fallback"})
|
||||
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, InterpretationState: interaction.InterpretationInterpreted, NeedsCognition: true, FallbackText: "fallback", InformationRequestKind: interaction.InformationRequestCognitiveChannelStatus})
|
||||
default:
|
||||
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
@@ -93,6 +146,9 @@ func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
|
||||
if cognitiveRequest.ActorID != "actor-1" || cognitiveRequest.PrincipalID != "principal-1" || cognitiveRequest.ScopeID != "domovoy-global-scope" || cognitiveRequest.Message != "Кто ты?" {
|
||||
t.Fatalf("unexpected trusted gateway request: %#v", cognitiveRequest)
|
||||
}
|
||||
if cognitiveRequest.Meaning.InformationRequestKind != interaction.InformationRequestCognitiveChannelStatus {
|
||||
t.Fatalf("information intent was not preserved: %#v", cognitiveRequest.Meaning)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatMessageReturnsHumanCapabilityMetadata(t *testing.T) {
|
||||
|
||||
+106
-21
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -8,7 +9,9 @@ import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
@@ -36,6 +39,12 @@ type gateway struct {
|
||||
cognitive *url.URL
|
||||
client *http.Client
|
||||
static http.Handler
|
||||
build buildMetadata
|
||||
}
|
||||
|
||||
type buildMetadata struct {
|
||||
revision string
|
||||
updated time.Time
|
||||
}
|
||||
|
||||
type routeTarget uint8
|
||||
@@ -74,7 +83,20 @@ func newHandler(coreURL, continuityURL, commitURL string, client *http.Client, c
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets))}), nil
|
||||
updated := time.Now().UTC()
|
||||
if raw := strings.TrimSpace(os.Getenv("ASSISTANT_BUILD_TIME")); raw != "" {
|
||||
if parsed, parseErr := time.Parse(time.RFC3339, raw); parseErr == nil {
|
||||
updated = parsed.UTC()
|
||||
}
|
||||
}
|
||||
revision := strings.TrimSpace(os.Getenv("ASSISTANT_BUILD_REVISION"))
|
||||
if revision == "" {
|
||||
revision = "development"
|
||||
}
|
||||
if len(revision) > 12 {
|
||||
revision = revision[:12]
|
||||
}
|
||||
return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets)), build: buildMetadata{revision: revision, updated: updated}}), nil
|
||||
}
|
||||
|
||||
func parseRuntimeURL(rawURL, name string) (*url.URL, error) {
|
||||
@@ -111,17 +133,31 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/" {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
if r.URL.Path == "/" || r.URL.Path == "/chat" || r.URL.Path == "/chat.html" {
|
||||
g.serveEmbeddedHTML(w, "chat.html")
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/chat" {
|
||||
r = r.Clone(r.Context())
|
||||
r.URL.Path = "/chat.html"
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
if r.URL.Path == "/admin" {
|
||||
g.serveEmbeddedHTML(w, "index.html")
|
||||
return
|
||||
}
|
||||
g.static.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (g *gateway) serveEmbeddedHTML(w http.ResponseWriter, name string) {
|
||||
body, err := staticFiles.ReadFile("static/" + name)
|
||||
if err != nil {
|
||||
http.Error(w, "page unavailable", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_REVISION__"), []byte(g.build.revision))
|
||||
body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_TIME__"), []byte(g.build.updated.Format("02.01.2006 15:04 UTC")))
|
||||
body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_DATETIME__"), []byte(g.build.updated.Format(time.RFC3339)))
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
type browserAdvisoryRequest struct {
|
||||
Message string `json:"message"`
|
||||
SessionID string `json:"session_id"`
|
||||
@@ -143,18 +179,23 @@ type browserChatRequest struct {
|
||||
}
|
||||
|
||||
type browserChatResponse struct {
|
||||
ConversationID string `json:"conversation_id"`
|
||||
UtteranceID string `json:"utterance_id"`
|
||||
SpeechAct string `json:"speech_act"`
|
||||
ReplyText string `json:"reply_text"`
|
||||
Cognition string `json:"cognition"`
|
||||
ActiveFocus string `json:"active_focus"`
|
||||
ProjectProvenance string `json:"project_provenance"`
|
||||
Clarification string `json:"clarification"`
|
||||
WorkPlanned bool `json:"work_planned"`
|
||||
CapabilityStatus string `json:"capability_status"`
|
||||
CapabilityKind string `json:"capability_kind"`
|
||||
ReminderScheduled bool `json:"reminder_scheduled"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
UtteranceID string `json:"utterance_id"`
|
||||
SpeechAct string `json:"speech_act"`
|
||||
ReplyText string `json:"reply_text"`
|
||||
Cognition string `json:"cognition"`
|
||||
ActiveFocus string `json:"active_focus"`
|
||||
ProjectProvenance string `json:"project_provenance"`
|
||||
Clarification string `json:"clarification"`
|
||||
WorkPlanned bool `json:"work_planned"`
|
||||
CapabilityStatus string `json:"capability_status"`
|
||||
CapabilityKind string `json:"capability_kind"`
|
||||
CapabilityHints []capability.CapabilityKind `json:"capability_hints,omitempty"`
|
||||
RequestedOutcome string `json:"requested_outcome,omitempty"`
|
||||
InterpretationConfidenceMillis int64 `json:"interpretation_confidence_millis,omitempty"`
|
||||
DelegationID string `json:"delegation_id"`
|
||||
DelegationState string `json:"delegation_state"`
|
||||
ReminderScheduled bool `json:"reminder_scheduled"`
|
||||
}
|
||||
|
||||
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -337,7 +378,7 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
||||
cognition = "unavailable"
|
||||
requestID, idErr := ids.New()
|
||||
if idErr == nil {
|
||||
trusted := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID), ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: "domovoy-global-scope", Message: browserRequest.Message, Context: []cognitive.ContextExcerpt{}, Locale: "ru-RU", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, EgressAcknowledged: false}
|
||||
trusted := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID), ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: "domovoy-global-scope", Message: browserRequest.Message, Context: localChatContext(saved), Locale: "ru-RU", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, EgressAcknowledged: false, Meaning: cognitive.TaskMeaning{SpeechAct: string(saved.SpeechAct), CapabilityKind: saved.CapabilityKind, CapabilityHints: saved.CapabilityHints, RequestedOutcome: saved.RequestedOutcome, InformationRequestKind: saved.InformationRequestKind, ConfidenceMillis: saved.InterpretationConfidenceMillis}}
|
||||
payload, err = json.Marshal(trusted)
|
||||
if err == nil {
|
||||
var advisory cognitive.GatewayAdvisoryResponse
|
||||
@@ -353,7 +394,49 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition, ActiveFocus: saved.ActiveProjectName, ProjectProvenance: string(saved.ProjectProvenance), Clarification: string(saved.Clarification), WorkPlanned: saved.WorkPlanned, CapabilityStatus: string(saved.CapabilityStatus), CapabilityKind: string(saved.CapabilityKind), ReminderScheduled: saved.Reminder != nil})
|
||||
_ = json.NewEncoder(w).Encode(browserChatResponse{
|
||||
ConversationID: string(saved.ConversationID),
|
||||
UtteranceID: string(saved.UtteranceID),
|
||||
SpeechAct: string(saved.SpeechAct),
|
||||
ReplyText: reply,
|
||||
Cognition: cognition,
|
||||
ActiveFocus: saved.ActiveProjectName,
|
||||
ProjectProvenance: string(saved.ProjectProvenance),
|
||||
Clarification: string(saved.Clarification),
|
||||
WorkPlanned: saved.WorkPlanned,
|
||||
CapabilityStatus: string(saved.CapabilityStatus),
|
||||
CapabilityKind: string(saved.CapabilityKind),
|
||||
CapabilityHints: saved.CapabilityHints,
|
||||
RequestedOutcome: saved.RequestedOutcome,
|
||||
InterpretationConfidenceMillis: saved.InterpretationConfidenceMillis,
|
||||
DelegationID: saved.DelegationID,
|
||||
DelegationState: saved.DelegationState,
|
||||
ReminderScheduled: saved.Reminder != nil,
|
||||
})
|
||||
}
|
||||
|
||||
func localChatContext(saved interaction.ChatMessageResponse) []cognitive.ContextExcerpt {
|
||||
if strings.TrimSpace(saved.ActiveProjectName) == "" {
|
||||
return []cognitive.ContextExcerpt{}
|
||||
}
|
||||
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{
|
||||
DataPolicy: capability.DataPolicyLocalOnly,
|
||||
MaxBytes: 2048,
|
||||
Candidates: []cognitive.ContextCandidate{{
|
||||
Excerpt: cognitive.ContextExcerpt{
|
||||
ID: cognitive.ContextExcerptID("active-project-focus"),
|
||||
SourceKind: "situation-active-project",
|
||||
SourceID: strings.TrimSpace(saved.ActiveProjectName),
|
||||
Sensitivity: cognitive.SensitivityPrivateAllowed,
|
||||
Text: "Текущий проект: " + strings.TrimSpace(saved.ActiveProjectName),
|
||||
},
|
||||
Relevance: 100,
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
return []cognitive.ContextExcerpt{}
|
||||
}
|
||||
return bundle.Excerpts
|
||||
}
|
||||
|
||||
func asksAttention(message string) bool {
|
||||
@@ -536,6 +619,8 @@ func creatorRoute(method, path string) (routeTarget, string, bool) {
|
||||
if validID(id) {
|
||||
return targetCore, "/v1/creator/capabilities/" + id + "/health", true
|
||||
}
|
||||
case method == http.MethodGet && path == "/api/learning/recent":
|
||||
return targetCore, "/v1/creator/learning/recent", true
|
||||
case method == http.MethodGet && strings.HasPrefix(path, "/api/prospective-items/"):
|
||||
id := strings.TrimPrefix(path, "/api/prospective-items/")
|
||||
if validID(id) {
|
||||
|
||||
@@ -10,8 +10,19 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
)
|
||||
|
||||
func TestLocalChatContextUsesOnlyActiveProject(t *testing.T) {
|
||||
context := localChatContext(interaction.ChatMessageResponse{ActiveProjectName: "Домовой"})
|
||||
if len(context) != 1 || context[0].SourceKind != "situation-active-project" || context[0].Text != "Текущий проект: Домовой" {
|
||||
t.Fatalf("unexpected local context: %#v", context)
|
||||
}
|
||||
if empty := localChatContext(interaction.ChatMessageResponse{}); len(empty) != 0 {
|
||||
t.Fatalf("expected empty context without active project: %#v", empty)
|
||||
}
|
||||
}
|
||||
|
||||
const legacyExternalMode = "legacy-external-test-mode"
|
||||
|
||||
func newHandlerWithAdvisoryMode(coreURL, continuityURL, commitURL string, client *http.Client, mode string, cognitiveURLs ...string) (http.Handler, error) {
|
||||
@@ -229,10 +240,21 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
request := httptest.NewRequest(http.MethodGet, "/admin", nil)
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Домовой") ||
|
||||
!strings.Contains(response.Body.String(), "context-preset") ||
|
||||
!strings.Contains(response.Body.String(), "context-state") ||
|
||||
!strings.Contains(response.Body.String(), "copy-context") ||
|
||||
!strings.Contains(response.Body.String(), "К содержимому") ||
|
||||
!strings.Contains(response.Body.String(), "aria-current=\"location\"") ||
|
||||
!strings.Contains(response.Body.String(), "section-nav-link") ||
|
||||
!strings.Contains(response.Body.String(), "data-page=\"overview\"") ||
|
||||
!strings.Contains(response.Body.String(), "data-page=\"diagnostics\"") ||
|
||||
!strings.Contains(response.Body.String(), "data-page=\"operations\"") ||
|
||||
!strings.Contains(response.Body.String(), "data-page=\"learning\"") ||
|
||||
!strings.Contains(response.Body.String(), "data-page=\"settings\"") ||
|
||||
!strings.Contains(response.Body.String(), "satisfaction-dialog") || !strings.Contains(response.Body.String(), "Подтверждаю переход") ||
|
||||
!strings.Contains(response.Body.String(), "action-history") || !strings.Contains(response.Body.String(), "Журнал действий") ||
|
||||
!strings.Contains(response.Body.String(), "advisory-form") || !strings.Contains(response.Body.String(), "Спросить Домового") ||
|
||||
|
||||
@@ -29,26 +29,60 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
|
||||
.login-form button, .composer button { border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
|
||||
.login-form button { width: 100%; height: 50px; margin-top: 18px; }
|
||||
#login-error { min-height: 20px; color: var(--bad); font-size: 13px; }
|
||||
.chat-view { min-height: 100vh; display: grid; grid-template-rows: 64px minmax(0, 1fr); }
|
||||
.chat-header { display: flex; align-items: center; gap: 18px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .96); }
|
||||
.chat-view { height: 100vh; min-height: 0; display: grid; grid-template-rows: 64px minmax(0, 1fr); overflow: hidden; }
|
||||
.chat-header { min-width: 0; display: flex; align-items: center; gap: 14px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .98); }
|
||||
.chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); }
|
||||
.chat-header span { color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
|
||||
#active-focus { margin-left: auto; color: var(--good); }
|
||||
#active-focus { color: var(--good); }
|
||||
.cognitive-status { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
|
||||
.chat-header .cognitive-status { width: auto; height: auto; padding: 0; border: 0; background: transparent; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
|
||||
.chat-header .cognitive-status { max-width: min(30vw, 320px); margin-left: auto; overflow: hidden; text-overflow: ellipsis; }
|
||||
.cognitive-status::before { content: ""; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 50%; background: var(--signal); }
|
||||
.cognitive-status[data-state="available"] { color: var(--good); }
|
||||
.cognitive-status[data-state="available"]::before { background: var(--good); }
|
||||
.cognitive-status[data-state="unavailable"] { color: var(--bad); }
|
||||
.cognitive-status[data-state="unavailable"]::before { background: var(--bad); }
|
||||
.cognitive-console-link { color: var(--signal); font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
|
||||
.build-meta { display: grid; gap: 3px; flex: 0 0 auto; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .08em; text-align: right; text-transform: uppercase; white-space: nowrap; }
|
||||
.build-meta time { color: #68776d; font-size: 8px; }
|
||||
.chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; }
|
||||
.human-nav { display: flex; align-items: center; gap: 8px; }
|
||||
.human-nav { display: grid; gap: 8px; }
|
||||
.nav-tab, .operator-link { height: 34px; padding: 0 12px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--muted); background: #0b110e; text-decoration: none; font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
|
||||
.chat-left .nav-tab { height: auto; min-height: 58px; padding: 10px 12px; display: grid; grid-template-columns: 28px 1fr; gap: 2px 8px; align-items: center; text-align: left; }
|
||||
.chat-left .nav-tab b { grid-row: span 2; color: var(--signal); font: 11px/1 var(--mono); }
|
||||
.chat-left .nav-tab span { color: var(--ink); font: 600 13px/1 var(--sans); text-transform: none; letter-spacing: 0; }
|
||||
.chat-left .nav-tab small { color: var(--muted); font: 10px/1 var(--mono); text-transform: none; letter-spacing: 0; }
|
||||
.nav-tab.active { color: #11150f; border-color: var(--signal); background: var(--signal); }
|
||||
.operator-link { color: var(--good); }
|
||||
.surface-panel { min-height: 0; position: relative; }
|
||||
.active-panel { display: grid; min-height: 0; }
|
||||
.messages { width: min(100%, 920px); margin: 0 auto; padding: 28px 18px 120px; display: flex; flex-direction: column; gap: 14px; overflow: auto; }
|
||||
.chat-header > .operator-link { flex: 0 0 auto; }
|
||||
.chat-layout { min-height: 0; height: 100%; display: grid; grid-template-columns: minmax(220px, 280px) minmax(0, 1fr) minmax(250px, 320px); }
|
||||
.chat-side { min-width: 0; min-height: 0; padding: 28px 20px; overflow-y: auto; background: #0e1512; }
|
||||
.chat-left { border-right: 1px solid var(--line); }
|
||||
.chat-right { border-left: 1px solid var(--line); }
|
||||
.side-intro h1, .side-intro h2 { margin: 12px 0; font: 400 clamp(28px, 2.5vw, 40px)/.95 var(--serif); letter-spacing: -.03em; }
|
||||
.side-intro p:not(.side-index) { color: var(--muted); line-height: 1.5; }
|
||||
.side-index, .side-focus span, .side-note span, .ai-info-card span { color: var(--signal); font: 10px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
|
||||
.side-focus, .side-note, .ai-info-card { margin-top: 28px; padding: 14px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
||||
.side-focus strong, .side-note strong, .ai-info-card strong { display: block; margin-top: 9px; color: var(--ink); font: 600 14px/1.3 var(--sans); }
|
||||
.side-note small, .ai-info-card p, .ai-data-form small { display: block; margin-top: 9px; color: var(--muted); line-height: 1.45; }
|
||||
.chat-main { min-width: 0; min-height: 0; overflow: hidden; display: grid; }
|
||||
.surface-panel { min-height: 0; position: relative; overflow-y: auto; }
|
||||
.active-panel { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; min-height: 0; }
|
||||
.messages { width: min(100%, 920px); min-height: 0; margin: 0 auto; padding: 28px 18px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; }
|
||||
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
|
||||
.message.user { align-self: flex-end; background: #151e1a; }
|
||||
.message.assistant { align-self: flex-start; background: #0f1613; }
|
||||
.message small { display: block; margin-top: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; }
|
||||
.composer { position: sticky; bottom: 0; width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 20px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
|
||||
.composer { width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 10px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
|
||||
.composer button { height: 48px; align-self: end; }
|
||||
.composer button:disabled { cursor: wait; filter: saturate(.3); }
|
||||
.conversation-starters { width: min(100%, 920px); margin: -4px auto 0; padding: 0 18px 18px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.conversation-starters button { min-height: 32px; padding: 0 10px; border: 1px solid var(--line); color: var(--ink); background: #101814; font: 10px/1 var(--mono); }
|
||||
.conversation-starters button:hover, .conversation-starters button:focus-visible { border-color: var(--signal); color: var(--signal); outline: none; }
|
||||
.ai-data-form { margin-top: 24px; }
|
||||
.ai-data-form textarea { min-height: 150px; resize: vertical; }
|
||||
.ai-data-form button { width: 100%; min-height: 42px; margin-top: 12px; border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
|
||||
.ai-data-error { margin: 10px 0 0; color: var(--bad); font-size: 12px; line-height: 1.4; }
|
||||
.side-console-link { display: flex; justify-content: space-between; margin-top: 26px; padding-top: 16px; border-top: 1px solid var(--line); color: var(--signal) !important; font: 11px/1.3 var(--mono) !important; text-transform: uppercase; letter-spacing: .06em; }
|
||||
.attention-surface, .history-surface { width: min(100%, 920px); margin: 0 auto; padding: 34px 18px; align-content: start; gap: 18px; }
|
||||
.surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 16px; }
|
||||
.surface-heading p { margin: 0; font: 400 36px/.95 var(--serif); }
|
||||
@@ -67,10 +101,25 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
|
||||
.empty-state { margin: 24px 0 0; color: var(--muted); }
|
||||
.empty-state.error { color: var(--bad); }
|
||||
@media (max-width: 640px) {
|
||||
.chat-view { height: auto; min-height: 100vh; overflow: visible; display: block; }
|
||||
.composer { grid-template-columns: 1fr; }
|
||||
.composer button { width: 100%; }
|
||||
.conversation-starters { align-items: stretch; }
|
||||
.conversation-starters span { width: 100%; }
|
||||
.chat-header { gap: 10px; padding: 0 12px; }
|
||||
.chat-header span { display: none; }
|
||||
.human-nav { overflow-x: auto; }
|
||||
.chat-header .cognitive-status { display: inline-flex; max-width: 150px; overflow: hidden; text-overflow: ellipsis; }
|
||||
.build-meta { font-size: 8px; letter-spacing: .04em; }
|
||||
.build-meta time { font-size: 7px; }
|
||||
.chat-layout { height: auto; display: block; }
|
||||
.chat-side, .chat-main { min-height: auto; max-height: none; overflow: visible; }
|
||||
.chat-left, .chat-right { border: 0; border-bottom: 1px solid var(--line); }
|
||||
.chat-main { height: 70vh; min-height: 520px; }
|
||||
.chat-left { padding-bottom: 20px; }
|
||||
.chat-right { padding-top: 20px; }
|
||||
.operator-link, .nav-tab { padding: 0 9px; }
|
||||
}
|
||||
@media (min-width: 641px) and (max-width: 1100px) {
|
||||
.chat-layout { grid-template-columns: 210px minmax(0, 1fr) 245px; }
|
||||
.chat-side { padding-inline: 14px; }
|
||||
.side-intro h1, .side-intro h2 { font-size: 30px; }
|
||||
}
|
||||
|
||||
@@ -8,14 +8,84 @@ const messageInput = document.querySelector("#message");
|
||||
const sendButton = document.querySelector("#send");
|
||||
const logoutButton = document.querySelector("#logout");
|
||||
const activeFocus = document.querySelector("#active-focus");
|
||||
const cognitiveStatus = document.querySelector("#cognitive-status");
|
||||
const cognitiveConsoleLink = document.querySelector("#cognitive-console-link");
|
||||
const todayList = document.querySelector("#today-list");
|
||||
const refreshToday = document.querySelector("#refresh-today");
|
||||
const historyList = document.querySelector("#history-list");
|
||||
const navTabs = Array.from(document.querySelectorAll(".nav-tab"));
|
||||
const conversationStarters = Array.from(document.querySelectorAll("[data-starter]"));
|
||||
const aiDataForm = document.querySelector("#ai-data-form");
|
||||
const aiDataInput = document.querySelector("#ai-data");
|
||||
const aiDataError = document.querySelector("#ai-data-error");
|
||||
|
||||
let token = sessionStorage.getItem("domovoy.chat.token") || "";
|
||||
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
|
||||
let localHistory = [];
|
||||
let cognitiveStatusTimer = null;
|
||||
|
||||
class ChatApiError extends Error {
|
||||
constructor(status, detail = "") {
|
||||
super(detail || `chat api returned ${status}`);
|
||||
this.name = "ChatApiError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
function containsSecretShapedText(value) {
|
||||
return /(-----BEGIN [A-Z ]+PRIVATE KEY-----|\b(?:password|passwd|пароль|token|токен|api[_ -]?key|секрет|ключ)\s*[:=]|\bsk-[a-zA-Z0-9_-]{12,})/i.test(value);
|
||||
}
|
||||
|
||||
function showAIDataError(message = "") {
|
||||
if (!aiDataError) return;
|
||||
aiDataError.textContent = message;
|
||||
aiDataError.hidden = !message;
|
||||
}
|
||||
|
||||
function renderCognitiveStatus(state) {
|
||||
if (!cognitiveStatus) return;
|
||||
const normalized = String(state || "UNKNOWN").toUpperCase();
|
||||
let displayState = "degraded";
|
||||
if (normalized === "AVAILABLE") displayState = "available";
|
||||
if (normalized === "UNAVAILABLE") displayState = "unavailable";
|
||||
const labels = {
|
||||
available: "канал мышления доступен",
|
||||
degraded: "канал мышления работает с ограничениями",
|
||||
unavailable: "Домовой рядом · основной канал недоступен",
|
||||
};
|
||||
cognitiveStatus.dataset.state = displayState;
|
||||
cognitiveStatus.textContent = labels[displayState];
|
||||
cognitiveStatus.title = displayState === "available" ? "Канал доступен. Нажмите, чтобы проверить ещё раз" : "Проверить канал ещё раз";
|
||||
if (cognitiveConsoleLink) cognitiveConsoleLink.hidden = displayState === "available";
|
||||
}
|
||||
|
||||
async function loadCognitiveStatus() {
|
||||
if (!cognitiveStatus || !token) return;
|
||||
cognitiveStatus.dataset.state = "checking";
|
||||
cognitiveStatus.textContent = "проверяю канал мышления";
|
||||
try {
|
||||
const result = await api("/api/capabilities/cognitive-proposal/health", {method: "GET"});
|
||||
renderCognitiveStatus(result.State || result.state || "UNKNOWN");
|
||||
} catch (error) {
|
||||
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
|
||||
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
|
||||
return;
|
||||
}
|
||||
renderCognitiveStatus("UNAVAILABLE");
|
||||
}
|
||||
}
|
||||
|
||||
function startCognitiveStatusPolling() {
|
||||
if (cognitiveStatusTimer) return;
|
||||
loadCognitiveStatus();
|
||||
cognitiveStatusTimer = window.setInterval(loadCognitiveStatus, 30000);
|
||||
}
|
||||
|
||||
function stopCognitiveStatusPolling() {
|
||||
if (!cognitiveStatusTimer) return;
|
||||
window.clearInterval(cognitiveStatusTimer);
|
||||
cognitiveStatusTimer = null;
|
||||
}
|
||||
|
||||
function showChat() {
|
||||
loginView.hidden = true;
|
||||
@@ -23,10 +93,12 @@ function showChat() {
|
||||
if (!messages.children.length) {
|
||||
appendMessage("assistant", "Я рядом. Напишите обычное сообщение, без внутренних идентификаторов.");
|
||||
}
|
||||
startCognitiveStatusPolling();
|
||||
messageInput.focus();
|
||||
}
|
||||
|
||||
function showLogin() {
|
||||
stopCognitiveStatusPolling();
|
||||
chatView.hidden = true;
|
||||
loginView.hidden = false;
|
||||
}
|
||||
@@ -56,17 +128,48 @@ async function api(path, options = {}) {
|
||||
const headers = {"Content-Type": "application/json", ...(options.headers || {})};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
const response = await fetch(path, {...options, headers});
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
if (!response.ok) throw new ChatApiError(response.status, await response.text());
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function expireSession(message = "") {
|
||||
token = "";
|
||||
conversationID = "";
|
||||
sessionStorage.removeItem("domovoy.chat.token");
|
||||
sessionStorage.removeItem("domovoy.chat.conversation");
|
||||
messages.replaceChildren();
|
||||
updateActiveFocus("");
|
||||
if (message) loginError.textContent = message;
|
||||
showLogin();
|
||||
}
|
||||
|
||||
function humanMeta(result) {
|
||||
if (result.reminder_scheduled) return "Напоминание сохранено. Я вернусь к нему сам.";
|
||||
if (result.delegation_id && result.delegation_state) return `Передал в coding-agent: ${humanDelegationState(result.delegation_state)}.`;
|
||||
if (result.capability_status && result.capability_status !== "AVAILABLE") return "Возможность пока не подключена.";
|
||||
if (result.work_planned) return "Я разложил это как работу.";
|
||||
return "";
|
||||
}
|
||||
|
||||
function humanDelegationState(state) {
|
||||
switch (state) {
|
||||
case "RUNNING":
|
||||
return "работа запущена";
|
||||
case "WAITING":
|
||||
return "ожидаю следующий шаг";
|
||||
case "BLOCKED":
|
||||
return "нужно решение человека";
|
||||
case "VERIFYING":
|
||||
return "результат проверяется";
|
||||
case "COMPLETED":
|
||||
return "работа завершена";
|
||||
case "FAILED":
|
||||
return "run завершился ошибкой";
|
||||
default:
|
||||
return state.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
if (!historyList) return;
|
||||
historyList.replaceChildren();
|
||||
@@ -169,8 +272,14 @@ async function sendChatMessage(text, options = {}) {
|
||||
await loadToday();
|
||||
activateTab("today-panel");
|
||||
}
|
||||
} catch {
|
||||
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
|
||||
} catch (error) {
|
||||
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
|
||||
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
|
||||
} else if (error instanceof ChatApiError && error.status >= 500) {
|
||||
appendMessage("assistant", "Домовой не смог завершить обработку: основной runtime сейчас недоступен. Встроенный канал проверяется; повторите сообщение через несколько секунд.");
|
||||
} else {
|
||||
appendMessage("assistant", "Домовой не принял сообщение. Проверьте текст и повторите попытку.");
|
||||
}
|
||||
} finally {
|
||||
sendButton.disabled = false;
|
||||
messageInput.focus();
|
||||
@@ -215,16 +324,35 @@ for (const tab of navTabs) {
|
||||
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
|
||||
}
|
||||
|
||||
for (const starter of conversationStarters) {
|
||||
starter.addEventListener("click", async () => {
|
||||
const text = starter.dataset.starter || "";
|
||||
if (!text || sendButton.disabled) return;
|
||||
messageInput.value = "";
|
||||
await sendChatMessage(text);
|
||||
});
|
||||
}
|
||||
|
||||
if (refreshToday) refreshToday.addEventListener("click", loadToday);
|
||||
if (cognitiveStatus) cognitiveStatus.addEventListener("click", loadCognitiveStatus);
|
||||
if (aiDataForm) {
|
||||
aiDataForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const details = aiDataInput.value.trim();
|
||||
if (!details || sendButton.disabled) return;
|
||||
if (containsSecretShapedText(details)) {
|
||||
showAIDataError("Не отправляю секреты. Удалите пароль, токен, API-ключ или приватный ключ и оставьте только описание модели.");
|
||||
return;
|
||||
}
|
||||
showAIDataError();
|
||||
aiDataInput.value = "";
|
||||
await sendChatMessage(`Передаю сведения о локальном ИИ для текущего разговора:\n${details}`);
|
||||
});
|
||||
}
|
||||
if (aiDataInput) aiDataInput.addEventListener("input", () => showAIDataError());
|
||||
|
||||
logoutButton.addEventListener("click", () => {
|
||||
token = "";
|
||||
conversationID = "";
|
||||
sessionStorage.removeItem("domovoy.chat.token");
|
||||
sessionStorage.removeItem("domovoy.chat.conversation");
|
||||
messages.replaceChildren();
|
||||
updateActiveFocus("");
|
||||
showLogin();
|
||||
expireSession();
|
||||
});
|
||||
|
||||
if (token) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
:root {
|
||||
--ink: #e9eee7;
|
||||
--muted: #8c9a91;
|
||||
--faint: #58645d;
|
||||
--base: #0b100e;
|
||||
--panel: #101714;
|
||||
--panel-high: #151e1a;
|
||||
--line: #2a352f;
|
||||
--line-soft: #1d2822;
|
||||
--ink: #eef2ec;
|
||||
--muted: #a7b2aa;
|
||||
--faint: #718078;
|
||||
--base: #0f1512;
|
||||
--panel: #17201b;
|
||||
--panel-high: #202b25;
|
||||
--line: #34423a;
|
||||
--line-soft: #26332c;
|
||||
--signal: #c9a85f;
|
||||
--signal-bright: #e7c97b;
|
||||
--good: #78c6a3;
|
||||
@@ -21,13 +21,15 @@
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
html { background: var(--base); color: var(--ink); font-family: var(--sans); }
|
||||
body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 73% -10%, rgba(83, 123, 101, .15), transparent 38%), var(--base); }
|
||||
html { background: var(--base); color: var(--ink); font-family: var(--sans); scroll-behavior: smooth; }
|
||||
body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 73% -10%, rgba(83, 123, 101, .13), transparent 38%), var(--base); }
|
||||
button, input, textarea { font: inherit; }
|
||||
button { color: inherit; }
|
||||
button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--signal-bright); outline-offset: 3px; }
|
||||
button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid var(--signal-bright); outline-offset: 3px; }
|
||||
.skip-link { position: fixed; top: 10px; left: 10px; z-index: 30; padding: 10px 13px; color: #11150f; background: var(--signal-bright); font: 600 10px/1 var(--mono); text-decoration: none; transform: translateY(-160%); transition: transform .18s; }
|
||||
.skip-link:focus { transform: translateY(0); }
|
||||
|
||||
.grain { position: fixed; inset: 0; pointer-events: none; opacity: .18; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E"); }
|
||||
.grain { position: fixed; inset: 0; pointer-events: none; opacity: .045; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E"); }
|
||||
.eyebrow, .rail-index { margin: 0 0 12px; color: var(--signal); font: 600 11px/1 var(--mono); letter-spacing: .18em; text-transform: uppercase; }
|
||||
|
||||
.login-shell { min-height: 100vh; display: grid; place-items: center; padding: 32px 20px; position: relative; overflow: hidden; }
|
||||
@@ -63,6 +65,10 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.wordmark strong { font: 400 20px/1 var(--serif); }
|
||||
.wordmark small { margin-top: 4px; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
|
||||
.topbar-actions { display: flex; align-items: center; gap: 10px; }
|
||||
.chat-return-link { color: var(--signal); text-decoration: none; font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
|
||||
.topbar-nav { display: flex; align-items: center; gap: 4px; margin-right: 8px; }
|
||||
.topbar-nav a { padding: 9px 10px; color: var(--muted); text-decoration: none; font: 600 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.topbar-nav a:hover, .topbar-nav a:focus-visible { color: var(--ink); background: var(--panel-high); outline: 0; }
|
||||
.connection { margin-right: 12px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.connection i { width: 7px; height: 7px; display: inline-block; margin-right: 8px; border-radius: 50%; background: var(--good); box-shadow: 0 0 14px rgba(120, 198, 163, .6); }
|
||||
.connection.offline i { background: var(--bad); box-shadow: none; }
|
||||
@@ -73,17 +79,55 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.context-rail h2 { margin: 0; font: 400 29px/1.1 var(--serif); }
|
||||
.rail-copy { margin: 14px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
|
||||
.context-form { display: grid; }
|
||||
.context-status-row { margin-bottom: 16px; padding: 10px 11px; display: flex; align-items: center; justify-content: space-between; gap: 10px; border: 1px solid var(--line-soft); background: rgba(120,198,163,.04); }
|
||||
.context-status-label { color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; }
|
||||
.context-status-row strong { color: var(--warn); font: 600 9px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.context-status-row strong[data-state="ready"] { color: var(--good); }
|
||||
.context-presets { display: grid; gap: 7px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid var(--line-soft); }
|
||||
.context-presets label { margin: 0; }
|
||||
.context-presets select { width: 100%; min-height: 38px; padding: 0 9px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; font: 10px/1 var(--mono); }
|
||||
.context-presets small { color: var(--faint); font-size: 10px; line-height: 1.35; }
|
||||
.context-field { display: grid; gap: 7px; }
|
||||
.context-field + .context-field { margin-top: 13px; }
|
||||
.context-field label { margin: 0; }
|
||||
.context-field label span { color: var(--signal); font-size: 8px; letter-spacing: .06em; }
|
||||
.context-input-row { display: grid; grid-template-columns: minmax(0, 1fr) 34px; gap: 5px; }
|
||||
.context-input-row input { min-width: 0; }
|
||||
.copy-context { width: 34px; height: 40px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--muted); background: transparent; cursor: pointer; }
|
||||
.copy-context:hover { color: var(--ink); border-color: var(--faint); background: var(--panel-high); }
|
||||
.id-actions { min-height: 31px; display: flex; align-items: center; gap: 7px; margin-top: 12px; }
|
||||
.id-action { min-height: 30px; padding: 0 9px; border: 1px solid var(--line-soft); border-radius: var(--radius); color: var(--muted); background: transparent; cursor: pointer; font: 9px/1 var(--mono); letter-spacing: .04em; }
|
||||
.id-action:hover { color: var(--ink); border-color: var(--signal); background: rgba(201,168,95,.07); }
|
||||
.icon-id-action { width: 30px; padding: 0; color: var(--faint); }
|
||||
.context-form label { margin-top: 15px; }
|
||||
.context-form label:first-child { margin-top: 0; }
|
||||
.context-form input { height: 40px; padding: 0 10px; font: 11px/1 var(--mono); }
|
||||
.context-hint { margin: 12px 0 0; color: var(--faint); font-size: 10px; line-height: 1.45; }
|
||||
.advanced-context { margin-top: 17px; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
|
||||
.advanced-context summary { padding: 12px 0; color: var(--muted); cursor: pointer; list-style: none; font: 600 9px/1.2 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.advanced-context summary::before { content: "+"; display: inline-block; width: 18px; color: var(--signal); font-size: 13px; }
|
||||
.advanced-context[open] summary::before { content: "−"; }
|
||||
.advanced-context > div { padding: 0 0 10px; }
|
||||
.advanced-context label { margin-top: 13px; }
|
||||
.context-actions { display: grid; gap: 8px; }
|
||||
.context-actions .secondary-button { margin-top: 18px; }
|
||||
.context-actions .text-button { margin: 0; justify-self: start; padding-inline: 0; }
|
||||
.secondary-button { height: 42px; margin-top: 20px; color: #11150f; background: var(--signal); font-size: 12px; }
|
||||
.section-nav { display: grid; gap: 3px; margin-top: 4px; }
|
||||
.section-nav-link { display: grid; grid-template-columns: 28px 1fr; gap: 2px 10px; padding: 11px 10px; border-left: 2px solid transparent; color: var(--muted); text-decoration: none; transition: background .18s, border-color .18s, color .18s; }
|
||||
.section-nav-link:hover, .section-nav-link:focus-visible, .section-nav-link.active, .section-nav-link[aria-current="location"] { border-left-color: var(--signal); color: var(--ink); background: rgba(201,168,95,.07); outline: 0; }
|
||||
.section-nav-link span { grid-row: 1 / span 2; color: var(--signal); font: 9px/1.4 var(--mono); }
|
||||
.section-nav-link b { font: 600 11px/1.2 var(--sans); }
|
||||
.section-nav-link small { color: var(--faint); font: 9px/1.2 var(--mono); }
|
||||
.session-stamp { margin-top: auto; padding-top: 20px; display: grid; gap: 6px; border-top: 1px solid var(--line); }
|
||||
.session-stamp span { margin-top: 8px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .14em; }
|
||||
.session-stamp strong { overflow: hidden; color: var(--muted); font: 10px/1.4 var(--mono); text-overflow: ellipsis; }
|
||||
|
||||
.workspace { grid-column: 2; grid-row: 2; padding: clamp(28px, 4vw, 58px); min-width: 0; }
|
||||
.workspace { grid-column: 2; grid-row: 2; width: min(100%, 1740px); justify-self: start; padding: 42px clamp(24px, 3vw, 48px) 64px; min-width: 0; }
|
||||
.workspace > [data-page][hidden] { display: none !important; }
|
||||
.workspace > section { scroll-margin-top: 94px; }
|
||||
.workspace-head { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 34px; }
|
||||
.workspace-head h1 { margin: 0; font: 400 clamp(36px, 5vw, 58px)/1 var(--serif); letter-spacing: -.035em; }
|
||||
.workspace-head h1 { margin: 0; font: 400 clamp(34px, 4vw, 50px)/1 var(--serif); letter-spacing: -.035em; }
|
||||
.last-update { color: var(--faint); font: 10px/1.4 var(--mono); text-align: right; }
|
||||
.signal-strip { display: grid; grid-template-columns: repeat(4, 1fr); margin-bottom: 18px; border: 1px solid var(--line); background: var(--panel); }
|
||||
.signal-strip div { padding: 16px 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-right: 1px solid var(--line); }
|
||||
@@ -94,6 +138,46 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.signal-strip strong[data-state="error"] { color: var(--bad); }
|
||||
.signal-strip strong[data-state="loading"] { color: var(--warn); animation: pulse 1.1s ease-in-out infinite; }
|
||||
|
||||
.diagnostics-panel, .settings-panel { margin-bottom: 18px; border: 1px solid var(--line); background: rgba(16,23,20,.82); }
|
||||
.diagnostics-panel { border-color: rgba(120,198,163,.32); box-shadow: inset 3px 0 0 rgba(120,198,163,.45); }
|
||||
.panel-heading { min-height: 68px; padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
|
||||
.panel-heading p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
|
||||
.panel-heading h2 { margin: 0; font: 400 23px/1 var(--serif); }
|
||||
.panel-heading-tools { display: flex; align-items: center; gap: 14px; }
|
||||
.panel-heading-tools > span, .settings-saved { color: var(--muted); font: 9px/1.3 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.compact-button { width: auto; min-width: 142px; height: 38px; margin: 0; padding: 0 13px; }
|
||||
.diagnostic-intro { padding: 13px 18px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--line-soft); color: var(--muted); }
|
||||
.diagnostic-intro p { margin: 0; font-size: 11px; line-height: 1.45; }
|
||||
.diagnostic-pulse { width: 7px; height: 7px; flex: 0 0 auto; border-radius: 50%; background: var(--good); box-shadow: 0 0 12px rgba(120,198,163,.7); }
|
||||
.diagnostic-list { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.diagnostic-list[aria-busy="true"] { opacity: .7; }
|
||||
.diagnostic-item { min-width: 0; min-height: 92px; padding: 14px 16px; display: grid; grid-template-columns: 9px 1fr auto; gap: 9px; align-items: start; border-right: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
|
||||
.diagnostic-item:nth-child(4n) { border-right: 0; }
|
||||
.diagnostic-item > i { width: 7px; height: 7px; margin-top: 4px; border-radius: 50%; background: var(--faint); }
|
||||
.diagnostic-item[data-state="ok"] > i { background: var(--good); box-shadow: 0 0 9px rgba(120,198,163,.6); }
|
||||
.diagnostic-item[data-state="error"] > i { background: var(--bad); }
|
||||
.diagnostic-item[data-state="loading"] > i { background: var(--warn); animation: pulse 1.1s ease-in-out infinite; }
|
||||
.diagnostic-item strong { display: block; color: var(--ink); font: 600 10px/1.3 var(--mono); }
|
||||
.diagnostic-item span { display: block; margin-top: 6px; color: var(--muted); font-size: 10px; line-height: 1.35; }
|
||||
.diagnostic-item em { color: var(--faint); font: normal 9px/1.2 var(--mono); text-transform: uppercase; }
|
||||
.diagnostic-item[data-state="ok"] em { color: var(--good); }
|
||||
.diagnostic-item[data-state="error"] em { color: var(--bad); }
|
||||
.settings-grid { padding: 18px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 20px; }
|
||||
.setting-field, .setting-check { min-width: 0; display: grid; gap: 7px; color: var(--ink); }
|
||||
.setting-field > span, .setting-check b { font: 600 10px/1.2 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.setting-field small, .setting-check small { color: var(--muted); font-size: 11px; line-height: 1.4; }
|
||||
.setting-field select { width: 100%; min-height: 40px; padding: 0 10px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; font: 11px/1 var(--mono); }
|
||||
.setting-check { grid-template-columns: 18px 1fr; align-items: start; gap: 10px; padding: 10px 12px; border: 1px solid var(--line-soft); }
|
||||
.setting-check input { width: 16px; height: 16px; margin: 2px 0 0; accent-color: var(--signal); }
|
||||
.setting-check span { display: grid; gap: 7px; }
|
||||
.settings-actions { display: flex; align-items: center; justify-content: flex-end; gap: 14px; grid-column: 1 / -1; padding-top: 2px; }
|
||||
.settings-actions .text-button { margin: 0; }
|
||||
html[data-density="compact"] .data-card { padding: 18px; }
|
||||
html[data-density="compact"] .data-card dl div { padding-block: 10px; }
|
||||
html[data-density="compact"] .activity-row { min-height: 58px; padding-block: 8px; }
|
||||
html[data-density="compact"] .learning-row { min-height: 54px; padding-block: 9px; }
|
||||
html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before, html[data-reduced-motion="true"] *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: .01ms !important; }
|
||||
|
||||
.advisor-panel { margin-bottom: 18px; padding: 24px; display: grid; grid-template-columns: minmax(220px, .72fr) minmax(320px, 1.28fr); gap: 18px 34px; border: 1px solid rgba(201,168,95,.42); background: linear-gradient(125deg, rgba(201,168,95,.1), rgba(16,23,20,.88) 44%, rgba(12,20,17,.96)); box-shadow: inset 3px 0 0 rgba(201,168,95,.58); }
|
||||
.advisor-intro { align-self: center; }
|
||||
.advisor-intro > p { margin: 0 0 10px; color: var(--signal); font: 600 9px/1 var(--mono); letter-spacing: .15em; }
|
||||
@@ -141,9 +225,11 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.register-empty { margin: 0; padding: 22px 18px; color: var(--faint); font-size: 11px; }
|
||||
|
||||
.activity-panel { margin-bottom: 18px; border: 1px solid var(--line); background: rgba(13, 20, 17, .9); }
|
||||
.learning-panel { margin-bottom: 18px; border: 1px solid rgba(120,198,163,.3); background: linear-gradient(135deg, rgba(22,31,27,.92), rgba(10,16,14,.94)); box-shadow: inset 3px 0 0 rgba(120,198,163,.38); }
|
||||
.activity-panel > header { min-height: 66px; padding: 13px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
|
||||
.activity-panel header p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
|
||||
.activity-panel h2 { margin: 0; font: 400 22px/1 var(--serif); }
|
||||
.learning-panel > header { min-height: 66px; padding: 13px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
|
||||
.activity-panel header p, .learning-panel header p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
|
||||
.activity-panel h2, .learning-panel h2 { margin: 0; font: 400 22px/1 var(--serif); }
|
||||
.activity-tools { display: flex; align-items: center; gap: 12px; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .1em; }
|
||||
.activity-tools .icon-button { color: var(--muted); font-size: 18px; }
|
||||
.activity-list { max-height: 300px; overflow: auto; scrollbar-color: var(--faint) transparent; }
|
||||
@@ -167,6 +253,23 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.activity-reconcile:disabled { opacity: .55; cursor: wait; }
|
||||
.activity-error { color: var(--bad); }
|
||||
|
||||
.learning-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.learning-grid article { min-width: 0; border-right: 1px solid var(--line-soft); }
|
||||
.learning-grid article:last-child { border-right: 0; }
|
||||
.learning-grid h3 { margin: 0; padding: 13px 18px 10px; color: var(--signal); font: 600 10px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; border-bottom: 1px solid var(--line-soft); }
|
||||
.learning-list { max-height: 238px; overflow: auto; scrollbar-color: var(--faint) transparent; }
|
||||
.learning-list[aria-busy="true"] { opacity: .72; }
|
||||
.learning-row { min-height: 66px; padding: 12px 18px; display: grid; gap: 6px; border-bottom: 1px solid var(--line-soft); }
|
||||
.learning-row:last-child { border-bottom: 0; }
|
||||
.learning-row:hover { background: rgba(120,198,163,.05); }
|
||||
.learning-row strong { color: var(--ink); font: 600 10px/1.35 var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.learning-row span { color: var(--muted); font: 10px/1.35 var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.learning-row time { color: var(--faint); font: 9px/1.35 var(--mono); }
|
||||
.learning-state { width: fit-content; padding: 5px 8px; border: 1px solid currentColor; border-radius: 100px; color: var(--faint); font: 600 8px/1 var(--mono); letter-spacing: .06em; }
|
||||
.learning-state[data-state="SUCCESS"], .learning-state[data-state="VALIDATED"] { color: var(--good); }
|
||||
.learning-state[data-state="USER_CORRECTED"], .learning-state[data-state="FAILED"], .learning-state[data-state="DEGRADED"], .learning-state[data-state="SUSPENDED"] { color: var(--bad); }
|
||||
.learning-state[data-state="CONFLICT"], .learning-state[data-state="OUT_OF_DISTRIBUTION"], .learning-state[data-state="OPEN"], .learning-state[data-state="EVALUATING"] { color: var(--warn); }
|
||||
|
||||
.data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
|
||||
.data-card { min-width: 0; padding: 24px; border: 1px solid var(--line); background: linear-gradient(145deg, rgba(22, 31, 27, .94), rgba(13, 20, 17, .98)); box-shadow: 0 10px 40px rgba(0,0,0,.12); animation: reveal .55s both; }
|
||||
.task-card { animation-delay: .08s; }
|
||||
@@ -259,6 +362,9 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.advisor-panel { grid-template-columns: 1fr; }
|
||||
.capability-card { grid-column: auto; }
|
||||
.prospective-card { grid-column: auto; }
|
||||
.diagnostic-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.diagnostic-item:nth-child(4n) { border-right: 1px solid var(--line-soft); }
|
||||
.diagnostic-item:nth-child(2n) { border-right: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
@@ -266,14 +372,26 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.console { display: block; }
|
||||
.topbar { height: 66px; padding: 0 14px; }
|
||||
.wordmark small, .connection, .text-button { display: none; }
|
||||
.chat-return-link { display: inline-block; }
|
||||
.topbar-nav { display: none; }
|
||||
.context-rail { padding: 24px 18px; border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.rail-copy { max-width: 48ch; }
|
||||
.session-stamp { margin-top: 8px; grid-template-columns: auto 1fr; align-items: center; }
|
||||
.session-stamp span { margin: 0; }
|
||||
.workspace { padding: 30px 16px 50px; }
|
||||
.section-nav { display: flex; gap: 6px; margin: 10px -18px -24px; padding: 10px 18px; overflow-x: auto; border-top: 1px solid var(--line-soft); background: #0e1512; scrollbar-width: thin; }
|
||||
.section-nav-link { min-width: 142px; padding: 10px; border-left: 0; border-bottom: 2px solid transparent; }
|
||||
.section-nav-link:hover, .section-nav-link:focus-visible, .section-nav-link.active, .section-nav-link[aria-current="location"] { border-left-color: transparent; border-bottom-color: var(--signal); }
|
||||
.workspace-head { align-items: start; flex-direction: column; }
|
||||
.last-update { text-align: left; }
|
||||
.signal-strip { grid-template-columns: 1fr; }
|
||||
.panel-heading { align-items: flex-start; flex-direction: column; }
|
||||
.panel-heading-tools { width: 100%; justify-content: space-between; }
|
||||
.diagnostic-list { grid-template-columns: 1fr; }
|
||||
.diagnostic-item, .diagnostic-item:nth-child(2n), .diagnostic-item:nth-child(4n) { border-right: 0; }
|
||||
.settings-grid { grid-template-columns: 1fr; padding: 16px; }
|
||||
.settings-actions { grid-column: auto; align-items: stretch; flex-direction: column; }
|
||||
.settings-actions .primary-button { width: 100%; }
|
||||
.signal-strip div { border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.advisor-panel { padding: 20px 16px; gap: 20px; }
|
||||
.advisor-controls { align-items: stretch; flex-direction: column; }
|
||||
@@ -281,7 +399,11 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.advisor-result { grid-template-columns: 1fr; gap: 10px; }
|
||||
.register-panel > header { align-items: start; }
|
||||
.activity-panel > header { align-items: flex-start; }
|
||||
.learning-panel > header { align-items: flex-start; }
|
||||
.activity-tools { align-items: flex-end; flex-direction: column-reverse; }
|
||||
.learning-grid { grid-template-columns: 1fr; }
|
||||
.learning-grid article { border-right: 0; border-bottom: 1px solid var(--line-soft); }
|
||||
.learning-grid article:last-child { border-bottom: 0; }
|
||||
.activity-row { padding: 14px 16px; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 12px; }
|
||||
.activity-state { grid-column: 2; grid-row: 1; }
|
||||
.activity-operation { grid-column: 1; grid-row: 1; }
|
||||
|
||||
@@ -7,14 +7,42 @@ const advisoryForm = byId("advisory-form");
|
||||
const loginError = byId("login-error");
|
||||
const toast = byId("toast");
|
||||
const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id"];
|
||||
const settingsStorageKey = "domovoy.creator.settings";
|
||||
const contextPresetsStorageKey = "domovoy.creator.context.presets";
|
||||
let sessionToken = sessionStorage.getItem("domovoy.creator.session") || "";
|
||||
let toastTimer;
|
||||
let refreshTimer;
|
||||
let currentTask = null;
|
||||
let currentProspective = null;
|
||||
let pendingCommand = null;
|
||||
let pendingNotification = null;
|
||||
let lastAuthorizedActionID = "";
|
||||
let advisoryPolicy = {Mode: "deterministic", ExternalEgress: false, AcknowledgementRequired: false};
|
||||
let uiSettings = {density: "comfortable", refresh: "off", motion: false};
|
||||
|
||||
const pageNavLinks = Array.from(document.querySelectorAll(".section-nav-link, .topbar-nav a"));
|
||||
const pageViews = Array.from(document.querySelectorAll("[data-page]"));
|
||||
const cabinetPages = ["overview", "diagnostics", "operations", "learning", "settings"];
|
||||
|
||||
function setActivePage(id, updateHistory = false) {
|
||||
const page = cabinetPages.includes(id) ? id : "overview";
|
||||
pageViews.forEach((view) => { view.hidden = view.dataset.page !== page; });
|
||||
document.body.dataset.page = page;
|
||||
pageNavLinks.forEach((link) => {
|
||||
const active = link.getAttribute("href") === `#${page}`;
|
||||
link.classList.toggle("active", active);
|
||||
if (active) link.setAttribute("aria-current", "location");
|
||||
else link.removeAttribute("aria-current");
|
||||
});
|
||||
if (updateHistory && window.location.hash !== `#${page}`) window.history.pushState({}, "", `#${page}`);
|
||||
}
|
||||
|
||||
pageNavLinks.forEach((link) => link.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
setActivePage(link.getAttribute("href").slice(1), true);
|
||||
}));
|
||||
window.addEventListener("popstate", () => setActivePage(window.location.hash.slice(1)));
|
||||
setActivePage(window.location.hash.slice(1));
|
||||
|
||||
function setText(id, value, fallback = "—") {
|
||||
byId(id).textContent = value === undefined || value === null || value === "" ? fallback : String(value);
|
||||
@@ -39,6 +67,18 @@ function notify(message) {
|
||||
toastTimer = setTimeout(() => { toast.hidden = true; }, 4200);
|
||||
}
|
||||
|
||||
function safeClientError(error) {
|
||||
const message = error && typeof error.message === "string" ? error.message : "";
|
||||
switch (message) {
|
||||
case "creator authentication denied":
|
||||
return "Логин или пароль не принят.";
|
||||
case "Сессия отклонена":
|
||||
return "Сессия истекла. Войдите снова.";
|
||||
default:
|
||||
return "Операция недоступна. Попробуйте ещё раз.";
|
||||
}
|
||||
}
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const headers = new Headers(options.headers || {});
|
||||
if (sessionToken) headers.set("Authorization", `Bearer ${sessionToken}`);
|
||||
@@ -50,7 +90,9 @@ async function request(path, options = {}) {
|
||||
}
|
||||
if (!response.ok) {
|
||||
const detail = (await response.text()).trim();
|
||||
throw new Error(detail || `Ошибка ${response.status}`);
|
||||
const error = new Error(detail || `Ошибка ${response.status}`);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
@@ -64,15 +106,57 @@ function restoreContext() {
|
||||
} catch (_) {
|
||||
localStorage.removeItem("domovoy.creator.context");
|
||||
}
|
||||
renderContextState();
|
||||
renderContextPresets();
|
||||
}
|
||||
|
||||
function saveContext() {
|
||||
const saved = {};
|
||||
contextFields.forEach((id) => { saved[id] = byId(id).value.trim(); });
|
||||
localStorage.setItem("domovoy.creator.context", JSON.stringify(saved));
|
||||
renderContextState();
|
||||
return saved;
|
||||
}
|
||||
|
||||
function readContextPresets() {
|
||||
try {
|
||||
const value = JSON.parse(localStorage.getItem(contextPresetsStorageKey) || "[]");
|
||||
return Array.isArray(value) ? value.filter((item) => item && item.session && item.workspace) : [];
|
||||
} catch (_) {
|
||||
localStorage.removeItem(contextPresetsStorageKey);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function renderContextState() {
|
||||
const state = byId("context-state");
|
||||
if (!state) return;
|
||||
const ready = Boolean(byId("session-id").value.trim() && byId("workspace-id").value.trim());
|
||||
state.textContent = ready ? "готов к проверке" : "не выбран";
|
||||
state.dataset.state = ready ? "ready" : "empty";
|
||||
}
|
||||
|
||||
function renderContextPresets() {
|
||||
const select = byId("context-preset");
|
||||
if (!select) return;
|
||||
const presets = readContextPresets();
|
||||
select.replaceChildren(new Option("Выберите сохранённый набор…", ""));
|
||||
presets.forEach((preset, index) => {
|
||||
const session = preset.session.length > 18 ? `${preset.session.slice(0, 8)}…${preset.session.slice(-6)}` : preset.session;
|
||||
const workspace = preset.workspace.length > 18 ? `${preset.workspace.slice(0, 8)}…${preset.workspace.slice(-6)}` : preset.workspace;
|
||||
select.appendChild(new Option(`${session} · ${workspace}`, String(index)));
|
||||
});
|
||||
}
|
||||
|
||||
function rememberContextPreset(context) {
|
||||
if (!context["session-id"] || !context["workspace-id"]) return;
|
||||
const next = {session: context["session-id"], workspace: context["workspace-id"], savedAt: Date.now()};
|
||||
const presets = readContextPresets().filter((item) => item.session !== next.session || item.workspace !== next.workspace);
|
||||
presets.unshift(next);
|
||||
localStorage.setItem(contextPresetsStorageKey, JSON.stringify(presets.slice(0, 5)));
|
||||
renderContextPresets();
|
||||
}
|
||||
|
||||
async function showConsole() {
|
||||
const session = await request("/api/session/current");
|
||||
setText("session-actor", session.ActorID);
|
||||
@@ -80,15 +164,22 @@ async function showConsole() {
|
||||
setText("session-expiry", expiry);
|
||||
loginView.hidden = true;
|
||||
consoleView.hidden = false;
|
||||
restoreSettings();
|
||||
restoreContext();
|
||||
await loadAdvisoryPolicy();
|
||||
const context = saveContext();
|
||||
if (context["session-id"] && context["workspace-id"]) loadDashboard();
|
||||
else loadActionHistory();
|
||||
else {
|
||||
loadActionHistory();
|
||||
loadLearning();
|
||||
}
|
||||
runDiagnostics();
|
||||
}
|
||||
|
||||
function endSession(message = "Сессия завершена.") {
|
||||
sessionToken = "";
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
sessionStorage.removeItem("domovoy.creator.session");
|
||||
consoleView.hidden = true;
|
||||
loginView.hidden = false;
|
||||
@@ -125,7 +216,7 @@ loginForm.addEventListener("submit", async (event) => {
|
||||
byId("creator-password").value = "";
|
||||
await showConsole();
|
||||
} catch (error) {
|
||||
loginError.textContent = error.message === "creator authentication denied" ? "Логин или пароль не принят." : error.message;
|
||||
loginError.textContent = safeClientError(error);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.firstElementChild.textContent = "Открыть консоль";
|
||||
@@ -141,13 +232,129 @@ byId("toggle-secret").addEventListener("click", () => {
|
||||
|
||||
contextForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
saveContext();
|
||||
const context = saveContext();
|
||||
rememberContextPreset(context);
|
||||
loadDashboard();
|
||||
});
|
||||
|
||||
for (const input of [byId("session-id"), byId("workspace-id")]) {
|
||||
input.addEventListener("input", renderContextState);
|
||||
}
|
||||
|
||||
byId("context-preset").addEventListener("change", (event) => {
|
||||
const index = Number(event.target.value);
|
||||
if (!Number.isInteger(index)) return;
|
||||
const preset = readContextPresets()[index];
|
||||
if (!preset) return;
|
||||
byId("session-id").value = preset.session;
|
||||
byId("workspace-id").value = preset.workspace;
|
||||
renderContextState();
|
||||
loadDashboard();
|
||||
notify("Контекст из недавнего набора загружен.");
|
||||
});
|
||||
|
||||
for (const button of document.querySelectorAll("[data-copy-context]")) {
|
||||
button.addEventListener("click", async () => {
|
||||
const value = byId(button.dataset.copyContext).value.trim();
|
||||
if (!value) {
|
||||
notify("Сначала укажите идентификатор.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
notify("Идентификатор скопирован.");
|
||||
} catch (_) {
|
||||
notify("Не удалось скопировать идентификатор.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const button of document.querySelectorAll("[data-copy-value], [data-use-value]")) {
|
||||
button.addEventListener("click", async () => {
|
||||
const source = byId(button.dataset.copyValue || button.dataset.useValue);
|
||||
const value = source?.textContent.trim() || "";
|
||||
if (!value || value === "—") {
|
||||
notify("Сначала загрузите значение.");
|
||||
return;
|
||||
}
|
||||
if (button.dataset.useValue) {
|
||||
const target = byId(button.dataset.targetId);
|
||||
if (!target) return;
|
||||
target.value = value;
|
||||
document.querySelector(".advanced-context").open = true;
|
||||
saveContext();
|
||||
notify("ID подставлен в расширенную проверку.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
notify("ID скопирован.");
|
||||
} catch (_) {
|
||||
notify("Не удалось скопировать ID.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
byId("reset-context").addEventListener("click", () => {
|
||||
contextFields.forEach((id) => { byId(id).value = id === "capability-id" ? "cognitive-proposal" : ""; });
|
||||
localStorage.removeItem("domovoy.creator.context");
|
||||
loadDashboard();
|
||||
notify("Рабочий контекст сброшен.");
|
||||
});
|
||||
|
||||
function restoreSettings() {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(settingsStorageKey) || "{}");
|
||||
uiSettings = {
|
||||
density: ["comfortable", "compact"].includes(saved.density) ? saved.density : "comfortable",
|
||||
refresh: ["off", "30", "60", "300"].includes(saved.refresh) ? saved.refresh : "off",
|
||||
motion: saved.motion === true,
|
||||
};
|
||||
} catch (_) {
|
||||
uiSettings = {density: "comfortable", refresh: "off", motion: false};
|
||||
}
|
||||
byId("setting-density").value = uiSettings.density;
|
||||
byId("setting-refresh").value = uiSettings.refresh;
|
||||
byId("setting-motion").checked = uiSettings.motion;
|
||||
applySettings();
|
||||
}
|
||||
|
||||
function applySettings() {
|
||||
document.documentElement.dataset.density = uiSettings.density;
|
||||
document.documentElement.dataset.reducedMotion = uiSettings.motion ? "true" : "false";
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
if (sessionToken && uiSettings.refresh !== "off") {
|
||||
refreshTimer = setInterval(() => loadDashboard(), Number(uiSettings.refresh) * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
byId("settings-form").addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
uiSettings = {
|
||||
density: byId("setting-density").value,
|
||||
refresh: byId("setting-refresh").value,
|
||||
motion: byId("setting-motion").checked,
|
||||
};
|
||||
localStorage.setItem(settingsStorageKey, JSON.stringify(uiSettings));
|
||||
applySettings();
|
||||
setText("settings-saved", "Сохранено только в этом браузере");
|
||||
notify("Настройки кабинета сохранены.");
|
||||
});
|
||||
|
||||
byId("reset-settings").addEventListener("click", () => {
|
||||
uiSettings = {density: "comfortable", refresh: "off", motion: false};
|
||||
localStorage.removeItem(settingsStorageKey);
|
||||
restoreSettings();
|
||||
setText("settings-saved", "Восстановлены значения по умолчанию");
|
||||
notify("Настройки сброшены.");
|
||||
});
|
||||
|
||||
byId("refresh-all").addEventListener("click", loadDashboard);
|
||||
byId("refresh-actions").addEventListener("click", loadActionHistory);
|
||||
byId("refresh-learning").addEventListener("click", loadLearning);
|
||||
byId("logout").addEventListener("click", () => endSession());
|
||||
byId("run-diagnostics").addEventListener("click", runDiagnostics);
|
||||
|
||||
advisoryForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
@@ -181,7 +388,7 @@ advisoryForm.addEventListener("submit", async (event) => {
|
||||
byId("advisory-result").hidden = false;
|
||||
} catch (error) {
|
||||
setText("advisory-state", "ОШИБКА");
|
||||
setText("advisory-text", error.message);
|
||||
setText("advisory-text", safeClientError(error));
|
||||
byId("advisory-result").hidden = false;
|
||||
} finally {
|
||||
byId("advisory-egress-ack").checked = false;
|
||||
@@ -234,6 +441,7 @@ async function loadDashboard() {
|
||||
}
|
||||
work.push(loadProspectiveRegister(context["prospective-id"]));
|
||||
work.push(loadActionHistory());
|
||||
work.push(loadLearning());
|
||||
if (context["capability-id"]) work.push(loadCapability(context["capability-id"]));
|
||||
else setSignal("capability", "не выбрана", "idle");
|
||||
|
||||
@@ -241,6 +449,157 @@ async function loadDashboard() {
|
||||
setText("last-update", `Обновлено ${new Intl.DateTimeFormat("ru-RU", {hour: "2-digit", minute: "2-digit", second: "2-digit"}).format(new Date())}`);
|
||||
}
|
||||
|
||||
async function runDiagnostics() {
|
||||
const list = byId("diagnostic-list");
|
||||
const summary = byId("diagnostic-summary");
|
||||
const checks = [
|
||||
["Сессия", "identity", () => request("/api/session/current")],
|
||||
["Политика совета", "advisory", () => request("/api/advisory-policy")],
|
||||
["Когнитивная возможность", "capability", () => request("/api/capabilities/cognitive-proposal/health")],
|
||||
["Сегодня", "today", () => request("/api/today")],
|
||||
["Внимание", "attention", () => request("/api/attention")],
|
||||
["Действия", "actions", () => request("/api/actions?limit=1")],
|
||||
["Learning evidence", "learning", () => request("/api/learning/recent?limit=1")],
|
||||
];
|
||||
list.setAttribute("aria-busy", "true");
|
||||
list.replaceChildren();
|
||||
checks.forEach(([label, key]) => list.append(createDiagnosticItem(label, key, "loading")));
|
||||
setText("diagnostic-summary", "Проверяю…");
|
||||
const results = await Promise.all(checks.map(async ([label, key, run]) => {
|
||||
const started = performance.now();
|
||||
try {
|
||||
await run();
|
||||
return {label, key, state: "ok", detail: `Ответ получен · ${Math.round(performance.now() - started)} мс`};
|
||||
} catch (error) {
|
||||
const detail = error?.status === 400 ? "Нужны параметры рабочего контекста" : error?.status === 401 ? "Сессия истекла" : error?.status >= 500 ? "Сервис временно недоступен" : "Проверка не пройдена";
|
||||
return {label, key, state: "error", detail};
|
||||
}
|
||||
}));
|
||||
list.replaceChildren(...results.map((result) => createDiagnosticItem(result.label, result.key, result.state, result.detail)));
|
||||
list.setAttribute("aria-busy", "false");
|
||||
const passed = results.filter((result) => result.state === "ok").length;
|
||||
setText("diagnostic-summary", `${passed}/${results.length} проверок пройдено`);
|
||||
}
|
||||
|
||||
function createDiagnosticItem(label, key, state, detail = "Проверяю…") {
|
||||
const item = document.createElement("article");
|
||||
item.className = "diagnostic-item";
|
||||
item.dataset.state = state;
|
||||
item.dataset.check = key;
|
||||
const marker = document.createElement("i");
|
||||
marker.setAttribute("aria-hidden", "true");
|
||||
const copy = document.createElement("div");
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = label;
|
||||
const description = document.createElement("span");
|
||||
description.textContent = detail;
|
||||
copy.append(title, description);
|
||||
const status = document.createElement("em");
|
||||
status.textContent = state === "ok" ? "OK" : state === "error" ? "ошибка" : "…";
|
||||
item.append(marker, copy, status);
|
||||
return item;
|
||||
}
|
||||
|
||||
async function loadLearning() {
|
||||
const experienceList = byId("experience-list");
|
||||
const reflectionList = byId("reflection-list");
|
||||
const skillList = byId("skill-list");
|
||||
[experienceList, reflectionList, skillList].forEach((list) => {
|
||||
list.setAttribute("aria-busy", "true");
|
||||
list.replaceChildren();
|
||||
const loading = document.createElement("p");
|
||||
loading.className = "register-empty";
|
||||
loading.textContent = "Читаю learning evidence…";
|
||||
list.append(loading);
|
||||
});
|
||||
try {
|
||||
const result = await request("/api/learning/recent?limit=8");
|
||||
const experiences = Array.isArray(result.Experiences) ? result.Experiences : [];
|
||||
const reflections = Array.isArray(result.Reflections) ? result.Reflections : [];
|
||||
const skills = Array.isArray(result.Skills) ? result.Skills : [];
|
||||
setText("learning-count", `${experiences.length + reflections.length + skills.length} СИГНАЛОВ`);
|
||||
renderLearningList(experienceList, experiences, createExperienceRow, "Experience пока не записан.");
|
||||
renderLearningList(reflectionList, reflections, createReflectionRow, "ReflectionCandidate пока нет.");
|
||||
renderLearningList(skillList, skills, createSkillRow, "SkillProfile пока не загружен.");
|
||||
} catch (error) {
|
||||
[experienceList, reflectionList, skillList].forEach((list) => {
|
||||
list.replaceChildren();
|
||||
const failed = document.createElement("p");
|
||||
failed.className = "register-empty activity-error";
|
||||
failed.textContent = safeClientError(error);
|
||||
list.append(failed);
|
||||
});
|
||||
setText("learning-count", "ОШИБКА");
|
||||
} finally {
|
||||
[experienceList, reflectionList, skillList].forEach((list) => list.setAttribute("aria-busy", "false"));
|
||||
}
|
||||
}
|
||||
|
||||
function renderLearningList(list, items, rowFactory, emptyText) {
|
||||
list.replaceChildren();
|
||||
if (items.length === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "register-empty";
|
||||
empty.textContent = emptyText;
|
||||
list.append(empty);
|
||||
return;
|
||||
}
|
||||
items.forEach((item) => list.append(rowFactory(item)));
|
||||
}
|
||||
|
||||
function createExperienceRow(item) {
|
||||
const row = document.createElement("article");
|
||||
row.className = "learning-row";
|
||||
const state = document.createElement("em");
|
||||
state.className = "learning-state";
|
||||
state.dataset.state = item.ActualOutcome || "UNKNOWN";
|
||||
state.textContent = item.ActualOutcome || "UNKNOWN";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = item.HelpMethod || "help method";
|
||||
const detail = document.createElement("span");
|
||||
detail.textContent = item.OutcomeDetail || item.OutcomeRef || "evidence recorded";
|
||||
const time = document.createElement("time");
|
||||
time.textContent = item.RecordedAtUnixMillis ? new Date(item.RecordedAtUnixMillis).toLocaleString("ru-RU") : "время неизвестно";
|
||||
row.append(state, title, detail, time);
|
||||
return row;
|
||||
}
|
||||
|
||||
function createReflectionRow(item) {
|
||||
const row = document.createElement("article");
|
||||
row.className = "learning-row";
|
||||
const state = document.createElement("em");
|
||||
state.className = "learning-state";
|
||||
state.dataset.state = item.State || "OPEN";
|
||||
state.textContent = item.Trigger || "REFLECTION";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = item.State || "OPEN";
|
||||
const detail = document.createElement("span");
|
||||
detail.textContent = item.Summary || "reflection candidate";
|
||||
const time = document.createElement("time");
|
||||
time.textContent = item.RecordedAtUnixMillis ? new Date(item.RecordedAtUnixMillis).toLocaleString("ru-RU") : "время неизвестно";
|
||||
row.append(state, title, detail, time);
|
||||
return row;
|
||||
}
|
||||
|
||||
function createSkillRow(item) {
|
||||
const row = document.createElement("article");
|
||||
row.className = "learning-row";
|
||||
const state = document.createElement("em");
|
||||
state.className = "learning-state";
|
||||
state.dataset.state = item.Status || "UNKNOWN";
|
||||
state.textContent = item.Status || "UNKNOWN";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = item.Name || "skill profile";
|
||||
const detail = document.createElement("span");
|
||||
const support = Array.isArray(item.SupportingExperienceRefs) ? item.SupportingExperienceRefs.length : 0;
|
||||
const failures = Array.isArray(item.KnownFailureExperienceRefs) ? item.KnownFailureExperienceRefs.length : 0;
|
||||
detail.textContent = `confidence ${item.ConfidenceMillis ?? "—"} · +${support} / -${failures}`;
|
||||
const time = document.createElement("time");
|
||||
time.textContent = item.RecordedAtUnixMillis ? new Date(item.RecordedAtUnixMillis).toLocaleString("ru-RU") : "время неизвестно";
|
||||
row.append(state, title, detail, time);
|
||||
return row;
|
||||
}
|
||||
|
||||
async function loadActionHistory() {
|
||||
const list = byId("action-history");
|
||||
list.setAttribute("aria-busy", "true");
|
||||
@@ -266,7 +625,7 @@ async function loadActionHistory() {
|
||||
list.replaceChildren();
|
||||
const failed = document.createElement("p");
|
||||
failed.className = "register-empty activity-error";
|
||||
failed.textContent = error.message;
|
||||
failed.textContent = safeClientError(error);
|
||||
list.append(failed);
|
||||
setText("action-count", "ОШИБКА");
|
||||
} finally {
|
||||
@@ -325,7 +684,7 @@ async function reconcileHistoryAction(actionID, button) {
|
||||
await loadActionHistory();
|
||||
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
|
||||
} catch (error) {
|
||||
notify(error.message);
|
||||
notify(safeClientError(error));
|
||||
button.disabled = false;
|
||||
button.textContent = "Сверить";
|
||||
}
|
||||
@@ -345,7 +704,7 @@ async function loadFocus(query) {
|
||||
} catch (error) {
|
||||
setSignal("focus", "ошибка", "error");
|
||||
setBadge("focus", "НЕДОСТУПЕН", "bad");
|
||||
setText("focus-message", error.message);
|
||||
setText("focus-message", safeClientError(error));
|
||||
byId("connection-state").classList.add("offline");
|
||||
}
|
||||
}
|
||||
@@ -367,7 +726,7 @@ async function loadTask(taskID, query) {
|
||||
currentTask = null;
|
||||
setSignal("task", "отклонена", "error");
|
||||
setBadge("task", "ВНЕ ФОКУСА", "bad");
|
||||
setText("task-message", error.message);
|
||||
setText("task-message", safeClientError(error));
|
||||
updateActionReadiness();
|
||||
}
|
||||
}
|
||||
@@ -406,12 +765,12 @@ async function loadProspectiveRegister(selectedID) {
|
||||
list.replaceChildren();
|
||||
const failed = document.createElement("p");
|
||||
failed.className = "register-empty";
|
||||
failed.textContent = error.message;
|
||||
failed.textContent = safeClientError(error);
|
||||
list.append(failed);
|
||||
setText("prospective-count", "ОШИБКА");
|
||||
setSignal("prospective", "ошибка", "error");
|
||||
setBadge("prospective", "НЕДОСТУПНА", "bad");
|
||||
setText("prospective-message", error.message);
|
||||
setText("prospective-message", safeClientError(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +826,7 @@ async function loadProspective(prospectiveID) {
|
||||
setSignal("prospective", "ошибка", "error");
|
||||
setBadge("prospective", "НЕДОСТУПНА", "bad");
|
||||
setText("prospective-state", "UNKNOWN");
|
||||
setText("prospective-message", error.message);
|
||||
setText("prospective-message", safeClientError(error));
|
||||
updateActionReadiness();
|
||||
}
|
||||
}
|
||||
@@ -489,7 +848,7 @@ async function loadCapability(capabilityID) {
|
||||
setSignal("capability", "ошибка", "error");
|
||||
setBadge("capability", "НЕДОСТУПНА", "bad");
|
||||
setText("capability-state", "UNKNOWN");
|
||||
setText("capability-reason", error.message);
|
||||
setText("capability-reason", safeClientError(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,7 +954,7 @@ byId("confirm-notification").addEventListener("click", async () => {
|
||||
await loadActionHistory();
|
||||
notify(result.State === "SUCCEEDED" ? "Receiver принял уведомление." : "Исход отправки требует сверки.");
|
||||
} catch (error) {
|
||||
setText("notification-confirmation-error", error.message);
|
||||
setText("notification-confirmation-error", safeClientError(error));
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = "Подтверждаю отправку";
|
||||
@@ -625,7 +984,7 @@ byId("confirm-satisfaction").addEventListener("click", async () => {
|
||||
notify(result.State === "SUCCEEDED" ? "Исполнение зафиксировано через Commit." : "Исход требует проверки.");
|
||||
await loadProspectiveRegister(command.ProspectiveItemID);
|
||||
} catch (error) {
|
||||
setText("confirmation-error", error.message);
|
||||
setText("confirmation-error", safeClientError(error));
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = "Подтверждаю переход";
|
||||
@@ -659,7 +1018,7 @@ byId("reconcile-action").addEventListener("click", async () => {
|
||||
notify(result.State === "SUCCEEDED" ? "Исход подтверждён." : "Исход пока неизвестен.");
|
||||
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
|
||||
} catch (error) {
|
||||
notify(error.message);
|
||||
notify(safeClientError(error));
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = "Проверить исход";
|
||||
|
||||
@@ -25,38 +25,70 @@
|
||||
|
||||
<section id="chat-view" class="chat-view" hidden>
|
||||
<header class="chat-header">
|
||||
<a href="/chat" aria-label="Домовой">Домовой</a>
|
||||
<nav class="human-nav" aria-label="Разделы Домового">
|
||||
<button class="nav-tab active" data-tab="chat-panel" type="button">Чат</button>
|
||||
<button class="nav-tab" data-tab="today-panel" type="button">Сегодня</button>
|
||||
<button class="nav-tab" data-tab="history-panel" type="button">История</button>
|
||||
<a href="/" class="operator-link">Оператор</a>
|
||||
</nav>
|
||||
<span id="active-focus">фокус: не выбран</span>
|
||||
<span id="session-state">сессия активна</span>
|
||||
<a href="/" aria-label="Домовой">Домовой</a>
|
||||
<button id="cognitive-status" class="cognitive-status" data-state="checking" type="button" role="status" aria-live="polite" title="Проверить канал ещё раз">проверяю канал мышления</button>
|
||||
<a id="cognitive-console-link" class="cognitive-console-link" href="/admin" hidden>настроить в консоли</a>
|
||||
<div class="build-meta" aria-label="Версия и время обновления">
|
||||
<span>версия __ASSISTANT_BUILD_REVISION__</span>
|
||||
<time datetime="__ASSISTANT_BUILD_DATETIME__">обновлено __ASSISTANT_BUILD_TIME__</time>
|
||||
</div>
|
||||
<a href="/admin" class="operator-link">Консоль</a>
|
||||
<button id="logout" type="button" aria-label="Выйти">↗</button>
|
||||
</header>
|
||||
<section id="chat-panel" class="surface-panel active-panel">
|
||||
<div id="messages" class="messages" aria-live="polite"></div>
|
||||
<form id="chat-form" class="composer">
|
||||
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
|
||||
<button id="send" type="submit">Отправить</button>
|
||||
</form>
|
||||
</section>
|
||||
<section id="today-panel" class="surface-panel attention-surface" hidden>
|
||||
<div class="surface-heading">
|
||||
<p>Сегодня / внимание</p>
|
||||
<button id="refresh-today" type="button">Обновить</button>
|
||||
</div>
|
||||
<div id="today-list" class="attention-list"></div>
|
||||
</section>
|
||||
<section id="history-panel" class="surface-panel history-surface" hidden>
|
||||
<div class="surface-heading">
|
||||
<p>История</p>
|
||||
<span>Последние сообщения текущего окна</span>
|
||||
</div>
|
||||
<div id="history-list" class="history-list"></div>
|
||||
</section>
|
||||
<div class="chat-layout">
|
||||
<aside class="chat-side chat-left" aria-label="Навигация и текущий фокус">
|
||||
<div class="side-intro">
|
||||
<p class="side-index">01 / РАЗГОВОР</p>
|
||||
<h1>Домовой рядом</h1>
|
||||
<p>Пишите обычными словами. Настройки и сведения можно передать прямо в текущий разговор.</p>
|
||||
</div>
|
||||
<div class="side-focus">
|
||||
<span>ТЕКУЩИЙ ФОКУС</span>
|
||||
<strong id="active-focus">не выбран</strong>
|
||||
</div>
|
||||
<nav class="human-nav" aria-label="Разделы Домового">
|
||||
<button class="nav-tab active" data-tab="chat-panel" type="button"><b>01</b><span>Чат</span><small>живой разговор</small></button>
|
||||
<button class="nav-tab" data-tab="today-panel" type="button"><b>02</b><span>Сегодня</span><small>что требует внимания</small></button>
|
||||
<button class="nav-tab" data-tab="history-panel" type="button"><b>03</b><span>История</span><small>это окно разговора</small></button>
|
||||
</nav>
|
||||
<div class="side-note"><span>СЕССИЯ</span><strong id="session-state">активна</strong><small>Сведения сохраняются в текущем разговоре.</small></div>
|
||||
</aside>
|
||||
<main class="chat-main">
|
||||
<section id="chat-panel" class="surface-panel active-panel">
|
||||
<div id="messages" class="messages" aria-live="polite"></div>
|
||||
<form id="chat-form" class="composer">
|
||||
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
|
||||
<button id="send" type="submit">Отправить</button>
|
||||
</form>
|
||||
<div class="conversation-starters" aria-label="Частые вопросы">
|
||||
<span>Можно начать так:</span>
|
||||
<button type="button" data-starter="Проверь локальный канал мышления">Проверь канал</button>
|
||||
<button type="button" data-starter="Что сейчас доступно?">Что доступно?</button>
|
||||
<button type="button" data-starter="Хочу изменить настройку локальной модели">Изменить настройку</button>
|
||||
</div>
|
||||
</section>
|
||||
<section id="today-panel" class="surface-panel attention-surface" hidden>
|
||||
<div class="surface-heading"><p>Сегодня / внимание</p><button id="refresh-today" type="button">Обновить</button></div>
|
||||
<div id="today-list" class="attention-list"></div>
|
||||
</section>
|
||||
<section id="history-panel" class="surface-panel history-surface" hidden>
|
||||
<div class="surface-heading"><p>История</p><span>Последние сообщения текущего окна</span></div>
|
||||
<div id="history-list" class="history-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
<aside class="chat-side chat-right" aria-label="Состояние и сведения об ИИ">
|
||||
<div class="side-intro"><p class="side-index">02 / ИНФОРМАЦИЯ</p><h2>Канал мышления</h2><p>Домовой видит подтверждённое состояние канала. Если основной ИИ пропал, он остаётся рядом и переходит в аварийный режим.</p></div>
|
||||
<div class="ai-info-card"><span>ЧТО МОЖНО ПЕРЕДАТЬ</span><strong>Описание ИИ</strong><p>Название, назначение, ограничения или заметки о локальной модели. Не вставляйте пароли, токены и ключи.</p></div>
|
||||
<form id="ai-data-form" class="ai-data-form">
|
||||
<label for="ai-data">Данные для текущего разговора</label>
|
||||
<textarea id="ai-data" rows="8" maxlength="12000" placeholder="Например: локальная модель нужна для коротких ответов на русском; контекст ограничен; внешняя сеть запрещена."></textarea>
|
||||
<button type="submit">Передать Домовому</button>
|
||||
<p id="ai-data-error" class="ai-data-error" role="alert" hidden></p>
|
||||
<small>Это отправится как обычное сообщение в чат.</small>
|
||||
</form>
|
||||
<a class="side-console-link" href="/admin">Открыть консоль настройки <span>→</span></a>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="/assets/chat.js"></script>
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
<meta name="color-scheme" content="dark">
|
||||
<meta name="theme-color" content="#101714">
|
||||
<title>Домовой — консоль создателя</title>
|
||||
<link rel="stylesheet" href="/assets/console.css">
|
||||
<link rel="stylesheet" href="/assets/console.css?v=20260821-context">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#overview">К содержимому</a>
|
||||
<div class="grain" aria-hidden="true"></div>
|
||||
|
||||
<main id="login-view" class="login-shell">
|
||||
@@ -43,7 +44,15 @@
|
||||
<span><strong>Домовой</strong><small>консоль создателя</small></span>
|
||||
</a>
|
||||
<div class="topbar-actions">
|
||||
<a class="chat-return-link" href="/">← Чат с Домовым</a>
|
||||
<span id="connection-state" class="connection"><i></i>система доступна</span>
|
||||
<nav class="topbar-nav" aria-label="Разделы кабинета">
|
||||
<a href="#overview">Обзор</a>
|
||||
<a href="#diagnostics">Проверки</a>
|
||||
<a href="#operations">Операции</a>
|
||||
<a href="#learning">Опыт</a>
|
||||
<a href="#settings">Настройки</a>
|
||||
</nav>
|
||||
<button id="refresh-all" class="text-button" type="button">Обновить</button>
|
||||
<button id="logout" class="icon-button" type="button" aria-label="Завершить сессию" title="Выйти">↗</button>
|
||||
</div>
|
||||
@@ -56,18 +65,46 @@
|
||||
<p class="rail-copy">Укажите рабочий контекст. Он сохраняется только в этом браузере и не даёт дополнительных полномочий.</p>
|
||||
</div>
|
||||
<form id="context-form" class="context-form">
|
||||
<label for="session-id">Идентификатор сессии</label>
|
||||
<input id="session-id" autocomplete="off" placeholder="session-id">
|
||||
<label for="workspace-id">Рабочая область</label>
|
||||
<input id="workspace-id" autocomplete="off" placeholder="workspace-id">
|
||||
<label for="task-id">Задача</label>
|
||||
<input id="task-id" autocomplete="off" placeholder="task-id">
|
||||
<label for="prospective-id">Выбранное ожидание</label>
|
||||
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
|
||||
<label for="capability-id">Возможность</label>
|
||||
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
|
||||
<button class="secondary-button" type="submit">Загрузить контекст</button>
|
||||
<div class="context-status-row">
|
||||
<span class="context-status-label">ТЕКУЩИЙ КОНТЕКСТ</span>
|
||||
<strong id="context-state" data-state="empty">не выбран</strong>
|
||||
</div>
|
||||
<div class="context-presets">
|
||||
<label for="context-preset">Недавние наборы</label>
|
||||
<select id="context-preset">
|
||||
<option value="">Выберите сохранённый набор…</option>
|
||||
</select>
|
||||
<small>Сохраняются только в этом браузере.</small>
|
||||
</div>
|
||||
<div class="context-field">
|
||||
<label for="session-id">Идентификатор сессии <span>обязательно</span></label>
|
||||
<div class="context-input-row"><input id="session-id" autocomplete="off" spellcheck="false" placeholder="Вставьте session-id"><button class="copy-context" data-copy-context="session-id" type="button" aria-label="Скопировать идентификатор сессии" title="Скопировать">⧉</button></div>
|
||||
</div>
|
||||
<div class="context-field">
|
||||
<label for="workspace-id">Рабочая область <span>обязательно</span></label>
|
||||
<div class="context-input-row"><input id="workspace-id" autocomplete="off" spellcheck="false" placeholder="Вставьте workspace-id"><button class="copy-context" data-copy-context="workspace-id" type="button" aria-label="Скопировать рабочую область" title="Скопировать">⧉</button></div>
|
||||
</div>
|
||||
<p class="context-hint">Для «Состояния» нужны только эти два значения. Задача, ожидание и возможность находятся ниже в расширенных полях.</p>
|
||||
<details class="advanced-context">
|
||||
<summary>Расширенные идентификаторы</summary>
|
||||
<div>
|
||||
<label for="task-id">Задача</label>
|
||||
<input id="task-id" autocomplete="off" placeholder="task-id">
|
||||
<label for="prospective-id">Выбранное ожидание</label>
|
||||
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
|
||||
<label for="capability-id">Возможность</label>
|
||||
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
|
||||
</div>
|
||||
</details>
|
||||
<div class="context-actions"><button class="secondary-button" type="submit">Загрузить контекст</button><button id="reset-context" class="text-button" type="button">Сбросить</button></div>
|
||||
</form>
|
||||
<nav class="section-nav" aria-label="Навигация по кабинету">
|
||||
<a class="section-nav-link active" aria-current="location" href="#overview"><span>01</span><b>Состояние</b><small>фокус и сигналы</small></a>
|
||||
<a class="section-nav-link" href="#diagnostics"><span>02</span><b>Проверки</b><small>доступность и границы</small></a>
|
||||
<a class="section-nav-link" href="#operations"><span>03</span><b>Операции</b><small>ожидания и действия</small></a>
|
||||
<a class="section-nav-link" href="#learning"><span>04</span><b>Опыт</b><small>reflection и навыки</small></a>
|
||||
<a class="section-nav-link" href="#settings"><span>05</span><b>Настройки</b><small>интерфейс и обновление</small></a>
|
||||
</nav>
|
||||
<div class="session-stamp">
|
||||
<span>ACTOR</span>
|
||||
<strong id="session-actor">—</strong>
|
||||
@@ -77,7 +114,7 @@
|
||||
</aside>
|
||||
|
||||
<main class="workspace">
|
||||
<section class="workspace-head">
|
||||
<section id="overview" class="workspace-head" data-page="overview">
|
||||
<div>
|
||||
<p class="eyebrow">операционная картина</p>
|
||||
<h1>Состояние сейчас</h1>
|
||||
@@ -85,14 +122,28 @@
|
||||
<p id="last-update" class="last-update">Данные ещё не запрошены</p>
|
||||
</section>
|
||||
|
||||
<section class="signal-strip" aria-label="Сводка состояния">
|
||||
<section class="signal-strip" aria-label="Сводка состояния" data-page="overview">
|
||||
<div><span>ФОКУС</span><strong id="focus-signal" data-state="idle">ожидает</strong></div>
|
||||
<div><span>ЗАДАЧА</span><strong id="task-signal" data-state="idle">ожидает</strong></div>
|
||||
<div><span>CONTINUITY</span><strong id="prospective-signal" data-state="idle">ожидает</strong></div>
|
||||
<div><span>ВОЗМОЖНОСТЬ</span><strong id="capability-signal" data-state="idle">ожидает</strong></div>
|
||||
</section>
|
||||
|
||||
<section class="advisor-panel" aria-labelledby="advisor-title">
|
||||
<section id="diagnostics" class="diagnostics-panel" aria-labelledby="diagnostics-title" data-page="diagnostics">
|
||||
<header class="panel-heading">
|
||||
<div><p>Проверяемая система</p><h2 id="diagnostics-title">Диагностика кабинета</h2></div>
|
||||
<div class="panel-heading-tools"><span id="diagnostic-summary">Проверка ещё не запускалась</span><button id="run-diagnostics" class="secondary-button compact-button" type="button">Проверить всё</button></div>
|
||||
</header>
|
||||
<div class="diagnostic-intro">
|
||||
<span class="diagnostic-pulse" aria-hidden="true"></span>
|
||||
<p>Здесь видны только ответы текущей авторизованной сессии. Проверка не меняет данные и помогает отличить отсутствие контекста от недоступности сервиса.</p>
|
||||
</div>
|
||||
<div id="diagnostic-list" class="diagnostic-list" aria-live="polite" aria-busy="false">
|
||||
<p class="register-empty">Нажмите «Проверить всё», чтобы получить актуальный снимок.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="advisor-panel" aria-labelledby="advisor-title" data-page="diagnostics">
|
||||
<div class="advisor-intro">
|
||||
<p>COGNITIVE / ADVISORY V1</p>
|
||||
<h2 id="advisor-title">Спросить Домового</h2>
|
||||
@@ -120,7 +171,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="register-panel" aria-labelledby="register-title">
|
||||
<section id="operations" class="register-panel" aria-labelledby="register-title" data-page="operations">
|
||||
<header>
|
||||
<div><p>Continuity / ближайшие</p><h2 id="register-title">Реестр ожиданий</h2></div>
|
||||
<span id="prospective-count">0 ЗАПИСЕЙ</span>
|
||||
@@ -130,7 +181,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="activity-panel" aria-labelledby="activity-title">
|
||||
<section class="activity-panel" aria-labelledby="activity-title" data-page="operations">
|
||||
<header>
|
||||
<div><p>Commit / владелец</p><h2 id="activity-title">Журнал действий</h2></div>
|
||||
<div class="activity-tools">
|
||||
@@ -143,7 +194,44 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="data-grid">
|
||||
<section id="learning" class="learning-panel" aria-labelledby="learning-title" data-page="learning">
|
||||
<header>
|
||||
<div><p>Learning / evidence</p><h2 id="learning-title">Опыт и reflection</h2></div>
|
||||
<div class="activity-tools">
|
||||
<span id="learning-count">0 СИГНАЛОВ</span>
|
||||
<button id="refresh-learning" class="icon-button" type="button" aria-label="Обновить опыт" title="Обновить опыт">↻</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="learning-grid">
|
||||
<article>
|
||||
<h3>Experience</h3>
|
||||
<div id="experience-list" class="learning-list" aria-live="polite" aria-busy="false"><p class="register-empty">Опыт ещё не загружен.</p></div>
|
||||
</article>
|
||||
<article>
|
||||
<h3>Reflection</h3>
|
||||
<div id="reflection-list" class="learning-list" aria-live="polite" aria-busy="false"><p class="register-empty">Reflection ещё не загружен.</p></div>
|
||||
</article>
|
||||
<article>
|
||||
<h3>Skills</h3>
|
||||
<div id="skill-list" class="learning-list" aria-live="polite" aria-busy="false"><p class="register-empty">Skill profile ещё не загружен.</p></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="settings" class="settings-panel" aria-labelledby="settings-title" data-page="settings">
|
||||
<header class="panel-heading">
|
||||
<div><p>Локальные предпочтения</p><h2 id="settings-title">Настройки рабочего кабинета</h2></div>
|
||||
<span id="settings-saved" class="settings-saved">Сохраняются в этом браузере</span>
|
||||
</header>
|
||||
<form id="settings-form" class="settings-grid">
|
||||
<label class="setting-field"><span>Плотность данных</span><small>Выберите, сколько строк помещается на экране.</small><select id="setting-density"><option value="comfortable">Комфортная</option><option value="compact">Компактная</option></select></label>
|
||||
<label class="setting-field"><span>Автообновление</span><small>Повторная проверка состояния без изменения данных.</small><select id="setting-refresh"><option value="off">Выключено</option><option value="30">Каждые 30 секунд</option><option value="60">Каждую минуту</option><option value="300">Каждые 5 минут</option></select></label>
|
||||
<label class="setting-check"><input id="setting-motion" type="checkbox"><span><b>Сократить анимацию</b><small>Оставить спокойный режим для длительной работы.</small></span></label>
|
||||
<div class="settings-actions"><button id="save-settings" class="primary-button compact-button" type="submit"><span>Сохранить настройки</span><span aria-hidden="true">→</span></button><button id="reset-settings" class="text-button" type="button">Сбросить</button></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="data-grid" data-page="overview">
|
||||
<article class="data-card focus-card">
|
||||
<header><span class="card-number">01</span><div><p>Situation</p><h2>Активный фокус</h2></div><span id="focus-badge" class="status-badge neutral">НЕ ЗАГРУЖЕН</span></header>
|
||||
<dl>
|
||||
@@ -164,6 +252,7 @@
|
||||
<div><dt>Ревизия</dt><dd id="task-revision">—</dd></div>
|
||||
</dl>
|
||||
<p id="task-message" class="card-message">Укажите задачу для проверки относительно активного фокуса.</p>
|
||||
<div class="id-actions"><button class="id-action" data-use-value="task-value" data-target-id="task-id" type="button">Использовать ID задачи</button><button class="id-action icon-id-action" data-copy-value="task-value" type="button" aria-label="Скопировать ID задачи" title="Скопировать ID">⧉</button></div>
|
||||
</article>
|
||||
|
||||
<article class="data-card prospective-card">
|
||||
@@ -181,6 +270,7 @@
|
||||
<div><dt>Ревизия</dt><dd id="prospective-revision">—</dd></div>
|
||||
</dl>
|
||||
<p id="prospective-message" class="card-message">Укажите идентификатор ProspectiveItem для проверки владельца и состояния.</p>
|
||||
<div class="id-actions"><button class="id-action" data-use-value="prospective-value" data-target-id="prospective-id" type="button">Использовать ID ожидания</button><button class="id-action icon-id-action" data-copy-value="prospective-value" type="button" aria-label="Скопировать ID ожидания" title="Скопировать ID">⧉</button></div>
|
||||
<section class="commit-zone" aria-labelledby="commit-zone-title">
|
||||
<div class="commit-zone-copy">
|
||||
<p>Commit / контролируемый переход</p>
|
||||
@@ -216,6 +306,7 @@
|
||||
<div><dt>Ревизия</dt><dd id="capability-revision">—</dd></div>
|
||||
<div><dt>Наблюдалось</dt><dd id="capability-observed">—</dd></div>
|
||||
</dl>
|
||||
<div class="id-actions"><button class="id-action" data-use-value="capability-value" data-target-id="capability-id" type="button">Использовать ID возможности</button><button class="id-action icon-id-action" data-copy-value="capability-value" type="button" aria-label="Скопировать ID возможности" title="Скопировать ID">⧉</button></div>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
@@ -262,6 +353,6 @@
|
||||
</dialog>
|
||||
|
||||
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
||||
<script src="/assets/console.js" defer></script>
|
||||
<script src="/assets/console.js?v=20260821-context" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -166,7 +166,7 @@ func loadSelectedTask(root string, selection taskSelection) (TaskPacket, string,
|
||||
|
||||
func loadTaskPacket(root, taskPath string) (TaskPacket, error) {
|
||||
var taskPacket TaskPacket
|
||||
if err := decodeJSONFile(filepath.Join(root, filepath.FromSlash(taskPath)), &taskPacket, false); err != nil {
|
||||
if err := decodeJSONFile(filepath.Join(root, filepath.FromSlash(taskPath)), &taskPacket, true); err != nil {
|
||||
return TaskPacket{}, fmt.Errorf("task packet: %w", err)
|
||||
}
|
||||
return taskPacket, nil
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var required = []string{
|
||||
"task_id", "task_revision", "architecture_revision", "base_repo_revision", "risk_tier", "intent",
|
||||
"requirement_ids", "allowed_change_scope", "forbidden_change_scope", "allowed_architecture_delta",
|
||||
"contracts", "semantic_owner", "canonical_state", "state_transitions", "side_effects_and_privileges",
|
||||
"persistence_and_migrations", "failure_retry_restart", "assumptions", "acceptance_scenarios",
|
||||
"required_tests", "terminal_gap_states",
|
||||
}
|
||||
|
||||
func main() {
|
||||
activePath := filepath.FromSlash("dev/ACTIVE_TASK.json")
|
||||
var active struct {
|
||||
TaskFile string `json:"task_file"`
|
||||
TaskID string `json:"task_id"`
|
||||
TaskRevision int `json:"task_revision"`
|
||||
}
|
||||
if err := decode(activePath, &active); err != nil {
|
||||
fail("active pointer: %v", err)
|
||||
}
|
||||
if active.TaskFile == "" || active.TaskID == "" || active.TaskRevision < 1 {
|
||||
fail("active pointer is incomplete")
|
||||
}
|
||||
var raw map[string]json.RawMessage
|
||||
if err := decode(filepath.FromSlash(active.TaskFile), &raw); err != nil {
|
||||
fail("task packet: %v", err)
|
||||
}
|
||||
for _, key := range required {
|
||||
if _, ok := raw[key]; !ok {
|
||||
fail("task packet missing required field %q", key)
|
||||
}
|
||||
}
|
||||
var packet struct {
|
||||
TaskID string `json:"task_id"`
|
||||
TaskRevision int `json:"task_revision"`
|
||||
}
|
||||
if err := decode(filepath.FromSlash(active.TaskFile), &packet); err != nil {
|
||||
fail("task identity: %v", err)
|
||||
}
|
||||
if packet.TaskID != active.TaskID || packet.TaskRevision != active.TaskRevision {
|
||||
fail("active pointer and packet identity differ")
|
||||
}
|
||||
fmt.Printf("independent governance verification: OK task=%s revision=%d\n", packet.TaskID, packet.TaskRevision)
|
||||
}
|
||||
|
||||
func decode(path string, target any) error {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, target)
|
||||
}
|
||||
|
||||
func fail(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "independent verifier: "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
+32
-11
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
@@ -24,19 +23,41 @@ type Registry struct {
|
||||
BoundaryClass string `json:"boundary_class"`
|
||||
MayDependOn []string `json:"may_depend_on"`
|
||||
Forbidden []string `json:"forbidden"`
|
||||
Privileged bool `json:"privileged"`
|
||||
} `json:"modules"`
|
||||
ProtectedPaths []string `json:"protected_paths"`
|
||||
}
|
||||
|
||||
type TaskPacket struct {
|
||||
TaskID string `json:"task_id"`
|
||||
TaskRevision int `json:"task_revision"`
|
||||
BaseRepoRevision string `json:"base_repo_revision"`
|
||||
RiskTier string `json:"risk_tier"`
|
||||
Intent string `json:"intent"`
|
||||
AllowedChangeScope struct {
|
||||
Paths []string `json:"paths"`
|
||||
TaskID string `json:"task_id"`
|
||||
TaskRevision int `json:"task_revision"`
|
||||
ArchitectureRevision string `json:"architecture_revision"`
|
||||
BaseRepoRevision string `json:"base_repo_revision"`
|
||||
RiskTier string `json:"risk_tier"`
|
||||
Intent string `json:"intent"`
|
||||
RequirementIDs []string `json:"requirement_ids"`
|
||||
AllowedChangeScope struct {
|
||||
Modules []string `json:"modules"`
|
||||
Paths []string `json:"paths"`
|
||||
} `json:"allowed_change_scope"`
|
||||
ForbiddenChangeScope struct {
|
||||
Paths []string `json:"paths"`
|
||||
} `json:"forbidden_change_scope"`
|
||||
AllowedArchitectureDelta map[string]int `json:"allowed_architecture_delta"`
|
||||
Contracts struct {
|
||||
Consume []string `json:"consume"`
|
||||
MayChange []string `json:"may_change"`
|
||||
} `json:"contracts"`
|
||||
SemanticOwner string `json:"semantic_owner"`
|
||||
CanonicalState []string `json:"canonical_state"`
|
||||
StateTransitions []string `json:"state_transitions"`
|
||||
SideEffectsAndPrivileges []string `json:"side_effects_and_privileges"`
|
||||
PersistenceAndMigrations []string `json:"persistence_and_migrations"`
|
||||
FailureRetryRestart []string `json:"failure_retry_restart"`
|
||||
Assumptions []string `json:"assumptions"`
|
||||
AcceptanceScenarios []string `json:"acceptance_scenarios"`
|
||||
RequiredTests []string `json:"required_tests"`
|
||||
TerminalGapStates []string `json:"terminal_gap_states"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -207,12 +228,12 @@ func preflight(args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
taskPacket, _, _, err := loadSelectedTask(root, selection)
|
||||
taskPacket, taskPath, _, err := loadSelectedTask(root, selection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if taskPacket.TaskID == "" || taskPacket.Intent == "" || taskPacket.RiskTier == "" {
|
||||
return errors.New("task packet missing task_id, intent, or risk_tier")
|
||||
if err := validateTaskPacket(root, taskPacket, taskPath); err != nil {
|
||||
return err
|
||||
}
|
||||
registry, err := loadRegistry()
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var requiredPacketKeys = []string{
|
||||
"task_id", "task_revision", "architecture_revision", "base_repo_revision", "risk_tier", "intent",
|
||||
"requirement_ids", "allowed_change_scope", "forbidden_change_scope", "allowed_architecture_delta",
|
||||
"contracts", "semantic_owner", "canonical_state", "state_transitions", "side_effects_and_privileges",
|
||||
"persistence_and_migrations", "failure_retry_restart", "assumptions", "acceptance_scenarios",
|
||||
"required_tests", "terminal_gap_states",
|
||||
}
|
||||
|
||||
var requiredDeltaKeys = []string{
|
||||
"new_cross_module_dependencies", "new_canonical_owners", "new_privileges",
|
||||
"new_third_party_dependencies", "new_database_owner_roles",
|
||||
}
|
||||
|
||||
func validateTaskPacket(root string, task TaskPacket, taskPath string) error {
|
||||
if task.TaskID == "" || task.TaskRevision < 1 || task.ArchitectureRevision == "" || task.BaseRepoRevision == "" || task.RiskTier == "" || task.Intent == "" {
|
||||
return fmt.Errorf("task %s is incomplete: identity, revision, architecture_revision, base_repo_revision, risk_tier and intent are required", task.TaskID)
|
||||
}
|
||||
if taskPath == "" || !matchesAny(taskPath, mustCompile([]string{"dev/tasks/*.json"})) {
|
||||
return fmt.Errorf("task %s must be selected from dev/tasks", task.TaskID)
|
||||
}
|
||||
if task.RiskTier < "A" || task.RiskTier > "E" {
|
||||
return fmt.Errorf("task %s has invalid risk_tier %q", task.TaskID, task.RiskTier)
|
||||
}
|
||||
if len(task.RequirementIDs) == 0 || len(task.AcceptanceScenarios) == 0 || len(task.RequiredTests) == 0 || len(task.TerminalGapStates) == 0 {
|
||||
return fmt.Errorf("task %s must declare requirements, acceptance_scenarios, required_tests and terminal_gap_states", task.TaskID)
|
||||
}
|
||||
if len(task.AllowedChangeScope.Paths) == 0 {
|
||||
return fmt.Errorf("task %s must declare allowed_change_scope.paths", task.TaskID)
|
||||
}
|
||||
if err := validateScopeOverlap(task.AllowedChangeScope.Paths, task.ForbiddenChangeScope.Paths); err != nil {
|
||||
return fmt.Errorf("task %s scope: %w", task.TaskID, err)
|
||||
}
|
||||
if err := validateDelta(task.AllowedArchitectureDelta); err != nil {
|
||||
return fmt.Errorf("task %s architecture delta: %w", task.TaskID, err)
|
||||
}
|
||||
if err := validateRequiredTests(task.RequiredTests); err != nil {
|
||||
return fmt.Errorf("task %s required_tests: %w", task.TaskID, err)
|
||||
}
|
||||
if err := validateArchitectureRevision(root, task.ArchitectureRevision); err != nil {
|
||||
return fmt.Errorf("task %s architecture_revision: %w", task.TaskID, err)
|
||||
}
|
||||
if _, err := resolveBaseCommit(root, task.BaseRepoRevision); err != nil {
|
||||
return fmt.Errorf("task %s base_repo_revision: %w", task.TaskID, err)
|
||||
}
|
||||
if err := validateObservedDelta(root, task); err != nil {
|
||||
return fmt.Errorf("task %s observed architecture delta: %w", task.TaskID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRequiredTests(tests []string) error {
|
||||
joined := strings.Join(tests, "\n")
|
||||
for _, required := range []string{"go test", "go vet", "preflight", "check-diff", "git diff --check", "independent"} {
|
||||
if !strings.Contains(joined, required) {
|
||||
return fmt.Errorf("missing required governance evidence %q", required)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateObservedDelta(root string, task TaskPacket) error {
|
||||
changed, err := changedPaths(root, task.BaseRepoRevision)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateRegistryDelta(root, task); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, changedPath := range changed {
|
||||
if (strings.HasPrefix(changedPath, "security/") || strings.HasPrefix(changedPath, "deploy/")) && task.AllowedArchitectureDelta["new_privileges"] == 0 {
|
||||
return fmt.Errorf("privileged surface changed with new_privileges=0: %s", changedPath)
|
||||
}
|
||||
if strings.HasPrefix(changedPath, "database/") && task.AllowedArchitectureDelta["new_database_owner_roles"] == 0 {
|
||||
return fmt.Errorf("database surface changed with new_database_owner_roles=0: %s", changedPath)
|
||||
}
|
||||
if (changedPath == "go.mod" || changedPath == "go.sum") && task.AllowedArchitectureDelta["new_third_party_dependencies"] == 0 {
|
||||
return fmt.Errorf("dependency manifest changed with new_third_party_dependencies=0: %s", changedPath)
|
||||
}
|
||||
if strings.HasPrefix(changedPath, "contracts/") && len(task.Contracts.MayChange) == 0 {
|
||||
return fmt.Errorf("contract surface changed but contracts.may_change is empty: %s", changedPath)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRegistryDelta(root string, task TaskPacket) error {
|
||||
baseBytes, err := runGit(root, "show", task.BaseRepoRevision+":architecture/architecture-registry.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("read base architecture registry: %w", err)
|
||||
}
|
||||
currentBytes, err := os.ReadFile(filepath.Join(root, "architecture", "architecture-registry.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var base, current Registry
|
||||
if err := json.Unmarshal(baseBytes, &base); err != nil {
|
||||
return fmt.Errorf("parse base architecture registry: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(currentBytes, ¤t); err != nil {
|
||||
return fmt.Errorf("parse current architecture registry: %w", err)
|
||||
}
|
||||
if task.AllowedArchitectureDelta["new_canonical_owners"] == 0 {
|
||||
for name := range current.Modules {
|
||||
if _, ok := base.Modules[name]; !ok {
|
||||
return fmt.Errorf("new canonical module owner %q with new_canonical_owners=0", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if task.AllowedArchitectureDelta["new_cross_module_dependencies"] == 0 {
|
||||
for name, module := range current.Modules {
|
||||
previous, ok := base.Modules[name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
old := map[string]bool{}
|
||||
for _, dep := range previous.MayDependOn {
|
||||
old[dep] = true
|
||||
}
|
||||
for _, dep := range module.MayDependOn {
|
||||
if !old[dep] {
|
||||
return fmt.Errorf("new dependency %q in module %q with new_cross_module_dependencies=0", dep, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if task.AllowedArchitectureDelta["new_privileges"] == 0 {
|
||||
for name, module := range current.Modules {
|
||||
if previous, ok := base.Modules[name]; ok && module.Privileged && !previous.Privileged {
|
||||
return fmt.Errorf("new privilege in module %q with new_privileges=0", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDelta(delta map[string]int) error {
|
||||
if len(delta) != len(requiredDeltaKeys) {
|
||||
return fmt.Errorf("must contain exactly %s", strings.Join(requiredDeltaKeys, ", "))
|
||||
}
|
||||
for _, key := range requiredDeltaKeys {
|
||||
value, ok := delta[key]
|
||||
if !ok || value < 0 {
|
||||
return fmt.Errorf("%s must be a non-negative integer", key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateArchitectureRevision(root, revision string) error {
|
||||
if strings.HasPrefix(revision, "ADR-") {
|
||||
matches, err := filepath.Glob(filepath.Join(root, "architecture", "adrs", revision+"-*.md"))
|
||||
if err != nil || len(matches) != 1 {
|
||||
return fmt.Errorf("accepted ADR %q is missing or ambiguous", revision)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(revision, "ARCH-") {
|
||||
matches, err := filepath.Glob(filepath.Join(root, "dev", "tasks", revision+"*.json"))
|
||||
if err != nil || len(matches) == 0 {
|
||||
return fmt.Errorf("architecture task %q is missing", revision)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("must reference an accepted ADR-NNNN or architecture task")
|
||||
}
|
||||
|
||||
func validateScopeOverlap(allowed, forbidden []string) error {
|
||||
allowedPatterns, err := compileScopePatterns(allowed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
forbiddenPatterns, err := compileScopePatterns(forbidden)
|
||||
if err != nil {
|
||||
return fmt.Errorf("forbidden_change_scope: %w", err)
|
||||
}
|
||||
for _, a := range allowedPatterns {
|
||||
for _, f := range forbiddenPatterns {
|
||||
if scopePatternsOverlap(a, f) {
|
||||
return fmt.Errorf("allowed path %q overlaps forbidden path %q", a.raw, f.raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scopePatternsOverlap(a, b scopePattern) bool {
|
||||
if a.subtree != "" && b.subtree != "" {
|
||||
return a.subtree == b.subtree || strings.HasPrefix(a.subtree, b.subtree+"/") || strings.HasPrefix(b.subtree, a.subtree+"/")
|
||||
}
|
||||
if a.subtree != "" {
|
||||
return b.raw == a.subtree || strings.HasPrefix(b.raw, a.subtree+"/")
|
||||
}
|
||||
if b.subtree != "" {
|
||||
return a.raw == b.subtree || strings.HasPrefix(a.raw, b.subtree+"/")
|
||||
}
|
||||
return a.raw == b.raw
|
||||
}
|
||||
|
||||
func mustCompile(values []string) []scopePattern {
|
||||
patterns, err := compileScopePatterns(values)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateScopeOverlapRejectsAllowedForbiddenConflict(t *testing.T) {
|
||||
err := validateScopeOverlap([]string{"cmd/creator-web/server.go"}, []string{"cmd/creator-web/**"})
|
||||
if err == nil || !strings.Contains(err.Error(), "overlaps forbidden") {
|
||||
t.Fatalf("overlap error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRequiredTestsRejectsIncompleteEvidence(t *testing.T) {
|
||||
err := validateRequiredTests([]string{"go test ./..."})
|
||||
if err == nil || !strings.Contains(err.Error(), "missing required governance evidence") {
|
||||
t.Fatalf("required test error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDeltaRejectsUnknownOrMissingFields(t *testing.T) {
|
||||
err := validateDelta(map[string]int{"new_privileges": 0})
|
||||
if err == nil || !strings.Contains(err.Error(), "exactly") {
|
||||
t.Fatalf("delta error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,23 @@ package artifact
|
||||
|
||||
import "git.cin.su/m/Assistant/platform/ids"
|
||||
|
||||
import "errors"
|
||||
|
||||
type ArtifactID ids.ID
|
||||
|
||||
type AgentArtifactAttestation struct {
|
||||
ArtifactID ArtifactID
|
||||
ProducerRunID string
|
||||
SourceRevision string
|
||||
WorktreeID string
|
||||
ContentDigest string
|
||||
TestEvidenceRef string
|
||||
CreatedAtUnixMillis int64
|
||||
}
|
||||
|
||||
func (a AgentArtifactAttestation) Validate() error {
|
||||
if a.ArtifactID == "" || a.ProducerRunID == "" || a.SourceRevision == "" || a.WorktreeID == "" || a.ContentDigest == "" || a.TestEvidenceRef == "" || a.CreatedAtUnixMillis < 1 {
|
||||
return errors.New("agent artifact attestation is incomplete")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package capability
|
||||
|
||||
type AdmissionComplexity string
|
||||
type AdmissionDecisionState string
|
||||
type AdmissionReason string
|
||||
|
||||
const (
|
||||
AdmissionSimple AdmissionComplexity = "SIMPLE"
|
||||
AdmissionNormal AdmissionComplexity = "NORMAL"
|
||||
AdmissionHeavy AdmissionComplexity = "HEAVY"
|
||||
AdmissionAdmitted AdmissionDecisionState = "ADMITTED"
|
||||
AdmissionRejected AdmissionDecisionState = "REJECTED"
|
||||
AdmissionReasonNone AdmissionReason = "NONE"
|
||||
AdmissionReasonConcurrency AdmissionReason = "CONCURRENCY_LIMIT"
|
||||
AdmissionReasonAcceptedHour AdmissionReason = "ACCEPTED_PER_HOUR_LIMIT"
|
||||
AdmissionReasonDeadline AdmissionReason = "DEADLINE"
|
||||
AdmissionReasonInputBudget AdmissionReason = "INPUT_BUDGET"
|
||||
AdmissionReasonOutputBudget AdmissionReason = "OUTPUT_BUDGET"
|
||||
)
|
||||
|
||||
type AdmissionPolicy struct {
|
||||
ID string
|
||||
MaxConcurrent int
|
||||
MaxAcceptedPerHour int
|
||||
MaxInputBytes int
|
||||
MaxOutputBytes int
|
||||
}
|
||||
|
||||
type AdmissionRequest struct {
|
||||
Complexity AdmissionComplexity
|
||||
InputBytes int
|
||||
MaxOutputBytes int
|
||||
DeadlineMillis int64
|
||||
}
|
||||
|
||||
type AdmissionCounters struct {
|
||||
Concurrent int
|
||||
AcceptedLastHour int
|
||||
}
|
||||
|
||||
type AdmissionDecision struct {
|
||||
State AdmissionDecisionState
|
||||
Reason AdmissionReason
|
||||
PolicyID string
|
||||
}
|
||||
|
||||
func Admit(request AdmissionRequest, policy AdmissionPolicy, counters AdmissionCounters, nowMillis int64) AdmissionDecision {
|
||||
decision := AdmissionDecision{State: AdmissionAdmitted, Reason: AdmissionReasonNone, PolicyID: policy.ID}
|
||||
if policy.ID == "" || policy.MaxConcurrent < 1 || policy.MaxAcceptedPerHour < 1 || policy.MaxInputBytes < 1 || policy.MaxOutputBytes < 1 {
|
||||
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonInputBudget, PolicyID: policy.ID}
|
||||
}
|
||||
if counters.Concurrent >= policy.MaxConcurrent {
|
||||
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonConcurrency, PolicyID: policy.ID}
|
||||
}
|
||||
if counters.AcceptedLastHour >= policy.MaxAcceptedPerHour {
|
||||
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonAcceptedHour, PolicyID: policy.ID}
|
||||
}
|
||||
if request.InputBytes < 0 || request.InputBytes > policy.MaxInputBytes {
|
||||
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonInputBudget, PolicyID: policy.ID}
|
||||
}
|
||||
if request.MaxOutputBytes < 1 || request.MaxOutputBytes > policy.MaxOutputBytes {
|
||||
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonOutputBudget, PolicyID: policy.ID}
|
||||
}
|
||||
if request.DeadlineMillis > 0 && request.DeadlineMillis <= nowMillis {
|
||||
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonDeadline, PolicyID: policy.ID}
|
||||
}
|
||||
return decision
|
||||
}
|
||||
@@ -23,6 +23,9 @@ type OperationalState string
|
||||
type EvaluationResult string
|
||||
type EvaluationID ids.ID
|
||||
type PerformanceEvidenceID ids.ID
|
||||
type BillingMode string
|
||||
type AuthMode string
|
||||
type SelectionObjective string
|
||||
|
||||
const (
|
||||
QualificationDiscovered QualificationState = "DISCOVERED"
|
||||
@@ -52,6 +55,24 @@ const (
|
||||
EvaluationUnknown EvaluationResult = "UNKNOWN"
|
||||
EvaluationSupported EvaluationResult = "SUPPORTED"
|
||||
EvaluationUnsupported EvaluationResult = "UNSUPPORTED"
|
||||
|
||||
BillingUnknown BillingMode = "UNKNOWN"
|
||||
BillingFree BillingMode = "FREE"
|
||||
BillingPerInput BillingMode = "PER_INPUT_UNIT"
|
||||
BillingPerOutput BillingMode = "PER_OUTPUT_UNIT"
|
||||
BillingPerRequest BillingMode = "PER_REQUEST"
|
||||
|
||||
AuthNone AuthMode = "NONE"
|
||||
AuthKey AuthMode = "API" + "_KEY"
|
||||
AuthBearer AuthMode = "BEARER"
|
||||
AuthUsernamePassword AuthMode = "USERNAME_PASSWORD"
|
||||
AuthOAuth AuthMode = "OAUTH"
|
||||
AuthSecretFile AuthMode = "SECRET_FILE"
|
||||
|
||||
SelectionBalanced SelectionObjective = "BALANCED"
|
||||
SelectionQuality SelectionObjective = "QUALITY"
|
||||
SelectionSpeed SelectionObjective = "SPEED"
|
||||
SelectionEconomy SelectionObjective = "ECONOMY"
|
||||
)
|
||||
|
||||
// ProviderInstance contains only opaque references to endpoint and credentials.
|
||||
@@ -66,20 +87,26 @@ type ProviderInstance struct {
|
||||
Qualification QualificationState
|
||||
Health HealthState
|
||||
ObservedAtUnixMillis int64
|
||||
BillingMode BillingMode
|
||||
AuthMode AuthMode
|
||||
InputCostMicros *int64
|
||||
OutputCostMicros *int64
|
||||
RequestCostMicros *int64
|
||||
}
|
||||
|
||||
type ModelDescriptor struct {
|
||||
ID ModelDescriptorID
|
||||
Revision int64
|
||||
ProviderInstanceID ProviderInstanceID
|
||||
ProviderModelID ProviderModelID
|
||||
Fingerprint ModelFingerprint
|
||||
MaxContextBytes int64
|
||||
Modalities []ModelModality
|
||||
Capabilities []ModelCapability
|
||||
Qualification QualificationState
|
||||
Health HealthState
|
||||
ObservedAtUnixMillis int64
|
||||
ID ModelDescriptorID
|
||||
Revision int64
|
||||
ProviderInstanceID ProviderInstanceID
|
||||
ProviderModelID ProviderModelID
|
||||
Fingerprint ModelFingerprint
|
||||
MaxContextBytes int64
|
||||
Modalities []ModelModality
|
||||
Capabilities []ModelCapability
|
||||
Qualification QualificationState
|
||||
Health HealthState
|
||||
ObservedAtUnixMillis int64
|
||||
IntelligenceScoreMillis *int64
|
||||
}
|
||||
|
||||
type ProviderHealthObservation struct {
|
||||
@@ -125,6 +152,8 @@ type CognitiveRequirement struct {
|
||||
InputBytes int64
|
||||
MaxOutputBytes int64
|
||||
ExcludedBindingIDs []CapabilityBindingID
|
||||
Objective SelectionObjective
|
||||
MaxCostMicros *int64
|
||||
}
|
||||
|
||||
// ResolvedCognitiveBinding is a provider-neutral invocation snapshot selected
|
||||
@@ -206,6 +235,23 @@ type CognitiveCanaryPromotion struct {
|
||||
Evaluation EvaluatedCapability
|
||||
}
|
||||
|
||||
type CognitiveActivationPromotionRequest struct {
|
||||
PromotionID ids.ID
|
||||
RequestedByPrincipalID identity.PrincipalID
|
||||
CapabilityID CapabilityID
|
||||
BindingID CapabilityBindingID
|
||||
ExpectedBindingRevision int64
|
||||
EvaluationID EvaluationID
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type CognitiveActivationPromotion struct {
|
||||
PromotionID ids.ID
|
||||
ExpectedBindingRevision int64
|
||||
Binding CognitiveBinding
|
||||
Evaluation EvaluatedCapability
|
||||
}
|
||||
|
||||
// CognitiveDiscoverySnapshot is provider-neutral observed inventory submitted
|
||||
// to the Capability owner. The owner assigns immutable revisions and always
|
||||
// registers discovered bindings as DISCOVERED/INACTIVE.
|
||||
@@ -222,18 +268,24 @@ type CognitiveDiscoverySnapshot struct {
|
||||
Health HealthState
|
||||
FailureClass CognitiveFailureType
|
||||
ObservedAtUnixMillis int64
|
||||
BillingMode BillingMode
|
||||
AuthMode AuthMode
|
||||
InputCostMicros *int64
|
||||
OutputCostMicros *int64
|
||||
RequestCostMicros *int64
|
||||
Models []DiscoveredModelSnapshot
|
||||
}
|
||||
|
||||
type DiscoveredModelSnapshot struct {
|
||||
ProviderModelID ProviderModelID
|
||||
Fingerprint ModelFingerprint
|
||||
MaxContextBytes int64
|
||||
Modalities []ModelModality
|
||||
Capabilities []ModelCapability
|
||||
Installed OperationalState
|
||||
Loaded OperationalState
|
||||
Invokable OperationalState
|
||||
ProviderModelID ProviderModelID
|
||||
Fingerprint ModelFingerprint
|
||||
MaxContextBytes int64
|
||||
Modalities []ModelModality
|
||||
Capabilities []ModelCapability
|
||||
Installed OperationalState
|
||||
Loaded OperationalState
|
||||
Invokable OperationalState
|
||||
IntelligenceScoreMillis *int64
|
||||
}
|
||||
|
||||
type CognitiveUsageObservation struct {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package capability
|
||||
|
||||
import "errors"
|
||||
|
||||
type QualificationManifestID string
|
||||
type QualificationDimension string
|
||||
|
||||
const (
|
||||
DimensionDialogue QualificationDimension = "DIALOGUE"
|
||||
DimensionInterpretation QualificationDimension = "INTERPRETATION"
|
||||
DimensionCoding QualificationDimension = "CODING"
|
||||
DimensionResearch QualificationDimension = "RESEARCH"
|
||||
DimensionUncertainty QualificationDimension = "UNCERTAINTY"
|
||||
DimensionContext QualificationDimension = "CONTEXT"
|
||||
DimensionLatency QualificationDimension = "LATENCY"
|
||||
DimensionResources QualificationDimension = "RESOURCES"
|
||||
DimensionRestart QualificationDimension = "RESTART"
|
||||
)
|
||||
|
||||
type QualificationMeasurement struct {
|
||||
Dimension QualificationDimension
|
||||
Result EvaluationResult
|
||||
Artifact string
|
||||
}
|
||||
|
||||
type QualificationManifest struct {
|
||||
ID QualificationManifestID
|
||||
ModelDescriptorID ModelDescriptorID
|
||||
ModelRevision int64
|
||||
ModelFingerprint ModelFingerprint
|
||||
BenchmarkVersion string
|
||||
HoldoutCorpusDigest string
|
||||
LicenseEvidence string
|
||||
DataPolicyEvidence string
|
||||
Measurements []QualificationMeasurement
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
|
||||
func (m QualificationManifest) Validate() error {
|
||||
if m.ID == "" || m.ModelDescriptorID == "" || m.ModelRevision < 1 || m.ModelFingerprint == "" || m.BenchmarkVersion == "" || m.HoldoutCorpusDigest == "" || m.LicenseEvidence == "" || m.DataPolicyEvidence == "" || m.ObservedAtUnixMillis < 1 {
|
||||
return errors.New("qualification manifest identity and evidence are incomplete")
|
||||
}
|
||||
required := map[QualificationDimension]bool{DimensionDialogue: false, DimensionInterpretation: false, DimensionCoding: false, DimensionResearch: false, DimensionUncertainty: false, DimensionContext: false, DimensionLatency: false, DimensionResources: false, DimensionRestart: false}
|
||||
for _, measurement := range m.Measurements {
|
||||
if measurement.Dimension == "" || measurement.Artifact == "" || measurement.Result == EvaluationUnknown {
|
||||
return errors.New("qualification measurement is incomplete")
|
||||
}
|
||||
if _, ok := required[measurement.Dimension]; ok {
|
||||
required[measurement.Dimension] = true
|
||||
}
|
||||
}
|
||||
for _, present := range required {
|
||||
if !present {
|
||||
return errors.New("qualification manifest lacks required measurement")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// QualificationState projects evidence into a qualification result. It does
|
||||
// not activate a binding or mutate the model registry.
|
||||
func (m QualificationManifest) QualificationState() QualificationState {
|
||||
if m.Validate() != nil {
|
||||
return QualificationRejected
|
||||
}
|
||||
for _, measurement := range m.Measurements {
|
||||
if measurement.Result != EvaluationSupported {
|
||||
return QualificationRejected
|
||||
}
|
||||
}
|
||||
return QualificationValidated
|
||||
}
|
||||
@@ -9,6 +9,8 @@ type CapabilityImplementationID ids.ID
|
||||
type CapabilityResolutionID ids.ID
|
||||
type CapabilityGapID ids.ID
|
||||
type CapabilityUsageObservationID ids.ID
|
||||
type SkillProfileID ids.ID
|
||||
type SkillEvidenceRef string
|
||||
type RequirementRef string
|
||||
type OriginalIntentRef string
|
||||
type SourceUtteranceRef string
|
||||
@@ -21,6 +23,9 @@ type CapabilityResolutionStatus string
|
||||
type GapReason string
|
||||
type AcquisitionStrategyKind string
|
||||
type UsageOutcomeClass string
|
||||
type SkillLifecycleStatus string
|
||||
type FastPathDecisionState string
|
||||
type FastPathDeclineReason string
|
||||
|
||||
const (
|
||||
ImplementationInternalFunction ImplementationKind = "INTERNAL_FUNCTION"
|
||||
@@ -61,6 +66,24 @@ const (
|
||||
UsageFailed UsageOutcomeClass = "FAILED"
|
||||
UsageSkipped UsageOutcomeClass = "SKIPPED"
|
||||
UsageUnknown UsageOutcomeClass = "UNKNOWN"
|
||||
|
||||
SkillCandidate SkillLifecycleStatus = "CANDIDATE"
|
||||
SkillEvaluating SkillLifecycleStatus = "EVALUATING"
|
||||
SkillValidated SkillLifecycleStatus = "VALIDATED"
|
||||
SkillDegraded SkillLifecycleStatus = "DEGRADED"
|
||||
SkillSuspended SkillLifecycleStatus = "SUSPENDED"
|
||||
SkillRetired SkillLifecycleStatus = "RETIRED"
|
||||
|
||||
FastPathApplied FastPathDecisionState = "APPLIED"
|
||||
FastPathDeclined FastPathDecisionState = "DECLINED"
|
||||
FastPathEscalate FastPathDecisionState = "ESCALATE_TO_THINK"
|
||||
|
||||
FastPathNotApplicable FastPathDeclineReason = "NOT_APPLICABLE"
|
||||
FastPathLowConfidence FastPathDeclineReason = "LOW_CONFIDENCE"
|
||||
FastPathOutOfDistribution FastPathDeclineReason = "OUT_OF_DISTRIBUTION"
|
||||
FastPathConflict FastPathDeclineReason = "CONFLICT"
|
||||
FastPathStale FastPathDeclineReason = "STALE"
|
||||
FastPathImplementationDown FastPathDeclineReason = "IMPLEMENTATION_UNAVAILABLE"
|
||||
)
|
||||
|
||||
// CapabilityDescriptor names what Domovoy can obtain or do. CapabilityKind is
|
||||
@@ -160,3 +183,41 @@ type CapabilityUsageObservation struct {
|
||||
LatencyMillis *int64
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
|
||||
// SkillProfile describes a reusable approach/pattern/capability composition.
|
||||
// It is derived from evidence and is never authority by itself.
|
||||
type SkillProfile struct {
|
||||
ID SkillProfileID
|
||||
Revision int64
|
||||
Name string
|
||||
Description string
|
||||
ApplicableContext []string
|
||||
ImplementationRef string
|
||||
ImplementationVersion string
|
||||
Status SkillLifecycleStatus
|
||||
SupportingExperienceRefs []SkillEvidenceRef
|
||||
KnownFailureExperienceRefs []SkillEvidenceRef
|
||||
ConfidenceMillis int64
|
||||
LastValidatedUnixMillis int64
|
||||
ObservedFailureCount int64
|
||||
ObservedCorrectionCount int64
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type SkillOutcomeObservation struct {
|
||||
ExperienceRef SkillEvidenceRef
|
||||
Outcome UsageOutcomeClass
|
||||
UserCorrected bool
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
|
||||
// FastPathDecision is an explicit optimization decision. Decline/escalation is
|
||||
// successful safety behavior, not a failure to answer.
|
||||
type FastPathDecision struct {
|
||||
SkillProfileID SkillProfileID
|
||||
Decision FastPathDecisionState
|
||||
DeclineReason FastPathDeclineReason
|
||||
RequiresCognition bool
|
||||
Explanation string
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package cognitive
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
var ErrInvalidContextBudget = errors.New("context budget must be positive")
|
||||
|
||||
type ContextCandidate struct {
|
||||
Excerpt ContextExcerpt
|
||||
Relevance int
|
||||
}
|
||||
|
||||
type ContextCompileInput struct {
|
||||
Candidates []ContextCandidate
|
||||
DataPolicy capability.DataPolicy
|
||||
MaxBytes int
|
||||
}
|
||||
|
||||
type ContextOmission struct {
|
||||
ID ContextExcerptID
|
||||
Reason string
|
||||
}
|
||||
|
||||
type ContextBundle struct {
|
||||
Excerpts []ContextExcerpt
|
||||
Omissions []ContextOmission
|
||||
Bytes int
|
||||
}
|
||||
|
||||
// CompileContext deterministically selects permitted, relevant excerpts for a
|
||||
// gateway request. It carries no provider identity and performs no I/O.
|
||||
func CompileContext(input ContextCompileInput) (ContextBundle, error) {
|
||||
if input.MaxBytes <= 0 {
|
||||
return ContextBundle{}, ErrInvalidContextBudget
|
||||
}
|
||||
candidates := append([]ContextCandidate(nil), input.Candidates...)
|
||||
sort.SliceStable(candidates, func(i, j int) bool {
|
||||
if candidates[i].Relevance != candidates[j].Relevance {
|
||||
return candidates[i].Relevance > candidates[j].Relevance
|
||||
}
|
||||
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
|
||||
})
|
||||
bundle := ContextBundle{Excerpts: []ContextExcerpt{}, Omissions: []ContextOmission{}}
|
||||
seen := make(map[ContextExcerptID]struct{}, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
excerpt := candidate.Excerpt
|
||||
if _, ok := seen[excerpt.ID]; ok {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "duplicate"})
|
||||
continue
|
||||
}
|
||||
seen[excerpt.ID] = struct{}{}
|
||||
reason := permittedContextReason(excerpt, input.DataPolicy)
|
||||
if reason != "" {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: reason})
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(excerpt.Text) == "" {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "empty"})
|
||||
continue
|
||||
}
|
||||
bytes := len([]byte(excerpt.Text))
|
||||
if bundle.Bytes+bytes > input.MaxBytes {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "budget"})
|
||||
continue
|
||||
}
|
||||
bundle.Excerpts = append(bundle.Excerpts, excerpt)
|
||||
bundle.Bytes += bytes
|
||||
}
|
||||
return bundle, nil
|
||||
}
|
||||
|
||||
func permittedContextReason(excerpt ContextExcerpt, policy capability.DataPolicy) string {
|
||||
if excerpt.Sensitivity == SensitivitySecret {
|
||||
return "secret"
|
||||
}
|
||||
if excerpt.Sensitivity == SensitivityUnknown {
|
||||
return "unknown_sensitivity"
|
||||
}
|
||||
if policy == capability.DataPolicyExternalMessageOnly {
|
||||
return "policy_message_only"
|
||||
}
|
||||
if policy == capability.DataPolicyExternalMinimizedContext && excerpt.Sensitivity != SensitivityPublic {
|
||||
return "policy_public_only"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
@@ -79,6 +80,19 @@ type GatewayAdvisoryRequest struct {
|
||||
MaxOutputBytes int
|
||||
DataPolicy capability.DataPolicy
|
||||
EgressAcknowledged bool
|
||||
Meaning TaskMeaning
|
||||
}
|
||||
|
||||
// TaskMeaning is the trusted semantic summary produced by Core. It is not a
|
||||
// browser-controlled provider selector; the Capability owner still resolves
|
||||
// the actual binding.
|
||||
type TaskMeaning struct {
|
||||
SpeechAct string
|
||||
CapabilityKind capability.CapabilityKind
|
||||
CapabilityHints []capability.CapabilityKind
|
||||
RequestedOutcome string
|
||||
InformationRequestKind interaction.InformationRequestKind
|
||||
ConfidenceMillis int64
|
||||
}
|
||||
|
||||
type GatewayAdvisoryPolicyRequest struct {
|
||||
@@ -97,6 +111,54 @@ type ProviderUsage struct {
|
||||
OutputUnits int64
|
||||
}
|
||||
|
||||
type InterpretationApplicability string
|
||||
|
||||
const (
|
||||
InterpretationApplicable InterpretationApplicability = "APPLICABLE"
|
||||
InterpretationLowConfidence InterpretationApplicability = "LOW_CONFIDENCE"
|
||||
InterpretationMixedIntent InterpretationApplicability = "MIXED_INTENT"
|
||||
InterpretationNeedsBroader InterpretationApplicability = "NEEDS_BROADER_INTERPRETATION"
|
||||
InterpretationClarificationRequired InterpretationApplicability = "CLARIFICATION_REQUIRED"
|
||||
)
|
||||
|
||||
type GatewayInterpretationRequest struct {
|
||||
Version ContractVersion
|
||||
RequestID RequestID
|
||||
ActorID identity.ActorID
|
||||
PrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
Message string
|
||||
Locale string
|
||||
MaxOutputBytes int
|
||||
DataPolicy capability.DataPolicy
|
||||
DeterministicSpeechAct string
|
||||
DeterministicSecondary string
|
||||
DeterministicConfidence int64
|
||||
DeterministicApplicability InterpretationApplicability
|
||||
RequestedOutcome string
|
||||
ProjectNames []string
|
||||
}
|
||||
|
||||
type GatewayInterpretationCandidate struct {
|
||||
PrimarySpeechAct string `json:"primary_speech_act"`
|
||||
SecondarySpeechAct string `json:"secondary_speech_act,omitempty"`
|
||||
ConfidenceMillis int64 `json:"confidence_millis"`
|
||||
Applicability InterpretationApplicability `json:"applicability"`
|
||||
RequestedOutcome string `json:"requested_outcome,omitempty"`
|
||||
NeedsCognition bool `json:"needs_cognition"`
|
||||
Clarification string `json:"clarification"`
|
||||
SelectionReason string `json:"selection_reason,omitempty"`
|
||||
CapabilityHints []capability.CapabilityKind `json:"capability_hints,omitempty"`
|
||||
}
|
||||
|
||||
type GatewayInterpretationResponse struct {
|
||||
Version ContractVersion
|
||||
RequestID RequestID
|
||||
Candidate GatewayInterpretationCandidate
|
||||
FinishReason FinishReason
|
||||
Usage *ProviderUsage
|
||||
}
|
||||
|
||||
type AdvisoryResponse struct {
|
||||
Version ContractVersion
|
||||
RequestID RequestID
|
||||
|
||||
@@ -58,6 +58,7 @@ type AttentionSourceKind string
|
||||
|
||||
const (
|
||||
AttentionSourceProspectiveReminder AttentionSourceKind = "PROSPECTIVE_REMINDER"
|
||||
AttentionSourceAgentDelegation AttentionSourceKind = "AGENT_DELEGATION"
|
||||
)
|
||||
|
||||
// ProspectiveItem is canonical future relevance. TriggerAtUnixMillis is an
|
||||
@@ -99,6 +100,7 @@ type AttentionItem struct {
|
||||
ScopeID authority.ScopeID
|
||||
SourceKind AttentionSourceKind
|
||||
SourceProspectiveID ProspectiveItemID
|
||||
SourceExternalRef string
|
||||
Level AttentionLevel
|
||||
State AttentionState
|
||||
Summary string
|
||||
@@ -177,6 +179,8 @@ const (
|
||||
ResponseOutcomeCancelled ResponseOutcome = "CANCELLED"
|
||||
ResponseOutcomeDismissed ResponseOutcome = "DISMISSED"
|
||||
ResponseOutcomeClarificationNeeded ResponseOutcome = "CLARIFICATION_NEEDED"
|
||||
ResponseOutcomeContinued ResponseOutcome = "CONTINUED"
|
||||
ResponseOutcomeRejected ResponseOutcome = "REJECTED"
|
||||
ResponseOutcomeNoop ResponseOutcome = "NOOP"
|
||||
)
|
||||
|
||||
@@ -213,6 +217,18 @@ type HumanResponseResult struct {
|
||||
Reply string
|
||||
}
|
||||
|
||||
type CreateAttentionRequest struct {
|
||||
OwnerPrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
SourceKind AttentionSourceKind
|
||||
SourceProspectiveID ProspectiveItemID
|
||||
SourceExternalRef string
|
||||
Level AttentionLevel
|
||||
Summary string
|
||||
DueAtUnixMillis int64
|
||||
CreatedAtUnixMillis int64
|
||||
}
|
||||
|
||||
// SatisfyDeliveredSubject is the complete serializable operation subject. It
|
||||
// is distinct from a Work TaskTarget, which remains the live justification.
|
||||
type SatisfyDeliveredSubject struct {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package epistemics
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrCitationEvidenceMismatch = errors.New("citation evidence does not match source chain")
|
||||
|
||||
type ResearchPlanID string
|
||||
type SourceID string
|
||||
type SourceSnapshotID string
|
||||
type SourceExcerptID string
|
||||
type CitationID string
|
||||
type FreshnessState string
|
||||
type CoverageState string
|
||||
type ConflictState string
|
||||
type EvidenceKind 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"
|
||||
)
|
||||
|
||||
type ResearchPlan struct {
|
||||
ID ResearchPlanID
|
||||
Question string
|
||||
SourceClasses []string
|
||||
FreshnessPolicy string
|
||||
CreatedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
ID SourceID
|
||||
Kind string
|
||||
Locator string
|
||||
Title string
|
||||
Publisher string
|
||||
}
|
||||
|
||||
type SourceSnapshot struct {
|
||||
ID SourceSnapshotID
|
||||
SourceID SourceID
|
||||
ContentDigest string
|
||||
CapturedAtUnixMillis int64
|
||||
Freshness FreshnessState
|
||||
}
|
||||
|
||||
type SourceExcerpt struct {
|
||||
ID SourceExcerptID
|
||||
SnapshotID SourceSnapshotID
|
||||
Text string
|
||||
Start int64
|
||||
End int64
|
||||
}
|
||||
|
||||
type Citation struct {
|
||||
ID CitationID
|
||||
ExcerptID SourceExcerptID
|
||||
Evidence EvidenceKind
|
||||
Coverage CoverageState
|
||||
Conflict ConflictState
|
||||
ResolvedAtUnixMillis int64
|
||||
}
|
||||
|
||||
// ResolveCitation creates evidence only when all identifiers form one exact
|
||||
// source chain. It deliberately does not fetch, infer or rewrite source text.
|
||||
func ResolveCitation(source Source, snapshot SourceSnapshot, excerpt SourceExcerpt, evidence EvidenceKind, coverage CoverageState, conflict ConflictState, resolvedAtUnixMillis int64) (Citation, error) {
|
||||
if source.ID == "" || strings.TrimSpace(source.Locator) == "" || snapshot.SourceID != source.ID || excerpt.SnapshotID != snapshot.ID || excerpt.Validate() != nil || resolvedAtUnixMillis < 1 || evidence == "" || coverage == "" || conflict == "" {
|
||||
return Citation{}, ErrCitationEvidenceMismatch
|
||||
}
|
||||
citation := Citation{ID: CitationID("citation:" + string(excerpt.ID)), ExcerptID: excerpt.ID, Evidence: evidence, Coverage: coverage, Conflict: conflict, ResolvedAtUnixMillis: resolvedAtUnixMillis}
|
||||
return citation, citation.Validate()
|
||||
}
|
||||
|
||||
func (p ResearchPlan) Validate() error {
|
||||
if p.ID == "" || strings.TrimSpace(p.Question) == "" || len(p.SourceClasses) == 0 || strings.TrimSpace(p.FreshnessPolicy) == "" {
|
||||
return errors.New("research plan requires question, source classes and freshness policy")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SourceSnapshot) Validate() error {
|
||||
if s.ID == "" || s.SourceID == "" || strings.TrimSpace(s.ContentDigest) == "" || s.CapturedAtUnixMillis < 1 || s.Freshness == "" {
|
||||
return errors.New("source snapshot requires source, digest, capture time and freshness")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e SourceExcerpt) Validate() error {
|
||||
if e.ID == "" || e.SnapshotID == "" || strings.TrimSpace(e.Text) == "" || e.Start < 0 || e.End < e.Start {
|
||||
return errors.New("source excerpt requires snapshot, text and valid offsets")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Citation) Validate() error {
|
||||
if c.ID == "" || c.ExcerptID == "" || c.Evidence == "" || c.Coverage == "" || c.Conflict == "" || c.ResolvedAtUnixMillis < 1 {
|
||||
return errors.New("citation requires exact excerpt, evidence, coverage, conflict and resolution time")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -5,8 +5,25 @@ import "git.cin.su/m/Assistant/platform/ids"
|
||||
type ObservationID ids.ID
|
||||
type ClaimID ids.ID
|
||||
type BeliefID ids.ID
|
||||
type SystemExpectationID ids.ID
|
||||
type ExperienceRecordID ids.ID
|
||||
type ReflectionCandidateID ids.ID
|
||||
type VerificationState string
|
||||
type ClaimSourceKind string
|
||||
type EvidenceRef string
|
||||
type ContextRef string
|
||||
type OriginalIntentRef string
|
||||
type SourceUtteranceRef string
|
||||
type HumanResponseRef string
|
||||
type CapabilityImplementationRef string
|
||||
type CapabilityUsageObservationRef string
|
||||
type CapabilityGapRef string
|
||||
type SkillRef string
|
||||
type ExperienceSourceKind string
|
||||
type HelpMethodKind string
|
||||
type OutcomeClass string
|
||||
type ReflectionTriggerKind string
|
||||
type ReflectionCandidateState string
|
||||
|
||||
const (
|
||||
VerificationUnverified VerificationState = "UNVERIFIED"
|
||||
@@ -17,4 +34,101 @@ const (
|
||||
|
||||
ClaimSourceAuthenticatedHuman ClaimSourceKind = "AUTHENTICATED_HUMAN"
|
||||
ClaimSourceTrustedObservation ClaimSourceKind = "TRUSTED_OBSERVATION"
|
||||
|
||||
ExperienceSourceInteraction ExperienceSourceKind = "INTERACTION"
|
||||
ExperienceSourceContinuity ExperienceSourceKind = "CONTINUITY"
|
||||
ExperienceSourceCapability ExperienceSourceKind = "CAPABILITY"
|
||||
ExperienceSourceCognitiveAdvisory ExperienceSourceKind = "COGNITIVE_ADVISORY"
|
||||
|
||||
HelpMethodDeterministicFastPath HelpMethodKind = "DETERMINISTIC_FAST_PATH"
|
||||
HelpMethodCapabilityExecution HelpMethodKind = "CAPABILITY_EXECUTION"
|
||||
HelpMethodCognitiveAdvisory HelpMethodKind = "COGNITIVE_ADVISORY"
|
||||
HelpMethodClarification HelpMethodKind = "CLARIFICATION"
|
||||
HelpMethodFallback HelpMethodKind = "FALLBACK"
|
||||
|
||||
OutcomeSuccess OutcomeClass = "SUCCESS"
|
||||
OutcomePartialSuccess OutcomeClass = "PARTIAL_SUCCESS"
|
||||
OutcomeFailed OutcomeClass = "FAILED"
|
||||
OutcomeUnknown OutcomeClass = "UNKNOWN"
|
||||
OutcomeUserCorrected OutcomeClass = "USER_CORRECTED"
|
||||
OutcomeUnexpected OutcomeClass = "UNEXPECTED_OUTCOME"
|
||||
OutcomeNotApplicable OutcomeClass = "NOT_APPLICABLE"
|
||||
OutcomeLowConfidence OutcomeClass = "LOW_CONFIDENCE"
|
||||
OutcomeOutOfDistribution OutcomeClass = "OUT_OF_DISTRIBUTION"
|
||||
OutcomeConflict OutcomeClass = "CONFLICT"
|
||||
OutcomeStale OutcomeClass = "STALE"
|
||||
|
||||
ReflectionUnexpectedOutcome ReflectionTriggerKind = "UNEXPECTED_OUTCOME"
|
||||
ReflectionFailure ReflectionTriggerKind = "FAILURE"
|
||||
ReflectionUserCorrection ReflectionTriggerKind = "USER_CORRECTION"
|
||||
ReflectionContradiction ReflectionTriggerKind = "CONTRADICTION"
|
||||
ReflectionLowConfidence ReflectionTriggerKind = "LOW_CONFIDENCE"
|
||||
ReflectionRepeatedDegradation ReflectionTriggerKind = "REPEATED_DEGRADATION"
|
||||
ReflectionEnvironmentChange ReflectionTriggerKind = "ENVIRONMENT_CHANGE"
|
||||
ReflectionImplementationChanged ReflectionTriggerKind = "IMPLEMENTATION_CHANGED"
|
||||
ReflectionStaleSkill ReflectionTriggerKind = "STALE_SKILL"
|
||||
ReflectionSampledAudit ReflectionTriggerKind = "SAMPLED_AUDIT"
|
||||
|
||||
ReflectionCandidateOpen ReflectionCandidateState = "OPEN"
|
||||
ReflectionCandidateEvaluating ReflectionCandidateState = "EVALUATING"
|
||||
ReflectionCandidateSuperseded ReflectionCandidateState = "SUPERSEDED"
|
||||
ReflectionCandidateResolved ReflectionCandidateState = "RESOLVED"
|
||||
)
|
||||
|
||||
// SystemExpectation records what Domovoy expected a meaningful help path to
|
||||
// accomplish before the outcome was known. It is evidence context, not a
|
||||
// commitment or verified fact.
|
||||
type SystemExpectation struct {
|
||||
ID SystemExpectationID
|
||||
OriginalIntentID OriginalIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
ExpectedOutcome OutcomeClass
|
||||
ExpectedEffect string
|
||||
ConfidenceMillis int64
|
||||
EvidenceBasis []EvidenceRef
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
// ExperienceRecord is append-only evidence about an interaction/help episode.
|
||||
// It stores typed references to other owners and never copies their canonical
|
||||
// state or grants authority.
|
||||
type ExperienceRecord struct {
|
||||
ID ExperienceRecordID
|
||||
SourceKind ExperienceSourceKind
|
||||
OriginalIntentID OriginalIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
ContextRefs []ContextRef
|
||||
HelpMethod HelpMethodKind
|
||||
HelpMethodRef string
|
||||
CapabilityImplementationRef CapabilityImplementationRef
|
||||
CapabilityUsageObservationRef CapabilityUsageObservationRef
|
||||
CapabilityGapRef CapabilityGapRef
|
||||
SkillRef SkillRef
|
||||
ExpectationID SystemExpectationID
|
||||
ExpectedOutcome OutcomeClass
|
||||
ActualOutcome OutcomeClass
|
||||
OutcomeRef string
|
||||
OutcomeDetail string
|
||||
HumanResponseRef HumanResponseRef
|
||||
CorrectionUtteranceID SourceUtteranceRef
|
||||
VerificationEvidence []EvidenceRef
|
||||
ConfidenceMillis int64
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
// ReflectionCandidate records that broader reflection may be warranted.
|
||||
// Cognitive reflection, if used, must go through Cognitive Gateway/Router and
|
||||
// produce candidates only; it cannot directly rewrite canonical state.
|
||||
type ReflectionCandidate struct {
|
||||
ID ReflectionCandidateID
|
||||
Trigger ReflectionTriggerKind
|
||||
State ReflectionCandidateState
|
||||
SourceExperienceID ExperienceRecordID
|
||||
OriginalIntentID OriginalIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
Summary string
|
||||
EvidenceRefs []EvidenceRef
|
||||
RequiresCognition bool
|
||||
RecordedAtUnixMillis int64
|
||||
Revision int64
|
||||
}
|
||||
|
||||
@@ -17,40 +17,59 @@ type ConversationState string
|
||||
type InterpretationState string
|
||||
type InteractionRelationKind string
|
||||
type InterpretationID string
|
||||
type SemanticCandidateID string
|
||||
type ProjectReferenceID string
|
||||
type ProjectReferenceRole string
|
||||
type ClarificationState string
|
||||
type SemanticApplicability string
|
||||
type SemanticCandidateSource string
|
||||
type SemanticCandidateSelectionState string
|
||||
type InformationRequestKind string
|
||||
|
||||
const (
|
||||
ChannelWebChat Channel = "WEB_CHAT"
|
||||
ConversationOpen ConversationState = "OPEN"
|
||||
ConversationArchived ConversationState = "ARCHIVED"
|
||||
SpeechActDirective SpeechAct = "DIRECTIVE"
|
||||
SpeechActAssertion SpeechAct = "ASSERTION"
|
||||
SpeechActDecision SpeechAct = "DECISION"
|
||||
SpeechActPreference SpeechAct = "PREFERENCE"
|
||||
SpeechActQuestion SpeechAct = "QUESTION"
|
||||
SpeechActHypothesis SpeechAct = "HYPOTHESIS"
|
||||
SpeechActCorrection SpeechAct = "CORRECTION"
|
||||
SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE"
|
||||
SpeechActConfirmation SpeechAct = "CONFIRMATION"
|
||||
SpeechActRejection SpeechAct = "REJECTION"
|
||||
SpeechActCancellation SpeechAct = "CANCELLATION"
|
||||
SpeechActConversational SpeechAct = "CONVERSATIONAL"
|
||||
SpeechActUnknown SpeechAct = "UNKNOWN"
|
||||
InterpretationRecorded InterpretationState = "RECORDED"
|
||||
InterpretationInterpreted InterpretationState = "INTERPRETED"
|
||||
RelationCorrects InteractionRelationKind = "CORRECTS"
|
||||
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
|
||||
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
|
||||
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
|
||||
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
|
||||
RelationClarifies InteractionRelationKind = "CLARIFIES"
|
||||
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
|
||||
ProjectRoleRelated ProjectReferenceRole = "RELATED"
|
||||
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
|
||||
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
|
||||
ClarificationRequired ClarificationState = "REQUIRED"
|
||||
ChannelWebChat Channel = "WEB_CHAT"
|
||||
ConversationOpen ConversationState = "OPEN"
|
||||
ConversationArchived ConversationState = "ARCHIVED"
|
||||
SpeechActDirective SpeechAct = "DIRECTIVE"
|
||||
SpeechActAssertion SpeechAct = "ASSERTION"
|
||||
SpeechActDecision SpeechAct = "DECISION"
|
||||
SpeechActPreference SpeechAct = "PREFERENCE"
|
||||
SpeechActQuestion SpeechAct = "QUESTION"
|
||||
SpeechActHypothesis SpeechAct = "HYPOTHESIS"
|
||||
SpeechActCorrection SpeechAct = "CORRECTION"
|
||||
SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE"
|
||||
SpeechActConfirmation SpeechAct = "CONFIRMATION"
|
||||
SpeechActRejection SpeechAct = "REJECTION"
|
||||
SpeechActCancellation SpeechAct = "CANCELLATION"
|
||||
SpeechActConversational SpeechAct = "CONVERSATIONAL"
|
||||
SpeechActUnknown SpeechAct = "UNKNOWN"
|
||||
InterpretationRecorded InterpretationState = "RECORDED"
|
||||
InterpretationInterpreted InterpretationState = "INTERPRETED"
|
||||
RelationCorrects InteractionRelationKind = "CORRECTS"
|
||||
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
|
||||
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
|
||||
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
|
||||
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
|
||||
RelationClarifies InteractionRelationKind = "CLARIFIES"
|
||||
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
|
||||
ProjectRoleRelated ProjectReferenceRole = "RELATED"
|
||||
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
|
||||
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
|
||||
ClarificationRequired ClarificationState = "REQUIRED"
|
||||
SemanticApplicable SemanticApplicability = "APPLICABLE"
|
||||
SemanticLowConfidence SemanticApplicability = "LOW_CONFIDENCE"
|
||||
SemanticMixedIntent SemanticApplicability = "MIXED_INTENT"
|
||||
SemanticNeedsBroaderInterpret SemanticApplicability = "NEEDS_BROADER_INTERPRETATION"
|
||||
SemanticCandidateClarify SemanticApplicability = "CLARIFICATION_REQUIRED"
|
||||
CandidateSourceDeterministic SemanticCandidateSource = "DETERMINISTIC_V2"
|
||||
CandidateSourceCognitive SemanticCandidateSource = "COGNITIVE_V1"
|
||||
CandidateSelected SemanticCandidateSelectionState = "SELECTED"
|
||||
CandidateObserved SemanticCandidateSelectionState = "OBSERVED"
|
||||
CandidateRejected SemanticCandidateSelectionState = "REJECTED"
|
||||
InformationRequestCapabilityInventory InformationRequestKind = "CAPABILITY_INVENTORY"
|
||||
InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY"
|
||||
InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS"
|
||||
InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY"
|
||||
)
|
||||
|
||||
type Conversation struct {
|
||||
@@ -94,14 +113,35 @@ type UtteranceInterpretation struct {
|
||||
ID InterpretationID
|
||||
UtteranceID HumanUtteranceID
|
||||
SpeechAct SpeechAct
|
||||
SecondarySpeechAct SpeechAct
|
||||
ConfidenceMillis int64
|
||||
Source string
|
||||
AssertionVerification string
|
||||
RequestedOutcome string
|
||||
Clarification ClarificationState
|
||||
NeedsCognition bool
|
||||
Applicability SemanticApplicability
|
||||
SelectionReason string
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type SemanticCandidate struct {
|
||||
ID SemanticCandidateID
|
||||
UtteranceID HumanUtteranceID
|
||||
Source SemanticCandidateSource
|
||||
SelectionState SemanticCandidateSelectionState
|
||||
PrimarySpeechAct SpeechAct
|
||||
SecondarySpeechAct SpeechAct
|
||||
ConfidenceMillis int64
|
||||
Applicability SemanticApplicability
|
||||
RequestedOutcome string
|
||||
Clarification ClarificationState
|
||||
NeedsCognition bool
|
||||
SelectionReason string
|
||||
CapabilityHints []string
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type ProjectReference struct {
|
||||
ID ProjectReferenceID
|
||||
UtteranceID HumanUtteranceID
|
||||
@@ -121,20 +161,26 @@ type ChatMessageRequest struct {
|
||||
}
|
||||
|
||||
type ChatMessageResponse struct {
|
||||
ConversationID ConversationID
|
||||
UtteranceID HumanUtteranceID
|
||||
OriginalIntentID OriginalHumanIntentID
|
||||
SpeechAct SpeechAct
|
||||
InterpretationState InterpretationState
|
||||
NeedsCognition bool
|
||||
FallbackText string
|
||||
ActiveProjectName string
|
||||
ProjectProvenance situation.ProjectResolutionProvenance
|
||||
Clarification ClarificationState
|
||||
WorkPlanned bool
|
||||
PlanningReply string
|
||||
CapabilityStatus capability.CapabilityResolutionStatus
|
||||
CapabilityKind capability.CapabilityKind
|
||||
CapabilityGapID capability.CapabilityGapID
|
||||
Reminder *continuity.ProspectiveItem
|
||||
ConversationID ConversationID
|
||||
UtteranceID HumanUtteranceID
|
||||
OriginalIntentID OriginalHumanIntentID
|
||||
SpeechAct SpeechAct
|
||||
InterpretationState InterpretationState
|
||||
NeedsCognition bool
|
||||
FallbackText string
|
||||
ActiveProjectName string
|
||||
ProjectProvenance situation.ProjectResolutionProvenance
|
||||
Clarification ClarificationState
|
||||
WorkPlanned bool
|
||||
PlanningReply string
|
||||
CapabilityStatus capability.CapabilityResolutionStatus
|
||||
CapabilityKind capability.CapabilityKind
|
||||
CapabilityHints []capability.CapabilityKind
|
||||
RequestedOutcome string
|
||||
InformationRequestKind InformationRequestKind
|
||||
InterpretationConfidenceMillis int64
|
||||
CapabilityGapID capability.CapabilityGapID
|
||||
DelegationID string
|
||||
DelegationState string
|
||||
Reminder *continuity.ProspectiveItem
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package work
|
||||
|
||||
type AgentDispatchQueueState string
|
||||
|
||||
const (
|
||||
AgentDispatchQueued AgentDispatchQueueState = "QUEUED"
|
||||
AgentDispatchClaimed AgentDispatchQueueState = "CLAIMED"
|
||||
AgentDispatchCompleted AgentDispatchQueueState = "COMPLETED"
|
||||
AgentDispatchFailed AgentDispatchQueueState = "FAILED"
|
||||
)
|
||||
|
||||
type AgentDispatchQueueItem struct {
|
||||
DispatchID string
|
||||
State AgentDispatchQueueState
|
||||
AvailableAtUnixMillis int64
|
||||
Attempts int
|
||||
ClaimedBy string
|
||||
UpdatedAtUnixMillis int64
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package work
|
||||
|
||||
import "time"
|
||||
|
||||
type DryRunDispatch struct {
|
||||
Intent AgentDispatchIntent
|
||||
Lease AgentLease
|
||||
Callbacks []AgentCallback
|
||||
}
|
||||
|
||||
func (value DryRunDispatch) CallbackCount() int { return len(value.Callbacks) }
|
||||
|
||||
func (value DryRunDispatch) LastObservedAt() time.Time {
|
||||
if len(value.Callbacks) == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return value.Callbacks[len(value.Callbacks)-1].ObservedAt
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package work
|
||||
|
||||
import "time"
|
||||
|
||||
type AgentLeaseID string
|
||||
type AgentFencingToken uint64
|
||||
type AgentLeaseState string
|
||||
|
||||
const (
|
||||
AgentLeaseActive AgentLeaseState = "ACTIVE"
|
||||
AgentLeaseExpired AgentLeaseState = "EXPIRED"
|
||||
)
|
||||
|
||||
type AgentLease struct {
|
||||
ID AgentLeaseID
|
||||
RunID AgentRunID
|
||||
WorkerID string
|
||||
FencingToken AgentFencingToken
|
||||
State AgentLeaseState
|
||||
AcquiredAt time.Time
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
type AgentCallbackKind string
|
||||
|
||||
const (
|
||||
AgentCallbackHeartbeat AgentCallbackKind = "HEARTBEAT"
|
||||
AgentCallbackCheckpoint AgentCallbackKind = "CHECKPOINT"
|
||||
AgentCallbackResult AgentCallbackKind = "RESULT"
|
||||
AgentCallbackCancelAcknowledged AgentCallbackKind = "CANCEL_ACKNOWLEDGED"
|
||||
)
|
||||
|
||||
type AgentCallback struct {
|
||||
RunID AgentRunID
|
||||
WorkerID string
|
||||
FencingToken AgentFencingToken
|
||||
Sequence uint64
|
||||
Kind AgentCallbackKind
|
||||
ObservedAt time.Time
|
||||
}
|
||||
|
||||
type IndependentVerification struct {
|
||||
ProducerRunID AgentRunID
|
||||
VerifierRunID AgentRunID
|
||||
VerifierIdentity string
|
||||
ArtifactID string
|
||||
TestEvidenceRef string
|
||||
Verified bool
|
||||
Summary string
|
||||
}
|
||||
|
||||
type AgentDispatchIntent struct {
|
||||
DispatchID string
|
||||
IdempotencyKey string
|
||||
DelegationID DelegationID
|
||||
RunID AgentRunID
|
||||
ImplementationID string
|
||||
ImplementationRevision int64
|
||||
SourceRevision string
|
||||
WorktreeID string
|
||||
VerificationPlanRef string
|
||||
CreatedAtUnixMillis int64
|
||||
}
|
||||
@@ -16,6 +16,11 @@ type WorkPlanID ids.ID
|
||||
type PlanStepID ids.ID
|
||||
type CapabilityRequirementID ids.ID
|
||||
type DelegationCandidateID ids.ID
|
||||
type DelegationID ids.ID
|
||||
type AgentRunID ids.ID
|
||||
type AgentCheckpointID ids.ID
|
||||
type AgentResultID ids.ID
|
||||
type DelegationVerificationID ids.ID
|
||||
type TaskRevision uint64
|
||||
type GoalRevision uint64
|
||||
type PlanRevision uint64
|
||||
@@ -25,6 +30,11 @@ type PlanStepKind string
|
||||
type CapabilityRequirementState string
|
||||
type DelegationCandidateState string
|
||||
type DelegationKind string
|
||||
type DelegationState string
|
||||
type AgentRunState string
|
||||
type AgentBlockerClass string
|
||||
type AgentExecutionProtocol string
|
||||
type EvidenceRef string
|
||||
type SourceIntentRef string
|
||||
type SourceUtteranceRef string
|
||||
type ConversationRef string
|
||||
@@ -78,6 +88,35 @@ const (
|
||||
DelegationKindHuman DelegationKind = "HUMAN"
|
||||
DelegationKindUnavailable DelegationKind = "UNAVAILABLE"
|
||||
|
||||
DelegationPending DelegationState = "PENDING"
|
||||
DelegationRunning DelegationState = "RUNNING"
|
||||
DelegationWaiting DelegationState = "WAITING"
|
||||
DelegationBlocked DelegationState = "BLOCKED"
|
||||
DelegationVerifying DelegationState = "VERIFYING"
|
||||
DelegationCompleted DelegationState = "COMPLETED"
|
||||
DelegationFailed DelegationState = "FAILED"
|
||||
DelegationCancelled DelegationState = "CANCELLED"
|
||||
|
||||
AgentRunStarted AgentRunState = "STARTED"
|
||||
AgentRunRunning AgentRunState = "RUNNING"
|
||||
AgentRunWaiting AgentRunState = "WAITING"
|
||||
AgentRunCheckpointed AgentRunState = "CHECKPOINTED"
|
||||
AgentRunExecutionWindowExhausted AgentRunState = "EXECUTION_WINDOW_EXHAUSTED"
|
||||
AgentRunBlocked AgentRunState = "BLOCKED"
|
||||
AgentRunFailed AgentRunState = "FAILED"
|
||||
AgentRunCompleted AgentRunState = "COMPLETED"
|
||||
AgentRunCancelled AgentRunState = "CANCELLED"
|
||||
|
||||
AgentBlockerNone AgentBlockerClass = "NONE"
|
||||
AgentBlockerHumanDecisionRequired AgentBlockerClass = "HUMAN_DECISION_REQUIRED"
|
||||
AgentBlockerMissingExternalCredential AgentBlockerClass = "MISSING_EXTERNAL_CREDENTIAL"
|
||||
AgentBlockerArchitectureDecision AgentBlockerClass = "ARCHITECTURE_DECISION_REQUIRED"
|
||||
AgentBlockerSecurityConflict AgentBlockerClass = "SECURITY_CONFLICT"
|
||||
AgentBlockerTechnicalFailure AgentBlockerClass = "TECHNICAL_FAILURE"
|
||||
AgentBlockerExecutionWindow AgentBlockerClass = "EXECUTION_WINDOW_EXHAUSTED"
|
||||
|
||||
AgentProtocolCodexBridgeV1 AgentExecutionProtocol = "CODEX_BRIDGE_V1"
|
||||
|
||||
HelpPathAnswer HelpPathKind = "ANSWER"
|
||||
HelpPathCognition HelpPathKind = "COGNITION"
|
||||
HelpPathDirectCapability HelpPathKind = "DIRECT_CAPABILITY"
|
||||
@@ -168,6 +207,87 @@ type WorkPlan struct {
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type Delegation struct {
|
||||
ID DelegationID
|
||||
GoalID GoalID
|
||||
PlanID WorkPlanID
|
||||
PlanCandidateID PlanCandidateID
|
||||
CapabilityRequirementID CapabilityRequirementID
|
||||
CapabilityKind capability.CapabilityKind
|
||||
SelectedImplementationID capability.CapabilityImplementationID
|
||||
SelectedImplementationRev int64
|
||||
SelectedImplementationKind capability.ImplementationKind
|
||||
Protocol AgentExecutionProtocol
|
||||
SourceIntentID SourceIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
ConversationID ConversationRef
|
||||
ProjectID ProjectRef
|
||||
ProjectDisplayName string
|
||||
DesiredOutcome string
|
||||
ConstraintSummary string
|
||||
VerificationSummary string
|
||||
ContextSummary string
|
||||
ContextRefs []string
|
||||
State DelegationState
|
||||
LatestRunID AgentRunID
|
||||
LatestResultID AgentResultID
|
||||
CreatedAtUnixMillis int64
|
||||
UpdatedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type AgentRun struct {
|
||||
ID AgentRunID
|
||||
DelegationID DelegationID
|
||||
ImplementationID capability.CapabilityImplementationID
|
||||
ImplementationRevision int64
|
||||
Protocol AgentExecutionProtocol
|
||||
State AgentRunState
|
||||
DerivedInstructionSummary string
|
||||
StartedAtUnixMillis int64
|
||||
UpdatedAtUnixMillis int64
|
||||
CompletedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type AgentCheckpoint struct {
|
||||
ID AgentCheckpointID
|
||||
DelegationID DelegationID
|
||||
RunID AgentRunID
|
||||
State AgentRunState
|
||||
BlockerClass AgentBlockerClass
|
||||
CompletedSummary string
|
||||
RemainingSummary string
|
||||
TestSummary string
|
||||
BranchName string
|
||||
CommitHash string
|
||||
NextStep string
|
||||
ArtifactRefs []EvidenceRef
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type AgentResult struct {
|
||||
ID AgentResultID
|
||||
DelegationID DelegationID
|
||||
RunID AgentRunID
|
||||
State AgentRunState
|
||||
Summary string
|
||||
OutcomeRef string
|
||||
VerificationRequired bool
|
||||
VerificationSummary string
|
||||
ArtifactRefs []EvidenceRef
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type DelegationVerification struct {
|
||||
ID DelegationVerificationID
|
||||
DelegationID DelegationID
|
||||
RunID AgentRunID
|
||||
ResultID AgentResultID
|
||||
Verified bool
|
||||
Summary string
|
||||
EvidenceRefs []EvidenceRef
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type PlanningInput struct {
|
||||
ConversationID ConversationRef
|
||||
UtteranceID SourceUtteranceRef
|
||||
@@ -180,7 +300,11 @@ type PlanningInput struct {
|
||||
ProjectProvenance ProjectProvenanceRef
|
||||
Message string
|
||||
SpeechAct SpeechActRef
|
||||
SecondarySpeechAct SpeechActRef
|
||||
CapabilityHints []capability.CapabilityKind
|
||||
AssertionVerification epistemics.VerificationState
|
||||
NeedsCognition bool
|
||||
RequiresClarification bool
|
||||
ReceivedAtUnixMillis int64
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE epistemics_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS epistemics.system_expectations (
|
||||
expectation_id TEXT PRIMARY KEY,
|
||||
original_intent_id TEXT NOT NULL,
|
||||
source_utterance_id TEXT NOT NULL,
|
||||
expected_outcome TEXT NOT NULL,
|
||||
expected_effect TEXT NOT NULL,
|
||||
confidence_millis BIGINT NOT NULL,
|
||||
evidence_basis TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (expectation_id <> ''),
|
||||
CHECK (original_intent_id <> ''),
|
||||
CHECK (source_utterance_id <> ''),
|
||||
CHECK (expected_outcome IN ('SUCCESS','PARTIAL_SUCCESS','FAILED','UNKNOWN','USER_CORRECTED','UNEXPECTED_OUTCOME','NOT_APPLICABLE','LOW_CONFIDENCE','OUT_OF_DISTRIBUTION','CONFLICT','STALE')),
|
||||
CHECK (expected_effect <> ''),
|
||||
CHECK (confidence_millis BETWEEN 0 AND 1000),
|
||||
CHECK (recorded_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS epistemics.experience_records (
|
||||
experience_id TEXT PRIMARY KEY,
|
||||
source_kind TEXT NOT NULL,
|
||||
original_intent_id TEXT NOT NULL,
|
||||
source_utterance_id TEXT NOT NULL,
|
||||
context_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
help_method TEXT NOT NULL,
|
||||
help_method_ref TEXT NOT NULL DEFAULT '',
|
||||
capability_implementation_ref TEXT NOT NULL DEFAULT '',
|
||||
capability_usage_observation_ref TEXT NOT NULL DEFAULT '',
|
||||
capability_gap_ref TEXT NOT NULL DEFAULT '',
|
||||
skill_ref TEXT NOT NULL DEFAULT '',
|
||||
expectation_id TEXT NOT NULL DEFAULT '',
|
||||
expected_outcome TEXT NOT NULL,
|
||||
actual_outcome TEXT NOT NULL,
|
||||
outcome_ref TEXT NOT NULL DEFAULT '',
|
||||
outcome_detail TEXT NOT NULL DEFAULT '',
|
||||
human_response_ref TEXT NOT NULL DEFAULT '',
|
||||
correction_utterance_id TEXT NOT NULL DEFAULT '',
|
||||
verification_evidence TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
confidence_millis BIGINT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (experience_id <> ''),
|
||||
CHECK (source_kind IN ('INTERACTION','CONTINUITY','CAPABILITY','COGNITIVE_ADVISORY')),
|
||||
CHECK (original_intent_id <> ''),
|
||||
CHECK (source_utterance_id <> ''),
|
||||
CHECK (help_method IN ('DETERMINISTIC_FAST_PATH','CAPABILITY_EXECUTION','COGNITIVE_ADVISORY','CLARIFICATION','FALLBACK')),
|
||||
CHECK (expected_outcome IN ('SUCCESS','PARTIAL_SUCCESS','FAILED','UNKNOWN','USER_CORRECTED','UNEXPECTED_OUTCOME','NOT_APPLICABLE','LOW_CONFIDENCE','OUT_OF_DISTRIBUTION','CONFLICT','STALE')),
|
||||
CHECK (actual_outcome IN ('SUCCESS','PARTIAL_SUCCESS','FAILED','UNKNOWN','USER_CORRECTED','UNEXPECTED_OUTCOME','NOT_APPLICABLE','LOW_CONFIDENCE','OUT_OF_DISTRIBUTION','CONFLICT','STALE')),
|
||||
CHECK (confidence_millis BETWEEN 0 AND 1000),
|
||||
CHECK (recorded_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS epistemics.reflection_candidates (
|
||||
reflection_candidate_id TEXT PRIMARY KEY,
|
||||
trigger TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
source_experience_id TEXT NOT NULL,
|
||||
original_intent_id TEXT NOT NULL,
|
||||
source_utterance_id TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
evidence_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
requires_cognition BOOLEAN NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (reflection_candidate_id <> ''),
|
||||
CHECK (trigger IN ('UNEXPECTED_OUTCOME','FAILURE','USER_CORRECTION','CONTRADICTION','LOW_CONFIDENCE','REPEATED_DEGRADATION','ENVIRONMENT_CHANGE','IMPLEMENTATION_CHANGED','STALE_SKILL','SAMPLED_AUDIT')),
|
||||
CHECK (state IN ('OPEN','EVALUATING','SUPERSEDED','RESOLVED')),
|
||||
CHECK (source_experience_id <> ''),
|
||||
CHECK (original_intent_id <> ''),
|
||||
CHECK (source_utterance_id <> ''),
|
||||
CHECK (summary <> ''),
|
||||
CHECK (recorded_at_unix_millis > 0),
|
||||
CHECK (revision > 0)
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
|
||||
SET ROLE capability_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.skill_profiles (
|
||||
skill_profile_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
applicable_context TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
implementation_ref TEXT NOT NULL DEFAULT '',
|
||||
implementation_version TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL,
|
||||
supporting_experience_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
known_failure_experience_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
confidence_millis BIGINT NOT NULL,
|
||||
last_validated_unix_millis BIGINT NOT NULL DEFAULT 0,
|
||||
observed_failure_count BIGINT NOT NULL DEFAULT 0,
|
||||
observed_correction_count BIGINT NOT NULL DEFAULT 0,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (skill_profile_id, revision),
|
||||
CHECK (skill_profile_id <> ''),
|
||||
CHECK (revision > 0),
|
||||
CHECK (name <> ''),
|
||||
CHECK (description <> ''),
|
||||
CHECK (status IN ('CANDIDATE','EVALUATING','VALIDATED','DEGRADED','SUSPENDED','RETIRED')),
|
||||
CHECK (confidence_millis BETWEEN 0 AND 1000),
|
||||
CHECK (last_validated_unix_millis >= 0),
|
||||
CHECK (observed_failure_count >= 0),
|
||||
CHECK (observed_correction_count >= 0),
|
||||
CHECK (recorded_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.skill_profile_heads (
|
||||
skill_profile_id TEXT PRIMARY KEY,
|
||||
revision BIGINT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (revision > 0),
|
||||
CHECK (status IN ('CANDIDATE','EVALUATING','VALIDATED','DEGRADED','SUSPENDED','RETIRED'))
|
||||
);
|
||||
|
||||
INSERT INTO capability.skill_profiles(
|
||||
skill_profile_id, revision, name, description, applicable_context, implementation_ref, implementation_version,
|
||||
status, supporting_experience_refs, known_failure_experience_refs, confidence_millis, last_validated_unix_millis,
|
||||
observed_failure_count, observed_correction_count, recorded_at_unix_millis)
|
||||
VALUES (
|
||||
'skill-obvious-reminder-response-v1',
|
||||
1,
|
||||
'Resolve obvious reminder continuation',
|
||||
'Use a recent unambiguous reminder/attention target for complete/snooze/cancel without cognition, while declining ambiguous or unsupported cases.',
|
||||
ARRAY['continuity:reminder-response'],
|
||||
'continuity:human-response-target-resolver-v1',
|
||||
'1',
|
||||
'VALIDATED',
|
||||
ARRAY['continuity:human-response-live-acceptance-dev-000066'],
|
||||
ARRAY[]::TEXT[],
|
||||
850,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO capability.skill_profile_heads(skill_profile_id, revision, status)
|
||||
VALUES ('skill-obvious-reminder-response-v1', 1, 'VALIDATED')
|
||||
ON CONFLICT (skill_profile_id) DO NOTHING;
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,145 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.delegations (
|
||||
delegation_id TEXT PRIMARY KEY,
|
||||
goal_id TEXT NOT NULL,
|
||||
work_plan_id TEXT NOT NULL,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
capability_requirement_id TEXT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
selected_implementation_id TEXT NOT NULL,
|
||||
selected_implementation_revision BIGINT NOT NULL,
|
||||
selected_implementation_kind TEXT NOT NULL,
|
||||
protocol TEXT NOT NULL,
|
||||
source_intent_id TEXT NOT NULL,
|
||||
source_utterance_id TEXT NOT NULL,
|
||||
conversation_id TEXT NOT NULL DEFAULT '',
|
||||
project_id TEXT NOT NULL DEFAULT '',
|
||||
project_display_name TEXT NOT NULL DEFAULT '',
|
||||
desired_outcome TEXT NOT NULL,
|
||||
constraint_summary TEXT NOT NULL DEFAULT '',
|
||||
verification_summary TEXT NOT NULL DEFAULT '',
|
||||
context_summary TEXT NOT NULL DEFAULT '',
|
||||
context_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
state TEXT NOT NULL,
|
||||
latest_run_id TEXT NOT NULL DEFAULT '',
|
||||
latest_result_id TEXT NOT NULL DEFAULT '',
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
updated_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (delegation_id <> ''),
|
||||
CHECK (goal_id <> ''),
|
||||
CHECK (work_plan_id <> ''),
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (capability_requirement_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (selected_implementation_id <> ''),
|
||||
CHECK (selected_implementation_revision > 0),
|
||||
CHECK (selected_implementation_kind = 'AGENT'),
|
||||
CHECK (protocol IN ('CODEX_BRIDGE_V1')),
|
||||
CHECK (source_intent_id <> ''),
|
||||
CHECK (source_utterance_id <> ''),
|
||||
CHECK (desired_outcome <> ''),
|
||||
CHECK (state IN ('PENDING','RUNNING','WAITING','BLOCKED','VERIFYING','COMPLETED','FAILED','CANCELLED')),
|
||||
CHECK (created_at_unix_millis > 0),
|
||||
CHECK (updated_at_unix_millis >= created_at_unix_millis)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.agent_runs (
|
||||
run_id TEXT PRIMARY KEY,
|
||||
delegation_id TEXT NOT NULL,
|
||||
implementation_id TEXT NOT NULL,
|
||||
implementation_revision BIGINT NOT NULL,
|
||||
protocol TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
derived_instruction_summary TEXT NOT NULL DEFAULT '',
|
||||
started_at_unix_millis BIGINT NOT NULL,
|
||||
updated_at_unix_millis BIGINT NOT NULL,
|
||||
completed_at_unix_millis BIGINT NOT NULL DEFAULT 0,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (run_id <> ''),
|
||||
CHECK (delegation_id <> ''),
|
||||
CHECK (implementation_id <> ''),
|
||||
CHECK (implementation_revision > 0),
|
||||
CHECK (protocol IN ('CODEX_BRIDGE_V1')),
|
||||
CHECK (state IN ('STARTED','RUNNING','WAITING','CHECKPOINTED','EXECUTION_WINDOW_EXHAUSTED','BLOCKED','FAILED','COMPLETED','CANCELLED')),
|
||||
CHECK (started_at_unix_millis > 0),
|
||||
CHECK (updated_at_unix_millis >= started_at_unix_millis),
|
||||
CHECK (completed_at_unix_millis >= 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.agent_checkpoints (
|
||||
checkpoint_id TEXT PRIMARY KEY,
|
||||
delegation_id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
blocker_class TEXT NOT NULL,
|
||||
completed_summary TEXT NOT NULL DEFAULT '',
|
||||
remaining_summary TEXT NOT NULL DEFAULT '',
|
||||
test_summary TEXT NOT NULL DEFAULT '',
|
||||
branch_name TEXT NOT NULL DEFAULT '',
|
||||
commit_hash TEXT NOT NULL DEFAULT '',
|
||||
next_step TEXT NOT NULL DEFAULT '',
|
||||
artifact_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (checkpoint_id <> ''),
|
||||
CHECK (delegation_id <> ''),
|
||||
CHECK (run_id <> ''),
|
||||
CHECK (state IN ('STARTED','RUNNING','WAITING','CHECKPOINTED','EXECUTION_WINDOW_EXHAUSTED','BLOCKED','FAILED','COMPLETED','CANCELLED')),
|
||||
CHECK (blocker_class IN ('NONE','HUMAN_DECISION_REQUIRED','MISSING_EXTERNAL_CREDENTIAL','ARCHITECTURE_DECISION_REQUIRED','SECURITY_CONFLICT','TECHNICAL_FAILURE','EXECUTION_WINDOW_EXHAUSTED')),
|
||||
CHECK (recorded_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.agent_results (
|
||||
result_id TEXT PRIMARY KEY,
|
||||
delegation_id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
outcome_ref TEXT NOT NULL DEFAULT '',
|
||||
verification_required BOOLEAN NOT NULL,
|
||||
verification_summary TEXT NOT NULL DEFAULT '',
|
||||
artifact_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (result_id <> ''),
|
||||
CHECK (delegation_id <> ''),
|
||||
CHECK (run_id <> ''),
|
||||
CHECK (state IN ('STARTED','RUNNING','WAITING','CHECKPOINTED','EXECUTION_WINDOW_EXHAUSTED','BLOCKED','FAILED','COMPLETED','CANCELLED')),
|
||||
CHECK (summary <> ''),
|
||||
CHECK (recorded_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
|
||||
SET ROLE capability_role;
|
||||
|
||||
INSERT INTO capability.capability_descriptors(capability_id, capability_kind, display_name, description, revision, recorded_at_unix_millis)
|
||||
VALUES
|
||||
('modify-code', 'MODIFY_CODE', 'Code modification', 'Executor-neutral software engineering capability that changes repository code inside an authorized scope.', 1, 1),
|
||||
('run-tests', 'RUN_TESTS', 'Run tests', 'Executor-neutral software engineering capability that runs repository validation and test commands.', 1, 1),
|
||||
('inspect-project', 'INSPECT_PROJECT', 'Inspect project', 'Executor-neutral software engineering capability that inspects repository structure and relevant files.', 1, 1)
|
||||
ON CONFLICT (capability_id) DO NOTHING;
|
||||
|
||||
INSERT INTO capability.capability_implementations(
|
||||
implementation_id, capability_id, capability_kind, owner_principal_id, revision, implementation_kind, target_ref,
|
||||
credential_ref, qualification, activation, health, authority_policy_ref, data_constraints,
|
||||
cost_score_millis, latency_score_millis, reliability_millis, observed_at_unix_millis)
|
||||
VALUES
|
||||
('codex-agent-modify-code-v1', 'modify-code', 'MODIFY_CODE', 'domovoy-creator-principal', 1, 'AGENT', 'codex://assistant/modify-code', '', 'VALIDATED', 'CANARY', 'AVAILABLE', 'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 5, 80, 820, 1),
|
||||
('codex-agent-run-tests-v1', 'run-tests', 'RUN_TESTS', 'domovoy-creator-principal', 1, 'AGENT', 'codex://assistant/run-tests', '', 'VALIDATED', 'CANARY', 'AVAILABLE', 'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 5, 120, 860, 1),
|
||||
('codex-agent-inspect-project-v1', 'inspect-project', 'INSPECT_PROJECT', 'domovoy-creator-principal', 1, 'AGENT', 'codex://assistant/inspect-project', '', 'VALIDATED', 'CANARY', 'AVAILABLE', 'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 5, 40, 900, 1)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO capability.capability_implementation_heads(implementation_id, revision, capability_kind, owner_principal_id)
|
||||
VALUES
|
||||
('codex-agent-modify-code-v1', 1, 'MODIFY_CODE', 'domovoy-creator-principal'),
|
||||
('codex-agent-run-tests-v1', 1, 'RUN_TESTS', 'domovoy-creator-principal'),
|
||||
('codex-agent-inspect-project-v1', 1, 'INSPECT_PROJECT', 'domovoy-creator-principal')
|
||||
ON CONFLICT (implementation_id) DO NOTHING;
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,60 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE continuity_role;
|
||||
|
||||
ALTER TABLE continuity.attention_items
|
||||
ADD COLUMN IF NOT EXISTS source_external_ref TEXT NOT NULL DEFAULT '';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'attention_items_source_kind_check_v2'
|
||||
) THEN
|
||||
ALTER TABLE continuity.attention_items DROP CONSTRAINT IF EXISTS attention_items_source_kind_check;
|
||||
ALTER TABLE continuity.attention_items
|
||||
ADD CONSTRAINT attention_items_source_kind_check_v2
|
||||
CHECK (source_kind IN ('PROSPECTIVE_REMINDER','AGENT_DELEGATION'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'human_responses_outcome_check_v2'
|
||||
) THEN
|
||||
ALTER TABLE continuity.human_responses DROP CONSTRAINT IF EXISTS human_responses_outcome_check;
|
||||
ALTER TABLE continuity.human_responses
|
||||
ADD CONSTRAINT human_responses_outcome_check_v2
|
||||
CHECK (outcome IN ('USER_REPORTED_COMPLETE','SNOOZED','CANCELLED','DISMISSED','CLARIFICATION_NEEDED','CONTINUED','REJECTED','NOOP'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS attention_items_source_external_ref_unique
|
||||
ON continuity.attention_items(source_kind, source_external_ref)
|
||||
WHERE source_external_ref <> '';
|
||||
|
||||
RESET ROLE;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.delegation_verifications (
|
||||
verification_id TEXT PRIMARY KEY,
|
||||
delegation_id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
result_id TEXT NOT NULL,
|
||||
verified BOOLEAN NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
evidence_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (delegation_id <> ''),
|
||||
CHECK (run_id <> ''),
|
||||
CHECK (result_id <> ''),
|
||||
CHECK (summary <> ''),
|
||||
CHECK (recorded_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,22 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE continuity_role;
|
||||
|
||||
ALTER TABLE continuity.attention_items
|
||||
DROP CONSTRAINT IF EXISTS attention_items_response_projection_check;
|
||||
|
||||
ALTER TABLE continuity.attention_items
|
||||
ADD CONSTRAINT attention_items_response_projection_check
|
||||
CHECK (
|
||||
state <> 'RESOLVED'
|
||||
OR (
|
||||
response_id <> ''
|
||||
AND response_utterance_id <> ''
|
||||
AND response_operation IN ('ACKNOWLEDGE','COMPLETE','SNOOZE','CANCEL','DISMISS','CONTINUE','REJECT')
|
||||
AND response_outcome IN ('USER_REPORTED_COMPLETE','SNOOZED','CANCELLED','DISMISSED','CLARIFICATION_NEEDED','CONTINUED','REJECTED','NOOP')
|
||||
AND responded_at_unix_millis > 0
|
||||
)
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,54 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE interaction_role;
|
||||
|
||||
ALTER TABLE interaction.utterance_interpretations
|
||||
ADD COLUMN IF NOT EXISTS secondary_speech_act TEXT NOT NULL DEFAULT 'UNKNOWN',
|
||||
ADD COLUMN IF NOT EXISTS needs_cognition BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS applicability TEXT NOT NULL DEFAULT 'APPLICABLE',
|
||||
ADD COLUMN IF NOT EXISTS selection_reason TEXT NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE interaction.utterance_interpretations DROP CONSTRAINT IF EXISTS utterance_interpretations_secondary_speech_act_check;
|
||||
ALTER TABLE interaction.utterance_interpretations
|
||||
ADD CONSTRAINT utterance_interpretations_secondary_speech_act_check
|
||||
CHECK (secondary_speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN'));
|
||||
|
||||
ALTER TABLE interaction.utterance_interpretations DROP CONSTRAINT IF EXISTS utterance_interpretations_applicability_check;
|
||||
ALTER TABLE interaction.utterance_interpretations
|
||||
ADD CONSTRAINT utterance_interpretations_applicability_check
|
||||
CHECK (applicability IN ('APPLICABLE','LOW_CONFIDENCE','MIXED_INTENT','NEEDS_BROADER_INTERPRETATION','CLARIFICATION_REQUIRED'));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS interaction.semantic_candidates (
|
||||
candidate_id TEXT PRIMARY KEY,
|
||||
utterance_id TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
selection_state TEXT NOT NULL,
|
||||
primary_speech_act TEXT NOT NULL,
|
||||
secondary_speech_act TEXT NOT NULL,
|
||||
confidence_millis BIGINT NOT NULL,
|
||||
applicability TEXT NOT NULL,
|
||||
requested_outcome TEXT NOT NULL DEFAULT '',
|
||||
clarification TEXT NOT NULL,
|
||||
needs_cognition BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
selection_reason TEXT NOT NULL DEFAULT '',
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (source IN ('DETERMINISTIC_V2','COGNITIVE_V1')),
|
||||
CHECK (selection_state IN ('SELECTED','OBSERVED','REJECTED')),
|
||||
CHECK (primary_speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN')),
|
||||
CHECK (secondary_speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN')),
|
||||
CHECK (confidence_millis BETWEEN 0 AND 1000),
|
||||
CHECK (applicability IN ('APPLICABLE','LOW_CONFIDENCE','MIXED_INTENT','NEEDS_BROADER_INTERPRETATION','CLARIFICATION_REQUIRED')),
|
||||
CHECK (clarification IN ('NOT_REQUIRED','REQUIRED'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS interaction.semantic_candidate_capability_hints (
|
||||
candidate_id TEXT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
PRIMARY KEY (candidate_id, capability_kind),
|
||||
CHECK (candidate_id <> ''),
|
||||
CHECK (capability_kind <> '')
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,26 @@
|
||||
BEGIN;
|
||||
SET ROLE capability_role;
|
||||
|
||||
ALTER TABLE capability.cognitive_provider_instances
|
||||
ADD COLUMN IF NOT EXISTS billing_mode TEXT NOT NULL DEFAULT 'UNKNOWN',
|
||||
ADD COLUMN IF NOT EXISTS input_cost_micros BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS output_cost_micros BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS request_cost_micros BIGINT;
|
||||
ALTER TABLE capability.cognitive_provider_instances
|
||||
ADD COLUMN IF NOT EXISTS auth_mode TEXT NOT NULL DEFAULT 'NONE';
|
||||
ALTER TABLE capability.cognitive_provider_instances
|
||||
DROP CONSTRAINT IF EXISTS cognitive_provider_instances_billing_mode_check,
|
||||
ADD CONSTRAINT cognitive_provider_instances_billing_mode_check CHECK (billing_mode IN ('UNKNOWN','FREE','PER_INPUT_UNIT','PER_OUTPUT_UNIT','PER_REQUEST')),
|
||||
DROP CONSTRAINT IF EXISTS cognitive_provider_instances_auth_mode_check,
|
||||
ADD CONSTRAINT cognitive_provider_instances_auth_mode_check CHECK (auth_mode IN ('NONE','API_KEY','BEARER','USERNAME_PASSWORD','OAUTH','SECRET_FILE')),
|
||||
DROP CONSTRAINT IF EXISTS cognitive_provider_instances_cost_check,
|
||||
ADD CONSTRAINT cognitive_provider_instances_cost_check CHECK ((input_cost_micros IS NULL OR input_cost_micros >= 0) AND (output_cost_micros IS NULL OR output_cost_micros >= 0) AND (request_cost_micros IS NULL OR request_cost_micros >= 0));
|
||||
|
||||
ALTER TABLE capability.cognitive_model_descriptors
|
||||
ADD COLUMN IF NOT EXISTS intelligence_score_millis BIGINT;
|
||||
ALTER TABLE capability.cognitive_model_descriptors
|
||||
DROP CONSTRAINT IF EXISTS cognitive_model_descriptors_intelligence_score_check,
|
||||
ADD CONSTRAINT cognitive_model_descriptors_intelligence_score_check CHECK (intelligence_score_millis IS NULL OR (intelligence_score_millis >= 0 AND intelligence_score_millis <= 1000));
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,43 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.agent_dispatch_intents (
|
||||
dispatch_id TEXT PRIMARY KEY,
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
delegation_id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
implementation_id TEXT NOT NULL,
|
||||
implementation_revision BIGINT NOT NULL,
|
||||
source_revision TEXT NOT NULL,
|
||||
worktree_id TEXT NOT NULL,
|
||||
verification_plan_ref TEXT NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (dispatch_id <> ''), CHECK (idempotency_key <> ''), CHECK (delegation_id <> ''),
|
||||
CHECK (run_id <> ''), CHECK (implementation_id <> ''), CHECK (implementation_revision > 0),
|
||||
CHECK (source_revision <> ''), CHECK (worktree_id <> ''), CHECK (verification_plan_ref <> ''),
|
||||
CHECK (created_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.agent_leases (
|
||||
lease_id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL UNIQUE,
|
||||
worker_id TEXT NOT NULL,
|
||||
fencing_token BIGINT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
acquired_at_unix_millis BIGINT NOT NULL,
|
||||
expires_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (lease_id <> ''), CHECK (run_id <> ''), CHECK (worker_id <> ''),
|
||||
CHECK (fencing_token > 0), CHECK (state IN ('ACTIVE','EXPIRED')),
|
||||
CHECK (acquired_at_unix_millis > 0), CHECK (expires_at_unix_millis > acquired_at_unix_millis)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.agent_callback_heads (
|
||||
run_id TEXT PRIMARY KEY,
|
||||
last_sequence BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (run_id <> ''), CHECK (last_sequence >= 0), CHECK (updated_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,22 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.agent_dispatch_queue (
|
||||
dispatch_id TEXT PRIMARY KEY REFERENCES work.agent_dispatch_intents(dispatch_id),
|
||||
state TEXT NOT NULL,
|
||||
available_at_unix_millis BIGINT NOT NULL,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
claimed_by TEXT NOT NULL DEFAULT '',
|
||||
updated_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (state IN ('QUEUED','CLAIMED','COMPLETED','FAILED')),
|
||||
CHECK (available_at_unix_millis > 0),
|
||||
CHECK (attempts >= 0),
|
||||
CHECK (updated_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS agent_dispatch_queue_claim_idx
|
||||
ON work.agent_dispatch_queue(state, available_at_unix_millis, dispatch_id);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -12,6 +12,7 @@ services:
|
||||
SITUATION_DB_PASSWORD: ${SITUATION_DB_PASSWORD:-assistant_situation_dev_only}
|
||||
WORK_DB_PASSWORD: ${WORK_DB_PASSWORD:-assistant_work_dev_only}
|
||||
CAPABILITY_DB_PASSWORD: ${CAPABILITY_DB_PASSWORD:-assistant_capability_dev_only}
|
||||
EPISTEMICS_DB_PASSWORD: ${EPISTEMICS_DB_PASSWORD:-assistant_epistemics_dev_only}
|
||||
COMMIT_DB_PASSWORD: ${COMMIT_DB_PASSWORD:-assistant_commit_dev_only}
|
||||
ports: ["15432:5432"]
|
||||
volumes:
|
||||
@@ -50,6 +51,8 @@ services:
|
||||
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD:-assistant_work_dev_only}
|
||||
CAPABILITY_POSTGRES_USER: capability_role
|
||||
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD:-assistant_capability_dev_only}
|
||||
EPISTEMICS_POSTGRES_USER: epistemics_role
|
||||
EPISTEMICS_POSTGRES_PASSWORD: ${EPISTEMICS_DB_PASSWORD:-assistant_epistemics_dev_only}
|
||||
ports: ["19080:8080"]
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
@@ -99,6 +102,7 @@ services:
|
||||
LISTEN_ADDR: ":8083"
|
||||
CORE_RUNTIME_URL: http://core-runtime:8080
|
||||
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
|
||||
LOCAL_LIFEBOAT_PROVIDER_URL: ${LOCAL_LIFEBOAT_PROVIDER_URL:-}
|
||||
LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434
|
||||
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
|
||||
ports: ["19083:8083"]
|
||||
|
||||
@@ -17,6 +17,7 @@ services:
|
||||
WORK_DB_PASSWORD: ${WORK_DB_PASSWORD}
|
||||
CAPABILITY_DB_PASSWORD: ${CAPABILITY_DB_PASSWORD}
|
||||
COMMIT_DB_PASSWORD: ${COMMIT_DB_PASSWORD}
|
||||
EPISTEMICS_DB_PASSWORD_FILE: /run/secrets/epistemics_db_password
|
||||
INTERACTION_DB_PASSWORD_FILE: /run/secrets/interaction_db_password
|
||||
volumes:
|
||||
- assistant_pg:/var/lib/postgresql
|
||||
@@ -72,6 +73,9 @@ services:
|
||||
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
|
||||
CAPABILITY_POSTGRES_USER: capability_role
|
||||
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD}
|
||||
EPISTEMICS_POSTGRES_USER: epistemics_role
|
||||
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
|
||||
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
|
||||
INTERACTION_POSTGRES_USER: interaction_role
|
||||
INTERACTION_POSTGRES_CREDENTIAL_REF: credential://domovoy/interaction/db-password/v1
|
||||
INTERACTION_POSTGRES_PASSWORD_FILE: /run/secrets/interaction_db_password
|
||||
@@ -80,6 +84,19 @@ services:
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
|
||||
agent-canary:
|
||||
profiles: ["agent-canary"]
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-canary, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: "no"
|
||||
command: ["-enable-canary", "-postgres"]
|
||||
environment:
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
|
||||
WORK_POSTGRES_USER: work_role
|
||||
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
|
||||
depends_on: { postgres: { condition: service_healthy } }
|
||||
|
||||
continuity-runtime:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
@@ -122,10 +139,42 @@ services:
|
||||
LISTEN_ADDR: ":8083"
|
||||
CORE_RUNTIME_URL: http://core-runtime:8080
|
||||
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
|
||||
LOCAL_LIFEBOAT_PROVIDER_URL: http://cognitive-lifeboat:8086/v1
|
||||
LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434
|
||||
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
|
||||
COGNITIVE_ALLOW_CANARY: "true"
|
||||
depends_on: [core-runtime]
|
||||
depends_on: [core-runtime, cognitive-lifeboat]
|
||||
|
||||
cognitive-lifeboat:
|
||||
image: ghcr.io/ggml-org/llama.cpp:server
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- --host
|
||||
- 0.0.0.0
|
||||
- --port
|
||||
- "8086"
|
||||
- --model
|
||||
- /models/Qwen3-8B-Q4_K_M.gguf
|
||||
- --alias
|
||||
- domovoy-lifeboat
|
||||
- --jinja
|
||||
- --ctx-size
|
||||
- "8192"
|
||||
- --n-gpu-layers
|
||||
- "0"
|
||||
- --threads
|
||||
- "32"
|
||||
- --parallel
|
||||
- "1"
|
||||
- --reasoning
|
||||
- "off"
|
||||
volumes:
|
||||
- llm_local_models:/models:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost:8086/health >/dev/null"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
creator-web:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: creator-web, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
@@ -136,6 +185,7 @@ services:
|
||||
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
|
||||
COMMIT_RUNTIME_URL: http://commit-runtime:8082
|
||||
COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083
|
||||
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
|
||||
ports: ["${CREATOR_WEB_PORT:-18080}:8084"]
|
||||
depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker]
|
||||
healthcheck:
|
||||
@@ -154,3 +204,6 @@ volumes:
|
||||
external: true
|
||||
assistant_notification_reader_secret:
|
||||
external: true
|
||||
llm_local_models:
|
||||
external: true
|
||||
name: llm-local-models
|
||||
|
||||
@@ -18,6 +18,13 @@ SQL
|
||||
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v capability_password="$CAPABILITY_DB_PASSWORD" <<'SQL'
|
||||
ALTER ROLE capability_role PASSWORD :'capability_password';
|
||||
SQL
|
||||
if [ "${EPISTEMICS_DB_PASSWORD_FILE:-}" != "" ]; then
|
||||
EPISTEMICS_DB_PASSWORD="$(tr -d '\r\n' < "$EPISTEMICS_DB_PASSWORD_FILE")"
|
||||
fi
|
||||
: "${EPISTEMICS_DB_PASSWORD:?EPISTEMICS_DB_PASSWORD or EPISTEMICS_DB_PASSWORD_FILE is required}"
|
||||
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v epistemics_password="$EPISTEMICS_DB_PASSWORD" <<'SQL'
|
||||
ALTER ROLE epistemics_role PASSWORD :'epistemics_password';
|
||||
SQL
|
||||
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v commit_password="$COMMIT_DB_PASSWORD" <<'SQL'
|
||||
ALTER ROLE commit_role PASSWORD :'commit_password';
|
||||
SQL
|
||||
|
||||
@@ -4,10 +4,15 @@ set -eu
|
||||
backup_root="${ASSISTANT_BACKUP_DIR:-/home/m/assistant-backups/postgres}"
|
||||
verify_script="${ASSISTANT_RESTORE_VERIFY_SCRIPT:-/home/m/assistant-ops/verify-postgres-restore.sh}"
|
||||
|
||||
latest="$(find "$backup_root" -mindepth 1 -maxdepth 1 -type d ! -name '.*' -printf '%f\n' | sort | tail -n 1)"
|
||||
latest="$(find "$backup_root" -mindepth 1 -maxdepth 1 -type d -regextype posix-extended -regex '.*/[0-9]{8}T[0-9]{6}Z$' -printf '%f\n' | sort | tail -n 1)"
|
||||
if [ -z "$latest" ]; then
|
||||
echo "No completed PostgreSQL backup found in $backup_root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$latest" in
|
||||
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]Z) ;;
|
||||
*) echo "Selected backup directory has invalid timestamp: $latest" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
exec "$verify_script" "$backup_root/$latest"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Endpoint = 'https://assistent.cin.su/delivery/v1/receipts?limit=100',
|
||||
[string]$Endpoint = 'https://assistant.cin.su/delivery/v1/receipts?limit=100',
|
||||
[string]$KeyRecordPath = "$env:LOCALAPPDATA\Domovoy\notification-receiver-read-key.dpapi.json",
|
||||
[string]$StatePath = "$env:LOCALAPPDATA\Domovoy\notification-receiver-state.json",
|
||||
[ValidateRange(1, 5)]
|
||||
@@ -24,7 +24,7 @@ function Write-AtomicJson {
|
||||
}
|
||||
|
||||
$uri = [Uri]$Endpoint
|
||||
if ($uri.Scheme -ne 'https' -or $uri.Host -ne 'assistent.cin.su' -or $uri.AbsolutePath -ne '/delivery/v1/receipts') {
|
||||
if ($uri.Scheme -ne 'https' -or $uri.Host -ne 'assistant.cin.su' -or $uri.AbsolutePath -ne '/delivery/v1/receipts') {
|
||||
throw 'Notification receiver endpoint is outside the fixed HTTPS binding'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $KeyRecordPath -PathType Leaf)) { throw "Notification receiver key not found: $KeyRecordPath" }
|
||||
|
||||
@@ -45,7 +45,7 @@ $cipherPartial = $null
|
||||
$manifestPartial = $null
|
||||
|
||||
try {
|
||||
$latest = ((& ssh.exe $SshHost "find '$RemoteBackupRoot' -mindepth 1 -maxdepth 1 -type d ! -name '.*' -printf '%f\n' | sort | tail -n1") -join '').Trim()
|
||||
$latest = ((& ssh.exe $SshHost "find '$RemoteBackupRoot' -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | grep -E '^[0-9]{8}T[0-9]{6}Z$' | sort | tail -n1") -join '').Trim()
|
||||
if ($LASTEXITCODE -ne 0 -or $latest -notmatch '^\d{8}T\d{6}Z$') { throw 'No valid completed production backup found' }
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $NasBackupRoot | Out-Null
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000066-human-response-continuation-semantics.json",
|
||||
"task_id": "DEV-000066",
|
||||
"task_file": "dev/tasks/DEV-000129-agent-dispatch-queue.json",
|
||||
"task_id": "DEV-000129",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"task_id": "ARCH-000011",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "generation-1",
|
||||
"base_repo_revision": "6f8c4e2bc7d4f86cad3fc0e773c8c580b19a29ee",
|
||||
"risk_tier": "E",
|
||||
"intent": "Produce a proposal-only architecture decision for a test Exploration profile with independent external audit, preserving useful local research and evidence while moving review visibility outside Domovoy without creating a direct model-to-world path.",
|
||||
"requirement_ids": ["FD-009", "FD-015", "FD-017", "FD-018", "FD-019", "FD-025", "INV-AUTH-001", "INV-EPI-001", "INV-PROJ-001", "USER-EXTERNAL-AUDIT-WITH-EXPLORATION-FREEDOM"],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"docs/EXTERNAL_AUDIT_EXPLORATION_PROPOSAL.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/ARCH-000011-external-audit-exploration-proposal.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "ci/**", ".gitea/**", "contracts/**", "modules/**", "adapters/**", "cmd/**", "database/**", "deploy/**", "security/**", "tests/**"]
|
||||
},
|
||||
"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": ["FD-009 model output boundary", "existing Work/Capability/Epistemics/Commit ownership", "existing transactional outbox direction"],
|
||||
"may_change": []
|
||||
},
|
||||
"semantic_owner": "proposal-only; no architectural decision, runtime behavior, audit deployment, or execution privilege is accepted by this task",
|
||||
"canonical_state": [],
|
||||
"state_transitions": [],
|
||||
"side_effects_and_privileges": ["select this proposal in the active task pointer", "no runtime action, provider call, credential handling, deployment, canonical mutation, or external action"],
|
||||
"persistence_and_migrations": [],
|
||||
"failure_retry_restart": ["proposal specifies reviewer loss as a non-failure for read-only exploration and a pending state for gated proposals", "no automatic execution or retry is introduced"],
|
||||
"assumptions": ["the user's explicit instruction authorizes analysis and proposal only, not removal of secret isolation, canonical history, authority validation, or commit/executor ownership", "external audit is independent observation rather than a competing executor", "docker-test remains prohibited"],
|
||||
"acceptance_scenarios": ["proposal defines useful local behavior during provider loss", "proposal separates canonical owners, audit projection, reviewer evidence and executor", "proposal defines audit outage and unknown-outcome semantics", "proposal identifies staged implementation and explicit decisions required", "no production behavior or protected architecture artifact changes"],
|
||||
"required_tests": ["task schema", "architecture preflight", "exact diff scope", "documentation review", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"task_id": "ARCH-000012",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "generation-1",
|
||||
"base_repo_revision": "948896c",
|
||||
"risk_tier": "E",
|
||||
"intent": "Accept the staged external-audit and Domovoy Exploration profile proposed by ARCH-000011, while retaining semantic ownership, redacted evidence, and the Commit/Executor boundary.",
|
||||
"requirement_ids": ["ARCH-000011", "FD-009", "FD-015", "FD-017", "FD-025", "INV-AUTH-001", "INV-EPI-001", "INV-PROJ-001", "USER-CONTINUE-AFTER-ARCH-000011"],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"architecture/adrs/ADR-0018-external-audit-exploration-mode.md",
|
||||
"architecture/adrs/README.md",
|
||||
"docs/EXTERNAL_AUDIT_EXPLORATION_PROPOSAL.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/ARCH-000012-accept-external-audit-exploration.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "architecture/CORE_SEMANTIC_MODEL_V1.md", "ci/**", ".gitea/**", "contracts/**", "modules/**", "adapters/**", "cmd/**", "database/**", "deploy/**", "security/**", "tests/**"]
|
||||
},
|
||||
"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": ["ARCH-000011 proposal", "existing Work/Epistemics/Capability/Commit ownership"], "may_change": []},
|
||||
"semantic_owner": "architecture decision only; no runtime, audit deployment, external action or canonical-state behavior is changed",
|
||||
"canonical_state": [],
|
||||
"state_transitions": [],
|
||||
"side_effects_and_privileges": ["accept ADR-0018", "no provider call, credential handling, deployment, canonical mutation, or external action"],
|
||||
"persistence_and_migrations": [],
|
||||
"failure_retry_restart": ["reviewer loss keeps review-governed proposals pending", "no automatic execution or retry is accepted"],
|
||||
"assumptions": ["the user's instruction 'делай' after ARCH-000011 accepts the recommended staged decision", "initial delivery has no review-governed external action class", "docker-test remains prohibited"],
|
||||
"acceptance_scenarios": ["ADR defines Exploration without new authority", "audit is a redacted rebuildable projection and not an executor", "reviewer outage semantics are explicit", "implementation is staged and no runtime behavior changes in this task"],
|
||||
"required_tests": ["task schema", "architecture preflight", "exact diff scope", "independent protected-path review", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"task_id": "DEV-000067",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0016",
|
||||
"base_repo_revision": "d4640d7617ed579a9b0852fb336905f777f80de6",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add provider-neutral Experience, Reflection and Skill Evolution foundation without creating a LearningManager or new canonical owner.",
|
||||
"requirement_ids": [
|
||||
"ADR-0016",
|
||||
"INV-OWN-001",
|
||||
"INV-EPI-001",
|
||||
"Experience preserves original intent, expectation, outcome and correction evidence",
|
||||
"Fast paths can decline and return to broader reasoning",
|
||||
"Skill replacement does not delete evidence"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"epistemics",
|
||||
"capability",
|
||||
"interaction",
|
||||
"continuity"
|
||||
],
|
||||
"paths": [
|
||||
"architecture/adrs/ADR-0016-experience-reflection-skill-evolution.md",
|
||||
"architecture/architecture-registry.yaml",
|
||||
"architecture/architecture-registry.json",
|
||||
"contracts/epistemics/**",
|
||||
"contracts/capability/**",
|
||||
"modules/epistemics/**",
|
||||
"modules/capability/**",
|
||||
"adapters/postgres/**",
|
||||
"database/migrations/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/creator-web/**",
|
||||
"deploy/windows/Invoke-DomovoyNotificationReceiver.ps1",
|
||||
"tests/owner_acl/**",
|
||||
"tests/cognitive_advisory/**",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000067-experience-reflection-skill-evolution.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"architecture/ARCHITECTURE_CONSTITUTION.md",
|
||||
"modules/authority/**",
|
||||
"modules/commit/**",
|
||||
"cmd/commit-runtime/**",
|
||||
"security/**",
|
||||
"ci/**",
|
||||
".gitea/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"Interaction HumanUtterance / OriginalHumanIntent / correction refs",
|
||||
"Continuity HumanResponse / Attention / ProspectiveItem refs",
|
||||
"Capability CapabilityUsageObservation / CapabilityGap refs",
|
||||
"Cognitive Gateway / Router for future reflection reasoning only"
|
||||
],
|
||||
"may_change": [
|
||||
"contracts/epistemics@v1",
|
||||
"contracts/capability@v1"
|
||||
]
|
||||
},
|
||||
"semantic_owner": "epistemics",
|
||||
"canonical_state": [
|
||||
"SystemExpectation",
|
||||
"ExperienceRecord",
|
||||
"ReflectionCandidate",
|
||||
"SkillProfile",
|
||||
"SkillEvidence",
|
||||
"FastPathDecision"
|
||||
],
|
||||
"state_transitions": [
|
||||
"successful target resolution + accepted human response -> positive experience evidence",
|
||||
"user correction -> USER_CORRECTED outcome and reflection candidate",
|
||||
"fast path outside supported semantics -> NOT_APPLICABLE/OUT_OF_DISTRIBUTION and escalation",
|
||||
"repeated capability/skill failure evidence -> DEGRADED skill profile without deleting history"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation is introduced",
|
||||
"No cognitive model is required for LLM-free tests",
|
||||
"No model output can directly rewrite skills, beliefs, source, deployment or authority",
|
||||
"Docker-test remains prohibited"
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"epistemics.system_expectations append-only table",
|
||||
"epistemics.experience_records append-only table",
|
||||
"epistemics.reflection_candidates append-only table",
|
||||
"capability.skill_profiles revisioned table",
|
||||
"capability.skill_profile_heads projection table",
|
||||
"no cross-owner FK",
|
||||
"no cross-owner SQL join dependency"
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Experience/reflection rows are append-only historical evidence",
|
||||
"Fast path decline is a valid non-failure decision and preserves return-to-thinking path",
|
||||
"DEGRADED/SUSPENDED/RETIRED skill profiles preserve supporting/failure evidence refs"
|
||||
],
|
||||
"assumptions": [
|
||||
"DEV-000066 is production baseline",
|
||||
"Existing owners are sufficient; no new canonical owner is needed",
|
||||
"V1 records evidence and lifecycle primitives, not autonomous self-improvement"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"Target resolver selected the right recent reminder and user accepted -> positive experience evidence",
|
||||
"User correction of wrong interpretation/target -> USER_CORRECTED outcome and reflection candidate",
|
||||
"Known deterministic path receives unsupported input -> fast path declines and escalates rather than guessing",
|
||||
"Capability implementation fails repeatedly -> related skill confidence/status can degrade without deleting history",
|
||||
"Deleting/replacing/disabling a skill implementation does not delete source observations, experience, corrections, outcomes or original human intent history",
|
||||
"LLM-free operation passes"
|
||||
],
|
||||
"required_tests": [
|
||||
"go test ./...",
|
||||
"go vet ./...",
|
||||
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"go run ./cmd/devctl check-architecture",
|
||||
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1",
|
||||
"tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1",
|
||||
"production /chat verification if runtime deployment is required"
|
||||
,
|
||||
"notification receiver endpoint uses canonical assistant.cin.su spelling"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"task_id": "DEV-000068",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0016",
|
||||
"base_repo_revision": "67ff1f73cddfd94f9f5745acc87f997aed49630a",
|
||||
"risk_tier": "E",
|
||||
"intent": "Activate the first production runtime Experience and Reflection loop for meaningful /chat, HumanResponse, capability and fast-path outcomes without creating a LearningManager.",
|
||||
"requirement_ids": [
|
||||
"ADR-0016",
|
||||
"INV-OWN-001",
|
||||
"INV-EPI-001",
|
||||
"Runtime Experience capture",
|
||||
"Runtime ReflectionCandidate triggers",
|
||||
"SkillEvidence from real reminder response outcomes",
|
||||
"Fast-path decline returns to reasoning/clarification"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"epistemics",
|
||||
"capability",
|
||||
"interaction",
|
||||
"continuity"
|
||||
],
|
||||
"paths": [
|
||||
"contracts/epistemics/**",
|
||||
"contracts/capability/**",
|
||||
"modules/epistemics/**",
|
||||
"modules/capability/**",
|
||||
"adapters/postgres/**",
|
||||
"database/migrations/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/creator-web/**",
|
||||
"cmd/continuity-runtime/**",
|
||||
"deploy/docker-compose.dev.yml",
|
||||
"deploy/docker-compose.prod.yml",
|
||||
"deploy/images/002_owner_passwords.sh",
|
||||
"deploy/windows/**",
|
||||
"tests/owner_acl/**",
|
||||
"tests/cognitive_advisory/**",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000068-runtime-experience-reflection-loop.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"architecture/ARCHITECTURE_CONSTITUTION.md",
|
||||
"modules/authority/**",
|
||||
"modules/commit/**",
|
||||
"cmd/commit-runtime/**",
|
||||
"security/**",
|
||||
"ci/**",
|
||||
".gitea/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0,
|
||||
"new_runtime_secret_file_for_existing_owner": "epistemics_role database password for core-runtime"
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"Interaction HumanUtterance / OriginalHumanIntent / Correction refs",
|
||||
"Continuity HumanResponse / Attention / ProspectiveItem refs",
|
||||
"Capability CapabilityUsageObservation / CapabilityGap / SkillProfile",
|
||||
"Epistemics SystemExpectation / ExperienceRecord / ReflectionCandidate"
|
||||
],
|
||||
"may_change": [
|
||||
"contracts/epistemics@v1",
|
||||
"contracts/capability@v1"
|
||||
]
|
||||
},
|
||||
"semantic_owner": "epistemics",
|
||||
"canonical_state": [
|
||||
"SystemExpectation",
|
||||
"ExperienceRecord",
|
||||
"ReflectionCandidate",
|
||||
"SkillProfile"
|
||||
],
|
||||
"state_transitions": [
|
||||
"meaningful chat help path -> SystemExpectation before outcome",
|
||||
"HumanResponse resolved COMPLETE/SNOOZE/CANCEL -> positive ExperienceRecord and SkillProfile supporting evidence",
|
||||
"Human correction -> USER_CORRECTED ExperienceRecord and ReflectionCandidate",
|
||||
"ambiguous/no-target/unsupported fast path -> decline/clarification ExperienceRecord and ReflectionCandidate",
|
||||
"repeated failure/correction evidence -> SkillProfile DEGRADED/SUSPENDED according to deterministic policy"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation is introduced",
|
||||
"No model output can directly mutate canonical state, Authority, source code, deployment or skill lifecycle",
|
||||
"Core runtime receives only the existing Epistemics owner database credential through secret-file/CredentialRef mechanism",
|
||||
"Creator Web remains a proxy/UI and receives no direct database credential",
|
||||
"Docker-test remains prohibited"
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"no new canonical owner",
|
||||
"no cross-owner FK",
|
||||
"no cross-owner SQL join dependency",
|
||||
"reuse DEV-000067 tables with optional indexes/projections only if needed"
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Experience/reflection capture is append-only and idempotent by generated IDs",
|
||||
"A failed learning write must not pretend success; production acceptance verifies rows",
|
||||
"Fast-path decline is valid safety behavior and does not mutate target state"
|
||||
],
|
||||
"assumptions": [
|
||||
"DEV-000067 production foundation is active",
|
||||
"ADR-0016 is sufficient; no new architecture owner is required",
|
||||
"V1 uses deterministic reflection triggers; cognitive reflection remains optional candidate generation only"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"Create reminder then complete it -> positive ExperienceRecord and SkillProfile supporting evidence",
|
||||
"Create ambiguous targets and correct Domovoy -> USER_CORRECTED ExperienceRecord and ReflectionCandidate",
|
||||
"Unsupported/uncertain response phrasing -> fast path declines and asks clarification/falls back without guessed mutation",
|
||||
"Operator can inspect recent ExperienceRecords, ReflectionCandidates and SkillProfile status",
|
||||
"Human chat does not expose internal learning IDs",
|
||||
"LLM-free operation passes"
|
||||
],
|
||||
"required_tests": [
|
||||
"go test ./...",
|
||||
"go vet ./...",
|
||||
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"go run ./cmd/devctl check-architecture",
|
||||
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1",
|
||||
"tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1",
|
||||
"production live learning acceptance A/B/C",
|
||||
"production health",
|
||||
"production /chat verification"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"task_id": "DEV-000069",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0017",
|
||||
"base_repo_revision": "d870e1361d6dcef3866d7339495d83b9756949c4",
|
||||
"risk_tier": "E",
|
||||
"intent": "Introduce a provider-neutral external intelligent agent delegation foundation and the first Codex bridge implementation for software-engineering work without making Codex a canonical subsystem.",
|
||||
"requirement_ids": [
|
||||
"ADR-0017",
|
||||
"ADR-0012",
|
||||
"ADR-0013",
|
||||
"ADR-0016",
|
||||
"INV-OWN-001",
|
||||
"INV-EPI-001",
|
||||
"INV-TOPO-001",
|
||||
"General external agent representation",
|
||||
"Durable delegation and resumable runs",
|
||||
"Codex bridge as first AGENT implementation",
|
||||
"Execution-window continuation",
|
||||
"Delegation experience evidence"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"work",
|
||||
"capability",
|
||||
"continuity",
|
||||
"epistemics",
|
||||
"interaction",
|
||||
"situation",
|
||||
"artifact"
|
||||
],
|
||||
"paths": [
|
||||
"contracts/work/**",
|
||||
"contracts/capability/**",
|
||||
"contracts/artifact/**",
|
||||
"contracts/epistemics/**",
|
||||
"contracts/continuity/**",
|
||||
"contracts/interaction/**",
|
||||
"modules/work/**",
|
||||
"modules/capability/**",
|
||||
"modules/epistemics/**",
|
||||
"modules/continuity/**",
|
||||
"adapters/postgres/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/creator-web/**",
|
||||
"cmd/continuity-runtime/**",
|
||||
"database/migrations/**",
|
||||
"deploy/docker-compose.dev.yml",
|
||||
"deploy/docker-compose.prod.yml",
|
||||
"deploy/windows/**",
|
||||
"tests/**",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"architecture/adrs/ADR-0017-general-agent-delegation-codex-bridge.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000069-general-agent-delegation-codex-bridge.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"architecture/ARCHITECTURE_CONSTITUTION.md",
|
||||
"architecture/architecture-registry.yaml",
|
||||
"architecture/architecture-registry.json",
|
||||
"modules/authority/**",
|
||||
"modules/commit/**",
|
||||
"cmd/commit-runtime/**",
|
||||
"security/**",
|
||||
"ci/**",
|
||||
".gitea/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0,
|
||||
"new_runtime_secret_file_for_existing_owner": "only if an already-approved bridge/runtime prerequisite requires a least-privilege credential"
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"Interaction HumanUtterance / OriginalHumanIntent",
|
||||
"Situation focus and project resolution",
|
||||
"Work Goal / PlanCandidate / CapabilityRequirement / DelegationCandidate",
|
||||
"Capability CapabilityImplementation / CapabilityResolution / CapabilityUsageObservation",
|
||||
"Continuity Attention / HumanResponse",
|
||||
"Epistemics ExperienceRecord / ReflectionCandidate",
|
||||
"Artifact metadata refs"
|
||||
],
|
||||
"may_change": [
|
||||
"contracts/work@v1",
|
||||
"contracts/capability@v1",
|
||||
"contracts/artifact@v1",
|
||||
"contracts/epistemics@v1"
|
||||
]
|
||||
},
|
||||
"semantic_owner": "work",
|
||||
"canonical_state": [
|
||||
"CapabilityImplementation(kind=AGENT)",
|
||||
"Delegation",
|
||||
"AgentRun",
|
||||
"AgentCheckpoint",
|
||||
"AgentResult"
|
||||
],
|
||||
"state_transitions": [
|
||||
"OriginalHumanIntent -> optional Goal/Plan -> CapabilityRequirement -> real Delegation",
|
||||
"Delegation -> AgentRun STARTED -> RUNNING/WAITING/CHECKPOINTED/EXECUTION_WINDOW_EXHAUSTED/BLOCKED/FAILED/COMPLETED/CANCELLED",
|
||||
"execution window exhaustion -> durable checkpoint -> resumed run on same higher-level Delegation",
|
||||
"human-dependent blocker -> AttentionItem -> natural HumanResponse -> continue delegation/work",
|
||||
"agent result evidence -> verification -> Work completion decision -> ExperienceRecord"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No new canonical owner and no Codex-specific canonical subsystem",
|
||||
"No direct external mutation outside existing Authority/Commit boundaries",
|
||||
"Agent-specific instruction is derived adapter context, not canonical human intent",
|
||||
"Codex bridge may use only the least-privilege runtime/credential path required for bounded repository work",
|
||||
"Docker-test remains prohibited"
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"owner-local Work and Capability schema extensions only",
|
||||
"no cross-owner FK",
|
||||
"no cross-owner SQL join dependency",
|
||||
"result evidence stored by typed refs/metadata rather than payload duplication where possible"
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"execution-window exhaustion is resumable and not automatic work failure",
|
||||
"technical agent failure is distinct from human-dependent blocker",
|
||||
"agent self-report is insufficient for Work completion without verification evidence",
|
||||
"Codex unavailable must create capability unavailability/gap rather than startup failure"
|
||||
],
|
||||
"assumptions": [
|
||||
"DEV-000068 production foundation is active",
|
||||
"CapabilityImplementation(kind=AGENT) is sufficient for a general agent descriptor V1",
|
||||
"Work is the minimal correct owner for durable delegation lifecycle",
|
||||
"Continuity attention/human-response foundations from DEV-000066 can be reused for delegation waiting/blockers"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"software-engineering requirement resolves through executor-neutral capability resolution",
|
||||
"Codex is represented as an implementation, not canonical capability identity",
|
||||
"delegation run checkpoints and resumes after execution-window exhaustion",
|
||||
"human-dependent blocker surfaces through Attention while technical failure does not masquerade as human blocker",
|
||||
"agent-specific execution instruction is derived from canonical context and OriginalHumanIntent remains unchanged",
|
||||
"safe real self-development scenario creates delegation, run, checkpoint/result evidence and ExperienceRecord",
|
||||
"LLM-free system starts when Codex bridge is unavailable"
|
||||
],
|
||||
"required_tests": [
|
||||
"go test ./...",
|
||||
"go vet ./...",
|
||||
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"go run ./cmd/devctl check-architecture",
|
||||
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1",
|
||||
"tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1",
|
||||
"delegation/resume/attention/provider-neutral contract tests",
|
||||
"production health",
|
||||
"safe live Codex delegation acceptance"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"task_id": "DEV-000070",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000069-baseline",
|
||||
"base_repo_revision": "ade274011f6619d2e5265fca467ebbf20d8dec56",
|
||||
"risk_tier": "E",
|
||||
"intent": "Build a replayable reality evaluation harness and baseline report that measure what current Domovoy actually does well or badly across heterogeneous assistance scenarios without patching product behavior to improve the baseline.",
|
||||
"requirement_ids": [
|
||||
"DEV-000069 baseline complete",
|
||||
"Measurement before improvement",
|
||||
"No new canonical owner for evaluation",
|
||||
"Truthfulness versus user assertion distinction",
|
||||
"Human intervention measurement",
|
||||
"False claim measurement",
|
||||
"Replayable baseline and candidate comparison foundation",
|
||||
"Operator review surface"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"interaction",
|
||||
"situation",
|
||||
"work",
|
||||
"capability",
|
||||
"continuity",
|
||||
"epistemics",
|
||||
"artifact"
|
||||
],
|
||||
"paths": [
|
||||
"contracts/epistemics/**",
|
||||
"contracts/capability/**",
|
||||
"contracts/work/**",
|
||||
"contracts/interaction/**",
|
||||
"contracts/situation/**",
|
||||
"contracts/continuity/**",
|
||||
"modules/epistemics/**",
|
||||
"modules/capability/**",
|
||||
"modules/work/**",
|
||||
"modules/interaction/**",
|
||||
"modules/situation/**",
|
||||
"adapters/postgres/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/creator-web/**",
|
||||
"cmd/cognitive-benchmark/**",
|
||||
"tests/**",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"docs/evidence/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000070-reality-evaluation-harness.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"architecture/ARCHITECTURE_CONSTITUTION.md",
|
||||
"architecture/architecture-registry.yaml",
|
||||
"architecture/architecture-registry.json",
|
||||
"architecture/adrs/**",
|
||||
"modules/authority/**",
|
||||
"modules/commit/**",
|
||||
"cmd/commit-runtime/**",
|
||||
"security/**",
|
||||
"ci/**",
|
||||
".gitea/**",
|
||||
"deploy/**",
|
||||
"database/migrations/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0,
|
||||
"new_database_owner_roles": 0,
|
||||
"new_durable_canonical_types": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"Interaction Conversation / HumanUtterance / OriginalHumanIntent / UtteranceInterpretation",
|
||||
"Situation ActiveProjectFocus / ProjectRegistry",
|
||||
"Work Goal / WorkPlan / Delegation / AgentRun / AgentResult / DelegationVerification",
|
||||
"Capability CapabilityResolution / CapabilitySelection / CapabilityGap / CapabilityUsageObservation",
|
||||
"Continuity ProspectiveItem / AttentionItem / HumanResponse",
|
||||
"Epistemics ExperienceRecord / ReflectionCandidate / SystemExpectation",
|
||||
"Artifact refs for immutable evaluation evidence"
|
||||
],
|
||||
"may_change": [
|
||||
"contracts/epistemics@v1",
|
||||
"contracts/work@v1",
|
||||
"contracts/capability@v1"
|
||||
]
|
||||
},
|
||||
"semantic_owner": "epistemics",
|
||||
"canonical_state": [
|
||||
"Existing ExperienceRecord evidence",
|
||||
"Existing CapabilityUsageObservation evidence",
|
||||
"Existing Work / Delegation / Verification evidence"
|
||||
],
|
||||
"state_transitions": [
|
||||
"evaluation case replay -> ordinary Domovoy runtime behavior -> existing canonical evidence observed",
|
||||
"baseline report classifies outcome dimensions without mutating product behavior to make a case pass",
|
||||
"operator review annotates findings without becoming normal human-chat state"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"Evaluation must not create uncontrolled real-world side effects",
|
||||
"Measurement tooling may use isolated or simulated capability availability where real side effects would be unsafe",
|
||||
"No self-improvement, automatic self-modification, or learning-loop expansion is authorized",
|
||||
"Do not redesign /chat",
|
||||
"Do not optimize specifically for Codex",
|
||||
"Docker-test remains prohibited"
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Prefer test/evaluation tooling and artifact evidence over new canonical persistence",
|
||||
"No new canonical owner",
|
||||
"No schema migration unless a genuine existing-owner evidence gap makes it unavoidable",
|
||||
"No cross-owner FK or cross-owner SQL join dependency"
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Baseline failures are evidence and must be recorded rather than patched away during this milestone",
|
||||
"Unknown / clarification / cannot verify are allowed correct outcomes",
|
||||
"Evaluation harness defects may be fixed, but product behavior must not be changed merely to improve baseline numbers"
|
||||
],
|
||||
"assumptions": [
|
||||
"DEV-000069 production baseline is active",
|
||||
"Existing ExperienceRecord / Verification / Capability evidence is sufficient for an initial reality harness",
|
||||
"Operator-facing review may be implemented through existing Creator/Operator surface rather than a new owner"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"Versioned heterogeneous evaluation suite of 30-50 cases exists",
|
||||
"Cases cover conversation, factual assertions, correction, ambiguity, reminders, context switching, capability gaps, delegation, failures, blockers, unsupported requests, consequence sensitivity and multi-turn continuation",
|
||||
"Baseline run records failures without patching product behavior",
|
||||
"Expectations are semantic/property-based rather than exact-string-based",
|
||||
"Objective evidence is used where possible and evaluator identity is recorded where semantic judgment is needed",
|
||||
"Human intervention, false claims, truthfulness and consequence awareness are measured separately",
|
||||
"Operator can review cases and findings outside normal /chat"
|
||||
],
|
||||
"required_tests": [
|
||||
"go test ./...",
|
||||
"go vet ./...",
|
||||
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"go run ./cmd/devctl check-architecture",
|
||||
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1",
|
||||
"tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1",
|
||||
"evaluation harness unit/replay tests",
|
||||
"baseline suite execution",
|
||||
"production health"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"task_id": "DEV-000071",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000070-baseline",
|
||||
"base_repo_revision": "3aa981bc596575926549f6133a1dad1a863e913f",
|
||||
"risk_tier": "E",
|
||||
"intent": "Audit the actual production cognitive path, router reality, deterministic boundary, and generalization limits of current Domovoy; extend the reality evaluation harness with a broader natural-language and cognitive-quality corpus without patching product behavior to improve scores.",
|
||||
"requirement_ids": [
|
||||
"DEV-000070 baseline complete",
|
||||
"Production evidence before behavior change",
|
||||
"Provider-neutral router reality audit",
|
||||
"Deterministic boundary audit",
|
||||
"Generalization and holdout baseline",
|
||||
"Free-form cognitive behavior measurement",
|
||||
"Real production failure evidence inclusion",
|
||||
"No behavior patching to improve evaluation scores"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"interaction",
|
||||
"situation",
|
||||
"work",
|
||||
"capability",
|
||||
"continuity",
|
||||
"epistemics",
|
||||
"artifact"
|
||||
],
|
||||
"paths": [
|
||||
"tests/**",
|
||||
"cmd/cognitive-benchmark/**",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"docs/evidence/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000071-cognitive-path-reality-generalization.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"architecture/ARCHITECTURE_CONSTITUTION.md",
|
||||
"architecture/architecture-registry.yaml",
|
||||
"architecture/architecture-registry.json",
|
||||
"architecture/adrs/**",
|
||||
"contracts/**",
|
||||
"modules/**",
|
||||
"adapters/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/creator-web/**",
|
||||
"cmd/cognitive-worker/**",
|
||||
"cmd/continuity-runtime/**",
|
||||
"cmd/commit-runtime/**",
|
||||
"modules/authority/**",
|
||||
"modules/commit/**",
|
||||
"security/**",
|
||||
"ci/**",
|
||||
".gitea/**",
|
||||
"deploy/**",
|
||||
"database/migrations/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0,
|
||||
"new_database_owner_roles": 0,
|
||||
"new_durable_canonical_types": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"Existing Interaction HumanUtterance / OriginalHumanIntent / UtteranceInterpretation / ProjectReference evidence",
|
||||
"Existing Capability cognitive registry / usage observation / implementation evidence",
|
||||
"Existing Work Goal / Delegation evidence",
|
||||
"Existing Epistemics ExperienceRecord evidence",
|
||||
"Existing Creator Web / Cognitive Worker route behavior as audited runtime behavior"
|
||||
],
|
||||
"may_change": []
|
||||
},
|
||||
"semantic_owner": "none-evaluation-audit-only",
|
||||
"canonical_state": [
|
||||
"No new canonical state",
|
||||
"Existing production evidence only",
|
||||
"Evaluation artifacts under docs/evidence"
|
||||
],
|
||||
"state_transitions": [
|
||||
"production evidence -> audit report",
|
||||
"existing suite_v1 preserved unchanged",
|
||||
"new generalization corpus -> baseline run against current unchanged Domovoy",
|
||||
"operator-readable evidence report updated without changing product runtime behavior"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"Read-only production audit over existing evidence and health endpoints",
|
||||
"No product behavior patches to improve scores",
|
||||
"No forced Ollama/GPU invocation",
|
||||
"No activation change for bindings",
|
||||
"No Docker-test usage"
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No migrations",
|
||||
"No new canonical persistence",
|
||||
"No cross-owner SQL join dependency in product code",
|
||||
"Evaluation remains tooling/artifact evidence"
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Poor scores are accepted evidence and must be reported honestly",
|
||||
"UNKNOWN must not be converted into zero when observability is insufficient",
|
||||
"Only evaluation/observability defects needed for truthful measurement may be fixed"
|
||||
],
|
||||
"assumptions": [
|
||||
"DEV-000070 merged baseline is current main",
|
||||
"Existing cognitive usage observations cover gateway invocations",
|
||||
"Production audit can rely on existing database evidence and runtime health without new instrumentation"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"real three-turn production failure sequence is audited from actual production evidence where available",
|
||||
"actual registered/discovered/eligible cognitive implementations are reported honestly",
|
||||
"answer is provided whether /chat has ever invoked Ollama/GPU",
|
||||
"deterministic boundary is documented from current runtime behavior",
|
||||
"suite_v1 is preserved unchanged",
|
||||
"approximately fifty additional generalization/cognitive cases are added",
|
||||
"free-form cognition quality, uncertainty, anti-sycophancy and consequence generalization are measured",
|
||||
"baseline is run against current unchanged Domovoy"
|
||||
],
|
||||
"required_tests": [
|
||||
"go test ./...",
|
||||
"go vet ./...",
|
||||
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"go run ./cmd/devctl check-architecture",
|
||||
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1",
|
||||
"tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1",
|
||||
"evaluation harness unit/replay tests",
|
||||
"baseline suite_v1 plus generalization suite execution",
|
||||
"production health"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"task_id": "DEV-000072",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000071-baseline",
|
||||
"base_repo_revision": "64870798b9a50f895c99d24441703f366df5c98a",
|
||||
"risk_tier": "E",
|
||||
"intent": "Implement semantic interpretation and deferral boundary V1 so deterministic interpretation acts as a fast-path candidate rather than unquestionable final truth; preserve provenance between deterministic and cognitive candidates; support mixed intent and safer deferral without activating new providers.",
|
||||
"requirement_ids": [
|
||||
"DEV-000071 measured baseline",
|
||||
"Deterministic candidate can decline/defer",
|
||||
"Mixed intent representation",
|
||||
"Cognition may challenge uncertain deterministic candidate",
|
||||
"No provider hardcoding",
|
||||
"No Ollama activation",
|
||||
"Known and holdout evaluation comparison",
|
||||
"Production chat acceptance"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"interaction",
|
||||
"work",
|
||||
"capability",
|
||||
"epistemics"
|
||||
],
|
||||
"paths": [
|
||||
"contracts/interaction/**",
|
||||
"contracts/work/**",
|
||||
"contracts/cognitive/**",
|
||||
"modules/interaction/**",
|
||||
"modules/work/**",
|
||||
"adapters/postgres/interaction_repository.go",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/creator-web/**",
|
||||
"cmd/cognitive-worker/**",
|
||||
"database/migrations/**",
|
||||
"tests/**",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"docs/evidence/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000072-semantic-interpretation-deferral-boundary.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"architecture/ARCHITECTURE_CONSTITUTION.md",
|
||||
"architecture/architecture-registry.yaml",
|
||||
"architecture/architecture-registry.json",
|
||||
"architecture/adrs/**",
|
||||
"security/**",
|
||||
"modules/authority/**",
|
||||
"modules/commit/**",
|
||||
"deploy/**",
|
||||
"ci/**",
|
||||
".gitea/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.identity",
|
||||
"contracts.interaction",
|
||||
"contracts.work",
|
||||
"contracts.capability",
|
||||
"contracts.cognitive",
|
||||
"contracts.epistemics",
|
||||
"contracts.situation"
|
||||
],
|
||||
"may_change": [
|
||||
"contracts.interaction",
|
||||
"contracts.work",
|
||||
"contracts.cognitive"
|
||||
]
|
||||
},
|
||||
"semantic_owner": "interaction",
|
||||
"canonical_state": [
|
||||
"Interaction-owned human utterance interpretation semantics",
|
||||
"Interaction-owned interpretation provenance between deterministic and cognitive candidates",
|
||||
"Owner-local persistence expansion for semantic candidate provenance",
|
||||
"Existing Work planning/help-path semantics updated to consume richer interaction interpretation without changing canonical ownership"
|
||||
],
|
||||
"state_transitions": [
|
||||
"human utterance -> deterministic semantic candidate",
|
||||
"candidate accepted or deferred",
|
||||
"deferred candidate -> cognitive candidate",
|
||||
"candidate selection -> persisted selected interpretation + provenance",
|
||||
"mixed/consequential ambiguity -> clarification or bounded safe response"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No direct external mutation",
|
||||
"Cognitive Gateway only through existing provider-neutral runtime",
|
||||
"No direct CPU endpoint calls from chat path",
|
||||
"No provider activation changes"
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Interaction owner-local migration expected",
|
||||
"No cross-owner SQL writes",
|
||||
"No cross-owner FK introduction",
|
||||
"No new DB owner roles"
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"If cognition is unavailable, obvious deterministic paths continue to work",
|
||||
"Uncertain consequential intent must fail closed via clarification or explicit limitation",
|
||||
"Model output must remain typed candidate evidence, never direct canonical fact/action"
|
||||
],
|
||||
"assumptions": [
|
||||
"Current production has only one proven eligible cognitive binding for human chat: CPU CANARY",
|
||||
"Ollama/GPU remains inactive for routing in this milestone",
|
||||
"DEV-000071 artifacts remain the baseline comparison set"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"ask-model request no longer finalized as assertion by default",
|
||||
"think/investigate wording can defer to cognition without lexical patching as the sole strategy",
|
||||
"mixed investigate+modify retains both semantic needs",
|
||||
"correction wording plus new directive preserves both aspects",
|
||||
"uncertain consequential request defers safely",
|
||||
"suite_v1 plus DEV-000071 development/holdout comparisons are reported honestly",
|
||||
"production /chat acceptance covers greeting, cognition question, named implementation request, mixed work, correction, correction+new directive, consequential ambiguity"
|
||||
],
|
||||
"required_tests": [
|
||||
"go test ./...",
|
||||
"go vet ./...",
|
||||
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"go run ./cmd/devctl check-architecture",
|
||||
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1",
|
||||
"tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1",
|
||||
"interaction domain tests",
|
||||
"work planning tests",
|
||||
"reality evaluation comparisons",
|
||||
"production /chat acceptance"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"task_id": "DEV-000074",
|
||||
"task_revision": 6,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "1cfbb4ae66e24d514e59dabafb936c35546bd418",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make the authenticated Creator Web cabinet understandable and operable for a human by adding section navigation, read-only diagnostics, and browser-local interface preferences without changing domain state or authority boundaries.",
|
||||
"requirement_ids": [
|
||||
"Human can move between overview, diagnostics, operations and settings",
|
||||
"Human can run all currently exposed read-only Creator checks from one diagnostic panel",
|
||||
"Interface preferences remain local to the browser and never change canonical state",
|
||||
"Existing explicit confirmation remains required for canonical mutations",
|
||||
"Desktop and mobile layouts remain usable"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cmd/creator-web"],
|
||||
"paths": [
|
||||
"cmd/creator-web/static/index.html",
|
||||
"cmd/creator-web/static/assets/console.css",
|
||||
"cmd/creator-web/static/assets/console.js",
|
||||
"cmd/creator-web/server_test.go",
|
||||
"cmd/creator-web/chat_test.go",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000074-creator-cabinet-operations.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"architecture/**",
|
||||
"security/**",
|
||||
"database/**",
|
||||
"modules/**",
|
||||
"contracts/**",
|
||||
"adapters/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/commit-runtime/**",
|
||||
"cmd/continuity-runtime/**",
|
||||
"deploy/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": ["existing authenticated Creator gateway endpoints"],
|
||||
"may_change": []
|
||||
},
|
||||
"semantic_owner": "none-interface-adapter",
|
||||
"canonical_state": [],
|
||||
"state_transitions": [
|
||||
"diagnostic request -> read-only gateway observation",
|
||||
"settings form submit -> browser-local preference update"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No new privilege or mutation endpoint",
|
||||
"No database migration",
|
||||
"Existing Commit actions retain explicit confirmation dialogs",
|
||||
"No deployment or neighboring project mutation"
|
||||
],
|
||||
"persistence_and_migrations": ["No canonical persistence; localStorage only for interface preferences"],
|
||||
"failure_retry_restart": ["Diagnostic failures are shown per check without exposing upstream error details"],
|
||||
"assumptions": [
|
||||
"Existing Creator API routes remain the source of truth",
|
||||
"Browser-local settings are disposable and do not affect domain behavior"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"Authenticated cabinet exposes navigation to all four human-facing sections",
|
||||
"read-only diagnostics checks session, advisory policy, capability, today, attention, actions and learning",
|
||||
"density, auto-refresh and reduced-motion preferences save and reset locally",
|
||||
"mobile layout remains readable",
|
||||
"existing Creator Web tests pass",
|
||||
"architecture and diff scope checks pass"
|
||||
],
|
||||
"required_tests": [
|
||||
"node --check cmd/creator-web/static/assets/console.js",
|
||||
"go test ./cmd/creator-web",
|
||||
"go test ./...",
|
||||
"go vet ./...",
|
||||
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"go run ./cmd/devctl check-architecture",
|
||||
"git diff --check",
|
||||
"responsive browser inspection"
|
||||
],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"task_id": "DEV-000075",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "39a27102c9665e41f29e85bc8f82a316bbbe3962",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make live conversation with Domovoy the public starting surface and keep the Creator operator console on a separate admin route.",
|
||||
"requirement_ids": [
|
||||
"Public root opens the human chat",
|
||||
"Operator console remains available at a separate admin route",
|
||||
"Existing /chat route remains compatible",
|
||||
"Chat does not expose provider, model, binding, session or workspace internals"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cmd/creator-web"],
|
||||
"paths": [
|
||||
"cmd/creator-web/server.go",
|
||||
"cmd/creator-web/server_test.go",
|
||||
"cmd/creator-web/chat_test.go",
|
||||
"cmd/creator-web/static/chat.html",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000075-creator-chat-primary-surface.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "deploy/**"]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {"consume": ["existing /chat and authenticated Creator endpoints"], "may_change": []},
|
||||
"semantic_owner": "none-interface-adapter",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["GET / -> chat surface", "GET /admin -> operator console surface"],
|
||||
"side_effects_and_privileges": ["No new privilege", "No canonical mutation", "No model/provider routing change"],
|
||||
"persistence_and_migrations": ["No persistence or migration"],
|
||||
"failure_retry_restart": ["Existing chat failure behavior remains unchanged"],
|
||||
"assumptions": ["/chat is the existing human chat surface", "The operator console may be reached at /admin"],
|
||||
"acceptance_scenarios": ["Root serves chat", "Admin serves console", "Chat compatibility remains", "Internal IDs remain absent from chat HTML"],
|
||||
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"task_id": "DEV-000076",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "26cf85108a63dfbd67866d9df0445dbcb54983be",
|
||||
"risk_tier": "E",
|
||||
"intent": "Show the human user whether Domovoy's primary cognitive channel is available without exposing provider internals or mixing technical events into the conversation.",
|
||||
"requirement_ids": [
|
||||
"Chat shows a human-readable cognitive channel status",
|
||||
"Status is read-only and refreshed periodically",
|
||||
"Chat does not expose provider, model or binding identifiers",
|
||||
"Chat remains usable while status observation fails"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cmd/creator-web"],
|
||||
"paths": [
|
||||
"cmd/creator-web/chat_test.go",
|
||||
"cmd/creator-web/static/chat.html",
|
||||
"cmd/creator-web/static/assets/chat.css",
|
||||
"cmd/creator-web/static/assets/chat.js",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000076-creator-chat-cognitive-status.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "deploy/**"]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {"consume": ["existing authenticated /api/capabilities/cognitive-proposal/health endpoint"], "may_change": []},
|
||||
"semantic_owner": "none-interface-adapter",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["Browser polls existing cognitive capability health observation", "Browser renders coarse human-readable status"],
|
||||
"side_effects_and_privileges": ["Read-only observation", "No new privilege", "No provider/model routing change"],
|
||||
"persistence_and_migrations": ["No persistence or migration"],
|
||||
"failure_retry_restart": ["Failed observation renders an unavailable/degraded notice; chat remains available", "Polling retries on the next interval"],
|
||||
"assumptions": ["The existing health endpoint returns State or state with AVAILABLE, DEGRADED or UNAVAILABLE semantics", "The chat token authorizes the existing health endpoint"],
|
||||
"acceptance_scenarios": ["Authenticated chat displays a status indicator", "Health polling does not reveal internal identifiers", "Mobile header keeps the status visible", "A failed health request does not disable the composer"],
|
||||
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"task_id": "DEV-000077",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "75abfb204bb427a0500a8c37440c374576c2153c",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make model configuration changes understandable from the human chat while keeping configuration mutation in the separate operator console.",
|
||||
"requirement_ids": [
|
||||
"User can manually re-check the cognitive channel from chat",
|
||||
"Chat clearly directs configuration changes to the operator console",
|
||||
"No configuration mutation is performed by the browser chat",
|
||||
"The conversation remains free of technical runtime identifiers"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cmd/creator-web"],
|
||||
"paths": [
|
||||
"cmd/creator-web/chat_test.go",
|
||||
"cmd/creator-web/static/chat.html",
|
||||
"cmd/creator-web/static/assets/chat.css",
|
||||
"cmd/creator-web/static/assets/chat.js",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000077-creator-chat-cognitive-bridge.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "deploy/**"]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {"consume": ["existing authenticated /api/capabilities/cognitive-proposal/health endpoint", "existing /admin operator surface"], "may_change": []},
|
||||
"semantic_owner": "none-interface-adapter",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["User requests a fresh read-only health observation", "Browser presents console handoff when the channel is not ready"],
|
||||
"side_effects_and_privileges": ["Read-only observation only", "No new privilege", "No provider/model routing change", "No chat mutation from status controls"],
|
||||
"persistence_and_migrations": ["No persistence or migration"],
|
||||
"failure_retry_restart": ["Manual check uses the same safe health route", "Polling remains the retry mechanism"],
|
||||
"assumptions": ["The operator console remains the authorized place for runtime configuration changes", "The existing health route is sufficient to observe readiness"],
|
||||
"acceptance_scenarios": ["Clicking status requests a fresh health observation", "Unavailable status exposes an operator-console handoff", "Available status hides the handoff", "Chat HTML remains free of internal identifiers"],
|
||||
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"task_id": "DEV-000078",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "0d10e6fd82a7c374bebbc8d3cee9c1ed0b420745",
|
||||
"risk_tier": "E",
|
||||
"intent": "Let Domovoy answer explicit questions about the local cognitive channel from the Capability-owned health observation and route configuration changes to the operator console without mutating runtime configuration.",
|
||||
"requirement_ids": [
|
||||
"Explicit model/channel questions use observed capability health",
|
||||
"Model configuration requests never call a provider directly from Core",
|
||||
"Configuration changes are explained as a separate operator-console action",
|
||||
"Provider and model identifiers remain hidden from human chat"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cmd/core-runtime"],
|
||||
"paths": [
|
||||
"cmd/core-runtime/main.go",
|
||||
"cmd/core-runtime/main_test.go",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000078-core-conversational-cognitive-health.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "deploy/**"]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {"consume": ["Capability repository LoadHealth", "existing cognitive-proposal health semantics", "existing /admin operator surface"], "may_change": []},
|
||||
"semantic_owner": "capability-observation-consumer",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["Human asks about local cognitive channel", "Core reads Capability-owned health observation", "Core returns observation or safe unavailable explanation"],
|
||||
"side_effects_and_privileges": ["No provider call for explicit status/configuration questions", "No runtime configuration mutation", "No new privilege"],
|
||||
"persistence_and_migrations": ["Existing utterance and semantic persistence only", "No migration"],
|
||||
"failure_retry_restart": ["Missing health observation produces an honest unavailable reply", "Future status polling remains available in chat UI"],
|
||||
"assumptions": ["The operator console is the authorized configuration surface", "Capability health is a sufficient provider-neutral observation for the first conversational response"],
|
||||
"acceptance_scenarios": ["Проверь локальную модель returns a human-readable health observation", "Переключи модель does not mutate configuration and directs to console", "Replies contain no provider/model/binding identifiers", "Unit tests cover available, unavailable and repository failure states"],
|
||||
"required_tests": ["go test ./cmd/core-runtime", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"task_id": "DEV-000079",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "b0d31012dac1526e721e57c691555933eb501a5a",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make the primary Domovoy chat self-explanatory with concise conversation starters for cognitive-channel checks and configuration handoff.",
|
||||
"requirement_ids": [
|
||||
"A new user can discover the main cognitive-channel conversation paths",
|
||||
"Conversation starters submit ordinary human text",
|
||||
"No starter exposes internal identifiers",
|
||||
"The chat remains usable on mobile"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cmd/creator-web"],
|
||||
"paths": [
|
||||
"cmd/creator-web/chat_test.go",
|
||||
"cmd/creator-web/static/chat.html",
|
||||
"cmd/creator-web/static/assets/chat.css",
|
||||
"cmd/creator-web/static/assets/chat.js",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000079-chat-conversation-starters.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "deploy/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_provider_adapters": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing chat message endpoint"], "may_change": []},
|
||||
"semantic_owner": "none-interface-adapter",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["User selects a conversation starter", "Browser submits its ordinary text through the existing chat flow"],
|
||||
"side_effects_and_privileges": ["Existing chat semantics only", "No new privilege", "No direct model or configuration mutation"],
|
||||
"persistence_and_migrations": ["Existing conversation persistence only", "No migration"],
|
||||
"failure_retry_restart": ["Starter uses existing chat error handling"],
|
||||
"assumptions": ["The user can still type any other request in the composer"],
|
||||
"acceptance_scenarios": ["Three human-readable starter actions are visible", "Clicking a starter sends its text through the normal chat flow", "No internal IDs appear in the page", "Starter layout remains usable at mobile width"],
|
||||
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"task_id": "DEV-000080",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "47fd0c4b027ff5ab2781b26c5bc4f7ddc45d961f",
|
||||
"risk_tier": "E",
|
||||
"intent": "Replace the unstable single-column chat presentation with a fixed three-column layout, explicit console return, and a safe human path for passing AI information into the current conversation.",
|
||||
"requirement_ids": [
|
||||
"Header controls keep stable dimensions",
|
||||
"Desktop chat has independently scrolling left, center and right regions",
|
||||
"Console has an explicit return-to-chat action",
|
||||
"User can pass non-secret AI notes as an ordinary chat message",
|
||||
"Mobile layout remains usable"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cmd/creator-web"],
|
||||
"paths": [
|
||||
"cmd/creator-web/chat_test.go",
|
||||
"cmd/creator-web/static/index.html",
|
||||
"cmd/creator-web/static/chat.html",
|
||||
"cmd/creator-web/static/assets/chat.css",
|
||||
"cmd/creator-web/static/assets/chat.js",
|
||||
"cmd/creator-web/static/assets/console.css",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000080-chat-three-column-layout.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "deploy/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_provider_adapters": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing chat message endpoint", "existing /admin route"], "may_change": []},
|
||||
"semantic_owner": "none-interface-adapter",
|
||||
"canonical_state": ["AI notes are submitted through the existing conversation; no new AI registry state is created"],
|
||||
"state_transitions": ["User submits non-secret AI notes as a normal chat message", "User navigates between chat and console using explicit links"],
|
||||
"side_effects_and_privileges": ["Existing chat persistence only", "No direct model/provider mutation", "No new privilege"],
|
||||
"persistence_and_migrations": ["No migration"],
|
||||
"failure_retry_restart": ["AI notes use existing chat failure handling", "Layout has no new runtime dependency"],
|
||||
"assumptions": ["The current conversation is the correct first destination for AI notes", "Sensitive credentials must never be pasted into chat"],
|
||||
"acceptance_scenarios": ["Desktop regions scroll independently", "Header status and actions do not resize when text changes", "Console visibly links back to chat", "AI notes form sends a clear message into chat", "Chat remains usable below 900px viewport width"],
|
||||
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user