Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7b32be1e0 | ||
|
|
d4640d7617 | ||
|
|
29b20bc5b0 | ||
|
|
071adb5ecb | ||
|
|
465a90b25d | ||
|
|
95d6ad2d6a | ||
|
|
1e554f6758 | ||
|
|
1c7d512710 | ||
|
|
8c6a7bebbd | ||
|
|
36774b37ae | ||
|
|
c79de1c757 | ||
|
|
acf012720f | ||
|
|
ccf790d130 | ||
|
|
54c2cc27a3 | ||
|
|
8b9b841645 | ||
|
|
e8e51ffdba | ||
|
|
aa01e8e99b | ||
|
|
1113ced623 | ||
|
|
f3ff430812 | ||
|
|
2ec53e40cf | ||
|
|
18fbc7f105 | ||
|
|
a0dce634f3 | ||
|
|
5d4e064a94 | ||
|
|
6e73318e49 | ||
|
|
1519af3310 | ||
|
|
8fe596264e | ||
|
|
ee51789ee2 | ||
|
|
a083bac65f | ||
|
|
9a84ac942c | ||
|
|
fe531ab56f | ||
|
|
d8f994e07e | ||
|
|
9d50a3ec63 |
@@ -14,6 +14,7 @@ import (
|
||||
var ErrCapabilityHealthNotFound = errors.New("capability health not found")
|
||||
var ErrCapabilityBindingNotFound = errors.New("capability binding not found")
|
||||
var ErrCapabilityBindingConflict = errors.New("capability binding conflict")
|
||||
var ErrCapabilityImplementationNotFound = errors.New("capability implementation not found")
|
||||
|
||||
type CapabilityRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
@@ -106,6 +107,145 @@ func (r *CapabilityRepository) SaveBinding(ctx context.Context, binding capabili
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) SaveCapabilityImplementation(ctx context.Context, value capability.CapabilityImplementation) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.capability_implementations (
|
||||
implementation_id,capability_id,capability_kind,owner_principal_id,revision,implementation_kind,target_ref,credential_ref,
|
||||
qualification,activation,health,authority_policy_ref,data_constraints,cost_score_millis,latency_score_millis,reliability_millis,observed_at_unix_millis)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT DO NOTHING`,
|
||||
value.ID, value.CapabilityID, value.CapabilityKind, value.OwnerPrincipalID, value.Revision, value.Kind, value.TargetRef,
|
||||
value.CredentialRef, value.Qualification, value.Activation, value.Health, value.AuthorityPolicyRef, value.DataConstraints,
|
||||
value.CostScoreMillis, value.LatencyScoreMillis, value.ReliabilityMillis, value.ObservedAtUnixMillis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.capability_implementation_heads(implementation_id,revision,capability_kind,owner_principal_id)
|
||||
VALUES ($1,$2,$3,$4)
|
||||
ON CONFLICT (implementation_id) DO UPDATE SET revision=EXCLUDED.revision,capability_kind=EXCLUDED.capability_kind,owner_principal_id=EXCLUDED.owner_principal_id,updated_at=CURRENT_TIMESTAMP
|
||||
WHERE capability.capability_implementation_heads.revision < EXCLUDED.revision`, value.ID, value.Revision, value.CapabilityKind, value.OwnerPrincipalID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) LoadCapabilityImplementations(ctx context.Context, kind capability.CapabilityKind, owner identity.PrincipalID) ([]capability.CapabilityImplementation, error) {
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
i.implementation_id,i.capability_id,i.capability_kind,i.owner_principal_id,i.revision,i.implementation_kind,i.target_ref,COALESCE(i.credential_ref,''),
|
||||
i.qualification,i.activation,i.health,i.authority_policy_ref,i.data_constraints,i.cost_score_millis,i.latency_score_millis,i.reliability_millis,i.observed_at_unix_millis
|
||||
FROM capability.capability_implementation_heads h
|
||||
JOIN capability.capability_implementations i ON i.implementation_id=h.implementation_id AND i.revision=h.revision
|
||||
WHERE i.capability_kind=$1 AND i.owner_principal_id=$2
|
||||
ORDER BY i.implementation_id`, kind, owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
result := make([]capability.CapabilityImplementation, 0)
|
||||
for rows.Next() {
|
||||
var value capability.CapabilityImplementation
|
||||
if err = rows.Scan(&value.ID, &value.CapabilityID, &value.CapabilityKind, &value.OwnerPrincipalID, &value.Revision, &value.Kind, &value.TargetRef,
|
||||
&value.CredentialRef, &value.Qualification, &value.Activation, &value.Health, &value.AuthorityPolicyRef, &value.DataConstraints,
|
||||
&value.CostScoreMillis, &value.LatencyScoreMillis, &value.ReliabilityMillis, &value.ObservedAtUnixMillis); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) AvailableCapabilityKinds(ctx context.Context, owner identity.PrincipalID) ([]capability.CapabilityKind, error) {
|
||||
rows, err := r.pool.Query(ctx, `SELECT DISTINCT i.capability_kind
|
||||
FROM capability.capability_implementation_heads h
|
||||
JOIN capability.capability_implementations i ON i.implementation_id=h.implementation_id AND i.revision=h.revision
|
||||
WHERE i.owner_principal_id=$1 AND i.qualification=$2 AND i.activation=$3 AND i.health=$4
|
||||
ORDER BY i.capability_kind`, owner, capability.QualificationValidated, capability.ActivationActive, capability.HealthAvailable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
result := make([]capability.CapabilityKind, 0)
|
||||
for rows.Next() {
|
||||
var kind capability.CapabilityKind
|
||||
if err := rows.Scan(&kind); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, kind)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) SaveCapabilityGap(ctx context.Context, value capability.CapabilityGap) error {
|
||||
strategies := make([]string, 0, len(value.Strategies))
|
||||
for _, strategy := range value.Strategies {
|
||||
strategies = append(strategies, string(strategy.Kind))
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.capability_gaps (
|
||||
gap_id,requirement_id,capability_kind,capability_id,owner_principal_id,original_intent_id,source_utterance_id,
|
||||
reason,detail,strategies,recorded_at_unix_millis)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`, value.ID, value.RequirementID,
|
||||
value.CapabilityKind, value.CapabilityID, value.OwnerPrincipalID, value.OriginalIntentID, value.SourceUtteranceID,
|
||||
value.Reason, value.Detail, strategies, value.RecordedAtUnixMillis)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) LatestCapabilityGap(ctx context.Context, kind capability.CapabilityKind, owner identity.PrincipalID) (capability.CapabilityGap, error) {
|
||||
var value capability.CapabilityGap
|
||||
var strategies []string
|
||||
err := r.pool.QueryRow(ctx, `SELECT gap_id,requirement_id,capability_kind,capability_id,owner_principal_id,original_intent_id,source_utterance_id,reason,detail,strategies,recorded_at_unix_millis
|
||||
FROM capability.capability_gaps WHERE capability_kind=$1 AND owner_principal_id=$2 ORDER BY recorded_at_unix_millis DESC, gap_id DESC LIMIT 1`, kind, owner).
|
||||
Scan(&value.ID, &value.RequirementID, &value.CapabilityKind, &value.CapabilityID, &value.OwnerPrincipalID, &value.OriginalIntentID, &value.SourceUtteranceID, &value.Reason, &value.Detail, &strategies, &value.RecordedAtUnixMillis)
|
||||
if err != nil {
|
||||
return capability.CapabilityGap{}, err
|
||||
}
|
||||
for _, strategy := range strategies {
|
||||
value.Strategies = append(value.Strategies, capability.AcquisitionStrategyCandidate{Kind: capability.AcquisitionStrategyKind(strategy)})
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) SaveCapabilityUsageObservation(ctx context.Context, value capability.CapabilityUsageObservation) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.capability_usage_observations (
|
||||
observation_id,implementation_id,implementation_revision,capability_id,capability_kind,owner_principal_id,
|
||||
original_intent_id,source_utterance_id,outcome,outcome_ref,latency_millis,observed_at_unix_millis)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT DO NOTHING`, value.ID, value.ImplementationID,
|
||||
value.ImplementationRevision, value.CapabilityID, value.CapabilityKind, value.OwnerPrincipalID, value.OriginalIntentID,
|
||||
value.SourceUtteranceID, value.Outcome, value.OutcomeRef, value.LatencyMillis, value.ObservedAtUnixMillis)
|
||||
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) 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,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -19,6 +21,9 @@ var (
|
||||
ErrProspectiveRevisionConflict = errors.New("prospective item revision conflict")
|
||||
ErrActionReceiptNotFound = errors.New("continuity action receipt not found")
|
||||
ErrActionIntentConflict = errors.New("action intent was already used for a different command")
|
||||
ErrAttentionNotFound = errors.New("attention item not found")
|
||||
ErrAttentionStateConflict = errors.New("attention item state conflict")
|
||||
ErrAttentionRevisionConflict = errors.New("attention item revision conflict")
|
||||
)
|
||||
|
||||
type ContinuityConfig struct{ Host, Port, Database, User, Password string }
|
||||
@@ -34,12 +39,12 @@ func OpenContinuity(ctx context.Context, config ContinuityConfig) (*ContinuityRe
|
||||
}
|
||||
func (r *ContinuityRepository) Close() { r.pool.Close() }
|
||||
func (r *ContinuityRepository) Save(ctx context.Context, item continuity.ProspectiveItem) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO continuity.prospective_items (prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, trigger_at_unix_millis, state, revision) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT (prospective_item_id) DO UPDATE SET state=EXCLUDED.state, revision=EXCLUDED.revision`, item.ID, item.CommitmentID, item.OwnerPrincipalID, item.ScopeID, item.GoalID, item.TriggerAtUnixMillis, item.State, item.Revision)
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO continuity.prospective_items (prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, source_intent_id, source_utterance_id, summary, trigger_at_unix_millis, state, revision) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT (prospective_item_id) DO UPDATE SET state=EXCLUDED.state, revision=EXCLUDED.revision`, item.ID, item.CommitmentID, item.OwnerPrincipalID, item.ScopeID, item.GoalID, item.SourceIntentID, item.SourceUtteranceID, item.Summary, item.TriggerAtUnixMillis, item.State, item.Revision)
|
||||
return err
|
||||
}
|
||||
func (r *ContinuityRepository) Load(ctx context.Context, id continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) {
|
||||
var item continuity.ProspectiveItem
|
||||
err := r.pool.QueryRow(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, trigger_at_unix_millis, state, revision FROM continuity.prospective_items WHERE prospective_item_id=$1`, id).Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.TriggerAtUnixMillis, &item.State, &item.Revision)
|
||||
err := r.pool.QueryRow(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, source_intent_id, source_utterance_id, summary, trigger_at_unix_millis, state, revision FROM continuity.prospective_items WHERE prospective_item_id=$1`, id).Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.SourceIntentID, &item.SourceUtteranceID, &item.Summary, &item.TriggerAtUnixMillis, &item.State, &item.Revision)
|
||||
if err != nil {
|
||||
return continuity.ProspectiveItem{}, fmt.Errorf("load prospective item: %w", err)
|
||||
}
|
||||
@@ -47,7 +52,7 @@ func (r *ContinuityRepository) Load(ctx context.Context, id continuity.Prospecti
|
||||
}
|
||||
|
||||
func (r *ContinuityRepository) ByOwner(ctx context.Context, owner identity.PrincipalID, limit int) ([]continuity.ProspectiveItem, error) {
|
||||
rows, err := r.pool.Query(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, trigger_at_unix_millis, state, revision FROM continuity.prospective_items WHERE owner_principal_id=$1 ORDER BY trigger_at_unix_millis, prospective_item_id LIMIT $2`, owner, limit)
|
||||
rows, err := r.pool.Query(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, source_intent_id, source_utterance_id, summary, trigger_at_unix_millis, state, revision FROM continuity.prospective_items WHERE owner_principal_id=$1 ORDER BY trigger_at_unix_millis, prospective_item_id LIMIT $2`, owner, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list prospective items by owner: %w", err)
|
||||
}
|
||||
@@ -55,7 +60,7 @@ func (r *ContinuityRepository) ByOwner(ctx context.Context, owner identity.Princ
|
||||
items := make([]continuity.ProspectiveItem, 0)
|
||||
for rows.Next() {
|
||||
var item continuity.ProspectiveItem
|
||||
if err := rows.Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.TriggerAtUnixMillis, &item.State, &item.Revision); err != nil {
|
||||
if err := rows.Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.SourceIntentID, &item.SourceUtteranceID, &item.Summary, &item.TriggerAtUnixMillis, &item.State, &item.Revision); err != nil {
|
||||
return nil, fmt.Errorf("scan prospective item: %w", err)
|
||||
}
|
||||
items = append(items, item)
|
||||
@@ -66,6 +71,361 @@ func (r *ContinuityRepository) ByOwner(ctx context.Context, owner identity.Princ
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *ContinuityRepository) DueArmed(ctx context.Context, nowUnixMillis int64, limit int) ([]continuity.ProspectiveItem, error) {
|
||||
if limit < 1 || limit > 100 {
|
||||
return nil, fmt.Errorf("invalid due reminder limit")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, source_intent_id, source_utterance_id, summary, trigger_at_unix_millis, state, revision
|
||||
FROM continuity.prospective_items
|
||||
WHERE state=$1 AND trigger_at_unix_millis <= $2
|
||||
ORDER BY trigger_at_unix_millis, prospective_item_id LIMIT $3`, continuity.ProspectiveArmed, nowUnixMillis, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list due prospective items: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]continuity.ProspectiveItem, 0)
|
||||
for rows.Next() {
|
||||
var item continuity.ProspectiveItem
|
||||
if err := rows.Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.SourceIntentID, &item.SourceUtteranceID, &item.Summary, &item.TriggerAtUnixMillis, &item.State, &item.Revision); err != nil {
|
||||
return nil, fmt.Errorf("scan due prospective item: %w", err)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (r *ContinuityRepository) ClaimDueReminder(ctx context.Context, id continuity.ProspectiveItemID, revision continuity.ProspectiveRevision, attentionID continuity.AttentionItemID, nowUnixMillis int64) (continuity.ProspectiveItem, continuity.AttentionItem, error) {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
var triggered continuity.ProspectiveItem
|
||||
err = tx.QueryRow(ctx, `UPDATE continuity.prospective_items
|
||||
SET state=$1, revision=revision+1
|
||||
WHERE prospective_item_id=$2 AND state=$3 AND revision=$4 AND trigger_at_unix_millis <= $5
|
||||
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,source_intent_id,source_utterance_id,summary,trigger_at_unix_millis,state,revision`,
|
||||
continuity.ProspectiveTriggered, id, continuity.ProspectiveArmed, revision, nowUnixMillis).Scan(
|
||||
&triggered.ID, &triggered.CommitmentID, &triggered.OwnerPrincipalID, &triggered.ScopeID, &triggered.GoalID,
|
||||
&triggered.SourceIntentID, &triggered.SourceUtteranceID, &triggered.Summary, &triggered.TriggerAtUnixMillis, &triggered.State, &triggered.Revision)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, ErrProspectiveRevisionConflict
|
||||
}
|
||||
if err != nil {
|
||||
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, fmt.Errorf("trigger due reminder: %w", err)
|
||||
}
|
||||
|
||||
var revalidating continuity.ProspectiveItem
|
||||
err = tx.QueryRow(ctx, `UPDATE continuity.prospective_items
|
||||
SET state=$1, revision=revision+1
|
||||
WHERE prospective_item_id=$2 AND state=$3 AND revision=$4
|
||||
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,source_intent_id,source_utterance_id,summary,trigger_at_unix_millis,state,revision`,
|
||||
continuity.ProspectiveRevalidating, triggered.ID, continuity.ProspectiveTriggered, triggered.Revision).Scan(
|
||||
&revalidating.ID, &revalidating.CommitmentID, &revalidating.OwnerPrincipalID, &revalidating.ScopeID, &revalidating.GoalID,
|
||||
&revalidating.SourceIntentID, &revalidating.SourceUtteranceID, &revalidating.Summary, &revalidating.TriggerAtUnixMillis, &revalidating.State, &revalidating.Revision)
|
||||
if err != nil {
|
||||
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, fmt.Errorf("begin due reminder revalidation: %w", err)
|
||||
}
|
||||
|
||||
var delivered continuity.ProspectiveItem
|
||||
err = tx.QueryRow(ctx, `UPDATE continuity.prospective_items
|
||||
SET state=$1, revision=revision+1
|
||||
WHERE prospective_item_id=$2 AND state=$3 AND revision=$4
|
||||
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,source_intent_id,source_utterance_id,summary,trigger_at_unix_millis,state,revision`,
|
||||
continuity.ProspectiveDelivered, revalidating.ID, continuity.ProspectiveRevalidating, revalidating.Revision).Scan(
|
||||
&delivered.ID, &delivered.CommitmentID, &delivered.OwnerPrincipalID, &delivered.ScopeID, &delivered.GoalID,
|
||||
&delivered.SourceIntentID, &delivered.SourceUtteranceID, &delivered.Summary, &delivered.TriggerAtUnixMillis, &delivered.State, &delivered.Revision)
|
||||
if err != nil {
|
||||
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, fmt.Errorf("finish due reminder revalidation: %w", err)
|
||||
}
|
||||
|
||||
attention, err := continuitydomain.BuildDueReminderAttention(delivered, attentionID, nowUnixMillis)
|
||||
if err != nil {
|
||||
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, err
|
||||
}
|
||||
err = tx.QueryRow(ctx, `INSERT INTO continuity.attention_items (
|
||||
attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
|
||||
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,
|
||||
action_attempt_id,delivery_outcome,response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,0,'','','','','','','','',0,1)
|
||||
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,
|
||||
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.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)
|
||||
}
|
||||
if err = tx.Commit(ctx); err != nil {
|
||||
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, err
|
||||
}
|
||||
return delivered, attention, nil
|
||||
}
|
||||
|
||||
func (r *ContinuityRepository) PendingAttention(ctx context.Context, limit int) ([]continuity.AttentionItem, error) {
|
||||
if limit < 1 || limit > 100 {
|
||||
return nil, fmt.Errorf("invalid attention limit")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
|
||||
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)
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanAttentionRows(rows)
|
||||
}
|
||||
|
||||
func (r *ContinuityRepository) AttentionByOwner(ctx context.Context, owner identity.PrincipalID, limit int) ([]continuity.AttentionItem, error) {
|
||||
if owner == "" || limit < 1 || limit > 100 {
|
||||
return nil, fmt.Errorf("invalid attention owner query")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
|
||||
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)
|
||||
}
|
||||
defer rows.Close()
|
||||
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,
|
||||
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.ResponseID, &item.ResponseUtteranceID,
|
||||
&item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return continuity.AttentionItem{}, ErrAttentionRevisionConflict
|
||||
}
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (r *ContinuityRepository) MarkAttentionDelivered(ctx context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision, intentID continuity.CommittedActionIntentID, authorizedID continuity.CommittedAuthorizedActionID, attemptID continuity.CommittedActionAttemptID, outcome string, deliveredAtUnixMillis int64) (continuity.AttentionItem, error) {
|
||||
var item continuity.AttentionItem
|
||||
err := r.pool.QueryRow(ctx, `UPDATE continuity.attention_items
|
||||
SET state=$1, revision=revision+1, delivered_at_unix_millis=$2, action_intent_id=$3, authorized_action_id=$4,
|
||||
action_attempt_id=$5, delivery_outcome=$6
|
||||
WHERE attention_item_id=$7 AND state=$8 AND revision=$9
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
|
||||
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.ResponseID, &item.ResponseUtteranceID,
|
||||
&item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return continuity.AttentionItem{}, ErrAttentionRevisionConflict
|
||||
}
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (r *ContinuityRepository) MarkAttentionFailed(ctx context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision, intentID continuity.CommittedActionIntentID, authorizedID continuity.CommittedAuthorizedActionID, attemptID continuity.CommittedActionAttemptID, outcome string) (continuity.AttentionItem, error) {
|
||||
var item continuity.AttentionItem
|
||||
err := r.pool.QueryRow(ctx, `UPDATE continuity.attention_items
|
||||
SET state=$1, revision=revision+1, action_intent_id=$2, authorized_action_id=$3,
|
||||
action_attempt_id=$4, delivery_outcome=$5
|
||||
WHERE attention_item_id=$6 AND state=$7 AND revision=$8
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
|
||||
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.ResponseID, &item.ResponseUtteranceID,
|
||||
&item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return continuity.AttentionItem{}, ErrAttentionRevisionConflict
|
||||
}
|
||||
return item, err
|
||||
}
|
||||
|
||||
// SatisfyDelivered owns both the Prospective CAS and its idempotency receipt in
|
||||
// one Continuity-local transaction. It never reaches Commit storage.
|
||||
func (r *ContinuityRepository) SatisfyDelivered(ctx context.Context, command continuity.SatisfyDeliveredCommand) (continuity.ActionReceipt, error) {
|
||||
@@ -95,10 +455,10 @@ func (r *ContinuityRepository) SatisfyDelivered(ctx context.Context, command con
|
||||
err = tx.QueryRow(ctx, `UPDATE continuity.prospective_items
|
||||
SET state=$1, revision=revision+1
|
||||
WHERE prospective_item_id=$2 AND owner_principal_id=$3 AND state=$4 AND revision=$5
|
||||
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,trigger_at_unix_millis,state,revision`,
|
||||
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,source_intent_id,source_utterance_id,summary,trigger_at_unix_millis,state,revision`,
|
||||
continuity.ProspectiveSatisfied, command.Subject.ProspectiveItemID, command.OwnerPrincipalID,
|
||||
continuity.ProspectiveDelivered, command.Subject.ExpectedRevision).Scan(
|
||||
&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID,
|
||||
&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.SourceIntentID, &item.SourceUtteranceID, &item.Summary,
|
||||
&item.TriggerAtUnixMillis, &item.State, &item.Revision)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return continuity.ActionReceipt{}, diagnoseProspectiveConflict(ctx, tx, command)
|
||||
@@ -135,6 +495,51 @@ 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() {
|
||||
var item continuity.AttentionItem
|
||||
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.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)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("scan attention items: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func loadActionReceipt(ctx context.Context, querier continuityRowQuerier, intentID continuity.CommittedActionIntentID) (continuity.ActionReceipt, error) {
|
||||
var receipt continuity.ActionReceipt
|
||||
err := querier.QueryRow(ctx, `SELECT action_intent_id,authorized_action_id,owner_principal_id,prospective_item_id,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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) 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,33 @@
|
||||
# ADR-0013 — Capability Resolution, Selection and Gap Handling Foundation
|
||||
|
||||
Status: ACCEPTED
|
||||
|
||||
Date: 2026-08-17
|
||||
|
||||
## Context
|
||||
|
||||
DEV-000063 introduced Work-owned `CapabilityRequirement` without selecting or executing concrete implementations. The system now needs a provider-neutral way to answer which real capabilities can satisfy a requirement, whether they are operationally usable, whether policy permits them, and what typed gap remains when no implementation is eligible.
|
||||
|
||||
The Core Semantic Model already assigns Capability registry/bindings ownership to the Capability module. Cognitive Registry is a specialized capability registry for model/provider/model-binding semantics. This ADR must not create a parallel cognitive source of truth.
|
||||
|
||||
## Decision
|
||||
|
||||
1. Keep `capability` as the canonical owner for capability descriptors, implementation registrations, implementation health/availability, resolution results, typed gaps, acquisition strategy candidates and generic capability usage observations.
|
||||
2. Represent a capability separately from an implementation:
|
||||
- `Capability` / `CapabilityRef`: what the system can obtain or do.
|
||||
- `CapabilityImplementation`: a concrete adapter, provider, runtime, agent or internal function that may satisfy that capability.
|
||||
3. Capability identity is typed but extensible. Known `CapabilityKind` constants are allowed for current semantics, but the registry stores string capability kinds and stable capability IDs without imposing a closed universe.
|
||||
4. Resolver V1 returns `AVAILABLE`, `PARTIALLY_AVAILABLE`, `UNAVAILABLE` or `UNKNOWN` with eligible implementations and typed exclusion/gap reasons.
|
||||
5. Selection V1 is deterministic and evidence-weighted. It must rank eligible implementations by typed evidence and stable IDs, not by hardcoded provider/adapter names.
|
||||
6. CredentialRef is allowed as implementation metadata, but credential existence never means capability availability or Authority. Secret values remain outside canonical state and are resolved only by trusted runtimes.
|
||||
7. Cognitive systems remain integrated through the existing Cognitive Registry and Router. General capability resolution may express the conceptual relation that cognition is a capability, but cognitive invocation continues through the existing gateway/router.
|
||||
8. Reminder scheduling is the first concrete production capability for this foundation. It creates Continuity-owned `ProspectiveItem` state through the Continuity runtime boundary. It does not create durable Work for simple reminders.
|
||||
9. Unsupported email sending creates a Capability-owned `CapabilityGap` correlated to the original human intent. It must not claim execution and must not become an automatic Codex/self-extension task.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `capability` gains generic implementation, resolution, gap and usage-observation contracts/tables.
|
||||
- `continuity` gains a narrow authenticated creator reminder-registration endpoint that owns creation of `ProspectiveItem` rows.
|
||||
- `/chat` can schedule a simple reminder through capability resolution and can persist a typed gap for unavailable capabilities.
|
||||
- No cognitive binding is promoted to `ACTIVE`.
|
||||
- No new canonical owner, broad plugin marketplace, autonomous adapter installation or self-modifying loop is introduced.
|
||||
@@ -0,0 +1,46 @@
|
||||
# ADR-0014 — Assistant Presence, Attention, and Human Surface V1
|
||||
|
||||
Status: Accepted
|
||||
|
||||
Task: DEV-000065
|
||||
|
||||
## Context
|
||||
|
||||
Domovoy already records human utterances, original human intent, current focus, capability requirements, capability gaps, and Continuity-owned reminder state. Before this decision, creating a reminder only persisted an `ARMED` `ProspectiveItem`; it did not by itself return to the human when the due condition arrived.
|
||||
|
||||
The system already has the correct outbound mutation foundation: Commit owns authorized external actions, Capability owns notification bindings, and the notification receiver persists idempotent receipts. DEV-000065 must therefore connect due Continuity state to the existing Commit/Executor path instead of creating a parallel reminder or notification subsystem.
|
||||
|
||||
## Decision
|
||||
|
||||
1. Continuity remains the canonical owner of future obligations and prospective state.
|
||||
2. Continuity also owns the V1 typed `AttentionItem` state because attention is the human-facing relevance decision derived from future obligations and other future internal events.
|
||||
3. `AttentionItem` V1 is deliberately narrow: it records source, owner, level, state, summary, due time, and delivery evidence references. It is not a task manager, dashboard model, or capability registry.
|
||||
4. A due reminder follows the existing prospective lifecycle: `ARMED → TRIGGERED → REVALIDATING → DELIVERED`. `DELIVERED` means the due obligation has reached the human-contact phase; it does not grant external mutation authority by itself.
|
||||
5. Human notification remains a Commit-owned external action using the existing `deliver-prospective-notification-v1` operation and Capability-owned active notification binding.
|
||||
6. Commit may authorize a prospective notification using a Continuity prospective item as the live justification. A Work `TaskTarget` is not mandatory for this operation when the operation is internally invoked for a validated due `ProspectiveItem`.
|
||||
7. The due processing runtime may poll canonical PostgreSQL state, but correctness must come from persisted state transitions and idempotency constraints, not from process-local timers.
|
||||
8. The Human Assistant web surface is separate from the Operator Console. Chat remains primary; Today/Attention and History are human-readable projections built through runtime APIs over canonical state.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Continuity writes only Continuity-owned prospective and attention state.
|
||||
- Commit writes only Commit-owned action/attempt state and invokes the existing notification executor.
|
||||
- Capability owns the notification binding and health/availability semantics.
|
||||
- Interaction owns conversations, utterances, interpretations, and original human intent.
|
||||
- No model/provider call is required for deterministic reminder firing.
|
||||
- Model output never directly mutates Continuity, Commit, or Capability state.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No Gmail, MCP, Codex Bridge, browser automation, mobile app, voice, or self-extension loop.
|
||||
- No new canonical owner.
|
||||
- No generic scheduler/event-bus/autonomous-agent loop.
|
||||
- No new third-party dependency.
|
||||
- No new notification channel beyond the existing receiver/Windows display path.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Reminders survive runtime restart because due detection scans canonical `ProspectiveItem` rows.
|
||||
- Duplicate notification is prevented by Continuity CAS/unique attention source and Commit/receiver idempotency.
|
||||
- The first attention source is reminder due events; future sources can reuse the same attention path without changing ownership.
|
||||
- Human UI no longer needs internal UUIDs, binding IDs, provider IDs, or raw lifecycle enums for ordinary use.
|
||||
@@ -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.
|
||||
@@ -84,7 +84,10 @@
|
||||
"Observation",
|
||||
"Claim",
|
||||
"Belief",
|
||||
"Provenance"
|
||||
"Provenance",
|
||||
"SystemExpectation",
|
||||
"ExperienceRecord",
|
||||
"ReflectionCandidate"
|
||||
],
|
||||
"database_schema": "epistemics",
|
||||
"boundary_class": "remote_capable",
|
||||
@@ -98,7 +101,9 @@
|
||||
"id": "mod.continuity",
|
||||
"owns": [
|
||||
"Commitment",
|
||||
"ProspectiveItem"
|
||||
"ProspectiveItem",
|
||||
"AttentionItem",
|
||||
"HumanResponse"
|
||||
],
|
||||
"database_schema": "continuity",
|
||||
"boundary_class": "remote_capable",
|
||||
@@ -114,7 +119,16 @@
|
||||
"owns": [
|
||||
"Capability",
|
||||
"CapabilityBinding",
|
||||
"CapabilityHealth"
|
||||
"CapabilityHealth",
|
||||
"CapabilityImplementation",
|
||||
"CapabilityResolution",
|
||||
"CapabilitySelection",
|
||||
"CapabilityGap",
|
||||
"AcquisitionStrategyCandidate",
|
||||
"CapabilityUsageObservation",
|
||||
"SkillProfile",
|
||||
"SkillEvidence",
|
||||
"FastPathDecision"
|
||||
],
|
||||
"database_schema": "capability",
|
||||
"boundary_class": "remote_capable",
|
||||
|
||||
@@ -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]
|
||||
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]
|
||||
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]
|
||||
|
||||
@@ -183,3 +183,29 @@ func (c creatorContinuityClient) Item(ctx context.Context, authorization string,
|
||||
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
|
||||
}
|
||||
}
|
||||
|
||||
func (c creatorContinuityClient) InternalItem(ctx context.Context, id continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) {
|
||||
target := *c.baseURL
|
||||
target.Path = "/v1/prospective-items/" + url.PathEscape(string(id))
|
||||
target.RawQuery = ""
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
|
||||
if err != nil {
|
||||
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
|
||||
}
|
||||
response, err := c.client.Do(request)
|
||||
if err != nil {
|
||||
return continuity.ProspectiveItem{}, fmt.Errorf("%w: %v", errCreatorQueryUnavailable, err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode == http.StatusNotFound {
|
||||
return continuity.ProspectiveItem{}, errCreatorSubjectMissing
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
|
||||
}
|
||||
var item continuity.ProspectiveItem
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<10)).Decode(&item); err != nil {
|
||||
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/notificationexecutor"
|
||||
commitcontract "git.cin.su/m/Assistant/contracts/commit"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
commitdomain "git.cin.su/m/Assistant/modules/commit/domain"
|
||||
"git.cin.su/m/Assistant/modules/commit/ports"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
const prospectiveNotificationActor identity.ActorID = "domovoy-creator-actor"
|
||||
|
||||
type internalNotificationContinuity interface {
|
||||
InternalItem(context.Context, continuity.ProspectiveItemID) (continuity.ProspectiveItem, error)
|
||||
}
|
||||
|
||||
type internalNotificationHandler struct {
|
||||
core creatorSessionSource
|
||||
continuity internalNotificationContinuity
|
||||
authority creatorAuthoritySource
|
||||
repository creatorCommitRepository
|
||||
executor ports.Executor
|
||||
newID func() (ids.ID, error)
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type internalProspectiveNotificationRequest struct {
|
||||
ProspectiveItemID continuity.ProspectiveItemID `json:"prospective_item_id"`
|
||||
ExpectedRevision continuity.ProspectiveRevision `json:"expected_revision"`
|
||||
}
|
||||
|
||||
func registerInternalNotificationRoutes(mux *http.ServeMux, handler internalNotificationHandler, token string) {
|
||||
mux.HandleFunc("POST /v1/internal/continuity/notify-due", requireCommitToken(token, handler.notifyDue))
|
||||
}
|
||||
|
||||
func (h internalNotificationHandler) notifyDue(w http.ResponseWriter, r *http.Request) {
|
||||
var request internalProspectiveNotificationRequest
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil || ensureCreatorEOF(decoder) != nil || request.ProspectiveItemID == "" || request.ExpectedRevision == 0 {
|
||||
http.Error(w, "invalid prospective notification request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
item, err := h.continuity.InternalItem(r.Context(), request.ProspectiveItemID)
|
||||
if errors.Is(err, errCreatorSubjectMissing) {
|
||||
http.Error(w, "prospective item not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "Continuity item unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
if item.State != continuity.ProspectiveDelivered || item.Revision != request.ExpectedRevision || item.OwnerPrincipalID == "" || item.ScopeID == "" {
|
||||
http.Error(w, "prospective item not deliverable", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
grant, scope, err := h.authority.LoadGrant(r.Context(), creatorGrantID)
|
||||
if err != nil || scope.ID != item.ScopeID {
|
||||
http.Error(w, "Creator authority unavailable", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
identityBinding, err := h.core.Binding(r.Context(), prospectiveNotificationActor, item.OwnerPrincipalID)
|
||||
if err != nil {
|
||||
http.Error(w, "Creator identity binding unavailable", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
capabilityBinding, err := h.core.ActiveCapabilityBinding(r.Context(), commitcontract.CapabilityProspectiveNotification, item.OwnerPrincipalID)
|
||||
if err != nil {
|
||||
http.Error(w, "notification capability unavailable", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
intentID, authorizedID, attemptID, err := internalActionIDs(h.newID)
|
||||
if err != nil {
|
||||
http.Error(w, "action identity unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
title := "Домовой: напоминание"
|
||||
message := "Пора: " + strings.TrimSpace(item.Summary)
|
||||
payload := notificationexecutor.Payload{ActionIntentID: intentID, TemplateCode: commitcontract.ProspectiveDeliveredTemplateV1, ProspectiveItemID: string(item.ID), ExpectedRevision: int64(item.Revision), Title: title, Message: message}
|
||||
digest, err := notificationexecutor.Digest(payload)
|
||||
if err != nil {
|
||||
http.Error(w, "notification evidence unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
proposal := commitcontract.ActionProposal{
|
||||
IntentID: intentID, ActorID: prospectiveNotificationActor, CapabilityID: commitcontract.CapabilityProspectiveNotification,
|
||||
Target: work.TaskTarget{ScopeID: item.ScopeID, Revision: 1}, Operation: commitcontract.OperationDeliverProspectiveNotification,
|
||||
NotificationSubject: commitcontract.ProspectiveNotificationSubject{ProspectiveItemID: item.ID, ExpectedRevision: item.Revision, TemplateCode: commitcontract.ProspectiveDeliveredTemplateV1, BindingID: capabilityBinding.ID, BindingRevision: capabilityBinding.Revision, RenderedTitle: title, RenderedMessage: message, RequestDigest: digest},
|
||||
}
|
||||
action, err := commitdomain.Authorize(proposal, commitdomain.AuthorizationContext{Binding: identityBinding, Scope: scope, Grant: grant}, authorityValidator{}, authorizedID, h.now().UnixMilli())
|
||||
if err != nil {
|
||||
http.Error(w, "Creator authorization denied", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if err = h.repository.SaveAuthorized(r.Context(), action); err != nil {
|
||||
http.Error(w, "commit authorized action", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
attempt, operationErr := executeAuthorized(r.Context(), h.repository, h.executor, action, attemptID)
|
||||
if operationErr != nil && !errors.Is(operationErr, commitdomain.ErrUnknownOutcome) {
|
||||
http.Error(w, "execute committed notification", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
status := http.StatusCreated
|
||||
if errors.Is(operationErr, commitdomain.ErrUnknownOutcome) {
|
||||
status = http.StatusAccepted
|
||||
}
|
||||
writeCreatorActionResponse(w, status, action, attempt)
|
||||
}
|
||||
|
||||
func internalActionIDs(newID func() (ids.ID, error)) (commitcontract.ActionIntentID, commitcontract.AuthorizedActionID, commitcontract.ActionAttemptID, error) {
|
||||
intent, err := newID()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
authorized, err := newID()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
attempt, err := newID()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
return commitcontract.ActionIntentID(intent), commitcontract.AuthorizedActionID(authorized), commitcontract.ActionAttemptID(attempt), nil
|
||||
}
|
||||
@@ -89,6 +89,10 @@ func main() {
|
||||
core: creatorCore, continuity: creatorContinuity, authority: authorityRepository, repository: repository,
|
||||
executor: dispatcher, reconciler: dispatcher, newID: ids.New, now: time.Now,
|
||||
})
|
||||
registerInternalNotificationRoutes(mux, internalNotificationHandler{
|
||||
core: creatorCore, continuity: creatorContinuity, authority: authorityRepository, repository: repository,
|
||||
executor: dispatcher, newID: ids.New, now: time.Now,
|
||||
}, executorToken)
|
||||
mux.HandleFunc("POST /v1/authorized-actions", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request authorizationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
type attentionRepository interface {
|
||||
AttentionByOwner(context.Context, identity.PrincipalID, int) ([]continuity.AttentionItem, error)
|
||||
}
|
||||
|
||||
func registerCreatorAttentionRoutes(mux *http.ServeMux, identityClient creatorIdentityClient, repository attentionRepository) {
|
||||
mux.HandleFunc("GET /v1/creator/attention", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := creatorSessionForAttention(w, r, identityClient)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
limit, err := parseListLimit(r.URL.Query().Get("limit"))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid attention limit", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
items, err := repository.AttentionByOwner(r.Context(), session.PrincipalID, limit)
|
||||
if err != nil {
|
||||
http.Error(w, "attention unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_ = json.NewEncoder(w).Encode(continuity.AttentionListResult{Items: items})
|
||||
})
|
||||
mux.HandleFunc("GET /v1/creator/today", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := creatorSessionForAttention(w, r, identityClient)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := repository.AttentionByOwner(r.Context(), session.PrincipalID, 50)
|
||||
if err != nil {
|
||||
http.Error(w, "today unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
result := continuity.TodayResult{Items: make([]continuity.TodayItem, 0, len(items))}
|
||||
for _, item := range items {
|
||||
result.Items = append(result.Items, continuity.TodayItem{
|
||||
Title: item.Summary,
|
||||
WhenUnixMillis: item.DueAtUnixMillis,
|
||||
AttentionLevel: item.Level,
|
||||
AttentionState: item.State,
|
||||
HumanState: humanAttentionState(item.State),
|
||||
DeliveryState: humanDeliveryState(item),
|
||||
SourceDescription: humanAttentionSource(item.SourceKind),
|
||||
ResponseHint: humanResponseHint(item),
|
||||
})
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
}
|
||||
|
||||
func creatorSessionForAttention(w http.ResponseWriter, r *http.Request, identityClient creatorIdentityClient) (identity.CreatorSession, bool) {
|
||||
session, err := identityClient.Session(r.Context(), r.Header.Get("Authorization"))
|
||||
if errors.Is(err, errCreatorSessionDenied) {
|
||||
http.Error(w, "creator session denied", http.StatusUnauthorized)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "identity service unavailable", http.StatusServiceUnavailable)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
return session, true
|
||||
}
|
||||
|
||||
func humanAttentionState(state continuity.AttentionState) string {
|
||||
switch state {
|
||||
case continuity.AttentionPending:
|
||||
return "ожидает внимания"
|
||||
case continuity.AttentionDelivering:
|
||||
return "доставляется"
|
||||
case continuity.AttentionDelivered:
|
||||
return "доставлено"
|
||||
case continuity.AttentionResolved:
|
||||
return "закрыто"
|
||||
case continuity.AttentionSuppressed:
|
||||
return "скрыто"
|
||||
case continuity.AttentionExpired:
|
||||
return "истекло"
|
||||
case continuity.AttentionFailed:
|
||||
return "не доставлено"
|
||||
default:
|
||||
return "состояние уточняется"
|
||||
}
|
||||
}
|
||||
|
||||
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 "уведомление отправлено"
|
||||
}
|
||||
if item.State == continuity.AttentionDelivering {
|
||||
return "уведомление отправляется"
|
||||
}
|
||||
return "уведомление ещё не отправлено"
|
||||
}
|
||||
|
||||
func humanAttentionSource(kind continuity.AttentionSourceKind) string {
|
||||
if kind == continuity.AttentionSourceProspectiveReminder {
|
||||
return "напоминание"
|
||||
}
|
||||
return "событие Домового"
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
commitcontract "git.cin.su/m/Assistant/contracts/commit"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
type dueReminderRepository interface {
|
||||
DueArmed(context.Context, int64, int) ([]continuity.ProspectiveItem, error)
|
||||
ClaimDueReminder(context.Context, continuity.ProspectiveItemID, continuity.ProspectiveRevision, continuity.AttentionItemID, int64) (continuity.ProspectiveItem, continuity.AttentionItem, error)
|
||||
PendingAttention(context.Context, int) ([]continuity.AttentionItem, error)
|
||||
MarkAttentionDelivering(context.Context, continuity.AttentionItemID, continuity.AttentionRevision) (continuity.AttentionItem, error)
|
||||
MarkAttentionDelivered(context.Context, continuity.AttentionItemID, continuity.AttentionRevision, continuity.CommittedActionIntentID, continuity.CommittedAuthorizedActionID, continuity.CommittedActionAttemptID, string, int64) (continuity.AttentionItem, error)
|
||||
MarkAttentionFailed(context.Context, continuity.AttentionItemID, continuity.AttentionRevision, continuity.CommittedActionIntentID, continuity.CommittedAuthorizedActionID, continuity.CommittedActionAttemptID, string) (continuity.AttentionItem, error)
|
||||
Load(context.Context, continuity.ProspectiveItemID) (continuity.ProspectiveItem, error)
|
||||
}
|
||||
|
||||
type dueNotificationClient struct {
|
||||
baseURL *url.URL
|
||||
token string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type dueNotifier interface {
|
||||
NotifyDue(context.Context, continuity.ProspectiveItem) (dueNotificationResponse, error)
|
||||
}
|
||||
|
||||
type dueNotificationRequest struct {
|
||||
ProspectiveItemID continuity.ProspectiveItemID `json:"prospective_item_id"`
|
||||
ExpectedRevision continuity.ProspectiveRevision `json:"expected_revision"`
|
||||
}
|
||||
|
||||
type dueNotificationResponse struct {
|
||||
AuthorizedActionID commitcontract.AuthorizedActionID `json:"AuthorizedActionID"`
|
||||
ActionIntentID commitcontract.ActionIntentID `json:"ActionIntentID"`
|
||||
ActionAttemptID commitcontract.ActionAttemptID `json:"ActionAttemptID"`
|
||||
State commitcontract.ActionState `json:"State"`
|
||||
}
|
||||
|
||||
func newDueNotificationClient(rawURL, token string, client *http.Client) (dueNotificationClient, error) {
|
||||
base, err := url.Parse(rawURL)
|
||||
if err != nil || base.Scheme == "" || base.Host == "" || token == "" {
|
||||
return dueNotificationClient{}, fmt.Errorf("invalid Commit due notification configuration")
|
||||
}
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return dueNotificationClient{baseURL: base, token: token, client: client}, nil
|
||||
}
|
||||
|
||||
func (c dueNotificationClient) NotifyDue(ctx context.Context, item continuity.ProspectiveItem) (dueNotificationResponse, error) {
|
||||
target := *c.baseURL
|
||||
target.Path = "/v1/internal/continuity/notify-due"
|
||||
payload, err := json.Marshal(dueNotificationRequest{ProspectiveItemID: item.ID, ExpectedRevision: item.Revision})
|
||||
if err != nil {
|
||||
return dueNotificationResponse{}, err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return dueNotificationResponse{}, err
|
||||
}
|
||||
request.Header.Set("Authorization", "Bearer "+c.token)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client.Do(request)
|
||||
if err != nil {
|
||||
return dueNotificationResponse{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusCreated && response.StatusCode != http.StatusAccepted {
|
||||
return dueNotificationResponse{}, fmt.Errorf("commit due notification status %d", response.StatusCode)
|
||||
}
|
||||
var result dueNotificationResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return dueNotificationResponse{}, err
|
||||
}
|
||||
if result.ActionIntentID == "" || result.AuthorizedActionID == "" || result.ActionAttemptID == "" {
|
||||
return dueNotificationResponse{}, fmt.Errorf("invalid commit due notification response")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type dueReminderWorker struct {
|
||||
repository dueReminderRepository
|
||||
commit dueNotifier
|
||||
now func() time.Time
|
||||
newID func() (ids.ID, error)
|
||||
interval time.Duration
|
||||
limit int
|
||||
}
|
||||
|
||||
func (w dueReminderWorker) Start(ctx context.Context) {
|
||||
if w.interval <= 0 {
|
||||
w.interval = 15 * time.Second
|
||||
}
|
||||
if w.limit <= 0 {
|
||||
w.limit = 10
|
||||
}
|
||||
go func() {
|
||||
w.process(ctx)
|
||||
ticker := time.NewTicker(w.interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
w.process(ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (w dueReminderWorker) process(ctx context.Context) {
|
||||
now := w.now()
|
||||
if err := w.claimDue(ctx, now); err != nil {
|
||||
log.Printf("due reminder claim skipped: %v", err)
|
||||
}
|
||||
if err := w.deliverPending(ctx, now); err != nil {
|
||||
log.Printf("due reminder delivery skipped: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w dueReminderWorker) claimDue(ctx context.Context, now time.Time) error {
|
||||
items, err := w.repository.DueArmed(ctx, now.UnixMilli(), w.limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
id, err := w.newID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err = w.repository.ClaimDueReminder(ctx, item.ID, item.Revision, continuity.AttentionItemID(id), now.UnixMilli()); err != nil {
|
||||
log.Printf("due reminder %s not claimed: %v", item.ID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w dueReminderWorker) deliverPending(ctx context.Context, now time.Time) error {
|
||||
items, err := w.repository.PendingAttention(ctx, w.limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, attention := range items {
|
||||
if attention.SourceKind != continuity.AttentionSourceProspectiveReminder || attention.SourceProspectiveID == "" {
|
||||
continue
|
||||
}
|
||||
delivering, err := w.repository.MarkAttentionDelivering(ctx, attention.ID, attention.Revision)
|
||||
if err != nil {
|
||||
log.Printf("attention %s not marked delivering: %v", attention.ID, err)
|
||||
continue
|
||||
}
|
||||
prospective, err := w.repository.Load(ctx, delivering.SourceProspectiveID)
|
||||
if err != nil {
|
||||
log.Printf("attention %s source unavailable: %v", attention.ID, err)
|
||||
continue
|
||||
}
|
||||
result, err := w.commit.NotifyDue(ctx, prospective)
|
||||
if err != nil {
|
||||
log.Printf("attention %s notification unavailable: %v", attention.ID, err)
|
||||
continue
|
||||
}
|
||||
if result.State != commitcontract.ActionSucceeded {
|
||||
_, err = w.repository.MarkAttentionFailed(ctx, delivering.ID, delivering.Revision,
|
||||
continuity.CommittedActionIntentID(result.ActionIntentID),
|
||||
continuity.CommittedAuthorizedActionID(result.AuthorizedActionID),
|
||||
continuity.CommittedActionAttemptID(result.ActionAttemptID),
|
||||
string(result.State))
|
||||
if err != nil {
|
||||
log.Printf("attention %s delivery failure outcome not persisted: %v", attention.ID, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
_, err = w.repository.MarkAttentionDelivered(ctx, delivering.ID, delivering.Revision,
|
||||
continuity.CommittedActionIntentID(result.ActionIntentID),
|
||||
continuity.CommittedAuthorizedActionID(result.AuthorizedActionID),
|
||||
continuity.CommittedActionAttemptID(result.ActionAttemptID),
|
||||
string(result.State), now.UnixMilli())
|
||||
if err != nil {
|
||||
log.Printf("attention %s delivery outcome not persisted: %v", attention.ID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
commitcontract "git.cin.su/m/Assistant/contracts/commit"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
type memoryDueRepo struct {
|
||||
item continuity.ProspectiveItem
|
||||
attention continuity.AttentionItem
|
||||
}
|
||||
|
||||
func (r *memoryDueRepo) DueArmed(context.Context, int64, int) ([]continuity.ProspectiveItem, error) {
|
||||
if r.item.State == continuity.ProspectiveArmed {
|
||||
return []continuity.ProspectiveItem{r.item}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *memoryDueRepo) ClaimDueReminder(_ context.Context, id continuity.ProspectiveItemID, revision continuity.ProspectiveRevision, attentionID continuity.AttentionItemID, now int64) (continuity.ProspectiveItem, continuity.AttentionItem, error) {
|
||||
if r.item.ID != id || r.item.Revision != revision || r.item.State != continuity.ProspectiveArmed {
|
||||
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, nil
|
||||
}
|
||||
r.item.State = continuity.ProspectiveDelivered
|
||||
r.item.Revision += 3
|
||||
r.attention = continuity.AttentionItem{ID: attentionID, OwnerPrincipalID: r.item.OwnerPrincipalID, ScopeID: r.item.ScopeID, SourceKind: continuity.AttentionSourceProspectiveReminder, SourceProspectiveID: r.item.ID, Level: continuity.AttentionDue, State: continuity.AttentionPending, Summary: r.item.Summary, DueAtUnixMillis: r.item.TriggerAtUnixMillis, CreatedAtUnixMillis: now, Revision: 1}
|
||||
return r.item, r.attention, nil
|
||||
}
|
||||
|
||||
func (r *memoryDueRepo) PendingAttention(context.Context, int) ([]continuity.AttentionItem, error) {
|
||||
if r.attention.State == continuity.AttentionPending {
|
||||
return []continuity.AttentionItem{r.attention}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *memoryDueRepo) MarkAttentionDelivering(_ context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision) (continuity.AttentionItem, error) {
|
||||
if r.attention.ID == id && r.attention.Revision == revision {
|
||||
r.attention.State = continuity.AttentionDelivering
|
||||
r.attention.Revision++
|
||||
}
|
||||
return r.attention, nil
|
||||
}
|
||||
|
||||
func (r *memoryDueRepo) MarkAttentionDelivered(_ context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision, intentID continuity.CommittedActionIntentID, authorizedID continuity.CommittedAuthorizedActionID, attemptID continuity.CommittedActionAttemptID, outcome string, deliveredAt int64) (continuity.AttentionItem, error) {
|
||||
if r.attention.ID == id && r.attention.Revision == revision {
|
||||
r.attention.State = continuity.AttentionDelivered
|
||||
r.attention.Revision++
|
||||
r.attention.ActionIntentID = intentID
|
||||
r.attention.AuthorizedActionID = authorizedID
|
||||
r.attention.ActionAttemptID = attemptID
|
||||
r.attention.DeliveryOutcome = outcome
|
||||
r.attention.DeliveredAtUnixMillis = deliveredAt
|
||||
}
|
||||
return r.attention, nil
|
||||
}
|
||||
|
||||
func (r *memoryDueRepo) MarkAttentionFailed(_ context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision, intentID continuity.CommittedActionIntentID, authorizedID continuity.CommittedAuthorizedActionID, attemptID continuity.CommittedActionAttemptID, outcome string) (continuity.AttentionItem, error) {
|
||||
if r.attention.ID == id && r.attention.Revision == revision {
|
||||
r.attention.State = continuity.AttentionFailed
|
||||
r.attention.Revision++
|
||||
r.attention.ActionIntentID = intentID
|
||||
r.attention.AuthorizedActionID = authorizedID
|
||||
r.attention.ActionAttemptID = attemptID
|
||||
r.attention.DeliveryOutcome = outcome
|
||||
}
|
||||
return r.attention, nil
|
||||
}
|
||||
|
||||
func (r *memoryDueRepo) Load(context.Context, continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) {
|
||||
return r.item, nil
|
||||
}
|
||||
|
||||
type fakeDueNotifier struct{ calls int }
|
||||
|
||||
func (n *fakeDueNotifier) NotifyDue(context.Context, continuity.ProspectiveItem) (dueNotificationResponse, error) {
|
||||
n.calls++
|
||||
return dueNotificationResponse{ActionIntentID: "intent", AuthorizedActionID: "authorized", ActionAttemptID: "attempt", State: commitcontract.ActionSucceeded}, nil
|
||||
}
|
||||
|
||||
type unknownDueNotifier struct{ calls int }
|
||||
|
||||
func (n *unknownDueNotifier) NotifyDue(context.Context, continuity.ProspectiveItem) (dueNotificationResponse, error) {
|
||||
n.calls++
|
||||
return dueNotificationResponse{ActionIntentID: "intent", AuthorizedActionID: "authorized", ActionAttemptID: "attempt", State: commitcontract.ActionUnknownOutcome}, nil
|
||||
}
|
||||
|
||||
func TestDueReminderProducesAttentionAndNotificationOnce(t *testing.T) {
|
||||
repo := &memoryDueRepo{item: continuity.ProspectiveItem{ID: "prospective", OwnerPrincipalID: identity.PrincipalID("owner"), ScopeID: authority.ScopeID("scope"), Summary: "проверить уведомление", TriggerAtUnixMillis: 10, State: continuity.ProspectiveArmed, Revision: 1}}
|
||||
notifier := &fakeDueNotifier{}
|
||||
now := time.UnixMilli(20)
|
||||
worker := dueReminderWorker{repository: repo, commit: notifier, now: func() time.Time { return now }, newID: func() (ids.ID, error) { return ids.ID("attention"), nil }, limit: 10}
|
||||
worker.process(context.Background())
|
||||
worker.process(context.Background())
|
||||
if repo.item.State != continuity.ProspectiveDelivered || repo.attention.State != continuity.AttentionDelivered {
|
||||
t.Fatalf("unexpected states: prospective=%s attention=%s", repo.item.State, repo.attention.State)
|
||||
}
|
||||
if notifier.calls != 1 {
|
||||
t.Fatalf("expected one notification call, got %d", notifier.calls)
|
||||
}
|
||||
if repo.attention.ActionIntentID == "" || repo.attention.DeliveredAtUnixMillis == 0 {
|
||||
t.Fatal("delivery evidence was not persisted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDueReminderUnknownOutcomeDoesNotMarkAttentionDelivered(t *testing.T) {
|
||||
repo := &memoryDueRepo{item: continuity.ProspectiveItem{ID: "prospective", OwnerPrincipalID: identity.PrincipalID("owner"), ScopeID: authority.ScopeID("scope"), Summary: "проверить уведомление", TriggerAtUnixMillis: 10, State: continuity.ProspectiveArmed, Revision: 1}}
|
||||
notifier := &unknownDueNotifier{}
|
||||
now := time.UnixMilli(20)
|
||||
worker := dueReminderWorker{repository: repo, commit: notifier, now: func() time.Time { return now }, newID: func() (ids.ID, error) { return ids.ID("attention"), nil }, limit: 10}
|
||||
worker.process(context.Background())
|
||||
if repo.attention.State != continuity.AttentionFailed {
|
||||
t.Fatalf("unknown outcome must not be delivered, got %s", repo.attention.State)
|
||||
}
|
||||
if repo.attention.DeliveryOutcome != string(commitcontract.ActionUnknownOutcome) {
|
||||
t.Fatalf("missing unknown outcome evidence: %#v", repo.attention)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
"git.cin.su/m/Assistant/runtime/health"
|
||||
)
|
||||
|
||||
@@ -34,7 +35,15 @@ func main() {
|
||||
if executorToken == "" {
|
||||
log.Fatal("COMMIT_EXECUTOR_TOKEN is required")
|
||||
}
|
||||
commitClient, err := newDueNotificationClient(envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"), executorToken, &http.Client{Timeout: 10 * time.Second})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
workerContext, cancelWorker := context.WithCancel(context.Background())
|
||||
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 {
|
||||
@@ -47,6 +56,7 @@ func main() {
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
})
|
||||
registerCreatorReminderRoute(mux, identityClient, repository)
|
||||
mux.HandleFunc("GET /v1/prospective-items/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
item, err := repository.Load(r.Context(), continuity.ProspectiveItemID(r.PathValue("id")))
|
||||
if err != nil {
|
||||
@@ -103,6 +113,7 @@ func main() {
|
||||
Items []continuity.ProspectiveItem
|
||||
}{Items: items})
|
||||
})
|
||||
registerCreatorAttentionRoutes(mux, identityClient, repository)
|
||||
log.Printf("continuity-runtime listening on %s", addr)
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
continuitydomain "git.cin.su/m/Assistant/modules/continuity/domain"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
type reminderRepository interface {
|
||||
Save(context.Context, continuity.ProspectiveItem) error
|
||||
}
|
||||
|
||||
func registerCreatorReminderRoute(mux *http.ServeMux, identityClient creatorIdentityClient, repository reminderRepository) {
|
||||
mux.HandleFunc("POST /v1/creator/reminders", 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.ScheduleReminderRequest
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid reminder", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
request.OwnerPrincipalID = session.PrincipalID
|
||||
id, err := ids.New()
|
||||
if err != nil {
|
||||
http.Error(w, "reminder unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
item, err := continuitydomain.BuildReminder(request, continuity.ProspectiveItemID(id))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid reminder", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := repository.Save(r.Context(), item); err != nil {
|
||||
http.Error(w, "save reminder", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(continuity.ScheduleReminderResult{Item: item})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
func TestCreatorReminderEndpointBuildsOwnedProspectiveItem(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 := &fakeReminderRepository{}
|
||||
mux := http.NewServeMux()
|
||||
registerCreatorReminderRoute(mux, identityClient, repository)
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/creator/reminders", strings.NewReader(`{"ScopeID":"scope-1","SourceIntentID":"intent-1","SourceUtteranceID":"utterance-1","Summary":"позвонить врачу","TriggerAtUnixMillis":2000,"RequestedAtUnixMillis":1000}`))
|
||||
request.Header.Set("Authorization", "Bearer ok")
|
||||
recorder := httptest.NewRecorder()
|
||||
mux.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusCreated || repository.saved.OwnerPrincipalID != "principal-1" || repository.saved.Summary != "позвонить врачу" || repository.saved.State != continuity.ProspectiveArmed {
|
||||
t.Fatalf("status=%d saved=%#v body=%s", recorder.Code, repository.saved, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
type fakeReminderRepository struct {
|
||||
saved continuity.ProspectiveItem
|
||||
}
|
||||
|
||||
func (f *fakeReminderRepository) Save(_ context.Context, item continuity.ProspectiveItem) error {
|
||||
f.saved = item
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
type continuityReminderClient struct {
|
||||
baseURL *url.URL
|
||||
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 == "" {
|
||||
return continuityReminderClient{}, fmt.Errorf("invalid Continuity Runtime URL")
|
||||
}
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
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"
|
||||
payload, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return continuity.ScheduleReminderResult{}, err
|
||||
}
|
||||
upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return continuity.ScheduleReminderResult{}, err
|
||||
}
|
||||
upstream.Header.Set("Content-Type", "application/json")
|
||||
upstream.Header.Set("Authorization", authorization)
|
||||
response, err := c.client.Do(upstream)
|
||||
if err != nil {
|
||||
return continuity.ScheduleReminderResult{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusCreated {
|
||||
return continuity.ScheduleReminderResult{}, fmt.Errorf("continuity reminder status %d", response.StatusCode)
|
||||
}
|
||||
var result continuity.ScheduleReminderResult
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return continuity.ScheduleReminderResult{}, err
|
||||
}
|
||||
if result.Item.ID == "" || result.Item.State != continuity.ProspectiveArmed {
|
||||
return continuity.ScheduleReminderResult{}, fmt.Errorf("invalid reminder result")
|
||||
}
|
||||
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 {
|
||||
return capability.CapabilityResolution{}, err
|
||||
}
|
||||
requirement := capability.CapabilityResolutionRequirement{
|
||||
RequirementID: capability.RequirementRef(requirementID),
|
||||
CapabilityKind: kind,
|
||||
OwnerPrincipalID: session.PrincipalID,
|
||||
ActorID: session.ActorID,
|
||||
OriginalIntentID: capability.OriginalIntentRef(intent.ID),
|
||||
SourceUtteranceID: capability.SourceUtteranceRef(utterance.ID),
|
||||
RequiredConstraints: []capability.DataConstraint{capability.DataConstraintLocalOnly},
|
||||
AuthorityContextRef: capability.AuthorityPolicyCreatorDirectiveV1,
|
||||
ObservedAtUnixMillis: now.UnixMilli(),
|
||||
}
|
||||
candidates, err := repository.LoadCapabilityImplementations(ctx, kind, session.PrincipalID)
|
||||
if err != nil {
|
||||
return capability.CapabilityResolution{}, err
|
||||
}
|
||||
resolution, err := capabilitydomain.ResolveCapability(requirement, candidates)
|
||||
if errors.Is(err, capabilitydomain.ErrNoCapabilityImplementation) && resolution.Gap != nil {
|
||||
if saveErr := repository.SaveCapabilityGap(ctx, *resolution.Gap); saveErr != nil {
|
||||
return capability.CapabilityResolution{}, saveErr
|
||||
}
|
||||
return resolution, err
|
||||
}
|
||||
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) {
|
||||
id, err := ids.New()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
latency := now.Sub(started).Milliseconds()
|
||||
observation := capability.CapabilityUsageObservation{
|
||||
ID: capability.CapabilityUsageObservationID(id),
|
||||
ImplementationID: selected.Implementation.ID,
|
||||
ImplementationRevision: selected.Implementation.Revision,
|
||||
CapabilityID: selected.Implementation.CapabilityID,
|
||||
CapabilityKind: selected.Implementation.CapabilityKind,
|
||||
OwnerPrincipalID: selected.Implementation.OwnerPrincipalID,
|
||||
OriginalIntentID: requirement.OriginalIntentID,
|
||||
SourceUtteranceID: requirement.SourceUtteranceID,
|
||||
Outcome: outcome,
|
||||
OutcomeRef: outcomeRef,
|
||||
LatencyMillis: &latency,
|
||||
ObservedAtUnixMillis: now.UnixMilli(),
|
||||
}
|
||||
_ = repository.SaveCapabilityUsageObservation(ctx, observation)
|
||||
}
|
||||
|
||||
func buildReminderRequest(message string, session identity.CreatorSession, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, now time.Time) (continuity.ScheduleReminderRequest, error) {
|
||||
trigger, err := parseReminderTime(message, now)
|
||||
if err != nil {
|
||||
return continuity.ScheduleReminderRequest{}, err
|
||||
}
|
||||
summary := reminderSummary(message)
|
||||
if summary == "" {
|
||||
summary = strings.TrimSpace(message)
|
||||
}
|
||||
return continuity.ScheduleReminderRequest{
|
||||
OwnerPrincipalID: session.PrincipalID,
|
||||
ScopeID: authority.ScopeID("domovoy-global-scope"),
|
||||
SourceIntentID: string(intent.ID),
|
||||
SourceUtteranceID: string(utterance.ID),
|
||||
Summary: summary,
|
||||
TriggerAtUnixMillis: trigger.UnixMilli(),
|
||||
RequestedAtUnixMillis: now.UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseReminderTime(message string, now time.Time) (time.Time, error) {
|
||||
lower := strings.ToLower(message)
|
||||
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{}, fmt.Errorf("invalid relative reminder time")
|
||||
}
|
||||
unit := match[2]
|
||||
if strings.HasPrefix(unit, "час") {
|
||||
if value > 72 {
|
||||
return time.Time{}, fmt.Errorf("relative reminder time is too far")
|
||||
}
|
||||
return now.Add(time.Duration(value) * time.Hour), nil
|
||||
}
|
||||
if value > 24*60 {
|
||||
return time.Time{}, fmt.Errorf("relative reminder time is too far")
|
||||
}
|
||||
return now.Add(time.Duration(value) * time.Minute), nil
|
||||
}
|
||||
day := now
|
||||
if strings.Contains(lower, "послезавтра") {
|
||||
day = now.AddDate(0, 0, 2)
|
||||
} else if strings.Contains(lower, "завтра") {
|
||||
day = now.AddDate(0, 0, 1)
|
||||
}
|
||||
hour, minute := 9, 0
|
||||
re := regexp.MustCompile(`(?:в|на)\s+([0-2]?\d)(?::([0-5]\d))?`)
|
||||
if match := re.FindStringSubmatch(lower); len(match) > 0 {
|
||||
parsedHour, err := strconv.Atoi(match[1])
|
||||
if err != nil || parsedHour > 23 {
|
||||
return time.Time{}, fmt.Errorf("invalid reminder hour")
|
||||
}
|
||||
hour = parsedHour
|
||||
if match[2] != "" {
|
||||
parsedMinute, err := strconv.Atoi(match[2])
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("invalid reminder minute")
|
||||
}
|
||||
minute = parsedMinute
|
||||
}
|
||||
}
|
||||
trigger := time.Date(day.Year(), day.Month(), day.Day(), hour, minute, 0, 0, now.Location())
|
||||
if !trigger.After(now) {
|
||||
trigger = trigger.AddDate(0, 0, 1)
|
||||
}
|
||||
return trigger, nil
|
||||
}
|
||||
|
||||
func reminderSummary(message string) string {
|
||||
clean := strings.TrimSpace(message)
|
||||
lower := strings.ToLower(clean)
|
||||
for _, prefix := range []string{"напомни мне", "напомни"} {
|
||||
if strings.HasPrefix(lower, prefix) {
|
||||
clean = strings.TrimSpace(clean[len(prefix):])
|
||||
lower = strings.ToLower(clean)
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, word := range []string{"послезавтра", "завтра", "сегодня"} {
|
||||
clean = strings.TrimSpace(strings.ReplaceAll(strings.ToLower(clean), word, ""))
|
||||
}
|
||||
relative := regexp.MustCompile(`(?i)через\s+\d{1,3}\s*(минуту|минуты|минут|час|часа|часов)`)
|
||||
clean = strings.TrimSpace(relative.ReplaceAllString(clean, ""))
|
||||
re := regexp.MustCompile(`(?i)(в|на)\s+[0-2]?\d(?::[0-5]\d)?`)
|
||||
clean = strings.TrimSpace(re.ReplaceAllString(clean, ""))
|
||||
clean = strings.Trim(clean, " .,!?:;")
|
||||
return clean
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
)
|
||||
|
||||
func TestBuildReminderRequestParsesTomorrowAtTen(t *testing.T) {
|
||||
now := time.Date(2026, 8, 17, 12, 0, 0, 0, time.FixedZone("MSK", 3*60*60))
|
||||
request, err := buildReminderRequest("Напомни завтра в 10 позвонить врачу.", identity.CreatorSession{PrincipalID: "principal-1"}, interaction.OriginalHumanIntent{ID: "intent-1"}, interaction.HumanUtterance{ID: "utterance-1"}, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
trigger := time.UnixMilli(request.TriggerAtUnixMillis).In(now.Location())
|
||||
if trigger.Year() != 2026 || trigger.Month() != 8 || trigger.Day() != 18 || trigger.Hour() != 10 || trigger.Minute() != 0 {
|
||||
t.Fatalf("unexpected trigger: %s", trigger)
|
||||
}
|
||||
if request.Summary != "позвонить врачу" || request.SourceIntentID != "intent-1" || request.SourceUtteranceID != "utterance-1" {
|
||||
t.Fatalf("unexpected request: %#v", request)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildReminderRequestParsesRelativeMinutes(t *testing.T) {
|
||||
now := time.Date(2026, 8, 17, 12, 0, 0, 0, time.FixedZone("MSK", 3*60*60))
|
||||
request, err := buildReminderRequest("Напомни мне через 3 минуты проверить уведомление.", identity.CreatorSession{PrincipalID: "principal-1"}, interaction.OriginalHumanIntent{ID: "intent-1"}, interaction.HumanUtterance{ID: "utterance-1"}, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
trigger := time.UnixMilli(request.TriggerAtUnixMillis).In(now.Location())
|
||||
if !trigger.Equal(now.Add(3 * time.Minute)) {
|
||||
t.Fatalf("unexpected trigger: %s", trigger)
|
||||
}
|
||||
if request.Summary != "проверить уведомление" {
|
||||
t.Fatalf("unexpected summary: %q", request.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContinuityReminderClientDoesNotAcceptFalseSuccess(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/creator/reminders" || r.Header.Get("Authorization") != "Bearer session" {
|
||||
t.Fatal("unexpected reminder request")
|
||||
}
|
||||
var request continuity.ScheduleReminderRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil || request.Summary != "позвонить врачу" {
|
||||
t.Fatalf("bad payload: %#v err=%v", request, err)
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(continuity.ScheduleReminderResult{Item: continuity.ProspectiveItem{ID: "reminder-1", State: continuity.ProspectiveArmed, Summary: request.Summary}})
|
||||
}))
|
||||
defer upstream.Close()
|
||||
client, err := newContinuityReminderClient(upstream.URL, upstream.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := client.Schedule(context.Background(), "Bearer session", continuity.ScheduleReminderRequest{Summary: "позвонить врачу"})
|
||||
if err != nil || result.Item.ID != "reminder-1" {
|
||||
t.Fatalf("result=%#v err=%v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReminderSummaryRemovesSchedulingWords(t *testing.T) {
|
||||
if summary := reminderSummary("Напомни завтра в 10 позвонить врачу."); summary != "позвонить врачу" || strings.Contains(summary, "10") {
|
||||
t.Fatalf("summary = %q", summary)
|
||||
}
|
||||
}
|
||||
+109
-2
@@ -8,6 +8,7 @@ import (
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
@@ -85,6 +86,14 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer interactionRepository.Close()
|
||||
reminderClient, err := newContinuityReminderClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * time.Second})
|
||||
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)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
|
||||
@@ -230,6 +239,12 @@ 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(),
|
||||
})
|
||||
activeFocusName := ""
|
||||
activeFocusProvenance := situation.ProjectResolutionProvenance("")
|
||||
if semantic.ActiveFocus != nil {
|
||||
@@ -239,7 +254,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
|
||||
}
|
||||
@@ -249,9 +264,13 @@ 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
|
||||
}
|
||||
planningInput := work.PlanningInput{
|
||||
ConversationID: work.ConversationRef(conversation.ID),
|
||||
UtteranceID: work.SourceUtteranceRef(utterance.ID),
|
||||
@@ -268,10 +287,66 @@ 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 {
|
||||
planningReply = "Сейчас у меня подключены реальные capabilities: " + humanCapabilityList(kinds) + ". Для рассуждений я использую отдельный Cognitive Gateway/Router, если локальный CANARY доступен. Возможности вроде отправки почты пока будут фиксироваться как capability gap, а не как выполненное действие."
|
||||
} else {
|
||||
planningReply = "Я умею записывать диалог и намерения, работать с текущим контекстом, строить планы и обращаться к Cognitive Gateway при доступности локального CANARY. Реальные action-capabilities подключаются через registry; если capability нет, я фиксирую gap."
|
||||
}
|
||||
needsCognition = false
|
||||
}
|
||||
if helpPath.Reply != "" {
|
||||
planningReply = helpPath.Reply
|
||||
needsCognition = false
|
||||
}
|
||||
var capabilityStatus capability.CapabilityResolutionStatus
|
||||
var capabilityKind capability.CapabilityKind
|
||||
var capabilityGapID capability.CapabilityGapID
|
||||
var reminder *continuity.ProspectiveItem
|
||||
if helpPath.Kind == work.HelpPathDirectCapability && len(helpPath.SuggestedCapabilityKinds) > 0 {
|
||||
capabilityKind = helpPath.SuggestedCapabilityKinds[0]
|
||||
started := time.Now()
|
||||
resolution, resolveErr := resolveChatCapability(r.Context(), capabilityRepository, capabilityKind, session, intent, utterance, now)
|
||||
capabilityStatus = resolution.Status
|
||||
if resolveErr == nil && len(resolution.Eligible) > 0 {
|
||||
reminderRequest, reminderErr := buildReminderRequest(utterance.Content, session, intent, utterance, now)
|
||||
if reminderErr == nil {
|
||||
result, scheduleErr := reminderClient.Schedule(r.Context(), r.Header.Get("Authorization"), reminderRequest)
|
||||
finished := time.Now()
|
||||
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)
|
||||
} else {
|
||||
planningReply = "Scheduling capability сейчас недоступна: напоминание не создано. Я не буду притворяться, что действие выполнено."
|
||||
observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageFailed, "continuity-reminder-unavailable", started, finished)
|
||||
}
|
||||
} else {
|
||||
planningReply = "Мне нужно уточнить время напоминания. Например: «Напомни завтра в 10 позвонить врачу»."
|
||||
capabilityStatus = capability.ResolutionPartiallyAvailable
|
||||
}
|
||||
} else if resolution.Gap != nil {
|
||||
capabilityGapID = resolution.Gap.ID
|
||||
planningReply = "Сейчас scheduling capability недоступна, поэтому напоминание не создано. Я зафиксировал capability gap."
|
||||
}
|
||||
needsCognition = false
|
||||
}
|
||||
if helpPath.Kind == work.HelpPathCapabilityGap && len(helpPath.SuggestedCapabilityKinds) > 0 {
|
||||
capabilityKind = helpPath.SuggestedCapabilityKinds[0]
|
||||
resolution, resolveErr := resolveChatCapability(r.Context(), capabilityRepository, capabilityKind, session, intent, utterance, now)
|
||||
capabilityStatus = resolution.Status
|
||||
if resolveErr == nil {
|
||||
planningReply = "Capability найдена, но в этом шаге она не исполняется без отдельного разрешённого action path."
|
||||
} else if resolution.Gap != nil {
|
||||
capabilityGapID = resolution.Gap.ID
|
||||
planningReply = "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксировал capability gap, без ложного выполнения."
|
||||
}
|
||||
needsCognition = false
|
||||
}
|
||||
if helpPath.NeedsDurableWork {
|
||||
planning, err := workdomain.BuildPlanningResult(planningInput, now)
|
||||
if err != nil {
|
||||
@@ -289,6 +364,7 @@ func main() {
|
||||
response := interaction.ChatMessageResponse{
|
||||
ConversationID: conversation.ID,
|
||||
UtteranceID: utterance.ID,
|
||||
OriginalIntentID: intent.ID,
|
||||
SpeechAct: semantic.Interpretation.SpeechAct,
|
||||
InterpretationState: utterance.InterpretationState,
|
||||
NeedsCognition: needsCognition,
|
||||
@@ -298,6 +374,10 @@ func main() {
|
||||
Clarification: semantic.Interpretation.Clarification,
|
||||
WorkPlanned: workPlanned,
|
||||
PlanningReply: planningReply,
|
||||
CapabilityStatus: capabilityStatus,
|
||||
CapabilityKind: capabilityKind,
|
||||
CapabilityGapID: capabilityGapID,
|
||||
Reminder: reminder,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
@@ -689,3 +769,30 @@ func readCredentialRefSecret(name, refName string) (string, error) {
|
||||
Path: fileName,
|
||||
})
|
||||
}
|
||||
|
||||
func envOr(name, fallback string) string {
|
||||
if value := os.Getenv(name); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func isCapabilityQuestion(message string) bool {
|
||||
lower := strings.ToLower(message)
|
||||
return strings.Contains(lower, "что ты умеешь") || strings.Contains(lower, "что умеешь") || strings.Contains(lower, "какие возможности")
|
||||
}
|
||||
|
||||
func humanCapabilityList(kinds []capability.CapabilityKind) string {
|
||||
labels := make([]string, 0, len(kinds))
|
||||
for _, kind := range kinds {
|
||||
switch kind {
|
||||
case capability.CapabilityKindScheduleReminder:
|
||||
labels = append(labels, "создавать напоминания")
|
||||
case capability.CapabilityKindCognitiveReasoning:
|
||||
labels = append(labels, "отвечать через когнитивный gateway")
|
||||
default:
|
||||
labels = append(labels, strings.ToLower(strings.ReplaceAll(string(kind), "_", " ")))
|
||||
}
|
||||
}
|
||||
return strings.Join(labels, ", ")
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
)
|
||||
@@ -93,3 +94,78 @@ func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
|
||||
t.Fatalf("unexpected trusted gateway request: %#v", cognitiveRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatMessageReturnsHumanCapabilityMetadata(t *testing.T) {
|
||||
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
|
||||
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActDirective, FallbackText: "fallback", PlanningReply: "Готово. Я создал напоминание: позвонить врачу.", CapabilityStatus: capability.ResolutionAvailable, CapabilityKind: capability.CapabilityKindScheduleReminder})
|
||||
default:
|
||||
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer core.Close()
|
||||
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), "http://cognitive")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Напомни завтра в 10 позвонить врачу."}`))
|
||||
request.Header.Set("Authorization", "Bearer session-token")
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var response browserChatResponse
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.ReplyText != "Готово. Я создал напоминание: позвонить врачу." || response.CapabilityStatus != string(capability.ResolutionAvailable) || response.CapabilityKind != string(capability.CapabilityKindScheduleReminder) {
|
||||
t.Fatalf("unexpected browser response: %#v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatAttentionQuestionUsesContinuityToday(t *testing.T) {
|
||||
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
|
||||
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, FallbackText: "Сейчас в фокусе: УПО."})
|
||||
default:
|
||||
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer core.Close()
|
||||
continuityRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/v1/creator/today" {
|
||||
t.Fatalf("unexpected continuity route %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(continuity.TodayResult{Items: []continuity.TodayItem{{Title: "проверить уведомление", HumanState: "доставлено", DeliveryState: "уведомление отправлено"}}})
|
||||
}))
|
||||
defer continuityRuntime.Close()
|
||||
handler, err := newHandler(core.URL, continuityRuntime.URL, "http://commit", core.Client(), "http://cognitive")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Что сейчас требует моего внимания?"}`))
|
||||
request.Header.Set("Authorization", "Bearer session-token")
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var response browserChatResponse
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(response.ReplyText, "проверить уведомление") || strings.Contains(response.ReplyText, "УПО") {
|
||||
t.Fatalf("attention reply did not use canonical today: %#v", response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"git.cin.su/m/Assistant/contracts/situation"
|
||||
@@ -151,6 +152,9 @@ type browserChatResponse struct {
|
||||
ProjectProvenance string `json:"project_provenance"`
|
||||
Clarification string `json:"clarification"`
|
||||
WorkPlanned bool `json:"work_planned"`
|
||||
CapabilityStatus string `json:"capability_status"`
|
||||
CapabilityKind string `json:"capability_kind"`
|
||||
ReminderScheduled bool `json:"reminder_scheduled"`
|
||||
}
|
||||
|
||||
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -320,6 +324,15 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
||||
if saved.PlanningReply != "" {
|
||||
reply = saved.PlanningReply
|
||||
}
|
||||
if asksAttention(browserRequest.Message) {
|
||||
var today continuity.TodayResult
|
||||
status, err = g.getTrustedJSON(r, g.continuity, "/v1/creator/today", "", authorization, &today)
|
||||
if err == nil && status == http.StatusOK {
|
||||
reply = renderTodayReply(today)
|
||||
cognition = "not_requested"
|
||||
saved.NeedsCognition = false
|
||||
}
|
||||
}
|
||||
if saved.NeedsCognition && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
|
||||
cognition = "unavailable"
|
||||
requestID, idErr := ids.New()
|
||||
@@ -340,7 +353,46 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition, ActiveFocus: saved.ActiveProjectName, ProjectProvenance: string(saved.ProjectProvenance), Clarification: string(saved.Clarification), WorkPlanned: saved.WorkPlanned})
|
||||
_ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition, ActiveFocus: saved.ActiveProjectName, ProjectProvenance: string(saved.ProjectProvenance), Clarification: string(saved.Clarification), WorkPlanned: saved.WorkPlanned, CapabilityStatus: string(saved.CapabilityStatus), CapabilityKind: string(saved.CapabilityKind), ReminderScheduled: saved.Reminder != nil})
|
||||
}
|
||||
|
||||
func asksAttention(message string) bool {
|
||||
lower := strings.ToLower(message)
|
||||
return strings.Contains(lower, "требует моего внимания") || strings.Contains(lower, "требует внимания") || strings.Contains(lower, "что сейчас важно")
|
||||
}
|
||||
|
||||
func renderTodayReply(today continuity.TodayResult) string {
|
||||
if len(today.Items) == 0 {
|
||||
return "Сейчас ничего срочного не требует внимания."
|
||||
}
|
||||
limit := len(today.Items)
|
||||
if limit > 5 {
|
||||
limit = 5
|
||||
}
|
||||
var builder strings.Builder
|
||||
builder.WriteString("Сейчас требует внимания:\n")
|
||||
for index := 0; index < limit; index++ {
|
||||
item := today.Items[index]
|
||||
title := strings.TrimSpace(item.Title)
|
||||
if title == "" {
|
||||
title = "событие Домового"
|
||||
}
|
||||
state := strings.TrimSpace(item.HumanState)
|
||||
if state == "" {
|
||||
state = "ожидает внимания"
|
||||
}
|
||||
builder.WriteString("- ")
|
||||
builder.WriteString(title)
|
||||
builder.WriteString(" — ")
|
||||
builder.WriteString(state)
|
||||
if item.DeliveryState != "" {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(item.DeliveryState)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
return strings.TrimSpace(builder.String())
|
||||
}
|
||||
|
||||
func validAdvisoryResponse(response cognitive.GatewayAdvisoryResponse, request cognitive.GatewayAdvisoryRequest) bool {
|
||||
@@ -491,6 +543,10 @@ func creatorRoute(method, path string) (routeTarget, string, bool) {
|
||||
}
|
||||
case method == http.MethodGet && path == "/api/prospective-items":
|
||||
return targetContinuity, "/v1/creator/prospective-items", true
|
||||
case method == http.MethodGet && path == "/api/attention":
|
||||
return targetContinuity, "/v1/creator/attention", true
|
||||
case method == http.MethodGet && path == "/api/today":
|
||||
return targetContinuity, "/v1/creator/today", true
|
||||
case method == http.MethodGet && path == "/api/actions":
|
||||
return targetCommit, "/v1/creator/authorized-actions", true
|
||||
case method == http.MethodPost && path == "/api/actions/satisfy-delivered":
|
||||
|
||||
@@ -29,12 +29,18 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
|
||||
.login-form button, .composer button { border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
|
||||
.login-form button { width: 100%; height: 50px; margin-top: 18px; }
|
||||
#login-error { min-height: 20px; color: var(--bad); font-size: 13px; }
|
||||
.chat-view { min-height: 100vh; display: grid; grid-template-rows: 64px minmax(0, 1fr) auto; }
|
||||
.chat-view { min-height: 100vh; display: grid; grid-template-rows: 64px minmax(0, 1fr); }
|
||||
.chat-header { display: flex; align-items: center; gap: 18px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .96); }
|
||||
.chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); }
|
||||
.chat-header span { color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
|
||||
#active-focus { margin-left: auto; color: var(--good); }
|
||||
.chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; }
|
||||
.human-nav { display: flex; align-items: center; gap: 8px; }
|
||||
.nav-tab, .operator-link { height: 34px; padding: 0 12px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--muted); background: #0b110e; text-decoration: none; font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
|
||||
.nav-tab.active { color: #11150f; border-color: var(--signal); background: var(--signal); }
|
||||
.operator-link { color: var(--good); }
|
||||
.surface-panel { min-height: 0; position: relative; }
|
||||
.active-panel { display: grid; min-height: 0; }
|
||||
.messages { width: min(100%, 920px); margin: 0 auto; padding: 28px 18px 120px; display: flex; flex-direction: column; gap: 14px; overflow: auto; }
|
||||
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
|
||||
.message.user { align-self: flex-end; background: #151e1a; }
|
||||
@@ -43,8 +49,28 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
|
||||
.composer { position: sticky; bottom: 0; width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 20px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
|
||||
.composer button { height: 48px; align-self: end; }
|
||||
.composer button:disabled { cursor: wait; filter: saturate(.3); }
|
||||
.attention-surface, .history-surface { width: min(100%, 920px); margin: 0 auto; padding: 34px 18px; align-content: start; gap: 18px; }
|
||||
.surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 16px; }
|
||||
.surface-heading p { margin: 0; font: 400 36px/.95 var(--serif); }
|
||||
.surface-heading span, .attention-card small, .history-item span { color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .1em; }
|
||||
.surface-heading button { height: 36px; padding: 0 12px; border: 1px solid var(--line); background: transparent; }
|
||||
.attention-list, .history-list { display: grid; gap: 12px; }
|
||||
.attention-card, .history-item { border: 1px solid var(--line); background: #0f1613; padding: 16px; }
|
||||
.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); }
|
||||
.empty-state.error { color: var(--bad); }
|
||||
@media (max-width: 640px) {
|
||||
.composer { grid-template-columns: 1fr; }
|
||||
.composer button { width: 100%; }
|
||||
.chat-header { gap: 10px; padding: 0 12px; }
|
||||
.chat-header span { display: none; }
|
||||
.human-nav { overflow-x: auto; }
|
||||
.operator-link, .nav-tab { padding: 0 9px; }
|
||||
}
|
||||
|
||||
@@ -8,9 +8,14 @@ const messageInput = document.querySelector("#message");
|
||||
const sendButton = document.querySelector("#send");
|
||||
const logoutButton = document.querySelector("#logout");
|
||||
const activeFocus = document.querySelector("#active-focus");
|
||||
const todayList = document.querySelector("#today-list");
|
||||
const refreshToday = document.querySelector("#refresh-today");
|
||||
const historyList = document.querySelector("#history-list");
|
||||
const navTabs = Array.from(document.querySelectorAll(".nav-tab"));
|
||||
|
||||
let token = sessionStorage.getItem("domovoy.chat.token") || "";
|
||||
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
|
||||
let localHistory = [];
|
||||
|
||||
function showChat() {
|
||||
loginView.hidden = true;
|
||||
@@ -37,11 +42,14 @@ function appendMessage(kind, text, meta = "") {
|
||||
}
|
||||
messages.appendChild(node);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
localHistory.push({kind, text, meta, at: new Date()});
|
||||
renderHistory();
|
||||
}
|
||||
|
||||
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 = {}) {
|
||||
@@ -52,6 +60,134 @@ async function api(path, options = {}) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function humanMeta(result) {
|
||||
if (result.reminder_scheduled) return "Напоминание сохранено. Я вернусь к нему сам.";
|
||||
if (result.capability_status && result.capability_status !== "AVAILABLE") return "Возможность пока не подключена.";
|
||||
if (result.work_planned) return "Я разложил это как работу.";
|
||||
return "";
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
if (!historyList) return;
|
||||
historyList.replaceChildren();
|
||||
const items = localHistory.slice(-30).reverse();
|
||||
if (!items.length) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "empty-state";
|
||||
empty.textContent = "История появится после сообщений в этом окне.";
|
||||
historyList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
for (const item of items) {
|
||||
const row = document.createElement("article");
|
||||
row.className = `history-item ${item.kind}`;
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = item.kind === "user" ? "Вы" : "Домовой";
|
||||
const body = document.createElement("p");
|
||||
body.textContent = item.text;
|
||||
const time = document.createElement("span");
|
||||
time.textContent = item.at.toLocaleTimeString("ru-RU", {hour: "2-digit", minute: "2-digit"});
|
||||
row.append(title, body, time);
|
||||
historyList.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadToday() {
|
||||
if (!todayList || !token) return;
|
||||
todayList.replaceChildren();
|
||||
const loading = document.createElement("p");
|
||||
loading.className = "empty-state";
|
||||
loading.textContent = "Смотрю, что требует внимания...";
|
||||
todayList.appendChild(loading);
|
||||
try {
|
||||
const result = await api("/api/today", {method: "GET"});
|
||||
todayList.replaceChildren();
|
||||
const items = result.Items || result.items || [];
|
||||
if (!items.length) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "empty-state";
|
||||
empty.textContent = "Сейчас ничего срочного не требует внимания.";
|
||||
todayList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
for (const item of items) {
|
||||
const card = document.createElement("article");
|
||||
card.className = "attention-card";
|
||||
const when = document.createElement("time");
|
||||
const stamp = item.WhenUnixMillis || item.when_unix_millis || 0;
|
||||
when.textContent = stamp ? new Date(stamp).toLocaleString("ru-RU", {day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit"}) : "сейчас";
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = item.Title || item.title || "Событие Домового";
|
||||
const state = document.createElement("p");
|
||||
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(" · ");
|
||||
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 {
|
||||
todayList.replaceChildren();
|
||||
const error = document.createElement("p");
|
||||
error.className = "empty-state error";
|
||||
error.textContent = "Внимание сейчас недоступно. Чат можно продолжать.";
|
||||
todayList.appendChild(error);
|
||||
}
|
||||
}
|
||||
|
||||
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")) {
|
||||
const active = panel.id === panelID;
|
||||
panel.hidden = !active;
|
||||
panel.classList.toggle("active-panel", active);
|
||||
}
|
||||
if (panelID === "today-panel") loadToday();
|
||||
if (panelID === "history-panel") renderHistory();
|
||||
}
|
||||
|
||||
loginForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
loginError.textContent = "";
|
||||
@@ -71,26 +207,16 @@ 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);
|
||||
const semanticMeta = [result.speech_act, `cognition=${result.cognition}`];
|
||||
if (result.work_planned) semanticMeta.push("work=planned");
|
||||
if (result.clarification) semanticMeta.push(`clarification=${result.clarification}`);
|
||||
appendMessage("assistant", result.reply_text || "Сообщение записано.", semanticMeta.join("; "));
|
||||
} catch {
|
||||
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
|
||||
} finally {
|
||||
sendButton.disabled = false;
|
||||
messageInput.focus();
|
||||
}
|
||||
await sendChatMessage(text);
|
||||
});
|
||||
|
||||
for (const tab of navTabs) {
|
||||
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
|
||||
}
|
||||
|
||||
if (refreshToday) refreshToday.addEventListener("click", loadToday);
|
||||
|
||||
logoutButton.addEventListener("click", () => {
|
||||
token = "";
|
||||
conversationID = "";
|
||||
|
||||
@@ -25,16 +25,38 @@
|
||||
|
||||
<section id="chat-view" class="chat-view" hidden>
|
||||
<header class="chat-header">
|
||||
<a href="/" aria-label="Консоль создателя">Домовой</a>
|
||||
<a href="/chat" aria-label="Домовой">Домовой</a>
|
||||
<nav class="human-nav" aria-label="Разделы Домового">
|
||||
<button class="nav-tab active" data-tab="chat-panel" type="button">Чат</button>
|
||||
<button class="nav-tab" data-tab="today-panel" type="button">Сегодня</button>
|
||||
<button class="nav-tab" data-tab="history-panel" type="button">История</button>
|
||||
<a href="/" class="operator-link">Оператор</a>
|
||||
</nav>
|
||||
<span id="active-focus">фокус: не выбран</span>
|
||||
<span id="session-state">сессия активна</span>
|
||||
<button id="logout" type="button" aria-label="Выйти">↗</button>
|
||||
</header>
|
||||
<div id="messages" class="messages" aria-live="polite"></div>
|
||||
<form id="chat-form" class="composer">
|
||||
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
|
||||
<button id="send" type="submit">Отправить</button>
|
||||
</form>
|
||||
<section id="chat-panel" class="surface-panel active-panel">
|
||||
<div id="messages" class="messages" aria-live="polite"></div>
|
||||
<form id="chat-form" class="composer">
|
||||
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
|
||||
<button id="send" type="submit">Отправить</button>
|
||||
</form>
|
||||
</section>
|
||||
<section id="today-panel" class="surface-panel attention-surface" hidden>
|
||||
<div class="surface-heading">
|
||||
<p>Сегодня / внимание</p>
|
||||
<button id="refresh-today" type="button">Обновить</button>
|
||||
</div>
|
||||
<div id="today-list" class="attention-list"></div>
|
||||
</section>
|
||||
<section id="history-panel" class="surface-panel history-surface" hidden>
|
||||
<div class="surface-heading">
|
||||
<p>История</p>
|
||||
<span>Последние сообщения текущего окна</span>
|
||||
</div>
|
||||
<div id="history-list" class="history-list"></div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
<script src="/assets/chat.js"></script>
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package capability
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
type ImplementationKind string
|
||||
type ImplementationTargetRef string
|
||||
type AuthorityPolicyRef string
|
||||
type DataConstraint string
|
||||
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"
|
||||
ImplementationCognitiveBinding ImplementationKind = "COGNITIVE_BINDING"
|
||||
ImplementationExternalAdapter ImplementationKind = "EXTERNAL_ADAPTER"
|
||||
ImplementationAgent ImplementationKind = "AGENT"
|
||||
|
||||
AuthorityPolicyCreatorDirectiveV1 AuthorityPolicyRef = "creator-directive-v1"
|
||||
AuthorityPolicyCognitiveGatewayV1 AuthorityPolicyRef = "cognitive-gateway-v1"
|
||||
|
||||
DataConstraintLocalOnly DataConstraint = "LOCAL_ONLY"
|
||||
DataConstraintExternalAllowed DataConstraint = "EXTERNAL_ALLOWED"
|
||||
DataConstraintNoCredentialForward DataConstraint = "NO_CREDENTIAL_FORWARD"
|
||||
|
||||
ResolutionAvailable CapabilityResolutionStatus = "AVAILABLE"
|
||||
ResolutionPartiallyAvailable CapabilityResolutionStatus = "PARTIALLY_AVAILABLE"
|
||||
ResolutionUnavailable CapabilityResolutionStatus = "UNAVAILABLE"
|
||||
ResolutionUnknown CapabilityResolutionStatus = "UNKNOWN"
|
||||
|
||||
GapNoImplementation GapReason = "NO_IMPLEMENTATION"
|
||||
GapUnhealthy GapReason = "UNHEALTHY"
|
||||
GapInactive GapReason = "INACTIVE"
|
||||
GapUnqualified GapReason = "UNQUALIFIED"
|
||||
GapUnauthorized GapReason = "UNAUTHORIZED"
|
||||
GapIncompatible GapReason = "INCOMPATIBLE"
|
||||
GapUnknown GapReason = "UNKNOWN"
|
||||
|
||||
AcquireUseAlternative AcquisitionStrategyKind = "USE_ALTERNATIVE"
|
||||
AcquireDiscoverExisting AcquisitionStrategyKind = "DISCOVER_EXISTING"
|
||||
AcquireConnectExisting AcquisitionStrategyKind = "CONNECT_EXISTING"
|
||||
AcquireConfigureExisting AcquisitionStrategyKind = "CONFIGURE_EXISTING"
|
||||
AcquireExternal AcquisitionStrategyKind = "ACQUIRE_EXTERNAL"
|
||||
AcquireBuildNew AcquisitionStrategyKind = "BUILD_NEW"
|
||||
AcquireRequestUserInput AcquisitionStrategyKind = "REQUEST_USER_INPUT"
|
||||
AcquireUnresolvableCurrently AcquisitionStrategyKind = "UNRESOLVABLE_CURRENTLY"
|
||||
|
||||
UsageSucceeded UsageOutcomeClass = "SUCCEEDED"
|
||||
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
|
||||
// intentionally extensible string semantics, not a closed-world enum.
|
||||
type CapabilityDescriptor struct {
|
||||
ID CapabilityID
|
||||
Kind CapabilityKind
|
||||
DisplayName string
|
||||
Description string
|
||||
Revision int64
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
// CapabilityImplementation is the generic implementation/binding layer. It
|
||||
// may point at a cognitive binding, internal function, agent, adapter or later
|
||||
// provider type. CredentialRef is metadata only and never grants authority.
|
||||
type CapabilityImplementation struct {
|
||||
ID CapabilityImplementationID
|
||||
CapabilityID CapabilityID
|
||||
CapabilityKind CapabilityKind
|
||||
OwnerPrincipalID identity.PrincipalID
|
||||
Revision int64
|
||||
Kind ImplementationKind
|
||||
TargetRef ImplementationTargetRef
|
||||
CredentialRef CredentialRef
|
||||
Qualification QualificationState
|
||||
Activation ActivationState
|
||||
Health HealthState
|
||||
AuthorityPolicyRef AuthorityPolicyRef
|
||||
DataConstraints []DataConstraint
|
||||
CostScoreMillis *int64
|
||||
LatencyScoreMillis *int64
|
||||
ReliabilityMillis *int64
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type CapabilityResolutionRequirement struct {
|
||||
RequirementID RequirementRef
|
||||
CapabilityKind CapabilityKind
|
||||
CapabilityID CapabilityID
|
||||
OwnerPrincipalID identity.PrincipalID
|
||||
ActorID identity.ActorID
|
||||
OriginalIntentID OriginalIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
RequiredConstraints []DataConstraint
|
||||
AllowCanary bool
|
||||
AuthorityContextRef AuthorityPolicyRef
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type ResolvedCapabilityImplementation struct {
|
||||
Implementation CapabilityImplementation
|
||||
Selected bool
|
||||
Rank int
|
||||
}
|
||||
|
||||
type CapabilityResolution struct {
|
||||
ID CapabilityResolutionID
|
||||
Requirement CapabilityResolutionRequirement
|
||||
Status CapabilityResolutionStatus
|
||||
Eligible []ResolvedCapabilityImplementation
|
||||
ExcludedReasons map[CapabilityImplementationID]GapReason
|
||||
Gap *CapabilityGap
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type CapabilityGap struct {
|
||||
ID CapabilityGapID
|
||||
RequirementID RequirementRef
|
||||
CapabilityKind CapabilityKind
|
||||
CapabilityID CapabilityID
|
||||
OwnerPrincipalID identity.PrincipalID
|
||||
OriginalIntentID OriginalIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
Reason GapReason
|
||||
Detail string
|
||||
Strategies []AcquisitionStrategyCandidate
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type AcquisitionStrategyCandidate struct {
|
||||
Kind AcquisitionStrategyKind
|
||||
Reason string
|
||||
}
|
||||
|
||||
type CapabilityUsageObservation struct {
|
||||
ID CapabilityUsageObservationID
|
||||
ImplementationID CapabilityImplementationID
|
||||
ImplementationRevision int64
|
||||
CapabilityID CapabilityID
|
||||
CapabilityKind CapabilityKind
|
||||
OwnerPrincipalID identity.PrincipalID
|
||||
OriginalIntentID OriginalIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
Outcome UsageOutcomeClass
|
||||
OutcomeRef string
|
||||
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
|
||||
}
|
||||
@@ -9,9 +9,14 @@ import (
|
||||
|
||||
type CommitmentID ids.ID
|
||||
type ProspectiveItemID ids.ID
|
||||
type AttentionItemID ids.ID
|
||||
type ProspectiveRevision uint64
|
||||
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
|
||||
|
||||
@@ -26,6 +31,35 @@ const (
|
||||
ProspectiveExpired ProspectiveState = "EXPIRED"
|
||||
)
|
||||
|
||||
type AttentionLevel string
|
||||
|
||||
const (
|
||||
AttentionUrgent AttentionLevel = "URGENT"
|
||||
AttentionActionRequired AttentionLevel = "ACTION_REQUIRED"
|
||||
AttentionDue AttentionLevel = "DUE"
|
||||
AttentionImportantChange AttentionLevel = "IMPORTANT_CHANGE"
|
||||
AttentionFYI AttentionLevel = "FYI"
|
||||
AttentionBackground AttentionLevel = "BACKGROUND"
|
||||
)
|
||||
|
||||
type AttentionState string
|
||||
|
||||
const (
|
||||
AttentionPending AttentionState = "PENDING"
|
||||
AttentionDelivering AttentionState = "DELIVERING"
|
||||
AttentionDelivered AttentionState = "DELIVERED"
|
||||
AttentionResolved AttentionState = "RESOLVED"
|
||||
AttentionSuppressed AttentionState = "SUPPRESSED"
|
||||
AttentionExpired AttentionState = "EXPIRED"
|
||||
AttentionFailed AttentionState = "FAILED"
|
||||
)
|
||||
|
||||
type AttentionSourceKind string
|
||||
|
||||
const (
|
||||
AttentionSourceProspectiveReminder AttentionSourceKind = "PROSPECTIVE_REMINDER"
|
||||
)
|
||||
|
||||
// ProspectiveItem is canonical future relevance. TriggerAtUnixMillis is an
|
||||
// absolute instant; scheduling engines are derived delivery mechanisms only.
|
||||
type ProspectiveItem struct {
|
||||
@@ -34,11 +68,151 @@ type ProspectiveItem struct {
|
||||
OwnerPrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
GoalID work.GoalID
|
||||
SourceIntentID string
|
||||
SourceUtteranceID string
|
||||
Summary string
|
||||
TriggerAtUnixMillis int64
|
||||
State ProspectiveState
|
||||
Revision ProspectiveRevision
|
||||
}
|
||||
|
||||
type ScheduleReminderRequest struct {
|
||||
OwnerPrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
SourceIntentID string
|
||||
SourceUtteranceID string
|
||||
Summary string
|
||||
TriggerAtUnixMillis int64
|
||||
RequestedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type ScheduleReminderResult struct {
|
||||
Item ProspectiveItem
|
||||
}
|
||||
|
||||
// AttentionItem is Continuity-owned human-facing relevance state. It can be
|
||||
// rendered by UIs, but it is not a notification receipt and does not itself
|
||||
// grant authority for external mutation.
|
||||
type AttentionItem struct {
|
||||
ID AttentionItemID
|
||||
OwnerPrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
SourceKind AttentionSourceKind
|
||||
SourceProspectiveID ProspectiveItemID
|
||||
Level AttentionLevel
|
||||
State AttentionState
|
||||
Summary string
|
||||
DueAtUnixMillis int64
|
||||
CreatedAtUnixMillis int64
|
||||
DeliveredAtUnixMillis int64
|
||||
ActionIntentID CommittedActionIntentID
|
||||
AuthorizedActionID CommittedAuthorizedActionID
|
||||
ActionAttemptID CommittedActionAttemptID
|
||||
DeliveryOutcome string
|
||||
ResponseID HumanResponseID
|
||||
ResponseUtteranceID string
|
||||
ResponseOperation HumanResponseOperation
|
||||
ResponseOutcome ResponseOutcome
|
||||
RespondedAtUnixMillis int64
|
||||
Revision AttentionRevision
|
||||
}
|
||||
|
||||
type AttentionListResult struct {
|
||||
Items []AttentionItem
|
||||
}
|
||||
|
||||
type TodayItem struct {
|
||||
Title string
|
||||
WhenUnixMillis int64
|
||||
AttentionLevel AttentionLevel
|
||||
AttentionState AttentionState
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package interaction
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/situation"
|
||||
)
|
||||
@@ -121,6 +123,7 @@ type ChatMessageRequest struct {
|
||||
type ChatMessageResponse struct {
|
||||
ConversationID ConversationID
|
||||
UtteranceID HumanUtteranceID
|
||||
OriginalIntentID OriginalHumanIntentID
|
||||
SpeechAct SpeechAct
|
||||
InterpretationState InterpretationState
|
||||
NeedsCognition bool
|
||||
@@ -130,4 +133,8 @@ type ChatMessageResponse struct {
|
||||
Clarification ClarificationState
|
||||
WorkPlanned bool
|
||||
PlanningReply string
|
||||
CapabilityStatus capability.CapabilityResolutionStatus
|
||||
CapabilityKind capability.CapabilityKind
|
||||
CapabilityGapID capability.CapabilityGapID
|
||||
Reminder *continuity.ProspectiveItem
|
||||
}
|
||||
|
||||
@@ -407,6 +407,145 @@ CREATE TABLE IF NOT EXISTS capability.cognitive_performance_evidence (
|
||||
CHECK (latency_micros IS NULL OR latency_micros >= 0), CHECK (memory_bytes IS NULL OR memory_bytes >= 0),
|
||||
CHECK (reliability_millis IS NULL OR reliability_millis >= 0), CHECK (observed_at_unix_millis > 0)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS capability.capability_descriptors (
|
||||
capability_id TEXT PRIMARY KEY,
|
||||
capability_kind TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (capability_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (display_name <> ''),
|
||||
CHECK (revision > 0),
|
||||
CHECK (recorded_at_unix_millis > 0)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS capability.capability_implementations (
|
||||
implementation_id TEXT NOT NULL,
|
||||
capability_id TEXT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
owner_principal_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
implementation_kind TEXT NOT NULL,
|
||||
target_ref TEXT NOT NULL,
|
||||
credential_ref TEXT NOT NULL DEFAULT '',
|
||||
qualification TEXT NOT NULL,
|
||||
activation TEXT NOT NULL,
|
||||
health TEXT NOT NULL,
|
||||
authority_policy_ref TEXT NOT NULL,
|
||||
data_constraints TEXT[] NOT NULL,
|
||||
cost_score_millis BIGINT,
|
||||
latency_score_millis BIGINT,
|
||||
reliability_millis BIGINT,
|
||||
observed_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (implementation_id, revision),
|
||||
CHECK (implementation_id <> ''),
|
||||
CHECK (capability_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (owner_principal_id <> ''),
|
||||
CHECK (revision > 0),
|
||||
CHECK (implementation_kind IN ('INTERNAL_FUNCTION','COGNITIVE_BINDING','EXTERNAL_ADAPTER','AGENT')),
|
||||
CHECK (target_ref <> ''),
|
||||
CHECK (qualification IN ('DISCOVERED','PROBING','EVALUATING','VALIDATED','REJECTED')),
|
||||
CHECK (activation IN ('INACTIVE','CANARY','ACTIVE','DEPRECATED','RETIRED')),
|
||||
CHECK (health IN ('UNKNOWN','AVAILABLE','DEGRADED','UNAVAILABLE')),
|
||||
CHECK (authority_policy_ref <> ''),
|
||||
CHECK (cardinality(data_constraints) > 0),
|
||||
CHECK (cost_score_millis IS NULL OR cost_score_millis >= 0),
|
||||
CHECK (latency_score_millis IS NULL OR latency_score_millis >= 0),
|
||||
CHECK (reliability_millis IS NULL OR reliability_millis >= 0),
|
||||
CHECK (observed_at_unix_millis > 0)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS capability.capability_implementation_heads (
|
||||
implementation_id TEXT PRIMARY KEY,
|
||||
revision BIGINT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
owner_principal_id TEXT NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (revision > 0)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS capability.capability_gaps (
|
||||
gap_id TEXT PRIMARY KEY,
|
||||
requirement_id TEXT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
capability_id TEXT NOT NULL DEFAULT '',
|
||||
owner_principal_id TEXT NOT NULL,
|
||||
original_intent_id TEXT NOT NULL,
|
||||
source_utterance_id TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
detail TEXT NOT NULL,
|
||||
strategies TEXT[] NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (requirement_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (owner_principal_id <> ''),
|
||||
CHECK (original_intent_id <> ''),
|
||||
CHECK (source_utterance_id <> ''),
|
||||
CHECK (reason IN ('NO_IMPLEMENTATION','UNHEALTHY','INACTIVE','UNQUALIFIED','UNAUTHORIZED','INCOMPATIBLE','UNKNOWN')),
|
||||
CHECK (detail <> ''),
|
||||
CHECK (cardinality(strategies) > 0),
|
||||
CHECK (recorded_at_unix_millis > 0)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS capability.capability_usage_observations (
|
||||
observation_id TEXT PRIMARY KEY,
|
||||
implementation_id TEXT NOT NULL,
|
||||
implementation_revision BIGINT NOT NULL,
|
||||
capability_id TEXT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
owner_principal_id TEXT NOT NULL,
|
||||
original_intent_id TEXT NOT NULL,
|
||||
source_utterance_id TEXT NOT NULL,
|
||||
outcome TEXT NOT NULL,
|
||||
outcome_ref TEXT NOT NULL DEFAULT '',
|
||||
latency_millis BIGINT,
|
||||
observed_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (implementation_id <> ''),
|
||||
CHECK (implementation_revision > 0),
|
||||
CHECK (capability_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (owner_principal_id <> ''),
|
||||
CHECK (original_intent_id <> ''),
|
||||
CHECK (source_utterance_id <> ''),
|
||||
CHECK (outcome IN ('SUCCEEDED','FAILED','SKIPPED','UNKNOWN')),
|
||||
CHECK (latency_millis IS NULL OR latency_millis >= 0),
|
||||
CHECK (observed_at_unix_millis > 0)
|
||||
);
|
||||
INSERT INTO capability.capability_descriptors(capability_id, capability_kind, display_name, description, revision, recorded_at_unix_millis)
|
||||
VALUES
|
||||
('schedule-reminder', 'SCHEDULE_REMINDER', 'Reminder scheduling', 'Create a Continuity-owned prospective reminder item from an authenticated human directive.', 1, 1),
|
||||
('creator-advisory', 'COGNITIVE_REASONING', 'Creator advisory cognition', 'Reasoning capability resolved through the specialized Cognitive Registry and Gateway.', 1, 1)
|
||||
ON CONFLICT (capability_id) DO NOTHING;
|
||||
INSERT INTO capability.capability_implementations(
|
||||
implementation_id, capability_id, capability_kind, owner_principal_id, revision, implementation_kind, target_ref,
|
||||
credential_ref, qualification, activation, health, authority_policy_ref, data_constraints,
|
||||
cost_score_millis, latency_score_millis, reliability_millis, observed_at_unix_millis)
|
||||
VALUES (
|
||||
'internal-reminder-scheduler-v1', 'schedule-reminder', 'SCHEDULE_REMINDER', 'domovoy-creator-principal',
|
||||
1, 'INTERNAL_FUNCTION', 'continuity://prospective-reminder-v1', '', 'VALIDATED', 'ACTIVE', 'AVAILABLE',
|
||||
'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 0, 10, 950, 1
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
INSERT INTO capability.capability_implementation_heads(implementation_id, revision, capability_kind, owner_principal_id)
|
||||
VALUES ('internal-reminder-scheduler-v1', 1, 'SCHEDULE_REMINDER', 'domovoy-creator-principal')
|
||||
ON CONFLICT (implementation_id) DO NOTHING;
|
||||
INSERT INTO capability.notification_bindings (
|
||||
binding_id, capability_id, owner_principal_id, revision, state, https_origin,
|
||||
delivery_path, receipt_path, protocol, secret_ref, verified_at_unix_millis, health
|
||||
)
|
||||
VALUES (
|
||||
'domovoy-notification-binding', 'prospective-notification-delivery', 'domovoy-creator-principal',
|
||||
2, 'ACTIVE', 'https://assistant.cin.su', '/delivery/v1/deliveries', '/delivery/v1/receipts',
|
||||
'IDEMPOTENT_RECEIPT_V1', 'domovoy-notification-write-v1', 1, 'AVAILABLE'
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
INSERT INTO capability.notification_binding_heads(capability_id, owner_principal_id, binding_id, revision)
|
||||
VALUES ('prospective-notification-delivery', 'domovoy-creator-principal', 'domovoy-notification-binding', 2)
|
||||
ON CONFLICT (capability_id, owner_principal_id) DO UPDATE SET binding_id=EXCLUDED.binding_id, revision=EXCLUDED.revision
|
||||
WHERE capability.notification_binding_heads.revision < EXCLUDED.revision;
|
||||
RESET ROLE;
|
||||
|
||||
SET ROLE continuity_role;
|
||||
@@ -416,6 +555,9 @@ CREATE TABLE IF NOT EXISTS continuity.prospective_items (
|
||||
owner_principal_id TEXT NOT NULL,
|
||||
scope_id TEXT NOT NULL,
|
||||
goal_id TEXT NOT NULL,
|
||||
source_intent_id TEXT NOT NULL DEFAULT '',
|
||||
source_utterance_id TEXT NOT NULL DEFAULT '',
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
trigger_at_unix_millis BIGINT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
@@ -436,6 +578,77 @@ CREATE TABLE IF NOT EXISTS continuity.action_receipts (
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (expected_revision > 0), CHECK (result_revision > expected_revision)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS continuity.attention_items (
|
||||
attention_item_id TEXT PRIMARY KEY,
|
||||
owner_principal_id TEXT NOT NULL,
|
||||
scope_id TEXT NOT NULL,
|
||||
source_kind TEXT NOT NULL,
|
||||
source_prospective_item_id TEXT NOT NULL,
|
||||
level TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
due_at_unix_millis BIGINT NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
delivered_at_unix_millis BIGINT NOT NULL DEFAULT 0,
|
||||
action_intent_id TEXT NOT NULL DEFAULT '',
|
||||
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','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 <> '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;
|
||||
|
||||
SET ROLE interaction_role;
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE capability_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.capability_descriptors (
|
||||
capability_id TEXT PRIMARY KEY,
|
||||
capability_kind TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (capability_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (display_name <> ''),
|
||||
CHECK (revision > 0),
|
||||
CHECK (recorded_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.capability_implementations (
|
||||
implementation_id TEXT NOT NULL,
|
||||
capability_id TEXT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
owner_principal_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
implementation_kind TEXT NOT NULL,
|
||||
target_ref TEXT NOT NULL,
|
||||
credential_ref TEXT NOT NULL DEFAULT '',
|
||||
qualification TEXT NOT NULL,
|
||||
activation TEXT NOT NULL,
|
||||
health TEXT NOT NULL,
|
||||
authority_policy_ref TEXT NOT NULL,
|
||||
data_constraints TEXT[] NOT NULL,
|
||||
cost_score_millis BIGINT,
|
||||
latency_score_millis BIGINT,
|
||||
reliability_millis BIGINT,
|
||||
observed_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (implementation_id, revision),
|
||||
CHECK (implementation_id <> ''),
|
||||
CHECK (capability_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (owner_principal_id <> ''),
|
||||
CHECK (revision > 0),
|
||||
CHECK (implementation_kind IN ('INTERNAL_FUNCTION','COGNITIVE_BINDING','EXTERNAL_ADAPTER','AGENT')),
|
||||
CHECK (target_ref <> ''),
|
||||
CHECK (qualification IN ('DISCOVERED','PROBING','EVALUATING','VALIDATED','REJECTED')),
|
||||
CHECK (activation IN ('INACTIVE','CANARY','ACTIVE','DEPRECATED','RETIRED')),
|
||||
CHECK (health IN ('UNKNOWN','AVAILABLE','DEGRADED','UNAVAILABLE')),
|
||||
CHECK (authority_policy_ref <> ''),
|
||||
CHECK (cardinality(data_constraints) > 0),
|
||||
CHECK (cost_score_millis IS NULL OR cost_score_millis >= 0),
|
||||
CHECK (latency_score_millis IS NULL OR latency_score_millis >= 0),
|
||||
CHECK (reliability_millis IS NULL OR reliability_millis >= 0),
|
||||
CHECK (observed_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.capability_implementation_heads (
|
||||
implementation_id TEXT PRIMARY KEY,
|
||||
revision BIGINT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
owner_principal_id TEXT NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (revision > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.capability_gaps (
|
||||
gap_id TEXT PRIMARY KEY,
|
||||
requirement_id TEXT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
capability_id TEXT NOT NULL DEFAULT '',
|
||||
owner_principal_id TEXT NOT NULL,
|
||||
original_intent_id TEXT NOT NULL,
|
||||
source_utterance_id TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
detail TEXT NOT NULL,
|
||||
strategies TEXT[] NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (requirement_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (owner_principal_id <> ''),
|
||||
CHECK (original_intent_id <> ''),
|
||||
CHECK (source_utterance_id <> ''),
|
||||
CHECK (reason IN ('NO_IMPLEMENTATION','UNHEALTHY','INACTIVE','UNQUALIFIED','UNAUTHORIZED','INCOMPATIBLE','UNKNOWN')),
|
||||
CHECK (detail <> ''),
|
||||
CHECK (cardinality(strategies) > 0),
|
||||
CHECK (recorded_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.capability_usage_observations (
|
||||
observation_id TEXT PRIMARY KEY,
|
||||
implementation_id TEXT NOT NULL,
|
||||
implementation_revision BIGINT NOT NULL,
|
||||
capability_id TEXT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
owner_principal_id TEXT NOT NULL,
|
||||
original_intent_id TEXT NOT NULL,
|
||||
source_utterance_id TEXT NOT NULL,
|
||||
outcome TEXT NOT NULL,
|
||||
outcome_ref TEXT NOT NULL DEFAULT '',
|
||||
latency_millis BIGINT,
|
||||
observed_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (implementation_id <> ''),
|
||||
CHECK (implementation_revision > 0),
|
||||
CHECK (capability_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (owner_principal_id <> ''),
|
||||
CHECK (original_intent_id <> ''),
|
||||
CHECK (source_utterance_id <> ''),
|
||||
CHECK (outcome IN ('SUCCEEDED','FAILED','SKIPPED','UNKNOWN')),
|
||||
CHECK (latency_millis IS NULL OR latency_millis >= 0),
|
||||
CHECK (observed_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
INSERT INTO capability.capability_descriptors(capability_id, capability_kind, display_name, description, revision, recorded_at_unix_millis)
|
||||
VALUES
|
||||
('schedule-reminder', 'SCHEDULE_REMINDER', 'Reminder scheduling', 'Create a Continuity-owned prospective reminder item from an authenticated human directive.', 1, 1),
|
||||
('creator-advisory', 'COGNITIVE_REASONING', 'Creator advisory cognition', 'Reasoning capability resolved through the specialized Cognitive Registry and Gateway.', 1, 1)
|
||||
ON CONFLICT (capability_id) DO NOTHING;
|
||||
|
||||
INSERT INTO capability.capability_implementations(
|
||||
implementation_id, capability_id, capability_kind, owner_principal_id, revision, implementation_kind, target_ref,
|
||||
credential_ref, qualification, activation, health, authority_policy_ref, data_constraints,
|
||||
cost_score_millis, latency_score_millis, reliability_millis, observed_at_unix_millis)
|
||||
VALUES (
|
||||
'internal-reminder-scheduler-v1', 'schedule-reminder', 'SCHEDULE_REMINDER', 'domovoy-creator-principal',
|
||||
1, 'INTERNAL_FUNCTION', 'continuity://prospective-reminder-v1', '', 'VALIDATED', 'ACTIVE', 'AVAILABLE',
|
||||
'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 0, 10, 950, 1
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO capability.capability_implementation_heads(implementation_id, revision, capability_kind, owner_principal_id)
|
||||
VALUES ('internal-reminder-scheduler-v1', 1, 'SCHEDULE_REMINDER', 'domovoy-creator-principal')
|
||||
ON CONFLICT (implementation_id) DO NOTHING;
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,10 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE continuity_role;
|
||||
|
||||
ALTER TABLE continuity.prospective_items ADD COLUMN IF NOT EXISTS source_intent_id TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE continuity.prospective_items ADD COLUMN IF NOT EXISTS source_utterance_id TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE continuity.prospective_items ADD COLUMN IF NOT EXISTS summary TEXT NOT NULL DEFAULT '';
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,35 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE continuity_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS continuity.attention_items (
|
||||
attention_item_id TEXT PRIMARY KEY,
|
||||
owner_principal_id TEXT NOT NULL,
|
||||
scope_id TEXT NOT NULL,
|
||||
source_kind TEXT NOT NULL,
|
||||
source_prospective_item_id TEXT NOT NULL,
|
||||
level TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
due_at_unix_millis BIGINT NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
delivered_at_unix_millis BIGINT NOT NULL DEFAULT 0,
|
||||
action_intent_id TEXT NOT NULL DEFAULT '',
|
||||
authorized_action_id TEXT NOT NULL DEFAULT '',
|
||||
action_attempt_id TEXT NOT NULL DEFAULT '',
|
||||
delivery_outcome TEXT NOT NULL DEFAULT '',
|
||||
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 (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 <> ''))
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,43 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE capability_role;
|
||||
|
||||
INSERT INTO capability.notification_bindings (
|
||||
binding_id, capability_id, owner_principal_id, revision, state, https_origin,
|
||||
delivery_path, receipt_path, protocol, secret_ref, verified_at_unix_millis, health
|
||||
)
|
||||
VALUES (
|
||||
'domovoy-notification-binding',
|
||||
'prospective-notification-delivery',
|
||||
'domovoy-creator-principal',
|
||||
2,
|
||||
'ACTIVE',
|
||||
'https://assistant.cin.su',
|
||||
'/delivery/v1/deliveries',
|
||||
'/delivery/v1/receipts',
|
||||
'IDEMPOTENT_RECEIPT_V1',
|
||||
'domovoy-notification-write-v1',
|
||||
FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
|
||||
'AVAILABLE'
|
||||
)
|
||||
ON CONFLICT (binding_id, revision) DO UPDATE SET
|
||||
https_origin = EXCLUDED.https_origin,
|
||||
verified_at_unix_millis = EXCLUDED.verified_at_unix_millis,
|
||||
health = EXCLUDED.health;
|
||||
|
||||
INSERT INTO capability.notification_binding_heads (
|
||||
capability_id, owner_principal_id, binding_id, revision
|
||||
)
|
||||
VALUES (
|
||||
'prospective-notification-delivery',
|
||||
'domovoy-creator-principal',
|
||||
'domovoy-notification-binding',
|
||||
2
|
||||
)
|
||||
ON CONFLICT (capability_id, owner_principal_id) DO UPDATE SET
|
||||
binding_id = EXCLUDED.binding_id,
|
||||
revision = EXCLUDED.revision
|
||||
WHERE capability.notification_binding_heads.revision < EXCLUDED.revision;
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -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;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000063-intent-work-planning-foundation.json",
|
||||
"task_id": "DEV-000063",
|
||||
"task_file": "dev/tasks/DEV-000067-experience-reflection-skill-evolution.json",
|
||||
"task_id": "DEV-000067",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"task_id": "DEV-000064",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0013",
|
||||
"base_repo_revision": "fe531ab56f43bb37a5786e38b094781d3eccb775",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add provider-neutral capability implementation resolution, selection and typed gap handling foundation, with reminder scheduling as the first production end-to-end capability.",
|
||||
"requirement_ids": [
|
||||
"ADR-0013",
|
||||
"INV-AUTH-001",
|
||||
"INV-EPI-001",
|
||||
"INV-OWN-001",
|
||||
"Capability availability is not Authority",
|
||||
"Cognitive Registry remains the specialized cognitive capability registry"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"capability",
|
||||
"continuity",
|
||||
"interaction",
|
||||
"work"
|
||||
],
|
||||
"paths": [
|
||||
"architecture/adrs/ADR-0013-capability-resolution-selection-gap-foundation.md",
|
||||
"architecture/architecture-registry.yaml",
|
||||
"architecture/architecture-registry.json",
|
||||
"contracts/capability/**",
|
||||
"contracts/continuity/**",
|
||||
"contracts/interaction/**",
|
||||
"contracts/work/**",
|
||||
"modules/capability/**",
|
||||
"modules/continuity/**",
|
||||
"modules/work/**",
|
||||
"adapters/postgres/**",
|
||||
"database/bootstrap/**",
|
||||
"database/migrations/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/continuity-runtime/**",
|
||||
"cmd/creator-web/**",
|
||||
"tests/capability_resolution/**",
|
||||
"tests/owner_acl/**",
|
||||
"tests/cognitive_advisory/**",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000064-capability-resolution-selection-gap-foundation.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": 3,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"Work CapabilityRequirement",
|
||||
"Interaction OriginalHumanIntent",
|
||||
"Continuity ProspectiveItem",
|
||||
"Cognitive Registry / Router",
|
||||
"Secret Resolver CredentialRef",
|
||||
"Authority boundary concepts"
|
||||
],
|
||||
"may_change": [
|
||||
"contracts/capability@v1",
|
||||
"contracts/continuity@v1",
|
||||
"contracts/interaction@v1"
|
||||
]
|
||||
},
|
||||
"semantic_owner": "capability",
|
||||
"canonical_state": [
|
||||
"Capability",
|
||||
"CapabilityImplementation",
|
||||
"CapabilityResolution",
|
||||
"CapabilitySelection",
|
||||
"CapabilityGap",
|
||||
"AcquisitionStrategyCandidate",
|
||||
"CapabilityUsageObservation"
|
||||
],
|
||||
"state_transitions": [
|
||||
"implementation qualification DISCOVERED/PROBING/EVALUATING/VALIDATED/REJECTED",
|
||||
"implementation activation INACTIVE/CANARY/ACTIVE/DEPRECATED/RETIRED",
|
||||
"implementation health UNKNOWN/AVAILABLE/DEGRADED/UNAVAILABLE",
|
||||
"resolution AVAILABLE/PARTIALLY_AVAILABLE/UNAVAILABLE/UNKNOWN",
|
||||
"gap records reasons without executing or granting Authority"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"Capability registry writes are owner-local",
|
||||
"Reminder scheduling creates Continuity-owned ProspectiveItem through Continuity runtime boundary",
|
||||
"No external mutation",
|
||||
"No Commit/Executor invocation for reminder registration",
|
||||
"No ACTIVE cognitive binding promotion",
|
||||
"No OpenAI activation",
|
||||
"CredentialRef is metadata and does not grant Authority"
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"capability owner-local implementation registry",
|
||||
"capability owner-local gap records",
|
||||
"capability owner-local generic usage observations",
|
||||
"continuity owner-local reminder creation endpoint using existing prospective_items",
|
||||
"no cross-owner FK",
|
||||
"no cross-owner SQL join dependency"
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"registered implementation can be unavailable without breaking startup",
|
||||
"resolver fails closed when implementation is missing, unhealthy, inactive, unqualified or unauthorized",
|
||||
"capability gap is persisted with OriginalHumanIntent correlation",
|
||||
"reminder creation is persisted as a Continuity ProspectiveItem",
|
||||
"cognitive router remains authoritative for cognitive invocation"
|
||||
],
|
||||
"assumptions": [
|
||||
"DEV-000063 is production baseline",
|
||||
"Capability owner is canonical Capability registry owner per Core Semantic Model",
|
||||
"Continuity owner remains canonical ProspectiveItem owner",
|
||||
"existing CPU CANARY remains not ACTIVE",
|
||||
"docker-test prohibited"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"Reminder resolves to scheduling implementation",
|
||||
"Reminder is actually created and can be verified",
|
||||
"Simple question still works without durable Work",
|
||||
"Unsupported SEND_EMAIL returns persisted CapabilityGap",
|
||||
"Unavailable implementation is excluded",
|
||||
"Same capability with two implementations can be represented",
|
||||
"Selection does not depend on hardcoded provider name",
|
||||
"CredentialRef does not grant Authority",
|
||||
"Capability is not Authority",
|
||||
"Capability gap preserves OriginalHumanIntent correlation",
|
||||
"One Goal can require multiple capabilities",
|
||||
"Cognitive binding remains functioning",
|
||||
"RTX unavailable does not break CPU cognition",
|
||||
"LLM-free mode 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 /chat acceptance"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"task_id": "DEV-000065",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0014",
|
||||
"base_repo_revision": "6e73318e49991b40a5ed750c604792cbd95bd325",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add assistant presence through Continuity-owned due reminder processing, typed Attention foundation, governed outbound notification delivery, and a human-facing Assistant surface with Chat, Today/Attention, and History while preserving the Operator Console.",
|
||||
"requirement_ids": [
|
||||
"ADR-0014",
|
||||
"ADR-0005",
|
||||
"INV-AUTH-001",
|
||||
"INV-OWN-001",
|
||||
"INV-CONT-001",
|
||||
"INV-EPI-001",
|
||||
"Capability availability is not Authority",
|
||||
"Durable future obligations survive runtime restart"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"continuity",
|
||||
"commit",
|
||||
"capability",
|
||||
"interaction"
|
||||
],
|
||||
"paths": [
|
||||
"architecture/adrs/ADR-0014-assistant-presence-attention-human-surface.md",
|
||||
"architecture/architecture-registry.yaml",
|
||||
"architecture/architecture-registry.json",
|
||||
"contracts/continuity/**",
|
||||
"contracts/commit/**",
|
||||
"contracts/capability/**",
|
||||
"modules/continuity/**",
|
||||
"modules/commit/**",
|
||||
"adapters/postgres/**",
|
||||
"database/bootstrap/**",
|
||||
"database/migrations/**",
|
||||
"cmd/continuity-runtime/**",
|
||||
"cmd/commit-runtime/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/creator-web/**",
|
||||
"tests/owner_acl/**",
|
||||
"tests/cognitive_advisory/**",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000065-assistant-presence-attention-human-surface.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"architecture/ARCHITECTURE_CONSTITUTION.md",
|
||||
"security/**",
|
||||
"ci/**",
|
||||
".gitea/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 1,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"Continuity ProspectiveItem",
|
||||
"Commit prospective notification operation",
|
||||
"Capability notification binding",
|
||||
"Interaction ChatMessage",
|
||||
"Authority Creator grant",
|
||||
"CredentialRef Secret Resolver foundation"
|
||||
],
|
||||
"may_change": [
|
||||
"contracts/continuity@v1",
|
||||
"contracts/commit@v1"
|
||||
]
|
||||
},
|
||||
"semantic_owner": "continuity",
|
||||
"canonical_state": [
|
||||
"ProspectiveItem",
|
||||
"AttentionItem"
|
||||
],
|
||||
"state_transitions": [
|
||||
"ProspectiveItem ARMED -> TRIGGERED -> REVALIDATING -> DELIVERED for due reminder processing",
|
||||
"AttentionItem PENDING -> DELIVERING -> DELIVERED for human contact outcome",
|
||||
"No ACTIVE cognitive binding promotion",
|
||||
"No OpenAI activation"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"Continuity runtime polls canonical prospective state and does not own external mutation",
|
||||
"Commit runtime remains the only outbound notification executor boundary",
|
||||
"Notification delivery uses existing CapabilityBinding and idempotent receiver protocol",
|
||||
"Human Assistant UI reads canonical runtime APIs and hides internal identifiers by default",
|
||||
"Docker-test remains prohibited"
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"continuity owner-local attention_items table",
|
||||
"no cross-owner FK",
|
||||
"no cross-owner SQL join dependency",
|
||||
"Commit authorized action and attempt persistence remains Commit-owned",
|
||||
"Notification receipt persistence remains receiver-owned delivery evidence"
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Due detection reads ARMED rows from canonical PostgreSQL state",
|
||||
"CAS/unique source constraints prevent duplicate AttentionItem creation",
|
||||
"Successful delivery moves AttentionItem to DELIVERED and prevents repeated delivery",
|
||||
"Unavailable LLM does not block deterministic reminder firing",
|
||||
"Unavailable notification delivery records non-success without inventing completion"
|
||||
],
|
||||
"assumptions": [
|
||||
"DEV-000064 is production baseline",
|
||||
"Continuity is canonical owner of future obligations and attention state for this generation",
|
||||
"Commit and existing notification receiver remain the outbound mutation path",
|
||||
"The first human notification channel is the existing production receiver/Windows display path",
|
||||
"CPU CANARY may remain available but reminder firing is deterministic and LLM-free"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"Reminder becomes due",
|
||||
"Due reminder produces governed AttentionItem",
|
||||
"Attention can produce notification through Commit",
|
||||
"Delivery outcome is persisted",
|
||||
"No duplicate delivery after success",
|
||||
"Runtime restart does not lose an ARMED reminder",
|
||||
"Cancelled/suppressed reminder is not delivered",
|
||||
"LLM unavailable does not prevent deterministic reminder delivery",
|
||||
"No cognitive model is required merely to fire a known reminder",
|
||||
"CapabilityGap is rendered human-readable",
|
||||
"Human UI requires no internal IDs",
|
||||
"Operator Console remains available",
|
||||
"Authority boundaries remain unchanged",
|
||||
"Owner ACL passes",
|
||||
"LLM-free audit passes",
|
||||
"Production live delayed reminder delivers once",
|
||||
"Today/Attention surface is built from canonical state"
|
||||
],
|
||||
"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 delayed reminder acceptance",
|
||||
"production /chat and /healthz verification"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT"
|
||||
]
|
||||
}
|
||||
@@ -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,136 @@
|
||||
{
|
||||
"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/**",
|
||||
"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"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT"
|
||||
]
|
||||
}
|
||||
+77
-5
@@ -1,17 +1,89 @@
|
||||
# Assistant project status
|
||||
|
||||
Audit date: 2026-08-17. Active implementation: DEV-000063 revision 1.
|
||||
Audit date: 2026-08-17. Active implementation: DEV-000067 revision 1.
|
||||
|
||||
## DEV-000067 checkpoint
|
||||
|
||||
- `DEV_000067_STATUS = IN_PROGRESS_LOCAL_FOUNDATION`
|
||||
- `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`
|
||||
- `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 = 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 = 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 = 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
|
||||
|
||||
- `DEV_000064_STATUS = COMPLETED`
|
||||
- `LAST_COMPLETED = ADR-0013, Capability-owned generic implementation registry/resolution/selection/gap contracts, deterministic evidence-weighted resolver V1, capability gap acquisition strategy foundation, generic capability usage observations, Continuity reminder metadata, authenticated creator reminder registration endpoint, /chat reminder scheduling path, /chat SEND_EMAIL gap path, capability-aware "what can you do" response`
|
||||
- `CAPABILITY_REGISTRY_STATUS = generic CapabilityImplementation + CapabilityGap + CapabilityUsageObservation foundation added under existing capability owner`
|
||||
- `COGNITIVE_RELATION_STATUS = cognition remains resolved/invoked through existing Cognitive Registry/Gateway; no parallel cognitive router introduced`
|
||||
- `REMINDER_CAPABILITY_STATUS = internal-reminder-scheduler-v1 seeded as VALIDATED/ACTIVE/AVAILABLE implementation for SCHEDULE_REMINDER`
|
||||
- `CAPABILITY_GAP_STATUS = unsupported SEND_EMAIL persists CapabilityGap with OriginalHumanIntent/Utterance correlation`
|
||||
- `AUTHORITY_STATUS = capability and CredentialRef do not grant Authority; reminder registration requires authenticated creator session and creates only Continuity-owned prospective state`
|
||||
- `ACTIVE_COGNITIVE_BINDINGS = none; CPU CANARY unchanged`
|
||||
- `OPENAI_REAL_CREDENTIAL = not required / not activated`
|
||||
- `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 /chat acceptance`
|
||||
- `LIVE_ACCEPTANCE = PASS: reminder created through external /chat and verified as Continuity ARMED prospective item; unsupported SEND_EMAIL created CapabilityGap; "Привет. Что ты умеешь?" returned capability-aware response; simple code explanation completed through Cognitive Gateway without durable Work`
|
||||
- `PRODUCTION_REVISION = 8fe596264e89b8f346c9e0cd297c5ccb2d5f76bb`
|
||||
- `PRODUCTION_HEALTH = healthy`
|
||||
- `NEXT_STEP = Direct delivery/revalidation loop for reminders or next concrete capability adapter, if separately approved`
|
||||
|
||||
## DEV-000063 checkpoint
|
||||
|
||||
- `DEV_000063_STATUS = IN_PROGRESS`
|
||||
- `LAST_COMPLETED = ADR-0012, HelpPath optionality foundation, Work-owned Goal/PlanCandidate/WorkPlan/PlanStep/CapabilityRequirement/DelegationCandidate contracts, deterministic planning V1, optional durable Work creation from /chat, capability-gap/self-extension readiness tests`
|
||||
- `DEV_000063_STATUS = COMPLETED`
|
||||
- `LAST_COMPLETED = ADR-0012, HelpPath optionality foundation, Work-owned Goal/PlanCandidate/WorkPlan/PlanStep/CapabilityRequirement/DelegationCandidate contracts, deterministic planning V1, optional durable Work creation from /chat, capability-gap/self-extension readiness tests, migration 017, production deployment and live /chat acceptance`
|
||||
- `HELP_PATH_OPTIONALITY = enabled; simple answer/cognition/direct capability/capability gap can bypass durable Goal/Work/Plan`
|
||||
- `GOAL_WORK_PLAN_OPTIONALITY = enabled; durable records are created only for complex work/planning cases`
|
||||
- `PROJECT_NEUTRALITY = project context is optional opaque Work reference; Project is not mandatory context`
|
||||
- `CODEX_NEUTRALITY = no Codex-specific canonical entity or executor selection`
|
||||
- `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`
|
||||
- `NEXT_STEP = commit, push, PR/promotion, migration 017, production deploy, live /chat acceptance`
|
||||
- `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 /chat acceptance`
|
||||
- `LIVE_ACCEPTANCE = PASS: simple help uses cognition without Work; reminder uses direct scheduling-capability path without Work; complex УПО investigation creates one Work Goal/Plan; capability gap remains executor-neutral without Work`
|
||||
- `PRODUCTION_REVISION = 9d50a3ec635782b59ab3335808d6d1e026be48a4`
|
||||
- `PRODUCTION_HEALTH = healthy`
|
||||
- `NEXT_STEP = DEV-000064 capability resolution/acquisition or concrete scheduler/mail/codex adapter foundation, if separately approved`
|
||||
- `REAL_BLOCKERS = none known`
|
||||
|
||||
## DEV-000062 checkpoint
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
var ErrInvalidCapabilityImplementation = errors.New("invalid capability implementation")
|
||||
var ErrInvalidCapabilityRequirement = errors.New("invalid capability requirement")
|
||||
var ErrNoCapabilityImplementation = errors.New("no eligible capability implementation")
|
||||
|
||||
func ValidateCapabilityImplementation(value capability.CapabilityImplementation) error {
|
||||
if value.ID == "" || value.CapabilityID == "" || value.CapabilityKind == "" || value.OwnerPrincipalID == "" ||
|
||||
value.Revision < 1 || value.Kind == "" || value.TargetRef == "" || value.AuthorityPolicyRef == "" ||
|
||||
value.ObservedAtUnixMillis < 1 || !validQualification(value.Qualification) || !validActivation(value.Activation) ||
|
||||
!validHealth(value.Health) {
|
||||
return ErrInvalidCapabilityImplementation
|
||||
}
|
||||
switch value.Kind {
|
||||
case capability.ImplementationInternalFunction, capability.ImplementationCognitiveBinding, capability.ImplementationExternalAdapter, capability.ImplementationAgent:
|
||||
default:
|
||||
return ErrInvalidCapabilityImplementation
|
||||
}
|
||||
for _, constraint := range value.DataConstraints {
|
||||
switch constraint {
|
||||
case capability.DataConstraintLocalOnly, capability.DataConstraintExternalAllowed, capability.DataConstraintNoCredentialForward:
|
||||
default:
|
||||
return ErrInvalidCapabilityImplementation
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResolveCapability(requirement capability.CapabilityResolutionRequirement, candidates []capability.CapabilityImplementation) (capability.CapabilityResolution, error) {
|
||||
if requirement.CapabilityKind == "" || requirement.OwnerPrincipalID == "" || requirement.OriginalIntentID == "" || requirement.SourceUtteranceID == "" || requirement.ObservedAtUnixMillis < 1 {
|
||||
return capability.CapabilityResolution{}, ErrInvalidCapabilityRequirement
|
||||
}
|
||||
resolutionID, err := ids.New()
|
||||
if err != nil {
|
||||
return capability.CapabilityResolution{}, err
|
||||
}
|
||||
resolution := capability.CapabilityResolution{
|
||||
ID: capability.CapabilityResolutionID(resolutionID),
|
||||
Requirement: requirement,
|
||||
Status: capability.ResolutionUnavailable,
|
||||
ExcludedReasons: map[capability.CapabilityImplementationID]capability.GapReason{},
|
||||
ObservedAtUnixMillis: requirement.ObservedAtUnixMillis,
|
||||
}
|
||||
eligible := make([]capability.CapabilityImplementation, 0, len(candidates))
|
||||
anyForCapability := false
|
||||
dominantReason := capability.GapNoImplementation
|
||||
for _, candidate := range candidates {
|
||||
if candidate.CapabilityKind != requirement.CapabilityKind {
|
||||
continue
|
||||
}
|
||||
if requirement.CapabilityID != "" && candidate.CapabilityID != requirement.CapabilityID {
|
||||
continue
|
||||
}
|
||||
anyForCapability = true
|
||||
reason := exclusionReason(requirement, candidate)
|
||||
if reason != "" {
|
||||
resolution.ExcludedReasons[candidate.ID] = reason
|
||||
if dominantReason == capability.GapNoImplementation {
|
||||
dominantReason = reason
|
||||
}
|
||||
continue
|
||||
}
|
||||
eligible = append(eligible, candidate)
|
||||
}
|
||||
if len(eligible) == 0 {
|
||||
if !anyForCapability {
|
||||
dominantReason = capability.GapNoImplementation
|
||||
}
|
||||
gap, err := BuildCapabilityGap(requirement, dominantReason, gapDetail(dominantReason), requirement.ObservedAtUnixMillis)
|
||||
if err != nil {
|
||||
return capability.CapabilityResolution{}, err
|
||||
}
|
||||
resolution.Gap = &gap
|
||||
return resolution, ErrNoCapabilityImplementation
|
||||
}
|
||||
sort.SliceStable(eligible, func(i, j int) bool { return betterImplementation(eligible[i], eligible[j]) })
|
||||
resolution.Status = capability.ResolutionAvailable
|
||||
resolution.Eligible = make([]capability.ResolvedCapabilityImplementation, 0, len(eligible))
|
||||
for index, item := range eligible {
|
||||
resolution.Eligible = append(resolution.Eligible, capability.ResolvedCapabilityImplementation{Implementation: item, Selected: index == 0, Rank: index + 1})
|
||||
}
|
||||
return resolution, nil
|
||||
}
|
||||
|
||||
func BuildCapabilityGap(requirement capability.CapabilityResolutionRequirement, reason capability.GapReason, detail string, millis int64) (capability.CapabilityGap, error) {
|
||||
id, err := ids.New()
|
||||
if err != nil {
|
||||
return capability.CapabilityGap{}, err
|
||||
}
|
||||
return capability.CapabilityGap{
|
||||
ID: capability.CapabilityGapID(id),
|
||||
RequirementID: requirement.RequirementID,
|
||||
CapabilityKind: requirement.CapabilityKind,
|
||||
CapabilityID: requirement.CapabilityID,
|
||||
OwnerPrincipalID: requirement.OwnerPrincipalID,
|
||||
OriginalIntentID: requirement.OriginalIntentID,
|
||||
SourceUtteranceID: requirement.SourceUtteranceID,
|
||||
Reason: reason,
|
||||
Detail: detail,
|
||||
Strategies: acquisitionStrategies(reason),
|
||||
RecordedAtUnixMillis: millis,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func exclusionReason(requirement capability.CapabilityResolutionRequirement, candidate capability.CapabilityImplementation) capability.GapReason {
|
||||
if ValidateCapabilityImplementation(candidate) != nil {
|
||||
return capability.GapIncompatible
|
||||
}
|
||||
if candidate.OwnerPrincipalID != requirement.OwnerPrincipalID {
|
||||
return capability.GapUnauthorized
|
||||
}
|
||||
if candidate.Qualification != capability.QualificationValidated {
|
||||
return capability.GapUnqualified
|
||||
}
|
||||
if candidate.Activation != capability.ActivationActive && !(requirement.AllowCanary && candidate.Activation == capability.ActivationCanary) {
|
||||
return capability.GapInactive
|
||||
}
|
||||
if candidate.Health != capability.HealthAvailable {
|
||||
return capability.GapUnhealthy
|
||||
}
|
||||
if requirement.AuthorityContextRef != "" && candidate.AuthorityPolicyRef != requirement.AuthorityContextRef {
|
||||
return capability.GapUnauthorized
|
||||
}
|
||||
for _, required := range requirement.RequiredConstraints {
|
||||
if !slices.Contains(candidate.DataConstraints, required) {
|
||||
return capability.GapIncompatible
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func betterImplementation(left, right capability.CapabilityImplementation) bool {
|
||||
if left.ReliabilityMillis != nil && right.ReliabilityMillis != nil && *left.ReliabilityMillis != *right.ReliabilityMillis {
|
||||
return *left.ReliabilityMillis > *right.ReliabilityMillis
|
||||
}
|
||||
if left.LatencyScoreMillis != nil && right.LatencyScoreMillis != nil && *left.LatencyScoreMillis != *right.LatencyScoreMillis {
|
||||
return *left.LatencyScoreMillis < *right.LatencyScoreMillis
|
||||
}
|
||||
if left.CostScoreMillis != nil && right.CostScoreMillis != nil && *left.CostScoreMillis != *right.CostScoreMillis {
|
||||
return *left.CostScoreMillis < *right.CostScoreMillis
|
||||
}
|
||||
return string(left.ID) < string(right.ID)
|
||||
}
|
||||
|
||||
func gapDetail(reason capability.GapReason) string {
|
||||
switch reason {
|
||||
case capability.GapNoImplementation:
|
||||
return "no registered implementation satisfies this capability requirement"
|
||||
case capability.GapUnhealthy:
|
||||
return "registered implementations are currently unavailable or degraded"
|
||||
case capability.GapInactive:
|
||||
return "registered implementations are not active for this use"
|
||||
case capability.GapUnqualified:
|
||||
return "registered implementations are not validated"
|
||||
case capability.GapUnauthorized:
|
||||
return "registered implementations are not allowed by the current authority or policy context"
|
||||
case capability.GapIncompatible:
|
||||
return "registered implementations do not match the required modality, data or environment constraints"
|
||||
default:
|
||||
return "capability availability is unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func acquisitionStrategies(reason capability.GapReason) []capability.AcquisitionStrategyCandidate {
|
||||
switch reason {
|
||||
case capability.GapNoImplementation:
|
||||
return []capability.AcquisitionStrategyCandidate{
|
||||
{Kind: capability.AcquireDiscoverExisting, Reason: "look for an already available adapter or service"},
|
||||
{Kind: capability.AcquireConnectExisting, Reason: "connect an existing provider after explicit authority"},
|
||||
{Kind: capability.AcquireBuildNew, Reason: "build a new implementation through a future approved coding capability"},
|
||||
}
|
||||
case capability.GapUnhealthy:
|
||||
return []capability.AcquisitionStrategyCandidate{
|
||||
{Kind: capability.AcquireUseAlternative, Reason: "try another eligible implementation"},
|
||||
{Kind: capability.AcquireConfigureExisting, Reason: "repair or reconfigure the registered implementation"},
|
||||
}
|
||||
case capability.GapUnauthorized:
|
||||
return []capability.AcquisitionStrategyCandidate{{Kind: capability.AcquireRequestUserInput, Reason: "request explicit authority or policy decision"}}
|
||||
default:
|
||||
return []capability.AcquisitionStrategyCandidate{{Kind: capability.AcquireUnresolvableCurrently, Reason: strings.ToLower(gapDetail(reason))}}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
func TestReminderResolvesToSchedulingImplementation(t *testing.T) {
|
||||
requirement := resolutionRequirement(capability.CapabilityKindScheduleReminder)
|
||||
resolution, err := ResolveCapability(requirement, []capability.CapabilityImplementation{implementation("scheduler-b", capability.CapabilityKindScheduleReminder, 900, 50), implementation("scheduler-a", capability.CapabilityKindScheduleReminder, 950, 75)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resolution.Status != capability.ResolutionAvailable || len(resolution.Eligible) != 2 || !resolution.Eligible[0].Selected || resolution.Eligible[0].Implementation.ID != "scheduler-a" {
|
||||
t.Fatalf("unexpected resolution: %#v", resolution)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnavailableImplementationExcluded(t *testing.T) {
|
||||
candidate := implementation("scheduler", capability.CapabilityKindScheduleReminder, 900, 50)
|
||||
candidate.Health = capability.HealthUnavailable
|
||||
resolution, err := ResolveCapability(resolutionRequirement(capability.CapabilityKindScheduleReminder), []capability.CapabilityImplementation{candidate})
|
||||
if !errors.Is(err, ErrNoCapabilityImplementation) || resolution.Gap == nil || resolution.Gap.Reason != capability.GapUnhealthy {
|
||||
t.Fatalf("expected unhealthy gap, got resolution=%#v err=%v", resolution, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectionDoesNotDependOnProviderName(t *testing.T) {
|
||||
left := implementation("zzz-provider-name", capability.CapabilityKindScheduleReminder, 980, 20)
|
||||
right := implementation("aaa-provider-name", capability.CapabilityKindScheduleReminder, 900, 10)
|
||||
resolution, err := ResolveCapability(resolutionRequirement(capability.CapabilityKindScheduleReminder), []capability.CapabilityImplementation{left, right})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resolution.Eligible[0].Implementation.ID != left.ID {
|
||||
t.Fatalf("selection appears provider-name driven: %#v", resolution.Eligible)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialRefDoesNotGrantAuthority(t *testing.T) {
|
||||
candidate := implementation("mail", capability.CapabilityKindSendEmail, 900, 50)
|
||||
candidate.CredentialRef = "credential://mail/token"
|
||||
candidate.AuthorityPolicyRef = "mail-send-approved-v1"
|
||||
resolution, err := ResolveCapability(resolutionRequirement(capability.CapabilityKindSendEmail), []capability.CapabilityImplementation{candidate})
|
||||
if !errors.Is(err, ErrNoCapabilityImplementation) || resolution.Gap == nil || resolution.Gap.Reason != capability.GapUnauthorized {
|
||||
t.Fatalf("credential ref must not grant authority: resolution=%#v err=%v", resolution, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilityGapPreservesOriginalIntentCorrelation(t *testing.T) {
|
||||
requirement := resolutionRequirement(capability.CapabilityKindSendEmail)
|
||||
resolution, err := ResolveCapability(requirement, nil)
|
||||
if !errors.Is(err, ErrNoCapabilityImplementation) || resolution.Gap == nil {
|
||||
t.Fatalf("expected gap, got resolution=%#v err=%v", resolution, err)
|
||||
}
|
||||
if resolution.Gap.OriginalIntentID != requirement.OriginalIntentID || resolution.Gap.SourceUtteranceID != requirement.SourceUtteranceID || len(resolution.Gap.Strategies) == 0 {
|
||||
t.Fatalf("gap lost correlation/strategies: %#v", resolution.Gap)
|
||||
}
|
||||
if strings.Contains(strings.ToLower(resolution.Gap.Detail), "codex") {
|
||||
t.Fatalf("gap must not become codex-specific: %#v", resolution.Gap)
|
||||
}
|
||||
}
|
||||
|
||||
func resolutionRequirement(kind capability.CapabilityKind) capability.CapabilityResolutionRequirement {
|
||||
return capability.CapabilityResolutionRequirement{
|
||||
CapabilityKind: kind,
|
||||
OwnerPrincipalID: "owner-1",
|
||||
ActorID: "actor-1",
|
||||
OriginalIntentID: "intent-1",
|
||||
SourceUtteranceID: "utterance-1",
|
||||
RequiredConstraints: []capability.DataConstraint{capability.DataConstraintLocalOnly},
|
||||
AuthorityContextRef: capability.AuthorityPolicyCreatorDirectiveV1,
|
||||
ObservedAtUnixMillis: 1000,
|
||||
}
|
||||
}
|
||||
|
||||
func implementation(id string, kind capability.CapabilityKind, reliability int64, latency int64) capability.CapabilityImplementation {
|
||||
return capability.CapabilityImplementation{
|
||||
ID: capability.CapabilityImplementationID(id),
|
||||
CapabilityID: capability.CapabilityID(strings.ToLower(string(kind))),
|
||||
CapabilityKind: kind,
|
||||
OwnerPrincipalID: "owner-1",
|
||||
Revision: 1,
|
||||
Kind: capability.ImplementationInternalFunction,
|
||||
TargetRef: capability.ImplementationTargetRef("internal://" + strings.ToLower(string(kind))),
|
||||
Qualification: capability.QualificationValidated,
|
||||
Activation: capability.ActivationActive,
|
||||
Health: capability.HealthAvailable,
|
||||
AuthorityPolicyRef: capability.AuthorityPolicyCreatorDirectiveV1,
|
||||
DataConstraints: []capability.DataConstraint{capability.DataConstraintLocalOnly, capability.DataConstraintNoCredentialForward},
|
||||
ReliabilityMillis: &reliability,
|
||||
LatencyScoreMillis: &latency,
|
||||
ObservedAtUnixMillis: 1000,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,17 @@ func Reconcile(action commitcontract.AuthorizedAction, previous commitcontract.A
|
||||
// Authorize is the Commit boundary. A proposal alone never reaches Executor.
|
||||
func Authorize(proposal commitcontract.ActionProposal, context AuthorizationContext, validator ports.AuthorityValidator, authorizedID commitcontract.AuthorizedActionID, authorizedAtUnixMillis int64) (commitcontract.AuthorizedAction, error) {
|
||||
if proposal.IntentID == "" || proposal.ActorID == "" || proposal.CapabilityID == capability.CapabilityID("") ||
|
||||
proposal.Target.TaskID == "" || proposal.Target.GoalID == "" || proposal.Target.ScopeID == "" || proposal.Target.Revision == 0 || proposal.Operation == "" || authorizedAtUnixMillis <= 0 {
|
||||
proposal.Operation == "" || authorizedAtUnixMillis <= 0 {
|
||||
return commitcontract.AuthorizedAction{}, ErrInvalidProposal
|
||||
}
|
||||
if proposal.ActorID != context.Binding.ActorID || proposal.Target.ScopeID != context.Scope.ID {
|
||||
if proposal.ActorID != context.Binding.ActorID {
|
||||
return commitcontract.AuthorizedAction{}, ErrInvalidProposal
|
||||
}
|
||||
if proposal.Operation == commitcontract.OperationDeliverProspectiveNotification {
|
||||
if proposal.Target.ScopeID != "" && proposal.Target.ScopeID != context.Scope.ID {
|
||||
return commitcontract.AuthorizedAction{}, ErrInvalidProposal
|
||||
}
|
||||
} else if proposal.Target.TaskID == "" || proposal.Target.GoalID == "" || proposal.Target.ScopeID == "" || proposal.Target.Revision == 0 || proposal.Target.ScopeID != context.Scope.ID {
|
||||
return commitcontract.AuthorizedAction{}, ErrInvalidProposal
|
||||
}
|
||||
if !allowedOperation(proposal, context) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ var (
|
||||
ErrProspectiveRevision = errors.New("prospective item revision conflict")
|
||||
ErrInvalidTransition = errors.New("invalid prospective item transition")
|
||||
ErrInvalidSatisfaction = errors.New("invalid delivered satisfaction command")
|
||||
ErrInvalidReminder = errors.New("invalid reminder request")
|
||||
ErrInvalidAttention = errors.New("invalid attention item")
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
@@ -28,6 +30,50 @@ func ValidateSatisfyDelivered(command contract.SatisfyDeliveredCommand) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildReminder(request contract.ScheduleReminderRequest, id contract.ProspectiveItemID) (contract.ProspectiveItem, error) {
|
||||
if id == "" || request.OwnerPrincipalID == "" || request.ScopeID == "" || strings.TrimSpace(request.SourceIntentID) == "" ||
|
||||
strings.TrimSpace(request.SourceUtteranceID) == "" || strings.TrimSpace(request.Summary) == "" ||
|
||||
request.TriggerAtUnixMillis <= request.RequestedAtUnixMillis || request.RequestedAtUnixMillis < 1 {
|
||||
return contract.ProspectiveItem{}, ErrInvalidReminder
|
||||
}
|
||||
return contract.ProspectiveItem{
|
||||
ID: id,
|
||||
CommitmentID: contract.CommitmentID(""),
|
||||
OwnerPrincipalID: request.OwnerPrincipalID,
|
||||
ScopeID: request.ScopeID,
|
||||
GoalID: "",
|
||||
SourceIntentID: strings.TrimSpace(request.SourceIntentID),
|
||||
SourceUtteranceID: strings.TrimSpace(request.SourceUtteranceID),
|
||||
Summary: strings.TrimSpace(request.Summary),
|
||||
TriggerAtUnixMillis: request.TriggerAtUnixMillis,
|
||||
State: contract.ProspectiveArmed,
|
||||
Revision: 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func BuildDueReminderAttention(item contract.ProspectiveItem, id contract.AttentionItemID, nowUnixMillis int64) (contract.AttentionItem, error) {
|
||||
if id == "" || item.ID == "" || item.OwnerPrincipalID == "" || item.ScopeID == "" ||
|
||||
strings.TrimSpace(item.Summary) == "" || item.TriggerAtUnixMillis < 1 || nowUnixMillis < 1 {
|
||||
return contract.AttentionItem{}, ErrInvalidAttention
|
||||
}
|
||||
if item.State != contract.ProspectiveDelivered && item.State != contract.ProspectiveRevalidating {
|
||||
return contract.AttentionItem{}, ErrInvalidAttention
|
||||
}
|
||||
return contract.AttentionItem{
|
||||
ID: id,
|
||||
OwnerPrincipalID: item.OwnerPrincipalID,
|
||||
ScopeID: item.ScopeID,
|
||||
SourceKind: contract.AttentionSourceProspectiveReminder,
|
||||
SourceProspectiveID: item.ID,
|
||||
Level: contract.AttentionDue,
|
||||
State: contract.AttentionPending,
|
||||
Summary: item.Summary,
|
||||
DueAtUnixMillis: item.TriggerAtUnixMillis,
|
||||
CreatedAtUnixMillis: nowUnixMillis,
|
||||
Revision: 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) SatisfyDelivered(id contract.ProspectiveItemID, revision contract.ProspectiveRevision, evidenceRef string) (contract.ProspectiveItem, error) {
|
||||
item, err := s.current(id, revision)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,9 +4,17 @@ import (
|
||||
"context"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/continuity"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
Save(context.Context, continuity.ProspectiveItem) error
|
||||
Load(context.Context, continuity.ProspectiveItemID) (continuity.ProspectiveItem, error)
|
||||
DueArmed(context.Context, int64, int) ([]continuity.ProspectiveItem, error)
|
||||
ClaimDueReminder(context.Context, continuity.ProspectiveItemID, continuity.ProspectiveRevision, continuity.AttentionItemID, int64) (continuity.ProspectiveItem, continuity.AttentionItem, error)
|
||||
PendingAttention(context.Context, int) ([]continuity.AttentionItem, error)
|
||||
MarkAttentionDelivering(context.Context, continuity.AttentionItemID, continuity.AttentionRevision) (continuity.AttentionItem, error)
|
||||
MarkAttentionDelivered(context.Context, continuity.AttentionItemID, continuity.AttentionRevision, continuity.CommittedActionIntentID, continuity.CommittedAuthorizedActionID, continuity.CommittedActionAttemptID, string, int64) (continuity.AttentionItem, error)
|
||||
MarkAttentionFailed(context.Context, continuity.AttentionItemID, continuity.AttentionRevision, continuity.CommittedActionIntentID, continuity.CommittedAuthorizedActionID, continuity.CommittedActionAttemptID, string) (continuity.AttentionItem, error)
|
||||
AttentionByOwner(context.Context, identity.PrincipalID, int) ([]continuity.AttentionItem, error)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -78,6 +78,8 @@ func DetermineHelpPath(input work.PlanningInput) work.HelpPath {
|
||||
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"}
|
||||
case containsAny(lower, "напомни", "напомин"):
|
||||
return work.HelpPath{Kind: work.HelpPathDirectCapability, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}, Reason: "reminder is a direct scheduling-capability path, not a coding/project workflow", Reply: "Понял: нужен reminder/scheduler capability. В DEV-000063 я фиксирую это как безопасный путь помощи; само напоминание ещё не зарегистрировано, потому что scheduling adapter не подключён."}
|
||||
case containsAny(lower, "отправь", "письм", "почт"):
|
||||
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindSendEmail}, CapabilityGap: true, Reason: "email sending needs a communication capability implementation and explicit authority", Reply: "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксирую этот capability gap, чтобы позже к нему можно было вернуться."}
|
||||
case containsAny(lower, "не умеешь", "подключи интеграцию", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер"):
|
||||
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."}
|
||||
case requiresDurableWork(lower):
|
||||
|
||||
Reference in New Issue
Block a user