Compare commits

..
Author SHA1 Message Date
m 924d8c3767 DEV-000068 activate runtime experience loop
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 16s
2026-08-17 18:35:46 +03:00
m 67ff1f73cd Merge DEV-000067 final production status
CI / test (push) Successful in 17s
DEV-000067 status promoted
2026-08-17 15:17:19 +00:00
m 0ad247bae3 DEV-000067 record production foundation status
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 16s
2026-08-17 18:16:43 +03:00
m 2de2885f2f Merge DEV-000067 notification receiver endpoint fix
CI / test (push) Successful in 15s
DEV-000067 follow-up promoted
2026-08-17 15:16:09 +00:00
m 069c3837f3 DEV-000067 correct notification receiver endpoint
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 16s
2026-08-17 18:15:32 +03:00
m 8a88565772 Merge DEV-000067 experience reflection skill evolution foundation
CI / test (push) Successful in 17s
DEV-000067 promoted via PR #53
2026-08-17 15:12:29 +00:00
m e7b32be1e0 DEV-000067 experience reflection skill foundation
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 16s
2026-08-17 18:11:23 +03:00
m d4640d7617 Promote DEV-000066 final production status
CI / test (push) Successful in 15s
DEV-000066 final production status docs-only
2026-08-17 14:44:38 +00:00
m 29b20bc5b0 DEV-000066 record production acceptance status
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 15s
2026-08-17 17:43:43 +03:00
m 071adb5ecb Promote DEV-000066 response candidate ordering
CI / test (push) Successful in 16s
DEV-000066 response candidate ordering follow-up
2026-08-17 14:38:15 +00:00
m 465a90b25d DEV-000066 prefer recent response candidates
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 16s
2026-08-17 17:37:20 +03:00
m 95d6ad2d6a Promote DEV-000066 response candidate window
CI / test (push) Successful in 19s
DEV-000066 response candidate window follow-up
2026-08-17 14:33:57 +00:00
m 1e554f6758 DEV-000066 widen response candidate resolution
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 16s
2026-08-17 17:33:05 +03:00
m 1c7d512710 Promote DEV-000066 prospective response summary resolution
CI / test (push) Successful in 16s
DEV-000066 prospective response summary resolution follow-up
2026-08-17 14:21:41 +00:00
m 8c6a7bebbd DEV-000066 resolve prospective responses by summary
CI / test (push) Successful in 17s
CI / test (pull_request) Successful in 14s
2026-08-17 17:20:44 +03:00
m 36774b37ae Promote DEV-000066 pre-delivery reminder responses
CI / test (push) Successful in 15s
DEV-000066 pre-delivery reminder response follow-up
2026-08-17 14:17:39 +00:00
m c79de1c757 DEV-000066 support pre-delivery reminder responses
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 19s
2026-08-17 17:16:29 +03:00
m acf012720f Promote DEV-000066 natural target resolution
CI / test (push) Successful in 15s
DEV-000066 natural target resolution follow-up
2026-08-17 14:11:51 +00:00
m ccf790d130 DEV-000066 natural response target resolution
CI / test (push) Successful in 17s
CI / test (pull_request) Successful in 16s
2026-08-17 17:11:10 +03:00
m 54c2cc27a3 Promote DEV-000066 human response continuation semantics
CI / test (push) Successful in 15s
DEV-000066 Human Response + Continuation Semantics V1
2026-08-17 14:04:11 +00:00
m 8b9b841645 DEV-000066 human response continuation semantics
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 16s
2026-08-17 17:02:33 +03:00
m e8e51ffdba DEV-000065 final project status
CI / test (push) Successful in 14s
Promote DEV-000065 final status docs.
2026-08-17 13:44:26 +00:00
m aa01e8e99b DEV-000065 final project status
CI / test (pull_request) Successful in 15s
CI / test (push) Successful in 21s
2026-08-17 16:43:33 +03:00
m 1113ced623 DEV-000065 answer attention from canonical today
CI / test (push) Successful in 15s
Promote DEV-000065 canonical attention chat answer.
2026-08-17 13:41:59 +00:00
42 changed files with 3111 additions and 64 deletions
@@ -218,6 +218,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,
+214 -14
View File
@@ -5,10 +5,12 @@ import (
"errors"
"fmt"
"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"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -145,17 +147,33 @@ func (r *ContinuityRepository) ClaimDueReminder(ctx context.Context, id continui
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,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,
action_attempt_id,delivery_outcome,revision)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,0,'','','','',1)
ON CONFLICT (source_kind, source_prospective_item_id) DO UPDATE SET source_prospective_item_id=EXCLUDED.source_prospective_item_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)
ON CONFLICT (source_kind, source_prospective_item_id) DO UPDATE SET
state=EXCLUDED.state,
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,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,revision`,
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.Level, &attention.State, &attention.Summary, &attention.DueAtUnixMillis, &attention.CreatedAtUnixMillis,
&attention.DeliveredAtUnixMillis, &attention.ActionIntentID, &attention.AuthorizedActionID, &attention.ActionAttemptID,
&attention.DeliveryOutcome, &attention.Revision)
&attention.DeliveryOutcome, &attention.ResponseID, &attention.ResponseUtteranceID, &attention.ResponseOperation,
&attention.ResponseOutcome, &attention.RespondedAtUnixMillis, &attention.Revision)
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, fmt.Errorf("create due reminder attention: %w", err)
}
@@ -170,7 +188,8 @@ func (r *ContinuityRepository) PendingAttention(ctx context.Context, limit int)
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,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision
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)
if err != nil {
return nil, fmt.Errorf("list pending attention: %w", err)
@@ -184,7 +203,8 @@ func (r *ContinuityRepository) AttentionByOwner(ctx context.Context, owner ident
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,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision
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)
if err != nil {
return nil, fmt.Errorf("list attention by owner: %w", err)
@@ -193,16 +213,171 @@ func (r *ContinuityRepository) AttentionByOwner(ctx context.Context, owner ident
return scanAttentionRows(rows)
}
func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner identity.PrincipalID, limit int) ([]continuitydomain.ResponseCandidate, error) {
if owner == "" || limit < 1 || limit > 50 {
return nil, fmt.Errorf("invalid response candidate query")
}
rows, err := r.pool.Query(ctx, `SELECT
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.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.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 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)
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, 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(
&attentionID, &attentionOwner, &attentionScope, &sourceKind, &sourceProspective, &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),
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 {
return nil, fmt.Errorf("list human response candidates: %w", err)
}
return candidates, nil
}
func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response continuity.HumanResponse) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
if _, err = tx.Exec(ctx, `INSERT INTO continuity.human_responses (
human_response_id,owner_principal_id,scope_id,source_utterance_id,source_intent_id,operation,target_kind,
target_attention_id,target_prospective_item_id,target_external_ref,resolution_state,outcome,clarification,
snooze_until_unix_millis,user_reported_not_verified,recorded_at_unix_millis,revision)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)`,
response.ID, response.OwnerPrincipalID, response.ScopeID, response.SourceUtteranceID, response.SourceIntentID,
response.Operation, response.TargetKind, response.TargetAttentionID, response.TargetProspectiveID, response.TargetExternalRef,
response.ResolutionState, response.Outcome, response.Clarification, response.SnoozeUntilUnixMillis,
response.UserReportedNotVerified, response.RecordedAtUnixMillis, response.Revision); err != nil {
return fmt.Errorf("save human response: %w", err)
}
if response.ResolutionState != continuity.ResponseResolved {
return tx.Commit(ctx)
}
switch response.Operation {
case continuity.ResponseComplete:
tag, err := tx.Exec(ctx, `UPDATE continuity.prospective_items SET state=$1, revision=revision+1
WHERE prospective_item_id=$2 AND owner_principal_id=$3 AND state IN ($4,$5)`,
continuity.ProspectiveSatisfied, response.TargetProspectiveID, response.OwnerPrincipalID,
continuity.ProspectiveDelivered, continuity.ProspectiveArmed)
if err != nil {
return fmt.Errorf("complete response prospective: %w", err)
}
if tag.RowsAffected() != 1 {
return ErrProspectiveStateConflict
}
if response.TargetAttentionID == "" {
return ErrAttentionNotFound
}
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
WHERE prospective_item_id=$3 AND owner_principal_id=$4 AND state IN ($5,$6)`,
continuity.ProspectiveArmed, response.SnoozeUntilUnixMillis, response.TargetProspectiveID,
response.OwnerPrincipalID, continuity.ProspectiveDelivered, continuity.ProspectiveArmed)
if err != nil {
return fmt.Errorf("snooze response prospective: %w", err)
}
if tag.RowsAffected() != 1 {
return ErrProspectiveStateConflict
}
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
WHERE prospective_item_id=$2 AND owner_principal_id=$3 AND state IN ($4,$5)`,
continuity.ProspectiveSuppressed, response.TargetProspectiveID, response.OwnerPrincipalID,
continuity.ProspectiveDelivered, continuity.ProspectiveArmed)
if err != nil {
return fmt.Errorf("cancel response prospective: %w", err)
}
if tag.RowsAffected() != 1 {
return ErrProspectiveStateConflict
}
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
}
}
return tx.Commit(ctx)
}
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,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
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.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.Revision)
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID,
&item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.AttentionItem{}, ErrAttentionRevisionConflict
}
@@ -216,12 +391,14 @@ func (r *ContinuityRepository) MarkAttentionDelivered(ctx context.Context, id co
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,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
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.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.Revision)
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID,
&item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.AttentionItem{}, ErrAttentionRevisionConflict
}
@@ -235,12 +412,14 @@ func (r *ContinuityRepository) MarkAttentionFailed(ctx context.Context, id conti
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,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
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.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.Revision)
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID,
&item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.AttentionItem{}, ErrAttentionRevisionConflict
}
@@ -316,12 +495,32 @@ type continuityRowQuerier interface {
QueryRow(context.Context, string, ...any) pgx.Row
}
type continuityExecutor interface {
Exec(context.Context, string, ...any) (pgconn.CommandTag, error)
}
type continuityRows interface {
Next() bool
Scan(...any) error
Err() error
}
func markAttentionResponded(ctx context.Context, executor continuityExecutor, response continuity.HumanResponse, state continuity.AttentionState) error {
tag, err := executor.Exec(ctx, `UPDATE continuity.attention_items
SET state=$1, response_id=$2, response_utterance_id=$3, response_operation=$4, response_outcome=$5,
responded_at_unix_millis=$6, revision=revision+1
WHERE attention_item_id=$7 AND owner_principal_id=$8 AND state IN ($9,$10,$11)`,
state, response.ID, response.SourceUtteranceID, response.Operation, response.Outcome, response.RecordedAtUnixMillis,
response.TargetAttentionID, response.OwnerPrincipalID, continuity.AttentionDelivered, continuity.AttentionPending, continuity.AttentionFailed)
if err != nil {
return fmt.Errorf("mark attention responded: %w", err)
}
if tag.RowsAffected() != 1 {
return ErrAttentionStateConflict
}
return nil
}
func scanAttentionRows(rows continuityRows) ([]continuity.AttentionItem, error) {
items := make([]continuity.AttentionItem, 0)
for rows.Next() {
@@ -329,7 +528,8 @@ func scanAttentionRows(rows continuityRows) ([]continuity.AttentionItem, error)
if err := rows.Scan(&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID,
&item.Level, &item.State, &item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis,
&item.DeliveredAtUnixMillis, &item.ActionIntentID, &item.AuthorizedActionID, &item.ActionAttemptID,
&item.DeliveryOutcome, &item.Revision); err != nil {
&item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID, &item.ResponseOperation,
&item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision); err != nil {
return nil, fmt.Errorf("scan attention item: %w", err)
}
items = append(items, item)
+108
View File
@@ -0,0 +1,108 @@
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) {
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)
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
}
@@ -0,0 +1,42 @@
# ADR-0015 — Human Response and Continuation Semantics V1
Status: Accepted
Task: DEV-000066
## Context
DEV-000065 made Domovoy present beyond an HTTP request: reminders can become due, produce Continuity-owned Attention, and deliver a governed notification. The next step is not a reminder command set; it is a general way to interpret a human response to an existing context.
Examples include: a reminder after notification, waiting work requiring a decision, a future email thread, or a future capability gap/acquisition context. V1 implements reminders as the first real target while preserving extensible typed target references for future owners.
## Decision
1. Continuity owns `HumanResponse` and `ResponseTargetResolution` V1 because they continue durable waits/attention and may mutate Continuity-owned future obligation state.
2. Interaction remains the owner of the original `HumanUtterance` and `OriginalHumanIntent`; Continuity stores references to those records, not copies of their authority.
3. V1 response operations are typed but extensible: `ACKNOWLEDGE`, `COMPLETE`, `SNOOZE`, `CANCEL`, `DISMISS`, `CONTINUE`, `REJECT`, plus `UNKNOWN` for fail-closed interpretation.
4. V1 target references are typed: `ATTENTION_ITEM`, `PROSPECTIVE_ITEM`, `WORK_TASK`, `CAPABILITY_GAP`, `INTERACTION_CONTEXT`, and `UNKNOWN`. DEV-000066 implements only Continuity-owned reminder/attention mutations.
5. Obvious deterministic responses may execute without cognition. Ambiguous responses must ask for clarification and record an append-only response record without mutating the target.
6. A human response is not automatic verified world truth. For reminder completion, V1 records `USER_REPORTED_COMPLETE`; this satisfies the reminder according to policy but does not create a verified external-world fact.
7. UI buttons, if present, must post the same semantic response text/API path as natural chat. They are projections, not a separate command system.
## Boundaries
- Continuity writes only Continuity-owned response, attention, and prospective state.
- Interaction writes conversations/utterances/intents before Continuity response handling.
- No external mutation or Commit/Executor operation is introduced in DEV-000066.
- No model output directly mutates canonical state.
- Future Codex/mail/capability-gap continuations are representable by typed references, but not implemented.
## Non-goals
- No Codex Bridge, Gmail, MCP, voice, mobile, browser automation, self-extension loop, or generalized orchestration.
- No universal object ontology.
- No reminder-specific parallel subsystem.
## Consequences
- “Готово”, “Отложи”, “Отмени” can continue the latest obvious reminder/attention context without internal IDs.
- Snooze re-arms the same `ProspectiveItem` with a new due time, preserving audit history.
- Cancel suppresses the future obligation so the due scanner will not deliver it.
- Today/Attention can show active, snoozed, completed, and cancelled human-readable states.
@@ -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.
+10 -3
View File
@@ -84,7 +84,10 @@
"Observation",
"Claim",
"Belief",
"Provenance"
"Provenance",
"SystemExpectation",
"ExperienceRecord",
"ReflectionCandidate"
],
"database_schema": "epistemics",
"boundary_class": "remote_capable",
@@ -99,7 +102,8 @@
"owns": [
"Commitment",
"ProspectiveItem",
"AttentionItem"
"AttentionItem",
"HumanResponse"
],
"database_schema": "continuity",
"boundary_class": "remote_capable",
@@ -121,7 +125,10 @@
"CapabilitySelection",
"CapabilityGap",
"AcquisitionStrategyCandidate",
"CapabilityUsageObservation"
"CapabilityUsageObservation",
"SkillProfile",
"SkillEvidence",
"FastPathDecision"
],
"database_schema": "capability",
"boundary_class": "remote_capable",
+3 -3
View File
@@ -34,21 +34,21 @@ modules:
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]
continuity:
id: mod.continuity
owns: [Commitment, ProspectiveItem, AttentionItem]
owns: [Commitment, ProspectiveItem, AttentionItem, HumanResponse]
database_schema: continuity
boundary_class: remote_capable
may_depend_on: [contracts.work, contracts.identity, contracts.authority, platform.events]
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]
+13
View File
@@ -54,6 +54,7 @@ func registerCreatorAttentionRoutes(mux *http.ServeMux, identityClient creatorId
HumanState: humanAttentionState(item.State),
DeliveryState: humanDeliveryState(item),
SourceDescription: humanAttentionSource(item.SourceKind),
ResponseHint: humanResponseHint(item),
})
}
w.Header().Set("Content-Type", "application/json")
@@ -83,6 +84,8 @@ func humanAttentionState(state continuity.AttentionState) string {
return "доставляется"
case continuity.AttentionDelivered:
return "доставлено"
case continuity.AttentionResolved:
return "закрыто"
case continuity.AttentionSuppressed:
return "скрыто"
case continuity.AttentionExpired:
@@ -94,6 +97,16 @@ func humanAttentionState(state continuity.AttentionState) string {
}
}
func humanResponseHint(item continuity.AttentionItem) string {
if item.State == continuity.AttentionDelivered || item.State == continuity.AttentionPending || item.State == continuity.AttentionFailed {
return "Можно ответить: «Готово», «Отложи на 10 минут» или «Отмени»."
}
if item.ResponseOutcome != "" {
return "Ответ записан: " + string(item.ResponseOutcome)
}
return ""
}
func humanDeliveryState(item continuity.AttentionItem) string {
if item.State == continuity.AttentionDelivered {
return "уведомление отправлено"
+66
View File
@@ -0,0 +1,66 @@
package main
import (
"context"
"encoding/json"
"errors"
"net/http"
"time"
"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"
"git.cin.su/m/Assistant/platform/ids"
)
type responseRepository interface {
ResponseCandidates(context.Context, identity.PrincipalID, int) ([]continuitydomain.ResponseCandidate, error)
ApplyHumanResponse(context.Context, continuity.HumanResponse) error
}
func registerCreatorResponseRoute(mux *http.ServeMux, identityClient creatorIdentityClient, repository responseRepository, now func() time.Time) {
mux.HandleFunc("POST /v1/creator/responses", func(w http.ResponseWriter, r *http.Request) {
session, err := identityClient.Session(r.Context(), r.Header.Get("Authorization"))
if errors.Is(err, errCreatorSessionDenied) {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
if err != nil {
http.Error(w, "identity service unavailable", http.StatusServiceUnavailable)
return
}
var request continuity.HumanResponseRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid human response", http.StatusBadRequest)
return
}
responseID, err := ids.New()
if err != nil {
http.Error(w, "human response unavailable", http.StatusServiceUnavailable)
return
}
candidates, err := repository.ResponseCandidates(r.Context(), session.PrincipalID, 20)
if err != nil {
http.Error(w, "response candidates unavailable", http.StatusInternalServerError)
return
}
result, err := continuitydomain.InterpretHumanResponse(request, session.PrincipalID, authority.ScopeID("domovoy-global-scope"), continuity.HumanResponseID(responseID), candidates, now())
if err != nil {
http.Error(w, "invalid human response", http.StatusBadRequest)
return
}
if result.Handled {
if err := repository.ApplyHumanResponse(r.Context(), result.Response); err != nil {
http.Error(w, "apply human response", 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(result)
})
}
@@ -0,0 +1,80 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
continuitydomain "git.cin.su/m/Assistant/modules/continuity/domain"
)
func TestCreatorResponseEndpointAppliesCompleteThroughContinuitySemantics(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer ok" {
t.Fatal("unexpected identity validation")
}
_ = 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: "attention-1", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", SourceKind: continuity.AttentionSourceProspectiveReminder, SourceProspectiveID: "prospective-1", State: continuity.AttentionDelivered, Summary: "проверить базу", Revision: 1},
Prospective: continuity.ProspectiveItem{ID: "prospective-1", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", State: continuity.ProspectiveDelivered, Summary: "проверить базу", Revision: 1},
}}}
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":"Готово","RequestedAtUnixMillis":100000}`))
request.Header.Set("Authorization", "Bearer ok")
recorder := httptest.NewRecorder()
mux.ServeHTTP(recorder, request)
if recorder.Code != http.StatusCreated || repository.applied.Operation != continuity.ResponseComplete || repository.applied.Outcome != continuity.ResponseOutcomeUserReportedComplete {
t.Fatalf("status=%d applied=%#v body=%s", recorder.Code, repository.applied, recorder.Body.String())
}
}
func TestCreatorResponseEndpointRecordsAmbiguousWithoutSilentTargetMutation(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: "attention-1", SourceProspectiveID: "prospective-1", State: continuity.AttentionDelivered, Summary: "первое"}},
{Attention: continuity.AttentionItem{ID: "attention-2", SourceProspectiveID: "prospective-2", 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":"Готово","RequestedAtUnixMillis":100000}`))
request.Header.Set("Authorization", "Bearer ok")
recorder := httptest.NewRecorder()
mux.ServeHTTP(recorder, request)
if recorder.Code != http.StatusCreated || repository.applied.ResolutionState != continuity.ResponseAmbiguous || repository.applied.TargetAttentionID != "" {
t.Fatalf("status=%d applied=%#v body=%s", recorder.Code, repository.applied, recorder.Body.String())
}
}
type fakeResponseRepository struct {
candidates []continuitydomain.ResponseCandidate
applied continuity.HumanResponse
}
func (f *fakeResponseRepository) ResponseCandidates(context.Context, identity.PrincipalID, int) ([]continuitydomain.ResponseCandidate, error) {
return f.candidates, nil
}
func (f *fakeResponseRepository) ApplyHumanResponse(_ context.Context, response continuity.HumanResponse) error {
f.applied = response
return nil
}
+1
View File
@@ -43,6 +43,7 @@ func main() {
defer cancelWorker()
dueReminderWorker{repository: repository, commit: commitClient, now: time.Now, newID: ids.New, interval: 5 * time.Second, limit: 10}.Start(workerContext)
registerInternalExecutorRoutes(mux, repository, executorToken)
registerCreatorResponseRoute(mux, identityClient, repository, time.Now)
mux.HandleFunc("POST /v1/prospective-items", func(w http.ResponseWriter, r *http.Request) {
var item continuity.ProspectiveItem
if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
+50 -3
View File
@@ -28,6 +28,11 @@ type continuityReminderClient struct {
client *http.Client
}
type continuityResponseClient 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 == "" {
@@ -39,6 +44,17 @@ func newContinuityReminderClient(rawURL string, client *http.Client) (continuity
return continuityReminderClient{baseURL: base, client: client}, nil
}
func newContinuityResponseClient(rawURL string, client *http.Client) (continuityResponseClient, error) {
base, err := url.Parse(rawURL)
if err != nil || base.Scheme == "" || base.Host == "" {
return continuityResponseClient{}, fmt.Errorf("invalid Continuity Runtime URL")
}
if client == nil {
client = http.DefaultClient
}
return continuityResponseClient{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"
@@ -70,6 +86,34 @@ func (c continuityReminderClient) Schedule(ctx context.Context, authorization st
return result, nil
}
func (c continuityResponseClient) Respond(ctx context.Context, authorization string, request continuity.HumanResponseRequest) (continuity.HumanResponseResult, error) {
target := *c.baseURL
target.Path = "/v1/creator/responses"
payload, err := json.Marshal(request)
if err != nil {
return continuity.HumanResponseResult{}, err
}
upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return continuity.HumanResponseResult{}, err
}
upstream.Header.Set("Content-Type", "application/json")
upstream.Header.Set("Authorization", authorization)
response, err := c.client.Do(upstream)
if err != nil {
return continuity.HumanResponseResult{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusCreated {
return continuity.HumanResponseResult{}, fmt.Errorf("continuity response status %d", response.StatusCode)
}
var result continuity.HumanResponseResult
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return continuity.HumanResponseResult{}, err
}
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 {
@@ -100,10 +144,10 @@ 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 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{
@@ -120,7 +164,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) {
+314
View File
@@ -0,0 +1,314 @@
package main
import (
"context"
"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
}
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 || r.epistemics.SaveSystemExpectation(ctx, expectation) != nil {
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,
SkillRef: epistemics.SkillRef(reminderResponseSkillID),
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(),
}
saved, ok := r.saveExperienceAndReflection(ctx, experience)
if ok {
r.updateReminderSkill(ctx, saved, capability.UsageFailed, true)
}
}
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) saveExperienceAndReflection(ctx context.Context, value epistemics.ExperienceRecord) (epistemics.ExperienceRecord, bool) {
value, err := epistemicsdomain.NewExperienceRecord(value)
if err != nil {
return epistemics.ExperienceRecord{}, false
}
if err := r.epistemics.SaveExperienceRecord(ctx, value); err != nil {
return epistemics.ExperienceRecord{}, false
}
if candidate, ok, err := epistemicsdomain.ReflectionForExperience(value); err == nil && ok {
_ = r.epistemics.SaveReflectionCandidate(ctx, candidate)
}
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 {
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 {
return
}
if err := r.capability.SaveSkillProfile(ctx, next); err != nil {
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
}
}
+81 -4
View File
@@ -68,6 +68,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)
@@ -90,6 +103,11 @@ func main() {
if err != nil {
log.Fatal(err)
}
responseClient, err := newContinuityResponseClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * 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())
@@ -168,6 +186,42 @@ 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 {
http.Error(w, "learning experience unavailable", http.StatusServiceUnavailable)
return
}
reflections, err := epistemicsRepository.RecentReflectionCandidates(r.Context(), limit)
if err != nil {
http.Error(w, "learning reflection unavailable", http.StatusServiceUnavailable)
return
}
skills, err := capabilityRepository.RecentSkillProfiles(r.Context(), limit)
if err != nil {
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 {
@@ -235,6 +289,15 @@ func main() {
http.Error(w, "save chat semantics", http.StatusInternalServerError)
return
}
responseResult, responseErr := responseClient.Respond(r.Context(), r.Header.Get("Authorization"), continuity.HumanResponseRequest{
SourceUtteranceID: string(utterance.ID),
SourceIntentID: string(intent.ID),
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 {
@@ -244,7 +307,7 @@ func main() {
}
activeFocusName = semantic.ActiveFocus.ProjectDisplayName
activeFocusProvenance = semantic.ActiveFocus.Provenance
} else if previousFocus != nil {
} else if len(semantic.Projects) > 0 && semantic.Projects[0].Provenance == situation.ProjectResolutionInheritedFocus && previousFocus != nil {
activeFocusName = previousFocus.ProjectDisplayName
activeFocusProvenance = previousFocus.Provenance
}
@@ -254,9 +317,16 @@ func main() {
projectID := ""
if semantic.ActiveFocus != nil {
projectID = string(semantic.ActiveFocus.ProjectID)
} else if previousFocus != nil {
} else if len(semantic.Projects) > 0 && semantic.Projects[0].Provenance == situation.ProjectResolutionInheritedFocus {
projectID = string(previousFocus.ProjectID)
}
if responseErr == nil && responseResult.Handled {
planningReply = responseResult.Reply
needsCognition = false
learning.recordReminderResponse(r.Context(), reminderResponseLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, At: now})
} 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),
UtteranceID: work.SourceUtteranceRef(utterance.ID),
@@ -273,6 +343,9 @@ func main() {
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
}
helpPath := workdomain.DetermineHelpPath(planningInput)
if responseErr == nil && responseResult.Handled {
helpPath = work.HelpPath{}
}
if isCapabilityQuestion(utterance.Content) {
kinds, err := capabilityRepository.AvailableCapabilityKinds(r.Context(), session.PrincipalID)
if err == nil && len(kinds) > 0 {
@@ -303,10 +376,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 позвонить врачу»."
@@ -315,6 +390,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
}
@@ -327,6 +403,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
}
+2
View File
@@ -536,6 +536,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) {
+3
View File
@@ -59,6 +59,9 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.attention-card time { color: var(--signal); font: 12px/1 var(--mono); }
.attention-card h2 { margin: 10px 0 8px; font: 400 24px/1.1 var(--serif); }
.attention-card p, .history-item p { margin: 0 0 10px; color: var(--ink); line-height: 1.45; }
.attention-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.attention-actions button { height: 34px; padding: 0 12px; border: 1px solid var(--line); background: #151e1a; color: var(--ink); font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
.attention-actions button:first-child { border-color: var(--good); color: var(--good); }
.history-item.user { border-color: #34443c; background: #151e1a; }
.history-item strong { display: block; margin-bottom: 8px; color: var(--signal); }
.empty-state { margin: 24px 0 0; color: var(--muted); }
+47 -20
View File
@@ -48,7 +48,8 @@ function appendMessage(kind, text, meta = "") {
function updateActiveFocus(value, provenance = "") {
if (!activeFocus) return;
activeFocus.textContent = value ? `фокус: ${value}${provenance ? ` · ${provenance}` : ""}` : "фокус: не выбран";
activeFocus.textContent = value ? `Сейчас обсуждаем: ${value}` : "";
activeFocus.hidden = !value;
}
async function api(path, options = {}) {
@@ -121,7 +122,27 @@ async function loadToday() {
state.textContent = item.HumanState || item.human_state || "ожидает внимания";
const source = document.createElement("small");
source.textContent = [item.SourceDescription || item.source_description, item.DeliveryState || item.delivery_state].filter(Boolean).join(" · ");
card.append(when, title, state, source);
const hintText = item.ResponseHint || item.response_hint || "";
const actions = document.createElement("div");
actions.className = "attention-actions";
for (const [label, text] of [["Готово", "Готово"], ["Отложить", "Отложи на 10 минут"], ["Отменить", "Отмени"]]) {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
button.addEventListener("click", async () => {
await sendChatMessage(text, {silentUser: false});
await loadToday();
activateTab("chat-panel");
});
actions.appendChild(button);
}
if (hintText) {
const hint = document.createElement("small");
hint.textContent = hintText;
card.append(when, title, state, source, hint, actions);
} else {
card.append(when, title, state, source, actions);
}
todayList.appendChild(card);
}
} catch {
@@ -133,6 +154,29 @@ async function loadToday() {
}
}
async function sendChatMessage(text, options = {}) {
const clean = text.trim();
if (!clean) return;
if (!options.silentUser) appendMessage("user", clean);
sendButton.disabled = true;
try {
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean})});
conversationID = result.conversation_id;
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
updateActiveFocus(result.active_focus, result.project_provenance);
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result));
if (/что сейчас требует моего внимания/i.test(clean)) {
await loadToday();
activateTab("today-panel");
}
} catch {
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
} finally {
sendButton.disabled = false;
messageInput.focus();
}
}
function activateTab(panelID) {
for (const tab of navTabs) tab.classList.toggle("active", tab.dataset.tab === panelID);
for (const panel of document.querySelectorAll(".surface-panel")) {
@@ -163,25 +207,8 @@ chatForm.addEventListener("submit", async (event) => {
event.preventDefault();
const text = messageInput.value.trim();
if (!text) return;
appendMessage("user", text);
messageInput.value = "";
sendButton.disabled = true;
try {
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: text})});
conversationID = result.conversation_id;
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
updateActiveFocus(result.active_focus, result.project_provenance);
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result));
if (/что сейчас требует моего внимания/i.test(text)) {
await loadToday();
activateTab("today-panel");
}
} catch {
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
} finally {
sendButton.disabled = false;
messageInput.focus();
}
await sendChatMessage(text);
});
for (const tab of navTabs) {
+25 -2
View File
@@ -141,9 +141,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 +169,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; }
@@ -281,7 +300,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; }
+106 -1
View File
@@ -84,7 +84,10 @@ async function showConsole() {
await loadAdvisoryPolicy();
const context = saveContext();
if (context["session-id"] && context["workspace-id"]) loadDashboard();
else loadActionHistory();
else {
loadActionHistory();
loadLearning();
}
}
function endSession(message = "Сессия завершена.") {
@@ -147,6 +150,7 @@ contextForm.addEventListener("submit", (event) => {
byId("refresh-all").addEventListener("click", loadDashboard);
byId("refresh-actions").addEventListener("click", loadActionHistory);
byId("refresh-learning").addEventListener("click", loadLearning);
byId("logout").addEventListener("click", () => endSession());
advisoryForm.addEventListener("submit", async (event) => {
@@ -234,6 +238,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 +246,106 @@ 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 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 = error.message;
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");
+24
View File
@@ -143,6 +143,30 @@
</div>
</section>
<section class="learning-panel" aria-labelledby="learning-title">
<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 class="data-grid">
<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>
+61
View File
@@ -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
}
+86
View File
@@ -15,6 +15,8 @@ type AttentionRevision uint64
type CommittedActionIntentID ids.ID
type CommittedAuthorizedActionID ids.ID
type CommittedActionAttemptID ids.ID
type HumanResponseID ids.ID
type HumanResponseRevision uint64
type ProspectiveState string
@@ -46,6 +48,7 @@ const (
AttentionPending AttentionState = "PENDING"
AttentionDelivering AttentionState = "DELIVERING"
AttentionDelivered AttentionState = "DELIVERED"
AttentionResolved AttentionState = "RESOLVED"
AttentionSuppressed AttentionState = "SUPPRESSED"
AttentionExpired AttentionState = "EXPIRED"
AttentionFailed AttentionState = "FAILED"
@@ -106,6 +109,11 @@ type AttentionItem struct {
AuthorizedActionID CommittedAuthorizedActionID
ActionAttemptID CommittedActionAttemptID
DeliveryOutcome string
ResponseID HumanResponseID
ResponseUtteranceID string
ResponseOperation HumanResponseOperation
ResponseOutcome ResponseOutcome
RespondedAtUnixMillis int64
Revision AttentionRevision
}
@@ -121,12 +129,90 @@ type TodayItem struct {
HumanState string
DeliveryState string
SourceDescription string
ResponseHint string
}
type TodayResult struct {
Items []TodayItem
}
type HumanResponseOperation string
const (
ResponseAcknowledge HumanResponseOperation = "ACKNOWLEDGE"
ResponseComplete HumanResponseOperation = "COMPLETE"
ResponseSnooze HumanResponseOperation = "SNOOZE"
ResponseCancel HumanResponseOperation = "CANCEL"
ResponseDismiss HumanResponseOperation = "DISMISS"
ResponseContinue HumanResponseOperation = "CONTINUE"
ResponseReject HumanResponseOperation = "REJECT"
ResponseUnknown HumanResponseOperation = "UNKNOWN"
)
type ResponseTargetKind string
const (
ResponseTargetAttentionItem ResponseTargetKind = "ATTENTION_ITEM"
ResponseTargetProspectiveItem ResponseTargetKind = "PROSPECTIVE_ITEM"
ResponseTargetWorkTask ResponseTargetKind = "WORK_TASK"
ResponseTargetCapabilityGap ResponseTargetKind = "CAPABILITY_GAP"
ResponseTargetInteractionContext ResponseTargetKind = "INTERACTION_CONTEXT"
ResponseTargetUnknown ResponseTargetKind = "UNKNOWN"
)
type ResponseResolutionState string
const (
ResponseResolved ResponseResolutionState = "RESOLVED"
ResponseAmbiguous ResponseResolutionState = "AMBIGUOUS"
ResponseNoTarget ResponseResolutionState = "NO_TARGET"
ResponseNoop ResponseResolutionState = "NOOP"
)
type ResponseOutcome string
const (
ResponseOutcomeUserReportedComplete ResponseOutcome = "USER_REPORTED_COMPLETE"
ResponseOutcomeSnoozed ResponseOutcome = "SNOOZED"
ResponseOutcomeCancelled ResponseOutcome = "CANCELLED"
ResponseOutcomeDismissed ResponseOutcome = "DISMISSED"
ResponseOutcomeClarificationNeeded ResponseOutcome = "CLARIFICATION_NEEDED"
ResponseOutcomeNoop ResponseOutcome = "NOOP"
)
type HumanResponse struct {
ID HumanResponseID
OwnerPrincipalID identity.PrincipalID
ScopeID authority.ScopeID
SourceUtteranceID string
SourceIntentID string
Operation HumanResponseOperation
TargetKind ResponseTargetKind
TargetAttentionID AttentionItemID
TargetProspectiveID ProspectiveItemID
TargetExternalRef string
ResolutionState ResponseResolutionState
Outcome ResponseOutcome
Clarification string
SnoozeUntilUnixMillis int64
UserReportedNotVerified bool
RecordedAtUnixMillis int64
Revision HumanResponseRevision
}
type HumanResponseRequest struct {
SourceUtteranceID string
SourceIntentID string
Message string
RequestedAtUnixMillis int64
}
type HumanResponseResult struct {
Response HumanResponse
Handled bool
Reply string
}
// SatisfyDeliveredSubject is the complete serializable operation subject. It
// is distinct from a Work TaskTarget, which remains the live justification.
type SatisfyDeliveredSubject struct {
+114
View File
@@ -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
}
+45 -2
View File
@@ -594,17 +594,60 @@ CREATE TABLE IF NOT EXISTS continuity.attention_items (
authorized_action_id TEXT NOT NULL DEFAULT '',
action_attempt_id TEXT NOT NULL DEFAULT '',
delivery_outcome TEXT NOT NULL DEFAULT '',
response_id TEXT NOT NULL DEFAULT '',
response_utterance_id TEXT NOT NULL DEFAULT '',
response_operation TEXT NOT NULL DEFAULT '',
response_outcome TEXT NOT NULL DEFAULT '',
responded_at_unix_millis BIGINT NOT NULL DEFAULT 0,
revision BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (source_kind, source_prospective_item_id),
CHECK (source_kind IN ('PROSPECTIVE_REMINDER')),
CHECK (level IN ('URGENT','ACTION_REQUIRED','DUE','IMPORTANT_CHANGE','FYI','BACKGROUND')),
CHECK (state IN ('PENDING','DELIVERING','DELIVERED','SUPPRESSED','EXPIRED','FAILED')),
CHECK (state IN ('PENDING','DELIVERING','DELIVERED','RESOLVED','SUPPRESSED','EXPIRED','FAILED')),
CHECK (summary <> ''),
CHECK (due_at_unix_millis > 0),
CHECK (created_at_unix_millis > 0),
CHECK (revision > 0),
CHECK (state <> 'DELIVERED' OR (delivered_at_unix_millis > 0 AND action_intent_id <> '' AND authorized_action_id <> '' AND action_attempt_id <> '' AND delivery_outcome <> ''))
CHECK (state <> 'DELIVERED' OR (delivered_at_unix_millis > 0 AND action_intent_id <> '' AND authorized_action_id <> '' AND action_attempt_id <> '' AND delivery_outcome <> '')),
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','NOOP') AND
responded_at_unix_millis > 0
))
);
CREATE TABLE IF NOT EXISTS continuity.human_responses (
human_response_id TEXT PRIMARY KEY,
owner_principal_id TEXT NOT NULL,
scope_id TEXT NOT NULL,
source_utterance_id TEXT NOT NULL,
source_intent_id TEXT NOT NULL,
operation TEXT NOT NULL,
target_kind TEXT NOT NULL,
target_attention_id TEXT NOT NULL DEFAULT '',
target_prospective_item_id TEXT NOT NULL DEFAULT '',
target_external_ref TEXT NOT NULL DEFAULT '',
resolution_state TEXT NOT NULL,
outcome TEXT NOT NULL,
clarification TEXT NOT NULL DEFAULT '',
snooze_until_unix_millis BIGINT NOT NULL DEFAULT 0,
user_reported_not_verified BOOLEAN NOT NULL DEFAULT FALSE,
recorded_at_unix_millis BIGINT NOT NULL,
revision BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (owner_principal_id <> ''),
CHECK (scope_id <> ''),
CHECK (source_utterance_id <> ''),
CHECK (source_intent_id <> ''),
CHECK (operation IN ('ACKNOWLEDGE','COMPLETE','SNOOZE','CANCEL','DISMISS','CONTINUE','REJECT','UNKNOWN')),
CHECK (target_kind IN ('ATTENTION_ITEM','PROSPECTIVE_ITEM','WORK_TASK','CAPABILITY_GAP','INTERACTION_CONTEXT','UNKNOWN')),
CHECK (resolution_state IN ('RESOLVED','AMBIGUOUS','NO_TARGET','NOOP')),
CHECK (outcome IN ('USER_REPORTED_COMPLETE','SNOOZED','CANCELLED','DISMISSED','CLARIFICATION_NEEDED','NOOP')),
CHECK (recorded_at_unix_millis > 0),
CHECK (revision > 0),
CHECK (operation <> 'SNOOZE' OR snooze_until_unix_millis > recorded_at_unix_millis),
CHECK (resolution_state <> 'RESOLVED' OR target_kind <> 'UNKNOWN')
);
RESET ROLE;
@@ -0,0 +1,84 @@
BEGIN;
SET ROLE continuity_role;
CREATE TABLE IF NOT EXISTS continuity.human_responses (
human_response_id TEXT PRIMARY KEY,
owner_principal_id TEXT NOT NULL,
scope_id TEXT NOT NULL,
source_utterance_id TEXT NOT NULL,
source_intent_id TEXT NOT NULL,
operation TEXT NOT NULL,
target_kind TEXT NOT NULL,
target_attention_id TEXT NOT NULL DEFAULT '',
target_prospective_item_id TEXT NOT NULL DEFAULT '',
target_external_ref TEXT NOT NULL DEFAULT '',
resolution_state TEXT NOT NULL,
outcome TEXT NOT NULL,
clarification TEXT NOT NULL DEFAULT '',
snooze_until_unix_millis BIGINT NOT NULL DEFAULT 0,
user_reported_not_verified BOOLEAN NOT NULL DEFAULT FALSE,
recorded_at_unix_millis BIGINT NOT NULL,
revision BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (owner_principal_id <> ''),
CHECK (scope_id <> ''),
CHECK (source_utterance_id <> ''),
CHECK (source_intent_id <> ''),
CHECK (operation IN ('ACKNOWLEDGE','COMPLETE','SNOOZE','CANCEL','DISMISS','CONTINUE','REJECT','UNKNOWN')),
CHECK (target_kind IN ('ATTENTION_ITEM','PROSPECTIVE_ITEM','WORK_TASK','CAPABILITY_GAP','INTERACTION_CONTEXT','UNKNOWN')),
CHECK (resolution_state IN ('RESOLVED','AMBIGUOUS','NO_TARGET','NOOP')),
CHECK (outcome IN ('USER_REPORTED_COMPLETE','SNOOZED','CANCELLED','DISMISSED','CLARIFICATION_NEEDED','NOOP')),
CHECK (recorded_at_unix_millis > 0),
CHECK (revision > 0),
CHECK (operation <> 'SNOOZE' OR snooze_until_unix_millis > recorded_at_unix_millis),
CHECK (resolution_state <> 'RESOLVED' OR target_kind <> 'UNKNOWN')
);
ALTER TABLE continuity.attention_items
ADD COLUMN IF NOT EXISTS response_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS response_utterance_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS response_operation TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS response_outcome TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS responded_at_unix_millis BIGINT NOT NULL DEFAULT 0;
DO $$
DECLARE
constraint_record RECORD;
BEGIN
FOR constraint_record IN
SELECT conname
FROM pg_constraint
WHERE conrelid = 'continuity.attention_items'::regclass
AND contype = 'c'
AND pg_get_constraintdef(oid) LIKE '%state%'
AND pg_get_constraintdef(oid) LIKE '%DELIVERED%'
AND pg_get_constraintdef(oid) LIKE '%FAILED%'
LOOP
EXECUTE format('ALTER TABLE continuity.attention_items DROP CONSTRAINT %I', constraint_record.conname);
END LOOP;
END $$;
ALTER TABLE continuity.attention_items
ADD CONSTRAINT attention_items_state_check
CHECK (state IN ('PENDING','DELIVERING','DELIVERED','RESOLVED','SUPPRESSED','EXPIRED','FAILED'));
ALTER TABLE continuity.attention_items
ADD CONSTRAINT attention_items_delivered_evidence_check
CHECK (state <> 'DELIVERED' OR (delivered_at_unix_millis > 0 AND action_intent_id <> '' AND authorized_action_id <> '' AND action_attempt_id <> '' AND delivery_outcome <> ''));
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','NOOP')
AND responded_at_unix_millis > 0
)
);
RESET ROLE;
COMMIT;
@@ -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;
+4
View File
@@ -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
+7
View File
@@ -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
@@ -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" }
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000065-assistant-presence-attention-human-surface.json",
"task_id": "DEV-000065",
"task_file": "dev/tasks/DEV-000068-runtime-experience-reflection-loop.json",
"task_id": "DEV-000068",
"task_revision": 1
}
@@ -0,0 +1,144 @@
{
"task_id": "DEV-000066",
"task_revision": 1,
"architecture_revision": "ADR-0015",
"base_repo_revision": "e8e51ffdba38ab901e275283e46f2b45bb6affe9",
"risk_tier": "E",
"intent": "Add provider-neutral human response and continuation semantics for existing context, with reminders/attention as the first implementation and no reminder-specific parallel subsystem.",
"requirement_ids": [
"ADR-0015",
"INV-AUTH-001",
"INV-OWN-001",
"INV-CONT-001",
"INV-EPI-001",
"Human response is not automatic world truth",
"Contextual response target resolution without internal IDs"
,
"ActiveProjectFocus is contextual evidence, not a global user mode"
],
"allowed_change_scope": {
"modules": [
"continuity",
"interaction"
],
"paths": [
"architecture/adrs/ADR-0015-human-response-continuation-semantics.md",
"architecture/architecture-registry.yaml",
"architecture/architecture-registry.json",
"contracts/continuity/**",
"contracts/interaction/**",
"modules/continuity/**",
"modules/interaction/**",
"adapters/postgres/**",
"database/bootstrap/**",
"database/migrations/**",
"cmd/continuity-runtime/**",
"cmd/core-runtime/**",
"cmd/creator-web/**",
"tests/owner_acl/**",
"tests/cognitive_advisory/**",
"docs/PROJECT_STATUS.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000066-human-response-continuation-semantics.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",
"Continuity ProspectiveItem",
"Continuity AttentionItem"
],
"may_change": [
"contracts/continuity@v1"
]
},
"semantic_owner": "continuity",
"canonical_state": [
"HumanResponse",
"ResponseTargetResolution",
"ProspectiveItem",
"AttentionItem"
],
"state_transitions": [
"single obvious delivered reminder attention + COMPLETE -> ProspectiveItem SATISFIED and Attention RESOLVED",
"single obvious delivered reminder attention + SNOOZE -> same ProspectiveItem ARMED at new due time and Attention RESOLVED",
"single obvious reminder/attention + CANCEL -> ProspectiveItem SUPPRESSED and Attention SUPPRESSED when present",
"ambiguous target -> clarification response record and no semantic mutation"
],
"side_effects_and_privileges": [
"No external mutation is introduced",
"Human responses mutate only Continuity-owned state through Continuity runtime",
"User-reported completion is recorded as USER_REPORTED_COMPLETE, not verified world truth",
"No cognitive model is required for obvious deterministic response operations",
"Docker-test remains prohibited"
],
"persistence_and_migrations": [
"continuity.human_responses append-only table",
"Attention response closure fields",
"no cross-owner FK",
"no cross-owner SQL join dependency"
],
"failure_retry_restart": [
"HumanResponse rows are append-only audit/history",
"Ambiguous target resolution fails closed with clarification",
"Snoozed reminders are re-armed in canonical prospective state and survive restart",
"Cancelled/suppressed reminders are excluded by the existing due scanner"
],
"assumptions": [
"DEV-000065 is production baseline",
"Continuity remains canonical owner for future obligation continuation",
"Interaction remains canonical owner of utterances and original human intent",
"Buttons are optional UI projection over the same chat response endpoint"
],
"acceptance_scenarios": [
"Single active reminder plus 'Готово' resolves the correct target",
"Single active reminder plus 'Отложи на час' reschedules the same prospective item",
"'Отмени' suppresses the reminder and prevents future delivery",
"Two possible targets plus ambiguous 'Отмени' asks clarification and does not mutate",
"Historical utterances remain append-only",
"Attention closes appropriately",
"User-reported completion does not become verified world fact",
"Obvious response operations work LLM-free",
"Cognition unavailable does not break obvious operations",
"UI buttons and natural language use the same backend semantics",
"Future non-reminder response target kinds can be represented"
,
"Unrelated new human intents do not inherit stale ActiveProjectFocus",
"Human UI does not expose technical project provenance labels"
],
"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 complete/snooze/cancel acceptance",
"production /chat and /today verification"
],
"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-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,138 @@
{
"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.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"
]
}
+47 -6
View File
@@ -1,22 +1,63 @@
# Assistant project status
Audit date: 2026-08-17. Active implementation: DEV-000065 revision 1.
Audit date: 2026-08-17. Active implementation: DEV-000067 revision 1.
## DEV-000067 checkpoint
- `DEV_000067_STATUS = PRODUCTION_FOUNDATION_ACTIVE`
- `LAST_COMPLETED = ADR-0016 accepted; no new Learning owner; Epistemics-owned SystemExpectation / ExperienceRecord / ReflectionCandidate contracts; Capability-owned SkillProfile / FastPathDecision lifecycle contracts; append-only migration 023; owner-local regression tests for positive reminder experience, user correction reflection, fast-path decline, and repeated failure skill degradation`
- `ADR_STATUS = ADR-0016 accepted`
- `OWNERSHIP_STATUS = no new owner; Experience/Expectation/Reflection belong to Epistemics; Skill/FastPath lifecycle belongs to Capability; Interaction/Continuity/Work/Capability state is referenced by typed refs only`
- `LEARNING_BOUNDARY_STATUS = no LearningManager, no self-modifying loop, no prompt optimizer, no automatic source/deployment/Authority mutation`
- `EXPERIENCE_STATUS = can represent OriginalHumanIntent, HumanUtterance, context refs, help method, capability usage/gap refs, expectation, actual outcome, HumanResponse/correction refs, verification evidence`
- `REFLECTION_STATUS = deterministic ReflectionCandidate foundation exists; future cognitive reflection must route through Cognitive Gateway/Router and produce validated candidates only`
- `SKILL_STATUS = SkillProfile lifecycle CANDIDATE/EVALUATING/VALIDATED/DEGRADED/SUSPENDED/RETIRED; fast path can decline with NOT_APPLICABLE/LOW_CONFIDENCE/OUT_OF_DISTRIBUTION/CONFLICT/STALE and return to thinking`
- `EVIDENCE_RETENTION_STATUS = skill degradation/replacement preserves supporting and failure experience refs; no source evidence deletion path introduced`
- `RUNTIME_STATUS = persistence foundation is present; production runtime credential wiring for Epistemics writes is intentionally not expanded in this checkpoint`
- `NOTIFICATION_RECEIVER_STATUS = local receiver endpoint corrected to canonical assistant.cin.su spelling`
- `PRODUCTION_SCHEMA_STATUS = migration 023 applied; epistemics system_expectations / experience_records / reflection_candidates present; capability skill_profiles / skill_profile_heads present; seed skill skill-obvious-reminder-response-v1 VALIDATED`
- `TESTS_PASS = 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; notification receiver no-display poll; production health; production /chat HTTP 200`
- `PRODUCTION_STATUS = healthy`
- `PRODUCTION_REVISION = origin/main 2de2885f2f143d41ce0d9cf09ca12a6e5751c52c; Docker runtime artifact not rebuilt because new runtime paths are not activated and migration/script/status were sufficient`
- `ACTIVE_COGNITIVE_BINDINGS = none; CPU CANARY unchanged`
- `OPENAI_REAL_CREDENTIAL = not required / not activated`
## DEV-000066 checkpoint
- `DEV_000066_STATUS = PRODUCTION_ACTIVE`
- `LAST_COMPLETED = ADR-0015 accepted; Continuity-owned HumanResponse contract foundation; ResponseTargetResolution semantics; append-only human_responses migration; Attention response projection fields; deterministic COMPLETE/SNOOZE/CANCEL/DISMISS interpretation; Creator Web response buttons over ordinary /chat; ActiveProjectFocus leak regression under Interaction semantics`
- `ADR_STATUS = ADR-0015 accepted`
- `OWNERSHIP_STATUS = no new owner; HumanResponse and response target resolution belong to Continuity; Interaction remains utterance/original-intent owner`
- `REMINDER_RESPONSE_STATUS = COMPLETE records USER_REPORTED_COMPLETE, SNOOZE rearms same ProspectiveItem, CANCEL suppresses reminder; ambiguous target asks clarification`
- `CONTEXT_STATUS = ActiveProjectFocus treated as contextual evidence, not global mode; unrelated new intents do not inherit stale project focus`
- `HUMAN_SURFACE_STATUS = /chat remains primary; Today buttons send natural language through the same backend semantics`
- `AUTHORITY_STATUS = no new external mutation authority; human reported completion is not verified world truth`
- `LLM_FREE_STATUS = obvious response operations are deterministic and do not require cognitive model`
- `TESTS_PASS = 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`
- `LIVE_ACCEPTANCE = PASS: external /chat created real reminders; SNOOZE after delivery re-armed the same ProspectiveItem and redelivered once without duplicate AttentionItem; COMPLETE after delivery closed Attention as USER_REPORTED_COMPLETE; CANCEL before delivery suppressed ProspectiveItem and produced no AttentionItem after due window; stale UPO ActiveProjectFocus cleared`
- `PRODUCTION_STATUS = healthy`
- `PRODUCTION_REVISION = 071adb5ecbf31325678a557c371c512c486d8274`
- `ACTIVE_COGNITIVE_BINDINGS = none; CPU CANARY unchanged`
- `OPENAI_REAL_CREDENTIAL = not required / not activated`
## DEV-000065 checkpoint
- `DEV_000065_STATUS = IN_PROGRESS`
- `DEV_000065_STATUS = COMPLETED`
- `LAST_COMPLETED = ADR-0014 accepted; Continuity-owned AttentionItem contract and persistence; due reminder scanner; Commit internal prospective notification endpoint; human-facing Chat / Today / History surface; relative reminder parsing for "через N минут"; deterministic due-delivery regression tests; notification binding origin corrected to assistant.cin.su revision 2`
- `ADR_STATUS = ADR-0014 accepted`
- `OWNERSHIP_STATUS = no new owner; AttentionItem belongs to Continuity; notification delivery remains Commit/Capability/receiver path`
- `REMINDER_LIFECYCLE_STATUS = ARMED -> TRIGGERED -> REVALIDATING -> DELIVERED implemented for due scanner`
- `REMINDER_DELIVERY_STATUS = implemented through Commit deliver-prospective-notification-v1 path; production live verification pending`
- `REMINDER_DELIVERY_STATUS = PASS: production live delayed reminder delivered through Commit deliver-prospective-notification-v1 and immutable receiver receipt`
- `ATTENTION_STATUS = typed AttentionItem PENDING -> DELIVERING -> DELIVERED foundation implemented`
- `HUMAN_SURFACE_STATUS = /chat remains primary; Today/Attention and History added; Operator Console preserved at /`
- `AUTHORITY_STATUS = external notification still requires Commit authorization, current Creator authority context, and active Capability notification binding`
- `LLM_FREE_STATUS = deterministic reminder firing does not require cognitive model; LLM-free env policy regression PASS`
- `TESTS_PASS_SO_FAR = 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`
- `PRODUCTION_STATUS = pending deploy/live acceptance`
- `NEXT_STEP = apply migration, deploy affected services, run owner ACL and live delayed reminder acceptance, then promote if PASS`
- `TESTS_PASS = 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 delayed reminder acceptance; production /chat attention and capability-gap acceptance`
- `LIVE_ACCEPTANCE = PASS: "Напомни мне через 3 минуты проверить уведомление." persisted ARMED reminder, transitioned to DELIVERED, generated AttentionItem, delivered notification through Commit, persisted SUCCEEDED attempt and receiver receipt, duplicate counts action_attempts=1 attention_items=1; "Отправь Ивану письмо." returned readable capability gap; "Что сейчас требует моего внимания?" answered from canonical Today with cognition=not_requested`
- `PRODUCTION_STATUS = healthy`
- `PRODUCTION_REVISION = origin/main 1113ced623896f1498d606653d71f36c361e9bb0; creator-web deployed at 1113ced623896f1498d606653d71f36c361e9bb0; continuity-runtime deployed at 2ec53e40cf19c609193c1aa74955573cac7abfe1; core-runtime/commit-runtime deployed at a0dce634f3c6efec0481f730c9dcc12797749ec4`
- `KNOWN_LIMITATION = one early DEV-000065 live probe occurred before notification binding origin revision 2 and produced UNKNOWN_OUTCOME; later regression prevents UNKNOWN_OUTCOME from being marked as delivered`
- `NEXT_STEP = Human response operations for attention/reminders: Done/Snooze/Cancel semantic commands, if separately approved`
## DEV-000064 checkpoint
+125
View File
@@ -0,0 +1,125 @@
package domain
import (
"errors"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
)
var ErrInvalidSkillProfile = errors.New("invalid skill profile")
var ErrInvalidFastPathInput = errors.New("invalid fast path input")
func ValidateSkillProfile(value capability.SkillProfile) error {
if value.ID == "" || value.Revision < 1 || strings.TrimSpace(value.Name) == "" ||
strings.TrimSpace(value.Description) == "" || value.Status == "" ||
value.ConfidenceMillis < 0 || value.ConfidenceMillis > 1000 || value.RecordedAtUnixMillis < 1 {
return ErrInvalidSkillProfile
}
switch value.Status {
case capability.SkillCandidate, capability.SkillEvaluating, capability.SkillValidated, capability.SkillDegraded, capability.SkillSuspended, capability.SkillRetired:
default:
return ErrInvalidSkillProfile
}
return nil
}
func ObserveSkillOutcome(profile capability.SkillProfile, observation capability.SkillOutcomeObservation) (capability.SkillProfile, error) {
if err := ValidateSkillProfile(profile); err != nil {
return capability.SkillProfile{}, err
}
if observation.ExperienceRef == "" || observation.ObservedAtUnixMillis < 1 {
return capability.SkillProfile{}, ErrInvalidSkillProfile
}
next := profile
next.Revision++
next.RecordedAtUnixMillis = observation.ObservedAtUnixMillis
switch observation.Outcome {
case capability.UsageSucceeded:
next.SupportingExperienceRefs = appendEvidenceRef(next.SupportingExperienceRefs, observation.ExperienceRef)
if next.Status == capability.SkillCandidate || next.Status == capability.SkillEvaluating {
next.Status = capability.SkillValidated
}
next.LastValidatedUnixMillis = observation.ObservedAtUnixMillis
next.ConfidenceMillis = clampConfidence(next.ConfidenceMillis + 25)
case capability.UsageFailed, capability.UsageUnknown:
next.KnownFailureExperienceRefs = appendEvidenceRef(next.KnownFailureExperienceRefs, observation.ExperienceRef)
next.ObservedFailureCount++
next.ConfidenceMillis = clampConfidence(next.ConfidenceMillis - 150)
case capability.UsageSkipped:
next.KnownFailureExperienceRefs = appendEvidenceRef(next.KnownFailureExperienceRefs, observation.ExperienceRef)
next.ConfidenceMillis = clampConfidence(next.ConfidenceMillis - 25)
default:
return capability.SkillProfile{}, ErrInvalidSkillProfile
}
if observation.UserCorrected {
next.ObservedCorrectionCount++
next.KnownFailureExperienceRefs = appendEvidenceRef(next.KnownFailureExperienceRefs, observation.ExperienceRef)
next.ConfidenceMillis = clampConfidence(next.ConfidenceMillis - 200)
}
if next.Status == capability.SkillValidated && (next.ObservedFailureCount >= 3 || next.ObservedCorrectionCount >= 2 || next.ConfidenceMillis < 500) {
next.Status = capability.SkillDegraded
}
if next.Status == capability.SkillDegraded && next.ConfidenceMillis < 250 {
next.Status = capability.SkillSuspended
}
return next, nil
}
func DecideFastPath(profile capability.SkillProfile, supported bool, confidenceMillis int64, environmentVersionChanged bool, conflict bool, observedAtUnixMillis int64) (capability.FastPathDecision, error) {
if err := ValidateSkillProfile(profile); err != nil {
return capability.FastPathDecision{}, err
}
if confidenceMillis < 0 || confidenceMillis > 1000 || observedAtUnixMillis < 1 {
return capability.FastPathDecision{}, ErrInvalidFastPathInput
}
decision := capability.FastPathDecision{
SkillProfileID: profile.ID,
Decision: capability.FastPathApplied,
Explanation: "validated fast path applied",
ObservedAtUnixMillis: observedAtUnixMillis,
}
switch {
case profile.Status == capability.SkillSuspended || profile.Status == capability.SkillRetired:
return decline(decision, capability.FastPathStale, "skill profile is not eligible for fast-path use"), nil
case environmentVersionChanged:
return decline(decision, capability.FastPathStale, "environment or implementation version changed since validation"), nil
case conflict:
return decline(decision, capability.FastPathConflict, "input conflicts with known fast-path assumptions"), nil
case !supported:
return decline(decision, capability.FastPathOutOfDistribution, "input is outside supported semantics"), nil
case confidenceMillis < 700 || profile.ConfidenceMillis < 700:
return decline(decision, capability.FastPathLowConfidence, "confidence is too low for optimized path"), nil
case profile.Status != capability.SkillValidated:
return decline(decision, capability.FastPathNotApplicable, "skill is not validated"), nil
default:
return decision, nil
}
}
func decline(decision capability.FastPathDecision, reason capability.FastPathDeclineReason, explanation string) capability.FastPathDecision {
decision.Decision = capability.FastPathEscalate
decision.DeclineReason = reason
decision.RequiresCognition = true
decision.Explanation = explanation
return decision
}
func appendEvidenceRef(values []capability.SkillEvidenceRef, ref capability.SkillEvidenceRef) []capability.SkillEvidenceRef {
for _, value := range values {
if value == ref {
return values
}
}
return append(values, ref)
}
func clampConfidence(value int64) int64 {
if value < 0 {
return 0
}
if value > 1000 {
return 1000
}
return value
}
+61
View File
@@ -0,0 +1,61 @@
package domain
import (
"testing"
"git.cin.su/m/Assistant/contracts/capability"
)
func TestRepeatedCapabilityFailuresDegradeSkillWithoutDeletingHistory(t *testing.T) {
profile := skillProfile()
profile.SupportingExperienceRefs = []capability.SkillEvidenceRef{"experience:success-1"}
for _, ref := range []capability.SkillEvidenceRef{"experience:failure-1", "experience:failure-2", "experience:failure-3"} {
next, err := ObserveSkillOutcome(profile, capability.SkillOutcomeObservation{ExperienceRef: ref, Outcome: capability.UsageFailed, ObservedAtUnixMillis: profile.RecordedAtUnixMillis + 100})
if err != nil {
t.Fatal(err)
}
profile = next
}
if profile.Status != capability.SkillDegraded {
t.Fatalf("expected degraded skill, got %#v", profile)
}
if len(profile.SupportingExperienceRefs) != 1 || len(profile.KnownFailureExperienceRefs) != 3 {
t.Fatalf("skill degradation must preserve success/failure evidence: %#v", profile)
}
}
func TestFastPathDeclinesUnsupportedInputAndEscalatesToThinking(t *testing.T) {
decision, err := DecideFastPath(skillProfile(), false, 900, false, false, 2000)
if err != nil {
t.Fatal(err)
}
if decision.Decision != capability.FastPathEscalate || decision.DeclineReason != capability.FastPathOutOfDistribution || !decision.RequiresCognition {
t.Fatalf("unexpected fast path decision: %#v", decision)
}
}
func TestValidatedSkillCanApplyFastPathWhenFreshAndConfident(t *testing.T) {
decision, err := DecideFastPath(skillProfile(), true, 950, false, false, 2000)
if err != nil {
t.Fatal(err)
}
if decision.Decision != capability.FastPathApplied || decision.RequiresCognition {
t.Fatalf("unexpected fast path decision: %#v", decision)
}
}
func skillProfile() capability.SkillProfile {
return capability.SkillProfile{
ID: "skill-obvious-reminder-response-v1",
Revision: 1,
Name: "Resolve obvious reminder continuation",
Description: "Use a recent unambiguous reminder/attention target for complete/snooze/cancel without cognition.",
ApplicableContext: []string{"continuity:reminder-response"},
ImplementationRef: "continuity:human-response-target-resolver-v1",
ImplementationVersion: "1",
Status: capability.SkillValidated,
ConfidenceMillis: 850,
LastValidatedUnixMillis: 1000,
RecordedAtUnixMillis: 1000,
}
}
+212
View File
@@ -0,0 +1,212 @@
package domain
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/authority"
contract "git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
)
var ErrInvalidHumanResponse = errors.New("invalid human response")
type ResponseCandidate struct {
Attention contract.AttentionItem
Prospective contract.ProspectiveItem
}
func InterpretHumanResponse(request contract.HumanResponseRequest, owner identity.PrincipalID, scope authority.ScopeID, id contract.HumanResponseID, candidates []ResponseCandidate, now time.Time) (contract.HumanResponseResult, error) {
message := strings.TrimSpace(request.Message)
if id == "" || owner == "" || scope == "" || request.SourceUtteranceID == "" || request.SourceIntentID == "" || message == "" || request.RequestedAtUnixMillis < 1 {
return contract.HumanResponseResult{}, ErrInvalidHumanResponse
}
operation, snoozeUntil := detectResponseOperation(message, now)
response := contract.HumanResponse{
ID: id,
OwnerPrincipalID: owner,
ScopeID: scope,
SourceUtteranceID: strings.TrimSpace(request.SourceUtteranceID),
SourceIntentID: strings.TrimSpace(request.SourceIntentID),
Operation: operation,
TargetKind: contract.ResponseTargetUnknown,
ResolutionState: contract.ResponseNoop,
Outcome: contract.ResponseOutcomeNoop,
SnoozeUntilUnixMillis: snoozeUntil.UnixMilli(),
RecordedAtUnixMillis: now.UnixMilli(),
Revision: 1,
UserReportedNotVerified: false,
}
if operation == contract.ResponseUnknown || operation == contract.ResponseContinue || operation == contract.ResponseReject {
return contract.HumanResponseResult{Response: response, Handled: false}, nil
}
if len(candidates) == 0 {
response.ResolutionState = contract.ResponseNoTarget
response.Outcome = contract.ResponseOutcomeClarificationNeeded
response.Clarification = "Я не вижу активного ожидания, к которому это относится."
return contract.HumanResponseResult{Response: response, Handled: true, Reply: response.Clarification}, nil
}
if len(candidates) > 1 {
matched := matchingCandidates(message, candidates)
if len(matched) == 1 {
candidates = matched
}
}
if len(candidates) > 1 {
response.ResolutionState = contract.ResponseAmbiguous
response.Outcome = contract.ResponseOutcomeClarificationNeeded
response.Clarification = "У меня есть несколько возможных ожиданий. Уточните, о каком именно речь: " + candidateSummary(candidates)
return contract.HumanResponseResult{Response: response, Handled: true, Reply: response.Clarification}, nil
}
candidate := candidates[0]
if candidate.Attention.ID != "" {
response.TargetKind = contract.ResponseTargetAttentionItem
response.TargetAttentionID = candidate.Attention.ID
response.TargetProspectiveID = candidate.Attention.SourceProspectiveID
} else {
response.TargetKind = contract.ResponseTargetProspectiveItem
response.TargetProspectiveID = candidate.Prospective.ID
}
response.ResolutionState = contract.ResponseResolved
switch operation {
case contract.ResponseComplete:
response.Outcome = contract.ResponseOutcomeUserReportedComplete
response.UserReportedNotVerified = true
return contract.HumanResponseResult{Response: response, Handled: true, Reply: "Готово, я закрыл это напоминание. Записал как «сообщено человеком», без внешней проверки."}, nil
case contract.ResponseSnooze:
if response.SnoozeUntilUnixMillis <= now.UnixMilli() {
response.ResolutionState = contract.ResponseNoop
response.Outcome = contract.ResponseOutcomeClarificationNeeded
response.Clarification = "На сколько отложить? Например: «Отложи на 10 минут»."
return contract.HumanResponseResult{Response: response, Handled: true, Reply: response.Clarification}, nil
}
response.Outcome = contract.ResponseOutcomeSnoozed
return contract.HumanResponseResult{Response: response, Handled: true, Reply: "Отложил. Напомню " + humanResponseTime(snoozeUntil) + "."}, nil
case contract.ResponseCancel:
response.Outcome = contract.ResponseOutcomeCancelled
return contract.HumanResponseResult{Response: response, Handled: true, Reply: "Отменил. Больше не буду напоминать об этом."}, nil
case contract.ResponseAcknowledge, contract.ResponseDismiss:
response.Outcome = contract.ResponseOutcomeDismissed
return contract.HumanResponseResult{Response: response, Handled: true, Reply: "Принял, убрал это из текущего внимания."}, nil
default:
response.ResolutionState = contract.ResponseNoop
response.Outcome = contract.ResponseOutcomeNoop
return contract.HumanResponseResult{Response: response, Handled: false}, nil
}
}
func detectResponseOperation(message string, now time.Time) (contract.HumanResponseOperation, time.Time) {
lower := strings.ToLower(strings.TrimSpace(message))
if containsAny(lower, "отложи", "перенеси", "позже", "потом") {
if until, ok := parseSnoozeUntil(lower, now); ok {
return contract.ResponseSnooze, until
}
return contract.ResponseSnooze, time.Time{}
}
if containsAny(lower, "отмени", "отменить", "не надо", "не нужно", "больше не напоминай") {
return contract.ResponseCancel, time.Time{}
}
if containsAny(lower, "готово", "сделал", "сделала", "выполнено", "закрыл", "закрыла") {
return contract.ResponseComplete, time.Time{}
}
if lower == "ок" || lower == "понял" || lower == "поняла" || lower == "принято" {
return contract.ResponseAcknowledge, time.Time{}
}
if containsAny(lower, "продолжай", "дальше") {
return contract.ResponseContinue, time.Time{}
}
if containsAny(lower, "отклоняю", "нет", "не согласен", "не согласна") {
return contract.ResponseReject, time.Time{}
}
return contract.ResponseUnknown, time.Time{}
}
func parseSnoozeUntil(lower string, now time.Time) (time.Time, bool) {
relative := regexp.MustCompile(`(?:через|на)\s+(\d{1,3})\s*(минуту|минуты|минут|час|часа|часов)`)
if match := relative.FindStringSubmatch(lower); len(match) > 0 {
value, err := strconv.Atoi(match[1])
if err != nil || value < 1 {
return time.Time{}, false
}
if strings.HasPrefix(match[2], "час") {
if value > 72 {
return time.Time{}, false
}
return now.Add(time.Duration(value) * time.Hour), true
}
if value > 24*60 {
return time.Time{}, false
}
return now.Add(time.Duration(value) * time.Minute), true
}
if strings.Contains(lower, "позже") || strings.Contains(lower, "потом") {
return now.Add(time.Hour), true
}
return time.Time{}, false
}
func candidateSummary(candidates []ResponseCandidate) string {
parts := make([]string, 0, len(candidates))
for _, candidate := range candidates {
text := strings.TrimSpace(candidate.Attention.Summary)
if text == "" {
text = strings.TrimSpace(candidate.Prospective.Summary)
}
if text == "" {
text = "ожидание"
}
parts = append(parts, text)
}
if len(parts) > 3 {
parts = parts[:3]
}
return strings.Join(parts, "; ")
}
func matchingCandidates(message string, candidates []ResponseCandidate) []ResponseCandidate {
lower := strings.ToLower(message)
var matched []ResponseCandidate
for _, candidate := range candidates {
summary := candidate.Attention.Summary
if summary == "" {
summary = candidate.Prospective.Summary
}
if summaryMatches(lower, summary) {
matched = append(matched, candidate)
}
}
return matched
}
func summaryMatches(lowerMessage string, summary string) bool {
for _, token := range strings.Fields(strings.ToLower(summary)) {
token = strings.Trim(token, ".,!?:;()[]«»\"'")
if len([]rune(token)) < 4 {
continue
}
if strings.Contains(lowerMessage, token) {
return true
}
}
return false
}
func humanResponseTime(value time.Time) string {
if value.IsZero() {
return "позже"
}
return fmt.Sprintf("%02d.%02d в %02d:%02d", value.Day(), value.Month(), value.Hour(), value.Minute())
}
func containsAny(value string, needles ...string) bool {
for _, needle := range needles {
if strings.Contains(value, needle) {
return true
}
}
return false
}
@@ -0,0 +1,125 @@
package domain
import (
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
)
func TestInterpretHumanResponseCompletesSingleDeliveredReminder(t *testing.T) {
result := interpretResponseForTest(t, "Готово", []ResponseCandidate{responseCandidate("a1", "p1", "позвонить врачу")})
if !result.Handled || result.Response.Operation != continuity.ResponseComplete || result.Response.Outcome != continuity.ResponseOutcomeUserReportedComplete || !result.Response.UserReportedNotVerified {
t.Fatalf("unexpected complete response: %#v", result)
}
}
func TestInterpretHumanResponseSnoozesSingleReminder(t *testing.T) {
result := interpretResponseForTest(t, "Отложи на 10 минут", []ResponseCandidate{responseCandidate("a1", "p1", "проверить базу")})
if !result.Handled || result.Response.Operation != continuity.ResponseSnooze || result.Response.Outcome != continuity.ResponseOutcomeSnoozed {
t.Fatalf("unexpected snooze response: %#v", result)
}
if result.Response.SnoozeUntilUnixMillis != time.Unix(100, 0).Add(10*time.Minute).UnixMilli() {
t.Fatalf("unexpected snooze time: %d", result.Response.SnoozeUntilUnixMillis)
}
}
func TestInterpretHumanResponseCancelsSingleReminder(t *testing.T) {
result := interpretResponseForTest(t, "Отмени", []ResponseCandidate{responseCandidate("a1", "p1", "встретить курьера")})
if !result.Handled || result.Response.Operation != continuity.ResponseCancel || result.Response.Outcome != continuity.ResponseOutcomeCancelled {
t.Fatalf("unexpected cancel response: %#v", result)
}
}
func TestInterpretHumanResponseCancelsArmedProspectiveBeforeAttention(t *testing.T) {
result := interpretResponseForTest(t, "Отмени встретить курьера", []ResponseCandidate{{
Prospective: continuity.ProspectiveItem{ID: "p1", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "встретить курьера", State: continuity.ProspectiveArmed, Revision: 1},
}})
if !result.Handled || result.Response.TargetKind != continuity.ResponseTargetProspectiveItem || result.Response.TargetProspectiveID != "p1" || result.Response.Outcome != continuity.ResponseOutcomeCancelled {
t.Fatalf("unexpected prospective cancel response: %#v", result)
}
}
func TestInterpretHumanResponseResolvesArmedProspectiveByNaturalSummaryReference(t *testing.T) {
result := interpretResponseForTest(t, "Отмени встретить курьера", []ResponseCandidate{
{Prospective: continuity.ProspectiveItem{ID: "p1", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "позвонить врачу", State: continuity.ProspectiveArmed, Revision: 1}},
{Prospective: continuity.ProspectiveItem{ID: "p2", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "встретить курьера", State: continuity.ProspectiveArmed, Revision: 1}},
})
if !result.Handled || result.Response.TargetKind != continuity.ResponseTargetProspectiveItem || result.Response.TargetProspectiveID != "p2" || result.Response.Outcome != continuity.ResponseOutcomeCancelled {
t.Fatalf("unexpected prospective natural target response: %#v", result)
}
}
func TestInterpretHumanResponseResolvesSummaryReferenceBeyondFirstThreeCandidates(t *testing.T) {
candidates := []ResponseCandidate{
responseCandidate("a1", "p1", "первое ожидание"),
responseCandidate("a2", "p2", "второе ожидание"),
responseCandidate("a3", "p3", "третье ожидание"),
{Prospective: continuity.ProspectiveItem{ID: "p4", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "встретить курьера", State: continuity.ProspectiveArmed, Revision: 1}},
}
result := interpretResponseForTest(t, "Отмени встретить курьера", candidates)
if !result.Handled || result.Response.ResolutionState != continuity.ResponseResolved || result.Response.TargetProspectiveID != "p4" {
t.Fatalf("expected target resolution beyond first three candidates, got %#v", result)
}
}
func TestInterpretHumanResponseAmbiguousTargetAsksClarificationWithoutResolvedMutation(t *testing.T) {
result := interpretResponseForTest(t, "Готово", []ResponseCandidate{responseCandidate("a1", "p1", "первое"), responseCandidate("a2", "p2", "второе")})
if !result.Handled || result.Response.ResolutionState != continuity.ResponseAmbiguous || result.Response.Outcome != continuity.ResponseOutcomeClarificationNeeded || !strings.Contains(result.Reply, "несколько") {
t.Fatalf("expected ambiguity clarification, got %#v", result)
}
}
func TestInterpretHumanResponseResolvesAmbiguousTargetByNaturalSummaryReference(t *testing.T) {
result := interpretResponseForTest(t, "Готово, проверить базу сделано", []ResponseCandidate{responseCandidate("a1", "p1", "позвонить врачу"), responseCandidate("a2", "p2", "проверить базу")})
if !result.Handled || result.Response.ResolutionState != continuity.ResponseResolved || result.Response.TargetAttentionID != "a2" {
t.Fatalf("expected natural summary target resolution, got %#v", result)
}
}
func TestInterpretHumanResponseNoTargetAsksClarification(t *testing.T) {
result := interpretResponseForTest(t, "Готово", nil)
if !result.Handled || result.Response.ResolutionState != continuity.ResponseNoTarget || result.Response.Outcome != continuity.ResponseOutcomeClarificationNeeded {
t.Fatalf("expected no-target clarification, got %#v", result)
}
}
func interpretResponseForTest(t *testing.T, message string, candidates []ResponseCandidate) continuity.HumanResponseResult {
t.Helper()
result, err := InterpretHumanResponse(continuity.HumanResponseRequest{
SourceUtteranceID: "utterance-1",
SourceIntentID: "intent-1",
Message: message,
RequestedAtUnixMillis: time.Unix(100, 0).UnixMilli(),
}, identity.PrincipalID("principal-1"), authority.ScopeID("scope-1"), continuity.HumanResponseID("response-1"), candidates, time.Unix(100, 0))
if err != nil {
t.Fatal(err)
}
return result
}
func responseCandidate(attentionID continuity.AttentionItemID, prospectiveID continuity.ProspectiveItemID, summary string) ResponseCandidate {
return ResponseCandidate{
Attention: continuity.AttentionItem{
ID: attentionID,
OwnerPrincipalID: "principal-1",
ScopeID: "scope-1",
SourceKind: continuity.AttentionSourceProspectiveReminder,
SourceProspectiveID: prospectiveID,
State: continuity.AttentionDelivered,
Summary: summary,
Revision: 1,
},
Prospective: continuity.ProspectiveItem{
ID: prospectiveID,
OwnerPrincipalID: "principal-1",
ScopeID: "scope-1",
Summary: summary,
State: continuity.ProspectiveDelivered,
Revision: 1,
},
}
}
+153
View File
@@ -0,0 +1,153 @@
package domain
import (
"errors"
"strings"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/platform/ids"
)
var ErrInvalidExperience = errors.New("invalid experience")
var ErrInvalidExpectation = errors.New("invalid expectation")
var ErrInvalidReflectionCandidate = errors.New("invalid reflection candidate")
func NewSystemExpectation(value epistemics.SystemExpectation) (epistemics.SystemExpectation, error) {
if value.ID == "" {
id, err := ids.New()
if err != nil {
return epistemics.SystemExpectation{}, err
}
value.ID = epistemics.SystemExpectationID(id)
}
if value.OriginalIntentID == "" || value.SourceUtteranceID == "" || value.ExpectedOutcome == "" ||
strings.TrimSpace(value.ExpectedEffect) == "" || value.ConfidenceMillis < 0 || value.ConfidenceMillis > 1000 ||
value.RecordedAtUnixMillis < 1 {
return epistemics.SystemExpectation{}, ErrInvalidExpectation
}
return value, nil
}
func NewExperienceRecord(value epistemics.ExperienceRecord) (epistemics.ExperienceRecord, error) {
if value.ID == "" {
id, err := ids.New()
if err != nil {
return epistemics.ExperienceRecord{}, err
}
value.ID = epistemics.ExperienceRecordID(id)
}
if value.SourceKind == "" || value.OriginalIntentID == "" || value.SourceUtteranceID == "" ||
value.HelpMethod == "" || value.ExpectedOutcome == "" || value.ActualOutcome == "" ||
value.ConfidenceMillis < 0 || value.ConfidenceMillis > 1000 || value.RecordedAtUnixMillis < 1 {
return epistemics.ExperienceRecord{}, ErrInvalidExperience
}
return value, nil
}
func ReflectionForExperience(value epistemics.ExperienceRecord) (epistemics.ReflectionCandidate, bool, error) {
if _, err := NewExperienceRecord(value); err != nil {
return epistemics.ReflectionCandidate{}, false, err
}
trigger, ok := reflectionTrigger(value)
if !ok {
return epistemics.ReflectionCandidate{}, false, nil
}
id, err := ids.New()
if err != nil {
return epistemics.ReflectionCandidate{}, false, err
}
candidate := epistemics.ReflectionCandidate{
ID: epistemics.ReflectionCandidateID(id),
Trigger: trigger,
State: epistemics.ReflectionCandidateOpen,
SourceExperienceID: value.ID,
OriginalIntentID: value.OriginalIntentID,
SourceUtteranceID: value.SourceUtteranceID,
Summary: reflectionSummary(trigger, value),
EvidenceRefs: reflectionEvidence(value),
RequiresCognition: trigger == epistemics.ReflectionContradiction || trigger == epistemics.ReflectionRepeatedDegradation,
RecordedAtUnixMillis: value.RecordedAtUnixMillis,
Revision: 1,
}
if err := ValidateReflectionCandidate(candidate); err != nil {
return epistemics.ReflectionCandidate{}, false, err
}
return candidate, true, nil
}
func ValidateReflectionCandidate(value epistemics.ReflectionCandidate) error {
if value.ID == "" || value.Trigger == "" || value.State == "" || value.SourceExperienceID == "" ||
value.OriginalIntentID == "" || value.SourceUtteranceID == "" || strings.TrimSpace(value.Summary) == "" ||
value.RecordedAtUnixMillis < 1 || value.Revision < 1 {
return ErrInvalidReflectionCandidate
}
return nil
}
func FastPathDeclineExperience(intent epistemics.OriginalIntentRef, utterance epistemics.SourceUtteranceRef, skill epistemics.SkillRef, reason string, millis int64) (epistemics.ExperienceRecord, error) {
outcome := epistemics.OutcomeNotApplicable
switch reason {
case string(epistemics.OutcomeLowConfidence):
outcome = epistemics.OutcomeLowConfidence
case string(epistemics.OutcomeOutOfDistribution):
outcome = epistemics.OutcomeOutOfDistribution
case string(epistemics.OutcomeConflict):
outcome = epistemics.OutcomeConflict
case string(epistemics.OutcomeStale):
outcome = epistemics.OutcomeStale
}
return NewExperienceRecord(epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceInteraction,
OriginalIntentID: intent,
SourceUtteranceID: utterance,
HelpMethod: epistemics.HelpMethodDeterministicFastPath,
SkillRef: skill,
ExpectedOutcome: epistemics.OutcomeSuccess,
ActualOutcome: outcome,
OutcomeDetail: "fast path declined and returned to broader reasoning: " + reason,
ConfidenceMillis: 1000,
RecordedAtUnixMillis: millis,
VerificationEvidence: []epistemics.EvidenceRef{"fast-path-decline"},
})
}
func reflectionTrigger(value epistemics.ExperienceRecord) (epistemics.ReflectionTriggerKind, bool) {
switch value.ActualOutcome {
case epistemics.OutcomeUserCorrected:
return epistemics.ReflectionUserCorrection, true
case epistemics.OutcomeUnexpected:
return epistemics.ReflectionUnexpectedOutcome, true
case epistemics.OutcomeFailed:
return epistemics.ReflectionFailure, true
case epistemics.OutcomeLowConfidence:
return epistemics.ReflectionLowConfidence, true
case epistemics.OutcomeOutOfDistribution, epistemics.OutcomeConflict, epistemics.OutcomeStale:
return epistemics.ReflectionSampledAudit, true
default:
return "", false
}
}
func reflectionSummary(trigger epistemics.ReflectionTriggerKind, value epistemics.ExperienceRecord) string {
if value.OutcomeDetail != "" {
return string(trigger) + ": " + value.OutcomeDetail
}
return string(trigger) + " for experience " + string(value.ID)
}
func reflectionEvidence(value epistemics.ExperienceRecord) []epistemics.EvidenceRef {
refs := append([]epistemics.EvidenceRef{}, value.VerificationEvidence...)
if value.HumanResponseRef != "" {
refs = append(refs, epistemics.EvidenceRef("continuity:human-response:"+value.HumanResponseRef))
}
if value.CorrectionUtteranceID != "" {
refs = append(refs, epistemics.EvidenceRef("interaction:correction:"+value.CorrectionUtteranceID))
}
if value.CapabilityUsageObservationRef != "" {
refs = append(refs, epistemics.EvidenceRef("capability:usage:"+value.CapabilityUsageObservationRef))
}
if value.CapabilityGapRef != "" {
refs = append(refs, epistemics.EvidenceRef("capability:gap:"+value.CapabilityGapRef))
}
return refs
}
@@ -0,0 +1,70 @@
package domain
import (
"testing"
"git.cin.su/m/Assistant/contracts/epistemics"
)
func TestAcceptedReminderResponseCreatesPositiveExperienceWithoutReflection(t *testing.T) {
experience, err := NewExperienceRecord(epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceContinuity,
OriginalIntentID: "intent-1",
SourceUtteranceID: "utterance-1",
ContextRefs: []epistemics.ContextRef{"continuity:attention:attention-1"},
HelpMethod: epistemics.HelpMethodDeterministicFastPath,
SkillRef: "skill:obvious-reminder-response-v1",
ExpectedOutcome: epistemics.OutcomeSuccess,
ActualOutcome: epistemics.OutcomeSuccess,
OutcomeRef: "continuity:prospective:p1",
HumanResponseRef: "response-1",
VerificationEvidence: []epistemics.EvidenceRef{"target-resolver:single-recent-reminder", "human-response:complete"},
ConfidenceMillis: 950,
RecordedAtUnixMillis: 1000,
})
if err != nil {
t.Fatal(err)
}
if _, ok, err := ReflectionForExperience(experience); err != nil || ok {
t.Fatalf("positive accepted experience should not force reflection: ok=%v err=%v", ok, err)
}
}
func TestUserCorrectionCreatesReflectionCandidate(t *testing.T) {
experience, err := NewExperienceRecord(epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceInteraction,
OriginalIntentID: "intent-1",
SourceUtteranceID: "utterance-2",
HelpMethod: epistemics.HelpMethodClarification,
ExpectedOutcome: epistemics.OutcomeSuccess,
ActualOutcome: epistemics.OutcomeUserCorrected,
OutcomeDetail: "user corrected wrong target/project interpretation",
CorrectionUtteranceID: "utterance-2",
ConfidenceMillis: 1000,
RecordedAtUnixMillis: 1100,
})
if err != nil {
t.Fatal(err)
}
candidate, ok, err := ReflectionForExperience(experience)
if err != nil || !ok {
t.Fatalf("expected reflection candidate, ok=%v err=%v", ok, err)
}
if candidate.Trigger != epistemics.ReflectionUserCorrection || candidate.State != epistemics.ReflectionCandidateOpen || len(candidate.EvidenceRefs) == 0 {
t.Fatalf("unexpected reflection candidate: %#v", candidate)
}
}
func TestFastPathDeclineReturnsToThinkingInsteadOfGuessing(t *testing.T) {
experience, err := FastPathDeclineExperience("intent-1", "utterance-1", "skill:obvious-reminder-response-v1", string(epistemics.OutcomeOutOfDistribution), 1200)
if err != nil {
t.Fatal(err)
}
if experience.ActualOutcome != epistemics.OutcomeOutOfDistribution || experience.HelpMethod != epistemics.HelpMethodDeterministicFastPath {
t.Fatalf("unexpected fast path decline experience: %#v", experience)
}
candidate, ok, err := ReflectionForExperience(experience)
if err != nil || !ok || candidate.Trigger != epistemics.ReflectionSampledAudit {
t.Fatalf("decline should preserve reflection/escalation evidence, candidate=%#v ok=%v err=%v", candidate, ok, err)
}
}
+12 -2
View File
@@ -112,7 +112,7 @@ func projectReferences(utterance interaction.HumanUtterance, projects []situatio
}
}
}
if len(refs) == 0 && previousFocus != nil && strings.Contains(lower, "туда") {
if len(refs) == 0 && previousFocus != nil && inheritedFocusAllowed(utterance.SpeechAct, utterance.Content) {
ref, err := newProjectReference(utterance.ID, previousFocus.ProjectID, previousFocus.ProjectDisplayName, interaction.ProjectRolePrimary, situation.ProjectResolutionInheritedFocus, 700, millis)
if err != nil {
return nil, err
@@ -196,7 +196,17 @@ func requestedOutcome(message string, act interaction.SpeechAct) string {
}
func inheritedFocusAllowed(act interaction.SpeechAct, message string) bool {
return strings.Contains(strings.ToLower(message), "туда") && act != interaction.SpeechActUnknown
lower := strings.ToLower(message)
if act == interaction.SpeechActUnknown {
return false
}
if strings.Contains(lower, "туда") || strings.Contains(lower, "там") || strings.Contains(lower, "в этом проекте") {
return true
}
if act == interaction.SpeechActQuestion || act == interaction.SpeechActRequestToInvestigate {
return strings.Contains(lower, "база") || strings.Contains(lower, "сервер") || strings.Contains(lower, "почему тормозит") || strings.Contains(lower, "что там")
}
return false
}
func isAmbiguousDestructive(act interaction.SpeechAct, message string, projects []interaction.ProjectReference) bool {
@@ -73,6 +73,25 @@ func TestInterpretUtteranceInheritedFocusForThere(t *testing.T) {
}
}
func TestProjectFocusIsContextualEvidenceNotGlobalMode(t *testing.T) {
explicit := interpretForTest(t, "Сейчас работаем с УПО.", nil, "")
if explicit.ActiveFocus == nil || explicit.ActiveFocus.ProjectID != "upo" {
t.Fatalf("expected explicit UPO focus, got %#v", explicit.ActiveFocus)
}
focusedQuestion := interpretForTest(t, "Почему тормозит база?", activeFocus("upo", "УПО"), "")
if len(focusedQuestion.Projects) != 1 || focusedQuestion.Projects[0].ProjectID != "upo" || focusedQuestion.Projects[0].Provenance != situation.ProjectResolutionInheritedFocus {
t.Fatalf("database follow-up can use previous project as contextual evidence, got %#v", focusedQuestion.Projects)
}
unrelated := interpretForTest(t, "Помоги ответить на письмо бухгалтеру.", activeFocus("upo", "УПО"), "")
if len(unrelated.Projects) != 0 {
t.Fatalf("unrelated mail intent must not inherit UPO focus, got %#v", unrelated.Projects)
}
returned := interpretForTest(t, "Вернемся к УПО.", activeFocus("assistant", "Assistant"), "")
if returned.ActiveFocus == nil || returned.ActiveFocus.ProjectID != "upo" {
t.Fatalf("expected explicit return to UPO, got %#v", returned.ActiveFocus)
}
}
func TestInterpretUtteranceMultiProjectReferenceRoles(t *testing.T) {
result := interpretForTest(t, "Посмотри, как это сделано в Assistant, и сделай аналогично в УПО.", nil, "")
if len(result.Projects) != 2 {