Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa01e8e99b | ||
|
|
1113ced623 | ||
|
|
f3ff430812 | ||
|
|
2ec53e40cf | ||
|
|
18fbc7f105 | ||
|
|
a0dce634f3 | ||
|
|
5d4e064a94 | ||
|
|
6e73318e49 | ||
|
|
1519af3310 | ||
|
|
8fe596264e | ||
|
|
ee51789ee2 | ||
|
|
a083bac65f | ||
|
|
9a84ac942c | ||
|
|
fe531ab56f | ||
|
|
d8f994e07e | ||
|
|
9d50a3ec63 | ||
|
|
2d142cdfdb | ||
|
|
06b6cc3f9d | ||
|
|
4fc85511c4 | ||
|
|
18750c2ae6 | ||
|
|
d1ce2c568e | ||
|
|
445e9edd7c | ||
|
|
48dfd364e4 | ||
|
|
e4021523b8 |
@@ -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,117 @@ 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) 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,
|
||||
|
||||
@@ -19,6 +19,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 +37,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 +50,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 +58,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 +69,184 @@ 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,revision)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,0,'','','','',1)
|
||||
ON CONFLICT (source_kind, source_prospective_item_id) DO UPDATE SET source_prospective_item_id=EXCLUDED.source_prospective_item_id
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
|
||||
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
|
||||
attention.ID, attention.OwnerPrincipalID, attention.ScopeID, attention.SourceKind, attention.SourceProspectiveID,
|
||||
attention.Level, attention.State, attention.Summary, attention.DueAtUnixMillis, attention.CreatedAtUnixMillis).Scan(
|
||||
&attention.ID, &attention.OwnerPrincipalID, &attention.ScopeID, &attention.SourceKind, &attention.SourceProspectiveID,
|
||||
&attention.Level, &attention.State, &attention.Summary, &attention.DueAtUnixMillis, &attention.CreatedAtUnixMillis,
|
||||
&attention.DeliveredAtUnixMillis, &attention.ActionIntentID, &attention.AuthorizedActionID, &attention.ActionAttemptID,
|
||||
&attention.DeliveryOutcome, &attention.Revision)
|
||||
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,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,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) MarkAttentionDelivering(ctx context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision) (continuity.AttentionItem, error) {
|
||||
var item continuity.AttentionItem
|
||||
err := r.pool.QueryRow(ctx, `UPDATE continuity.attention_items SET state=$1, revision=revision+1
|
||||
WHERE attention_item_id=$2 AND state=$3 AND revision=$4
|
||||
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
|
||||
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
|
||||
continuity.AttentionDelivering, id, continuity.AttentionPending, revision).Scan(
|
||||
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
|
||||
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
|
||||
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.Revision)
|
||||
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,revision`,
|
||||
continuity.AttentionDelivered, deliveredAtUnixMillis, intentID, authorizedID, attemptID, outcome,
|
||||
id, continuity.AttentionDelivering, revision).Scan(
|
||||
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
|
||||
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
|
||||
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.Revision)
|
||||
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,revision`,
|
||||
continuity.AttentionFailed, intentID, authorizedID, attemptID, outcome,
|
||||
id, continuity.AttentionDelivering, revision).Scan(
|
||||
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
|
||||
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
|
||||
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.Revision)
|
||||
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 +276,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 +316,30 @@ type continuityRowQuerier interface {
|
||||
QueryRow(context.Context, string, ...any) pgx.Row
|
||||
}
|
||||
|
||||
type continuityRows interface {
|
||||
Next() bool
|
||||
Scan(...any) error
|
||||
Err() error
|
||||
}
|
||||
|
||||
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.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,
|
||||
|
||||
@@ -39,3 +39,39 @@ func (r *WorkRepository) Load(ctx context.Context, id work.TaskID) (work.TaskTar
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SavePlanningResult(ctx context.Context, result work.PlanningResult) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
goal := result.Goal
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.goals(goal_id,source_intent_id,source_utterance_id,actor_id,principal_id,scope_id,project_id,project_display_name,desired_outcome,constraints_text,state,provenance,revision,created_at_unix_millis,updated_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT(goal_id) DO NOTHING`, goal.ID, goal.SourceIntentID, goal.SourceUtteranceID, goal.ActorID, goal.PrincipalID, goal.ScopeID, goal.ProjectID, goal.ProjectDisplayName, goal.DesiredOutcome, goal.Constraints, goal.State, goal.Provenance, goal.Revision, goal.CreatedAtUnixMillis, goal.UpdatedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
candidate := result.Candidate
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.plan_candidates(plan_candidate_id,goal_id,source,state,summary,validation_summary,requires_clarification,accepted_at_unix_millis,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT(plan_candidate_id) DO NOTHING`, candidate.ID, candidate.GoalID, candidate.Source, candidate.State, candidate.Summary, candidate.ValidationSummary, candidate.RequiresClarification, candidate.AcceptedAtUnixMillis, candidate.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
plan := result.Plan
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.work_plans(work_plan_id,goal_id,accepted_candidate_id,revision,state,summary,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(work_plan_id) DO NOTHING`, plan.ID, plan.GoalID, plan.AcceptedCandidateID, plan.Revision, plan.State, plan.Summary, plan.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, requirement := range candidate.Requirements {
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.capability_requirements(capability_requirement_id,plan_candidate_id,capability_kind,capability_id,purpose,state,epistemic_precondition,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(capability_requirement_id) DO NOTHING`, requirement.ID, requirement.PlanCandidateID, requirement.CapabilityKind, requirement.CapabilityID, requirement.Purpose, requirement.State, requirement.EpistemicPrecondition, requirement.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, step := range candidate.Steps {
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.plan_steps(plan_step_id,plan_candidate_id,ordinal,kind,description,requires_capability_id,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(plan_step_id) DO NOTHING`, step.ID, step.PlanCandidateID, step.Ordinal, step.Kind, step.Description, step.RequiresCapabilityID, step.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, delegation := range candidate.Delegations {
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO work.delegation_candidates(delegation_candidate_id,plan_candidate_id,capability_requirement_id,kind,executor_hint,state,reason,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(delegation_candidate_id) DO NOTHING`, delegation.ID, delegation.PlanCandidateID, delegation.CapabilityRequirementID, delegation.Kind, delegation.ExecutorHint, delegation.State, delegation.Reason, delegation.RecordedAtUnixMillis); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# ADR-0012 - Intent To Work Planning Foundation
|
||||
|
||||
Status: Accepted
|
||||
Date: 2026-08-17
|
||||
Accepted under: DEV-000063 milestone decision
|
||||
|
||||
## Decision
|
||||
|
||||
`work` remains the canonical owner for Goal, Task, Decision and the new V1 planning foundation. No new canonical owner is introduced.
|
||||
|
||||
Help is the central user-facing concept. Goal, Work and Plan are optional durable coordination primitives, not mandatory records for every conversation. A simple explanation, ordinary answer, short cognitive response, direct reminder capability, safe deterministic operation, or capability-gap response may bypass durable Work entirely.
|
||||
|
||||
Human chat must not reduce Domovoy to `chat -> prompt -> model` or to a Codex-specific task generator. The canonical pipeline is:
|
||||
|
||||
When durable coordination is actually needed, the canonical path is:
|
||||
|
||||
`HumanUtterance -> OriginalHumanIntent -> Goal -> PlanCandidate -> accepted Work/Plan state -> CapabilityRequirement -> DelegationCandidate/Action/Investigation/Observation/Outcome`.
|
||||
|
||||
Prompts, Codex instructions, mail payloads, reminders and provider requests are executor-adapter representations. They are not canonical human intent, Goal or Plan.
|
||||
|
||||
## Ownership
|
||||
|
||||
- `interaction` continues to own append-only human utterances, original intent provenance and typed utterance interpretations.
|
||||
- `work` owns durable Goals, lifecycle, planning state, plan candidates, accepted plan summaries, capability requirements and general delegation candidates.
|
||||
- `capability` owns available capability/binding registry and health. Work may reference capability requirements by typed IDs/kinds but does not select a concrete provider as canonical truth.
|
||||
- `epistemics` owns verified/unverified/unknown evidence semantics. Work planning must preserve epistemic state and may add verification requirements before risky work.
|
||||
- `authority` and `commit` remain required for any real mutation or external side effect. Planning creates no permission and no execution claim.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Work PlanCandidates are structured candidates that must be validated before persistence or acceptance. Cognitive planning may later propose candidates only through the Cognitive Gateway/Router; model output never directly mutates canonical Work state. DEV-000063 may implement deterministic V1 candidate planning and keep cognitive candidate generation as a typed extension point.
|
||||
|
||||
CapabilityRequirement is executor-neutral. Examples include `READ_EMAIL`, `SEND_EMAIL`, `READ_DATABASE`, `QUERY_LOGS`, `INSPECT_PROJECT`, `MODIFY_CODE`, `RUN_TESTS`, `WEB_RESEARCH`, `COGNITIVE_REASONING`, and `SCHEDULE_REMINDER`. This is an extensible vocabulary, not a closed ontology and not an executor list.
|
||||
|
||||
DelegationCandidate is general. `Codex` is only a possible future implementation for `MODIFY_CODE`; mail, research, scheduler, device and database adapters can use the same canonical Work model without changing OriginalHumanIntent or Goal.
|
||||
|
||||
## Consequences
|
||||
|
||||
`/chat` can choose a HelpPath first. Only the durable-work path creates a planning-only Goal and Plan. Other paths can answer, use cognition, report a missing capability, or describe a direct capability requirement without materializing a Goal/Plan. It must not pretend work was completed. Completion remains tied to outcome semantics, not to a model response.
|
||||
|
||||
Project is only one possible context reference. The Work foundation stores project context as optional opaque references and must later permit people, emails, documents, images, servers, organizations, research subjects and other context types without redesigning Goal/Plan.
|
||||
|
||||
Capability gaps are first-class planning information. "I cannot do this yet" is not necessarily terminal in future milestones: the system may later discover, connect, configure or build a missing capability and return to the original need. DEV-000063 does not implement self-extension; it preserves the extension path.
|
||||
@@ -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.
|
||||
@@ -57,14 +57,24 @@
|
||||
"id": "mod.work",
|
||||
"owns": [
|
||||
"Goal",
|
||||
"HelpPath",
|
||||
"GoalLifecycle",
|
||||
"Task",
|
||||
"Decision"
|
||||
"Decision",
|
||||
"PlanCandidate",
|
||||
"WorkPlan",
|
||||
"PlanStep",
|
||||
"CapabilityRequirement",
|
||||
"DelegationCandidate"
|
||||
],
|
||||
"database_schema": "work",
|
||||
"boundary_class": "remote_capable",
|
||||
"may_depend_on": [
|
||||
"contracts.identity",
|
||||
"contracts.authority",
|
||||
"contracts.epistemics",
|
||||
"contracts.capability",
|
||||
"platform.ids",
|
||||
"platform.events"
|
||||
]
|
||||
},
|
||||
@@ -88,7 +98,8 @@
|
||||
"id": "mod.continuity",
|
||||
"owns": [
|
||||
"Commitment",
|
||||
"ProspectiveItem"
|
||||
"ProspectiveItem",
|
||||
"AttentionItem"
|
||||
],
|
||||
"database_schema": "continuity",
|
||||
"boundary_class": "remote_capable",
|
||||
@@ -104,7 +115,13 @@
|
||||
"owns": [
|
||||
"Capability",
|
||||
"CapabilityBinding",
|
||||
"CapabilityHealth"
|
||||
"CapabilityHealth",
|
||||
"CapabilityImplementation",
|
||||
"CapabilityResolution",
|
||||
"CapabilitySelection",
|
||||
"CapabilityGap",
|
||||
"AcquisitionStrategyCandidate",
|
||||
"CapabilityUsageObservation"
|
||||
],
|
||||
"database_schema": "capability",
|
||||
"boundary_class": "remote_capable",
|
||||
|
||||
@@ -27,10 +27,10 @@ modules:
|
||||
|
||||
work:
|
||||
id: mod.work
|
||||
owns: [Goal, Task, Decision]
|
||||
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate]
|
||||
database_schema: work
|
||||
boundary_class: remote_capable
|
||||
may_depend_on: [contracts.identity, contracts.authority, platform.events]
|
||||
may_depend_on: [contracts.identity, contracts.authority, contracts.epistemics, contracts.capability, platform.ids, platform.events]
|
||||
|
||||
epistemics:
|
||||
id: mod.epistemics
|
||||
@@ -41,14 +41,14 @@ modules:
|
||||
|
||||
continuity:
|
||||
id: mod.continuity
|
||||
owns: [Commitment, ProspectiveItem]
|
||||
owns: [Commitment, ProspectiveItem, AttentionItem]
|
||||
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]
|
||||
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,112 @@
|
||||
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),
|
||||
})
|
||||
}
|
||||
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.AttentionSuppressed:
|
||||
return "скрыто"
|
||||
case continuity.AttentionExpired:
|
||||
return "истекло"
|
||||
case continuity.AttentionFailed:
|
||||
return "не доставлено"
|
||||
default:
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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,6 +35,13 @@ 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)
|
||||
mux.HandleFunc("POST /v1/prospective-items", func(w http.ResponseWriter, r *http.Request) {
|
||||
var item continuity.ProspectiveItem
|
||||
@@ -47,6 +55,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 +112,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,214 @@
|
||||
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
|
||||
}
|
||||
|
||||
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 (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 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)
|
||||
}
|
||||
}
|
||||
+139
-1
@@ -6,7 +6,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"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"
|
||||
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
||||
@@ -16,6 +19,7 @@ import (
|
||||
identitydomain "git.cin.su/m/Assistant/modules/identity/domain"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
situationdomain "git.cin.su/m/Assistant/modules/situation/domain"
|
||||
workdomain "git.cin.su/m/Assistant/modules/work/domain"
|
||||
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
|
||||
"git.cin.su/m/Assistant/runtime/health"
|
||||
"github.com/jackc/pgx/v5"
|
||||
@@ -82,6 +86,10 @@ 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)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
|
||||
@@ -240,16 +248,119 @@ func main() {
|
||||
activeFocusName = previousFocus.ProjectDisplayName
|
||||
activeFocusProvenance = previousFocus.Provenance
|
||||
}
|
||||
var planningReply string
|
||||
workPlanned := false
|
||||
needsCognition := semantic.NeedsCognition
|
||||
projectID := ""
|
||||
if semantic.ActiveFocus != nil {
|
||||
projectID = string(semantic.ActiveFocus.ProjectID)
|
||||
} else if previousFocus != nil {
|
||||
projectID = string(previousFocus.ProjectID)
|
||||
}
|
||||
planningInput := work.PlanningInput{
|
||||
ConversationID: work.ConversationRef(conversation.ID),
|
||||
UtteranceID: work.SourceUtteranceRef(utterance.ID),
|
||||
IntentID: work.SourceIntentRef(intent.ID),
|
||||
ActorID: session.ActorID,
|
||||
PrincipalID: session.PrincipalID,
|
||||
ScopeID: authority.ScopeID("domovoy-global-scope"),
|
||||
ProjectID: work.ProjectRef(projectID),
|
||||
ProjectDisplayName: activeFocusName,
|
||||
ProjectProvenance: work.ProjectProvenanceRef(activeFocusProvenance),
|
||||
Message: utterance.Content,
|
||||
SpeechAct: work.SpeechActRef(semantic.Interpretation.SpeechAct),
|
||||
AssertionVerification: epistemics.VerificationState(semantic.Interpretation.AssertionVerification),
|
||||
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
|
||||
}
|
||||
helpPath := workdomain.DetermineHelpPath(planningInput)
|
||||
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 {
|
||||
http.Error(w, "plan chat work", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
if err := workRepository.SavePlanningResult(r.Context(), planning); err != nil {
|
||||
http.Error(w, "save work plan", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
planningReply = planning.Reply
|
||||
workPlanned = true
|
||||
needsCognition = false
|
||||
}
|
||||
response := interaction.ChatMessageResponse{
|
||||
ConversationID: conversation.ID,
|
||||
UtteranceID: utterance.ID,
|
||||
OriginalIntentID: intent.ID,
|
||||
SpeechAct: semantic.Interpretation.SpeechAct,
|
||||
InterpretationState: utterance.InterpretationState,
|
||||
NeedsCognition: semantic.NeedsCognition,
|
||||
NeedsCognition: needsCognition,
|
||||
FallbackText: semantic.Reply,
|
||||
ActiveProjectName: activeFocusName,
|
||||
ProjectProvenance: activeFocusProvenance,
|
||||
Clarification: semantic.Interpretation.Clarification,
|
||||
WorkPlanned: workPlanned,
|
||||
PlanningReply: planningReply,
|
||||
CapabilityStatus: capabilityStatus,
|
||||
CapabilityKind: capabilityKind,
|
||||
CapabilityGapID: capabilityGapID,
|
||||
Reminder: reminder,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
@@ -641,3 +752,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"
|
||||
@@ -150,6 +151,10 @@ type browserChatResponse struct {
|
||||
ActiveFocus string `json:"active_focus"`
|
||||
ProjectProvenance string `json:"project_provenance"`
|
||||
Clarification string `json:"clarification"`
|
||||
WorkPlanned bool `json:"work_planned"`
|
||||
CapabilityStatus string `json:"capability_status"`
|
||||
CapabilityKind string `json:"capability_kind"`
|
||||
ReminderScheduled bool `json:"reminder_scheduled"`
|
||||
}
|
||||
|
||||
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -316,7 +321,19 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
reply := saved.FallbackText
|
||||
cognition := "not_requested"
|
||||
if saved.NeedsCognition && saved.Clarification != interaction.ClarificationRequired {
|
||||
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()
|
||||
if idErr == nil {
|
||||
@@ -336,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)})
|
||||
_ = 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 {
|
||||
@@ -487,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,25 @@ 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; }
|
||||
.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,6 +42,8 @@ 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 = "") {
|
||||
@@ -52,6 +59,91 @@ 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(" · ");
|
||||
card.append(when, title, state, source);
|
||||
todayList.appendChild(card);
|
||||
}
|
||||
} catch {
|
||||
todayList.replaceChildren();
|
||||
const error = document.createElement("p");
|
||||
error.className = "empty-state error";
|
||||
error.textContent = "Внимание сейчас недоступно. Чат можно продолжать.";
|
||||
todayList.appendChild(error);
|
||||
}
|
||||
}
|
||||
|
||||
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 = "";
|
||||
@@ -79,9 +171,11 @@ chatForm.addEventListener("submit", async (event) => {
|
||||
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.clarification) semanticMeta.push(`clarification=${result.clarification}`);
|
||||
appendMessage("assistant", result.reply_text || "Сообщение записано.", semanticMeta.join("; "));
|
||||
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result));
|
||||
if (/что сейчас требует моего внимания/i.test(text)) {
|
||||
await loadToday();
|
||||
activateTab("today-panel");
|
||||
}
|
||||
} catch {
|
||||
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
|
||||
} finally {
|
||||
@@ -90,6 +184,12 @@ chatForm.addEventListener("submit", async (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
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,162 @@
|
||||
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 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
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
type CapabilityID ids.ID
|
||||
type CapabilityBindingID ids.ID
|
||||
type CapabilityKind string
|
||||
|
||||
type BindingState string
|
||||
type BindingProtocol string
|
||||
@@ -16,6 +17,18 @@ const (
|
||||
BindingRetired BindingState = "RETIRED"
|
||||
|
||||
ProtocolIdempotentReceiptV1 BindingProtocol = "IDEMPOTENT_RECEIPT_V1"
|
||||
|
||||
CapabilityKindReadEmail CapabilityKind = "READ_EMAIL"
|
||||
CapabilityKindSendEmail CapabilityKind = "SEND_EMAIL"
|
||||
CapabilityKindReadDatabase CapabilityKind = "READ_DATABASE"
|
||||
CapabilityKindQueryLogs CapabilityKind = "QUERY_LOGS"
|
||||
CapabilityKindInspectProject CapabilityKind = "INSPECT_PROJECT"
|
||||
CapabilityKindModifyCode CapabilityKind = "MODIFY_CODE"
|
||||
CapabilityKindRunTests CapabilityKind = "RUN_TESTS"
|
||||
CapabilityKindWebResearch CapabilityKind = "WEB_RESEARCH"
|
||||
CapabilityKindCognitiveReasoning CapabilityKind = "COGNITIVE_REASONING"
|
||||
CapabilityKindScheduleReminder CapabilityKind = "SCHEDULE_REMINDER"
|
||||
CapabilityKindVerifyFact CapabilityKind = "VERIFY_FACT"
|
||||
)
|
||||
|
||||
// Binding is Capability-owned correctness state. SecretRef is an opaque
|
||||
|
||||
@@ -9,9 +9,12 @@ 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 ProspectiveState string
|
||||
|
||||
@@ -26,6 +29,34 @@ 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"
|
||||
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 +65,68 @@ 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
|
||||
Revision AttentionRevision
|
||||
}
|
||||
|
||||
type AttentionListResult struct {
|
||||
Items []AttentionItem
|
||||
}
|
||||
|
||||
type TodayItem struct {
|
||||
Title string
|
||||
WhenUnixMillis int64
|
||||
AttentionLevel AttentionLevel
|
||||
AttentionState AttentionState
|
||||
HumanState string
|
||||
DeliveryState string
|
||||
SourceDescription string
|
||||
}
|
||||
|
||||
type TodayResult struct {
|
||||
Items []TodayItem
|
||||
}
|
||||
|
||||
// SatisfyDeliveredSubject is the complete serializable operation subject. It
|
||||
// is distinct from a Work TaskTarget, which remains the live justification.
|
||||
type SatisfyDeliveredSubject struct {
|
||||
|
||||
@@ -12,6 +12,7 @@ const (
|
||||
VerificationUnverified VerificationState = "UNVERIFIED"
|
||||
VerificationVerified VerificationState = "VERIFIED"
|
||||
VerificationRejected VerificationState = "REJECTED"
|
||||
VerificationUnknown VerificationState = "UNKNOWN"
|
||||
VerificationNotApplicable VerificationState = "NOT_APPLICABLE"
|
||||
|
||||
ClaimSourceAuthenticatedHuman ClaimSourceKind = "AUTHENTICATED_HUMAN"
|
||||
|
||||
@@ -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
|
||||
@@ -128,4 +131,10 @@ type ChatMessageResponse struct {
|
||||
ActiveProjectName string
|
||||
ProjectProvenance situation.ProjectResolutionProvenance
|
||||
Clarification ClarificationState
|
||||
WorkPlanned bool
|
||||
PlanningReply string
|
||||
CapabilityStatus capability.CapabilityResolutionStatus
|
||||
CapabilityKind capability.CapabilityKind
|
||||
CapabilityGapID capability.CapabilityGapID
|
||||
Reminder *continuity.ProspectiveItem
|
||||
}
|
||||
|
||||
@@ -2,13 +2,88 @@ package work
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
type GoalID ids.ID
|
||||
type TaskID ids.ID
|
||||
type DecisionID ids.ID
|
||||
type PlanCandidateID ids.ID
|
||||
type WorkPlanID ids.ID
|
||||
type PlanStepID ids.ID
|
||||
type CapabilityRequirementID ids.ID
|
||||
type DelegationCandidateID ids.ID
|
||||
type TaskRevision uint64
|
||||
type GoalRevision uint64
|
||||
type PlanRevision uint64
|
||||
type GoalState string
|
||||
type PlanCandidateState string
|
||||
type PlanStepKind string
|
||||
type CapabilityRequirementState string
|
||||
type DelegationCandidateState string
|
||||
type DelegationKind string
|
||||
type SourceIntentRef string
|
||||
type SourceUtteranceRef string
|
||||
type ConversationRef string
|
||||
type ProjectRef string
|
||||
type ProjectProvenanceRef string
|
||||
type SpeechActRef string
|
||||
type HelpPathKind string
|
||||
|
||||
const (
|
||||
GoalProposed GoalState = "PROPOSED"
|
||||
GoalUnderstood GoalState = "UNDERSTOOD"
|
||||
GoalPlanning GoalState = "PLANNING"
|
||||
GoalReady GoalState = "READY"
|
||||
GoalInProgress GoalState = "IN_PROGRESS"
|
||||
GoalBlocked GoalState = "BLOCKED"
|
||||
GoalCompleted GoalState = "COMPLETED"
|
||||
GoalFailed GoalState = "FAILED"
|
||||
GoalCancelled GoalState = "CANCELLED"
|
||||
GoalSuperseded GoalState = "SUPERSEDED"
|
||||
|
||||
PlanCandidateProposed PlanCandidateState = "PROPOSED"
|
||||
PlanCandidateValidated PlanCandidateState = "VALIDATED"
|
||||
PlanCandidateAccepted PlanCandidateState = "ACCEPTED"
|
||||
PlanCandidateRejected PlanCandidateState = "REJECTED"
|
||||
|
||||
PlanStepInvestigate PlanStepKind = "INVESTIGATE"
|
||||
PlanStepObserve PlanStepKind = "OBSERVE"
|
||||
PlanStepVerify PlanStepKind = "VERIFY"
|
||||
PlanStepReason PlanStepKind = "REASON"
|
||||
PlanStepReport PlanStepKind = "REPORT"
|
||||
PlanStepPrepare PlanStepKind = "PREPARE"
|
||||
PlanStepCommunicate PlanStepKind = "COMMUNICATE"
|
||||
PlanStepSchedule PlanStepKind = "SCHEDULE"
|
||||
PlanStepModify PlanStepKind = "MODIFY"
|
||||
PlanStepRunChecks PlanStepKind = "RUN_CHECKS"
|
||||
PlanStepClarify PlanStepKind = "CLARIFY"
|
||||
PlanStepNoExecution PlanStepKind = "NO_EXECUTION"
|
||||
|
||||
CapabilityRequirementRequired CapabilityRequirementState = "REQUIRED"
|
||||
CapabilityRequirementAvailable CapabilityRequirementState = "AVAILABLE"
|
||||
CapabilityRequirementMissing CapabilityRequirementState = "MISSING"
|
||||
CapabilityRequirementBlocked CapabilityRequirementState = "BLOCKED"
|
||||
|
||||
DelegationCandidateProposed DelegationCandidateState = "PROPOSED"
|
||||
DelegationCandidateSelected DelegationCandidateState = "SELECTED"
|
||||
DelegationCandidateBlocked DelegationCandidateState = "BLOCKED"
|
||||
|
||||
DelegationKindAgent DelegationKind = "AGENT"
|
||||
DelegationKindTool DelegationKind = "TOOL"
|
||||
DelegationKindAdapter DelegationKind = "ADAPTER"
|
||||
DelegationKindHuman DelegationKind = "HUMAN"
|
||||
DelegationKindUnavailable DelegationKind = "UNAVAILABLE"
|
||||
|
||||
HelpPathAnswer HelpPathKind = "ANSWER"
|
||||
HelpPathCognition HelpPathKind = "COGNITION"
|
||||
HelpPathDirectCapability HelpPathKind = "DIRECT_CAPABILITY"
|
||||
HelpPathDurableWork HelpPathKind = "DURABLE_WORK"
|
||||
HelpPathCapabilityGap HelpPathKind = "CAPABILITY_GAP"
|
||||
)
|
||||
|
||||
// TaskTarget carries the integrity context that a work-owned mutation must match.
|
||||
type TaskTarget struct {
|
||||
@@ -17,3 +92,112 @@ type TaskTarget struct {
|
||||
ScopeID authority.ScopeID
|
||||
Revision TaskRevision
|
||||
}
|
||||
|
||||
type Goal struct {
|
||||
ID GoalID
|
||||
SourceIntentID SourceIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
ActorID identity.ActorID
|
||||
PrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
ProjectID ProjectRef
|
||||
ProjectDisplayName string
|
||||
DesiredOutcome string
|
||||
Constraints string
|
||||
State GoalState
|
||||
Provenance ProjectProvenanceRef
|
||||
Revision GoalRevision
|
||||
CreatedAtUnixMillis int64
|
||||
UpdatedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type CapabilityRequirement struct {
|
||||
ID CapabilityRequirementID
|
||||
PlanCandidateID PlanCandidateID
|
||||
CapabilityKind capability.CapabilityKind
|
||||
CapabilityID capability.CapabilityID
|
||||
Purpose string
|
||||
State CapabilityRequirementState
|
||||
EpistemicPrecondition epistemics.VerificationState
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type PlanStep struct {
|
||||
ID PlanStepID
|
||||
PlanCandidateID PlanCandidateID
|
||||
Ordinal int
|
||||
Kind PlanStepKind
|
||||
Description string
|
||||
RequiresCapabilityID CapabilityRequirementID
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type DelegationCandidate struct {
|
||||
ID DelegationCandidateID
|
||||
PlanCandidateID PlanCandidateID
|
||||
CapabilityRequirementID CapabilityRequirementID
|
||||
Kind DelegationKind
|
||||
ExecutorHint string
|
||||
State DelegationCandidateState
|
||||
Reason string
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type PlanCandidate struct {
|
||||
ID PlanCandidateID
|
||||
GoalID GoalID
|
||||
Source string
|
||||
State PlanCandidateState
|
||||
Summary string
|
||||
ValidationSummary string
|
||||
RequiresClarification bool
|
||||
AcceptedAtUnixMillis int64
|
||||
RecordedAtUnixMillis int64
|
||||
Steps []PlanStep
|
||||
Requirements []CapabilityRequirement
|
||||
Delegations []DelegationCandidate
|
||||
}
|
||||
|
||||
type WorkPlan struct {
|
||||
ID WorkPlanID
|
||||
GoalID GoalID
|
||||
AcceptedCandidateID PlanCandidateID
|
||||
Revision PlanRevision
|
||||
State PlanCandidateState
|
||||
Summary string
|
||||
RecordedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type PlanningInput struct {
|
||||
ConversationID ConversationRef
|
||||
UtteranceID SourceUtteranceRef
|
||||
IntentID SourceIntentRef
|
||||
ActorID identity.ActorID
|
||||
PrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
ProjectID ProjectRef
|
||||
ProjectDisplayName string
|
||||
ProjectProvenance ProjectProvenanceRef
|
||||
Message string
|
||||
SpeechAct SpeechActRef
|
||||
AssertionVerification epistemics.VerificationState
|
||||
ReceivedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type PlanningResult struct {
|
||||
Goal Goal
|
||||
Candidate PlanCandidate
|
||||
Plan WorkPlan
|
||||
Reply string
|
||||
}
|
||||
|
||||
type HelpPath struct {
|
||||
Kind HelpPathKind
|
||||
NeedsDurableGoal bool
|
||||
NeedsDurableWork bool
|
||||
NeedsDurablePlan bool
|
||||
SuggestedCapabilityKinds []capability.CapabilityKind
|
||||
CapabilityGap bool
|
||||
Reason string
|
||||
Reply string
|
||||
}
|
||||
|
||||
@@ -135,6 +135,97 @@ CREATE TABLE IF NOT EXISTS work.tasks (
|
||||
task_id TEXT PRIMARY KEY, goal_id TEXT NOT NULL, scope_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL, CHECK (revision > 0)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work.goals (
|
||||
goal_id TEXT PRIMARY KEY,
|
||||
source_intent_id TEXT NOT NULL,
|
||||
source_utterance_id TEXT NOT NULL,
|
||||
actor_id TEXT NOT NULL,
|
||||
principal_id TEXT NOT NULL,
|
||||
scope_id TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL DEFAULT '',
|
||||
project_display_name TEXT NOT NULL DEFAULT '',
|
||||
desired_outcome TEXT NOT NULL,
|
||||
constraints_text TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
provenance TEXT NOT NULL DEFAULT '',
|
||||
revision BIGINT NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
updated_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (desired_outcome <> ''),
|
||||
CHECK (state IN ('PROPOSED','UNDERSTOOD','PLANNING','READY','IN_PROGRESS','BLOCKED','COMPLETED','FAILED','CANCELLED','SUPERSEDED')),
|
||||
CHECK (revision > 0)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work.plan_candidates (
|
||||
plan_candidate_id TEXT PRIMARY KEY,
|
||||
goal_id TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
validation_summary TEXT NOT NULL,
|
||||
requires_clarification BOOLEAN NOT NULL,
|
||||
accepted_at_unix_millis BIGINT NOT NULL DEFAULT 0,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (goal_id <> ''),
|
||||
CHECK (source <> ''),
|
||||
CHECK (summary <> ''),
|
||||
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work.work_plans (
|
||||
work_plan_id TEXT PRIMARY KEY,
|
||||
goal_id TEXT NOT NULL,
|
||||
accepted_candidate_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (goal_id <> ''),
|
||||
CHECK (accepted_candidate_id <> ''),
|
||||
CHECK (revision > 0),
|
||||
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work.capability_requirements (
|
||||
capability_requirement_id TEXT PRIMARY KEY,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
capability_id TEXT NOT NULL DEFAULT '',
|
||||
purpose TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
epistemic_precondition TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (purpose <> ''),
|
||||
CHECK (state IN ('REQUIRED','AVAILABLE','MISSING','BLOCKED')),
|
||||
CHECK (epistemic_precondition IN ('UNVERIFIED','VERIFIED','REJECTED','UNKNOWN','NOT_APPLICABLE'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work.plan_steps (
|
||||
plan_step_id TEXT PRIMARY KEY,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
ordinal INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
requires_capability_id TEXT NOT NULL DEFAULT '',
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (ordinal > 0),
|
||||
CHECK (kind IN ('INVESTIGATE','OBSERVE','VERIFY','REASON','REPORT','PREPARE','COMMUNICATE','SCHEDULE','MODIFY','RUN_CHECKS','CLARIFY','NO_EXECUTION')),
|
||||
CHECK (description <> '')
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work.delegation_candidates (
|
||||
delegation_candidate_id TEXT PRIMARY KEY,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
capability_requirement_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
executor_hint TEXT NOT NULL DEFAULT '',
|
||||
state TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (capability_requirement_id <> ''),
|
||||
CHECK (kind IN ('AGENT','TOOL','ADAPTER','HUMAN','UNAVAILABLE')),
|
||||
CHECK (state IN ('PROPOSED','SELECTED','BLOCKED')),
|
||||
CHECK (reason <> '')
|
||||
);
|
||||
RESET ROLE;
|
||||
|
||||
SET ROLE situation_role;
|
||||
@@ -316,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;
|
||||
@@ -325,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,
|
||||
@@ -345,6 +578,34 @@ 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 '',
|
||||
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;
|
||||
|
||||
SET ROLE interaction_role;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.goals (
|
||||
goal_id TEXT PRIMARY KEY,
|
||||
source_intent_id TEXT NOT NULL,
|
||||
source_utterance_id TEXT NOT NULL,
|
||||
actor_id TEXT NOT NULL,
|
||||
principal_id TEXT NOT NULL,
|
||||
scope_id TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL DEFAULT '',
|
||||
project_display_name TEXT NOT NULL DEFAULT '',
|
||||
desired_outcome TEXT NOT NULL,
|
||||
constraints_text TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
provenance TEXT NOT NULL DEFAULT '',
|
||||
revision BIGINT NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
updated_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (desired_outcome <> ''),
|
||||
CHECK (state IN ('PROPOSED','UNDERSTOOD','PLANNING','READY','IN_PROGRESS','BLOCKED','COMPLETED','FAILED','CANCELLED','SUPERSEDED')),
|
||||
CHECK (revision > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.plan_candidates (
|
||||
plan_candidate_id TEXT PRIMARY KEY,
|
||||
goal_id TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
validation_summary TEXT NOT NULL,
|
||||
requires_clarification BOOLEAN NOT NULL,
|
||||
accepted_at_unix_millis BIGINT NOT NULL DEFAULT 0,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (goal_id <> ''),
|
||||
CHECK (source <> ''),
|
||||
CHECK (summary <> ''),
|
||||
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.work_plans (
|
||||
work_plan_id TEXT PRIMARY KEY,
|
||||
goal_id TEXT NOT NULL,
|
||||
accepted_candidate_id TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (goal_id <> ''),
|
||||
CHECK (accepted_candidate_id <> ''),
|
||||
CHECK (revision > 0),
|
||||
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.capability_requirements (
|
||||
capability_requirement_id TEXT PRIMARY KEY,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
capability_kind TEXT NOT NULL,
|
||||
capability_id TEXT NOT NULL DEFAULT '',
|
||||
purpose TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
epistemic_precondition TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (capability_kind <> ''),
|
||||
CHECK (purpose <> ''),
|
||||
CHECK (state IN ('REQUIRED','AVAILABLE','MISSING','BLOCKED')),
|
||||
CHECK (epistemic_precondition IN ('UNVERIFIED','VERIFIED','REJECTED','UNKNOWN','NOT_APPLICABLE'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.plan_steps (
|
||||
plan_step_id TEXT PRIMARY KEY,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
ordinal INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
requires_capability_id TEXT NOT NULL DEFAULT '',
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (ordinal > 0),
|
||||
CHECK (kind IN ('INVESTIGATE','OBSERVE','VERIFY','REASON','REPORT','PREPARE','COMMUNICATE','SCHEDULE','MODIFY','RUN_CHECKS','CLARIFY','NO_EXECUTION')),
|
||||
CHECK (description <> '')
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.delegation_candidates (
|
||||
delegation_candidate_id TEXT PRIMARY KEY,
|
||||
plan_candidate_id TEXT NOT NULL,
|
||||
capability_requirement_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
executor_hint TEXT NOT NULL DEFAULT '',
|
||||
state TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (plan_candidate_id <> ''),
|
||||
CHECK (capability_requirement_id <> ''),
|
||||
CHECK (kind IN ('AGENT','TOOL','ADAPTER','HUMAN','UNAVAILABLE')),
|
||||
CHECK (state IN ('PROPOSED','SELECTED','BLOCKED')),
|
||||
CHECK (reason <> '')
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
|
||||
@@ -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;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000062-human-intent-evidence-project-context.json",
|
||||
"task_id": "DEV-000062",
|
||||
"task_file": "dev/tasks/DEV-000065-assistant-presence-attention-human-surface.json",
|
||||
"task_id": "DEV-000065",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"task_id": "DEV-000063",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0012",
|
||||
"base_repo_revision": "18750c2ae69c90336900b333821cfbcf68769c16",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add general HelpPath and optional Intent -> Goal -> Work Planning foundation without making Domovoy a Codex prompt generator or granting execution authority.",
|
||||
"requirement_ids": [
|
||||
"ADR-0012",
|
||||
"INV-AUTH-001",
|
||||
"INV-EPI-001",
|
||||
"INV-OWN-001"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"work",
|
||||
"interaction",
|
||||
"situation",
|
||||
"epistemics",
|
||||
"capability"
|
||||
],
|
||||
"paths": [
|
||||
"architecture/adrs/ADR-0012-intent-work-planning-foundation.md",
|
||||
"architecture/architecture-registry.yaml",
|
||||
"architecture/architecture-registry.json",
|
||||
"contracts/work/**",
|
||||
"contracts/interaction/**",
|
||||
"contracts/capability/**",
|
||||
"contracts/epistemics/**",
|
||||
"modules/work/**",
|
||||
"modules/interaction/**",
|
||||
"adapters/postgres/**",
|
||||
"database/bootstrap/**",
|
||||
"database/migrations/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/creator-web/**",
|
||||
"tests/work_planning/**",
|
||||
"tests/architecture/**",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000063-intent-work-planning-foundation.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"architecture/ARCHITECTURE_CONSTITUTION.md",
|
||||
"modules/authority/**",
|
||||
"modules/commit/**",
|
||||
"cmd/commit-runtime/**",
|
||||
"cmd/cognitive-worker/**",
|
||||
"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": [
|
||||
"Interaction OriginalHumanIntent",
|
||||
"Interaction UtteranceInterpretation",
|
||||
"Situation ActiveProjectFocus",
|
||||
"Epistemics VerificationState",
|
||||
"Capability IDs/health/registry concepts",
|
||||
"Authority boundary concepts"
|
||||
],
|
||||
"may_change": [
|
||||
"contracts/work@v1",
|
||||
"contracts/interaction@v1",
|
||||
"contracts/capability@v1"
|
||||
]
|
||||
},
|
||||
"semantic_owner": "work",
|
||||
"canonical_state": [
|
||||
"Goal",
|
||||
"HelpPath",
|
||||
"GoalLifecycle",
|
||||
"PlanCandidate",
|
||||
"WorkPlan",
|
||||
"PlanStep",
|
||||
"CapabilityRequirement",
|
||||
"DelegationCandidate"
|
||||
],
|
||||
"state_transitions": [
|
||||
"help path ANSWER/COGNITION/DIRECT_CAPABILITY/DURABLE_WORK/CAPABILITY_GAP selected before durable planning",
|
||||
"goal PROPOSED->UNDERSTOOD->PLANNING->READY when durable work is actually needed",
|
||||
"plan candidate PROPOSED->VALIDATED->ACCEPTED",
|
||||
"no transition to IN_PROGRESS without future execution authority"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"planning-only canonical Work persistence",
|
||||
"ordinary answer/direct capability/capability gap paths without durable Work persistence",
|
||||
"no external mutation",
|
||||
"no Commit/Executor invocation",
|
||||
"no ACTIVE binding promotion",
|
||||
"cognitive routing only through existing gateway when later enabled"
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"work owner-local goals",
|
||||
"work owner-local plan candidates",
|
||||
"work owner-local plan steps",
|
||||
"work owner-local capability requirements",
|
||||
"work owner-local delegation candidates",
|
||||
"no cross-owner FK",
|
||||
"no cross-owner SQL join dependency"
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"intent persists even if planning falls back deterministically",
|
||||
"Goal/Work/Plan are optional and not created for simple help",
|
||||
"invalid plan candidate fails closed",
|
||||
"cognition unavailable keeps deterministic planning available",
|
||||
"planning never claims execution/completion"
|
||||
],
|
||||
"assumptions": [
|
||||
"DEV-000062 is production baseline",
|
||||
"Work owner is canonical Goal/Task/Decision owner per Core Semantic Model",
|
||||
"existing CPU CANARY remains not ACTIVE",
|
||||
"docker-test prohibited"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"investigate UPO performance creates investigation plan, not Codex prompt",
|
||||
"add support X in Assistant creates MODIFY_CODE requirement without hardcoded executor",
|
||||
"review new email creates READ_EMAIL and COGNITIVE_REASONING requirements without Codex",
|
||||
"reminder creates SCHEDULE_REMINDER requirement",
|
||||
"unverified disk assertion creates verification requirement before risky model install",
|
||||
"send email creates SEND_EMAIL planning without execution or authority claim",
|
||||
"cognition unavailable still persists intent/goal/plan when durable work is needed",
|
||||
"simple explanation can use answer/cognition path without durable Goal/Work/Plan",
|
||||
"capability gap remains executor-neutral and future self-extension-ready"
|
||||
],
|
||||
"required_tests": [
|
||||
"go test ./...",
|
||||
"go vet ./...",
|
||||
"architecture",
|
||||
"owner ACL",
|
||||
"LLM-free",
|
||||
"work planning",
|
||||
"production live acceptance"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
+59
-5
@@ -1,16 +1,70 @@
|
||||
# Assistant project status
|
||||
|
||||
Audit date: 2026-08-17. Active implementation: DEV-000062 revision 1.
|
||||
Audit date: 2026-08-17. Active implementation: DEV-000065 revision 1.
|
||||
|
||||
## 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 = 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; 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
|
||||
|
||||
- `DEV_000062_STATUS = IN_PROGRESS`
|
||||
- `LAST_COMPLETED = DEV-000062 packet rev.1, ADR-0011, Interaction/Situation/Epistemics contract extensions, Situation Project Registry V1, active project focus semantics, Interaction utterance interpretation/project references, append-only correction/supersession foundation, Core Runtime semantic persistence wiring, Creator Web /chat focus display`
|
||||
- `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`
|
||||
- `DEV_000062_STATUS = COMPLETED`
|
||||
- `LAST_COMPLETED = DEV-000062 packet rev.1, ADR-0011, Interaction/Situation/Epistemics contract extensions, Situation Project Registry V1, active project focus semantics, Interaction utterance interpretation/project references, append-only correction/supersession foundation, Core Runtime semantic persistence wiring, Creator Web /chat focus display, production deployment and 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`
|
||||
- `ACTIVE_BINDINGS = none`
|
||||
- `CANARY_BINDING = unchanged; not promoted to ACTIVE`
|
||||
- `OPENAI_REAL_CREDENTIAL = not required / not activated`
|
||||
- `NEXT_STEP = commit, push, PR/promotion, apply migration 016, rebuild/deploy affected production services if required, then run live DEV-000062 /chat acceptance`
|
||||
- `PRODUCTION_REVISION = 445e9edd7c5b614b58dd925f89aa9fe49cf0a1d6`
|
||||
- `HUMAN_CHAT_URL = https://assistant.cin.su/chat`
|
||||
- `LIVE_ACCEPTANCE = PASS: explicit УПО focus, unverified assertion, directive/no execution, correction, explicit Assistant focus, current-focus question answered without cognition`
|
||||
- `NEXT_STEP = DEV-000063 Project Copilot planning, if separately approved`
|
||||
- `REAL_BLOCKERS = none known`
|
||||
|
||||
## Current milestone
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -101,11 +101,11 @@ func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) in
|
||||
switch {
|
||||
case corrects != "" || strings.Contains(lower, "исправ") || strings.Contains(lower, "не так") || strings.HasPrefix(lower, "нет,"):
|
||||
return interaction.SpeechActCorrection
|
||||
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(lower, "кто "):
|
||||
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(lower, "кто ") || strings.Contains(lower, "объясни"):
|
||||
return interaction.SpeechActQuestion
|
||||
case strings.Contains(lower, "проверь") || strings.Contains(lower, "исслед") || strings.Contains(lower, "разбер"):
|
||||
return interaction.SpeechActRequestToInvestigate
|
||||
case strings.Contains(lower, "сделай") || strings.Contains(lower, "запусти") || strings.Contains(lower, "создай") || strings.Contains(lower, "настрой") || strings.Contains(lower, "добавь") || strings.Contains(lower, "измени") || strings.Contains(lower, "поменяй") || strings.Contains(lower, "используй") || strings.Contains(lower, "удали") || strings.Contains(lower, "удалить"):
|
||||
case strings.Contains(lower, "сделай") || strings.Contains(lower, "запусти") || strings.Contains(lower, "создай") || strings.Contains(lower, "настрой") || strings.Contains(lower, "добавь") || strings.Contains(lower, "измени") || strings.Contains(lower, "поменяй") || strings.Contains(lower, "используй") || strings.Contains(lower, "удали") || strings.Contains(lower, "удалить") || strings.Contains(lower, "напомни") || strings.Contains(lower, "подключи"):
|
||||
return interaction.SpeechActDirective
|
||||
case strings.HasPrefix(lower, "я решил") || strings.Contains(lower, "решение"):
|
||||
return interaction.SpeechActDecision
|
||||
|
||||
@@ -36,6 +36,19 @@ func TestNewHumanUtteranceAppendsCorrectionRelation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHumanUtteranceClassifiesReminderAndCapabilityConnectionAsDirective(t *testing.T) {
|
||||
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
|
||||
for _, message := range []string{"Напомни завтра позвонить врачу.", "Подключи интеграцию с неизвестным сервисом."} {
|
||||
utterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Unix(10, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if utterance.SpeechAct != interaction.SpeechActDirective {
|
||||
t.Fatalf("expected directive for %q, got %s", message, utterance.SpeechAct)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConversationRequiresActorAndPrincipal(t *testing.T) {
|
||||
_, err := NewConversation(identity.ActorID(""), identity.PrincipalID("principal-1"), time.Unix(10, 0))
|
||||
if err == nil {
|
||||
|
||||
@@ -71,6 +71,9 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati
|
||||
result.Relations = append(result.Relations, relations...)
|
||||
}
|
||||
result.Reply = semanticReply(utterance.Content, result, previousFocus)
|
||||
if isCurrentFocusQuestion(utterance.Content, act) && previousFocus != nil {
|
||||
result.NeedsCognition = false
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -229,3 +232,11 @@ func semanticReply(message string, result SemanticResult, previousFocus *situati
|
||||
}
|
||||
return FallbackText(result.Interpretation.SpeechAct)
|
||||
}
|
||||
|
||||
func isCurrentFocusQuestion(message string, act interaction.SpeechAct) bool {
|
||||
if act != interaction.SpeechActQuestion {
|
||||
return false
|
||||
}
|
||||
lower := strings.ToLower(message)
|
||||
return strings.Contains(lower, "сейчас") || strings.Contains(lower, "что мы") || strings.Contains(lower, "делаем")
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ func TestInterpretUtteranceQuestionAboutCurrentWorkUsesFocus(t *testing.T) {
|
||||
if !strings.Contains(result.Reply, "Assistant") {
|
||||
t.Fatalf("expected focus-aware reply, got %q", result.Reply)
|
||||
}
|
||||
if result.NeedsCognition {
|
||||
t.Fatalf("focus question should be answered from canonical focus without cognition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpretUtteranceInheritedFocusForThere(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
var ErrInvalidPlanningInput = errors.New("invalid planning input")
|
||||
var ErrInvalidPlanCandidate = errors.New("invalid plan candidate")
|
||||
|
||||
func BuildPlanningResult(input work.PlanningInput, now time.Time) (work.PlanningResult, error) {
|
||||
if input.UtteranceID == "" || input.IntentID == "" || input.ActorID == "" || input.PrincipalID == "" || strings.TrimSpace(input.Message) == "" {
|
||||
return work.PlanningResult{}, ErrInvalidPlanningInput
|
||||
}
|
||||
millis := now.UnixMilli()
|
||||
goalID, err := ids.New()
|
||||
if err != nil {
|
||||
return work.PlanningResult{}, err
|
||||
}
|
||||
candidateID, err := ids.New()
|
||||
if err != nil {
|
||||
return work.PlanningResult{}, err
|
||||
}
|
||||
planID, err := ids.New()
|
||||
if err != nil {
|
||||
return work.PlanningResult{}, err
|
||||
}
|
||||
goal := work.Goal{
|
||||
ID: work.GoalID(goalID),
|
||||
SourceIntentID: input.IntentID,
|
||||
SourceUtteranceID: input.UtteranceID,
|
||||
ActorID: input.ActorID,
|
||||
PrincipalID: input.PrincipalID,
|
||||
ScopeID: input.ScopeID,
|
||||
ProjectID: input.ProjectID,
|
||||
ProjectDisplayName: input.ProjectDisplayName,
|
||||
DesiredOutcome: desiredOutcome(input),
|
||||
Constraints: "planning-only; no execution authority; no external mutation",
|
||||
State: work.GoalReady,
|
||||
Provenance: input.ProjectProvenance,
|
||||
Revision: 1,
|
||||
CreatedAtUnixMillis: millis,
|
||||
UpdatedAtUnixMillis: millis,
|
||||
}
|
||||
candidate := work.PlanCandidate{
|
||||
ID: work.PlanCandidateID(candidateID),
|
||||
GoalID: goal.ID,
|
||||
Source: "deterministic-planner-v1",
|
||||
State: work.PlanCandidateAccepted,
|
||||
Summary: planSummary(input),
|
||||
ValidationSummary: "validated: structured deterministic candidate; executor-neutral; authority not granted",
|
||||
RequiresClarification: false,
|
||||
AcceptedAtUnixMillis: millis,
|
||||
RecordedAtUnixMillis: millis,
|
||||
}
|
||||
addScenarioPlan(input, millis, &candidate)
|
||||
if hasRiskyUnverifiedAssertion(input) {
|
||||
addRequirement(&candidate, capability.CapabilityKindVerifyFact, "verify the user-provided resource assertion before risky work", epistemics.VerificationUnverified, millis)
|
||||
addStep(&candidate, work.PlanStepVerify, "Verify the reported server free-space/capacity using trusted observations before any large model installation.", len(candidate.Steps)+1, millis)
|
||||
}
|
||||
if err := ValidatePlanCandidate(candidate); err != nil {
|
||||
return work.PlanningResult{}, err
|
||||
}
|
||||
plan := work.WorkPlan{ID: work.WorkPlanID(planID), GoalID: goal.ID, AcceptedCandidateID: candidate.ID, Revision: 1, State: candidate.State, Summary: candidate.Summary, RecordedAtUnixMillis: millis}
|
||||
return work.PlanningResult{Goal: goal, Candidate: candidate, Plan: plan, Reply: planningReply(goal, candidate)}, nil
|
||||
}
|
||||
|
||||
func DetermineHelpPath(input work.PlanningInput) work.HelpPath {
|
||||
lower := strings.ToLower(input.Message)
|
||||
switch {
|
||||
case containsAny(lower, "объясни", "расскажи", "что делает"):
|
||||
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):
|
||||
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, delegation, or outcome tracking"}
|
||||
default:
|
||||
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "ordinary interaction does not require durable Work"}
|
||||
}
|
||||
}
|
||||
|
||||
func ValidatePlanCandidate(candidate work.PlanCandidate) error {
|
||||
if candidate.ID == "" || candidate.GoalID == "" || candidate.Source == "" || candidate.Summary == "" || len(candidate.Steps) == 0 {
|
||||
return ErrInvalidPlanCandidate
|
||||
}
|
||||
switch candidate.State {
|
||||
case work.PlanCandidateProposed, work.PlanCandidateValidated, work.PlanCandidateAccepted, work.PlanCandidateRejected:
|
||||
default:
|
||||
return ErrInvalidPlanCandidate
|
||||
}
|
||||
ordinals := map[int]bool{}
|
||||
for _, step := range candidate.Steps {
|
||||
if step.ID == "" || step.PlanCandidateID != candidate.ID || step.Ordinal < 1 || step.Kind == "" || step.Description == "" || ordinals[step.Ordinal] {
|
||||
return ErrInvalidPlanCandidate
|
||||
}
|
||||
ordinals[step.Ordinal] = true
|
||||
}
|
||||
seenRequirements := map[work.CapabilityRequirementID]bool{}
|
||||
for _, requirement := range candidate.Requirements {
|
||||
if requirement.ID == "" || requirement.PlanCandidateID != candidate.ID || requirement.CapabilityKind == "" || requirement.Purpose == "" || requirement.State == "" || requirement.EpistemicPrecondition == "" {
|
||||
return ErrInvalidPlanCandidate
|
||||
}
|
||||
seenRequirements[requirement.ID] = true
|
||||
}
|
||||
for _, delegation := range candidate.Delegations {
|
||||
if delegation.ID == "" || delegation.PlanCandidateID != candidate.ID || delegation.CapabilityRequirementID == "" || delegation.Kind == "" || delegation.State == "" || !seenRequirements[delegation.CapabilityRequirementID] {
|
||||
return ErrInvalidPlanCandidate
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.PlanCandidate) {
|
||||
lower := strings.ToLower(input.Message)
|
||||
switch {
|
||||
case containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему"):
|
||||
addRequirement(candidate, capability.CapabilityKindInspectProject, "inspect project context", epistemics.VerificationNotApplicable, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindReadDatabase, "collect database performance observations", epistemics.VerificationUnknown, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindQueryLogs, "collect runtime/log observations", epistemics.VerificationUnknown, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "compare evidence and identify likely causes", epistemics.VerificationUnknown, millis)
|
||||
addStep(candidate, work.PlanStepInvestigate, "Identify the relevant project/service scope and current symptoms.", 1, millis)
|
||||
addStep(candidate, work.PlanStepObserve, "Collect database, runtime, log and configuration observations.", 2, millis)
|
||||
addStep(candidate, work.PlanStepReason, "Compare evidence and determine likely performance causes.", 3, millis)
|
||||
addStep(candidate, work.PlanStepReport, "Report findings and what remains unverified.", 4, millis)
|
||||
case containsAny(lower, "добавь", "поддержк", "возможность", "реализ"):
|
||||
addRequirement(candidate, capability.CapabilityKindModifyCode, "modify software project", epistemics.VerificationNotApplicable, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindRunTests, "validate software change", epistemics.VerificationNotApplicable, millis)
|
||||
addStep(candidate, work.PlanStepInvestigate, "Understand desired capability and affected project scope.", 1, millis)
|
||||
addStep(candidate, work.PlanStepModify, "Prepare a code-change work item for a capable coding executor.", 2, millis)
|
||||
addStep(candidate, work.PlanStepRunChecks, "Run relevant tests and architecture checks before outcome claim.", 3, millis)
|
||||
case containsAny(lower, "письм", "почт"):
|
||||
addRequirement(candidate, capability.CapabilityKindReadEmail, "read incoming communications", epistemics.VerificationUnknown, millis)
|
||||
if containsAny(lower, "ответ", "ответить", "скажи") {
|
||||
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "classify messages needing response", epistemics.VerificationUnknown, millis)
|
||||
}
|
||||
if containsAny(lower, "отправь", "напиши") {
|
||||
addRequirement(candidate, capability.CapabilityKindSendEmail, "send email only after authority", epistemics.VerificationNotApplicable, millis)
|
||||
}
|
||||
addStep(candidate, work.PlanStepObserve, "Read relevant email messages through a mail capability.", 1, millis)
|
||||
addStep(candidate, work.PlanStepReason, "Classify which messages need a response and why.", 2, millis)
|
||||
addStep(candidate, work.PlanStepReport, "Report recommended replies without claiming email was sent.", 3, millis)
|
||||
case containsAny(lower, "напомни", "завтра", "напомин"):
|
||||
addRequirement(candidate, capability.CapabilityKindScheduleReminder, "schedule a future reminder after temporal interpretation", epistemics.VerificationNotApplicable, millis)
|
||||
addStep(candidate, work.PlanStepSchedule, "Interpret the requested reminder time and reminder subject.", 1, millis)
|
||||
addStep(candidate, work.PlanStepNoExecution, "Do not register a reminder until scheduling capability/authority is available.", 2, millis)
|
||||
case containsAny(lower, "отправь", "ивану", "сообщи"):
|
||||
addRequirement(candidate, capability.CapabilityKindSendEmail, "communicate with the named recipient after channel/authority resolution", epistemics.VerificationNotApplicable, millis)
|
||||
addStep(candidate, work.PlanStepPrepare, "Resolve recipient and appropriate communication channel.", 1, millis)
|
||||
addStep(candidate, work.PlanStepCommunicate, "Prepare the message for a communication adapter; do not send without authority.", 2, millis)
|
||||
default:
|
||||
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "clarify/decompose ambiguous human intent", epistemics.VerificationUnknown, millis)
|
||||
addStep(candidate, work.PlanStepClarify, "Clarify desired outcome and required capabilities.", 1, millis)
|
||||
candidate.RequiresClarification = true
|
||||
}
|
||||
for _, requirement := range candidate.Requirements {
|
||||
addDelegation(candidate, requirement, millis)
|
||||
}
|
||||
}
|
||||
|
||||
func requiresDurableWork(lower string) bool {
|
||||
return containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему", "добавь", "поддержк", "возможность", "реализ", "письм", "почт", "отправь", "ивану", "сообщи", "установи", "большую модель")
|
||||
}
|
||||
|
||||
func addRequirement(candidate *work.PlanCandidate, kind capability.CapabilityKind, purpose string, precondition epistemics.VerificationState, millis int64) {
|
||||
id, err := ids.New()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
candidate.Requirements = append(candidate.Requirements, work.CapabilityRequirement{ID: work.CapabilityRequirementID(id), PlanCandidateID: candidate.ID, CapabilityKind: kind, Purpose: purpose, State: work.CapabilityRequirementMissing, EpistemicPrecondition: precondition, RecordedAtUnixMillis: millis})
|
||||
}
|
||||
|
||||
func addStep(candidate *work.PlanCandidate, kind work.PlanStepKind, description string, ordinal int, millis int64) {
|
||||
id, err := ids.New()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var requirementID work.CapabilityRequirementID
|
||||
if len(candidate.Requirements) > 0 {
|
||||
requirementID = candidate.Requirements[len(candidate.Requirements)-1].ID
|
||||
}
|
||||
candidate.Steps = append(candidate.Steps, work.PlanStep{ID: work.PlanStepID(id), PlanCandidateID: candidate.ID, Ordinal: ordinal, Kind: kind, Description: description, RequiresCapabilityID: requirementID, RecordedAtUnixMillis: millis})
|
||||
}
|
||||
|
||||
func addDelegation(candidate *work.PlanCandidate, requirement work.CapabilityRequirement, millis int64) {
|
||||
id, err := ids.New()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
candidate.Delegations = append(candidate.Delegations, work.DelegationCandidate{ID: work.DelegationCandidateID(id), PlanCandidateID: candidate.ID, CapabilityRequirementID: requirement.ID, Kind: delegationKind(requirement.CapabilityKind), ExecutorHint: "", State: work.DelegationCandidateBlocked, Reason: "no concrete executor selected in DEV-000063 planning-only mode", RecordedAtUnixMillis: millis})
|
||||
}
|
||||
|
||||
func delegationKind(kind capability.CapabilityKind) work.DelegationKind {
|
||||
switch kind {
|
||||
case capability.CapabilityKindModifyCode, capability.CapabilityKindCognitiveReasoning, capability.CapabilityKindWebResearch:
|
||||
return work.DelegationKindAgent
|
||||
case capability.CapabilityKindReadEmail, capability.CapabilityKindSendEmail, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs, capability.CapabilityKindScheduleReminder, capability.CapabilityKindInspectProject, capability.CapabilityKindRunTests, capability.CapabilityKindVerifyFact:
|
||||
return work.DelegationKindAdapter
|
||||
default:
|
||||
return work.DelegationKindUnavailable
|
||||
}
|
||||
}
|
||||
|
||||
func desiredOutcome(input work.PlanningInput) string {
|
||||
lower := strings.ToLower(input.Message)
|
||||
switch {
|
||||
case containsAny(lower, "тормозит", "производительн"):
|
||||
return "identify likely performance cause with sufficient evidence"
|
||||
case containsAny(lower, "письм", "почт"):
|
||||
return "review communications and identify what needs response"
|
||||
case containsAny(lower, "напомни", "напомин"):
|
||||
return "create a future reminder when scheduling capability is available"
|
||||
case containsAny(lower, "отправь", "сообщи"):
|
||||
return "communicate requested information to the recipient after authority"
|
||||
case containsAny(lower, "добавь", "поддержк", "возможность"):
|
||||
return "software project gains the requested capability after validated implementation"
|
||||
default:
|
||||
return strings.TrimSpace(input.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func planSummary(input work.PlanningInput) string {
|
||||
return "Planning foundation for: " + desiredOutcome(input)
|
||||
}
|
||||
|
||||
func planningReply(goal work.Goal, candidate work.PlanCandidate) string {
|
||||
requirements := make([]string, 0, len(candidate.Requirements))
|
||||
for _, requirement := range candidate.Requirements {
|
||||
requirements = append(requirements, string(requirement.CapabilityKind))
|
||||
}
|
||||
project := goal.ProjectDisplayName
|
||||
if project == "" {
|
||||
project = "текущий контекст"
|
||||
}
|
||||
return "Понял цель: " + goal.DesiredOutcome + ". Контекст: " + project + ". План сохранён в безопасном режиме: нужны capability " + strings.Join(requirements, ", ") + ". Исполнение этих инструментов пока не выполнено и требует отдельной capability/authority."
|
||||
}
|
||||
|
||||
func hasRiskyUnverifiedAssertion(input work.PlanningInput) bool {
|
||||
lower := strings.ToLower(input.Message)
|
||||
return input.AssertionVerification == epistemics.VerificationUnverified && containsAny(lower, "300 гб", "свободно", "большую модель", "установи", "модель")
|
||||
}
|
||||
|
||||
func containsAny(value string, needles ...string) bool {
|
||||
for _, needle := range needles {
|
||||
if strings.Contains(value, needle) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
func TestPlanningInvestigationIsNotCodexPrompt(t *testing.T) {
|
||||
result := planForTest(t, "Разберись, почему УПО тормозит.", "REQUEST_TO_INVESTIGATE", epistemics.VerificationNotApplicable)
|
||||
requireKinds(t, result, capability.CapabilityKindInspectProject, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs, capability.CapabilityKindCognitiveReasoning)
|
||||
for _, delegation := range result.Candidate.Delegations {
|
||||
if strings.Contains(strings.ToLower(delegation.ExecutorHint), "codex") {
|
||||
t.Fatalf("planning must not hardcode Codex executor: %#v", delegation)
|
||||
}
|
||||
}
|
||||
if strings.Contains(strings.ToLower(result.Candidate.Summary), "prompt") {
|
||||
t.Fatalf("canonical plan must not be prompt-centric: %q", result.Candidate.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanningCodeChangeRequiresModifyCodeWithoutExecutor(t *testing.T) {
|
||||
result := planForTest(t, "В Assistant добавь поддержку X.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
requireKinds(t, result, capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests)
|
||||
for _, delegation := range result.Candidate.Delegations {
|
||||
if delegation.ExecutorHint != "" {
|
||||
t.Fatalf("executor must remain unselected in canonical planning: %#v", delegation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanningEmailReviewIsNotCodexSpecific(t *testing.T) {
|
||||
result := planForTest(t, "Посмотри новые письма и скажи, на какие нужно ответить.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
requireKinds(t, result, capability.CapabilityKindReadEmail, capability.CapabilityKindCognitiveReasoning)
|
||||
requireNoKind(t, result, capability.CapabilityKindModifyCode)
|
||||
}
|
||||
|
||||
func TestPlanningReminderUsesSchedulingRequirement(t *testing.T) {
|
||||
result := planForTest(t, "Напомни завтра позвонить врачу.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
requireKinds(t, result, capability.CapabilityKindScheduleReminder)
|
||||
requireNoKind(t, result, capability.CapabilityKindModifyCode)
|
||||
}
|
||||
|
||||
func TestPlanningRiskyUnverifiedAssertionRequiresVerification(t *testing.T) {
|
||||
result := planForTest(t, "На сервере 300 ГБ свободно, установи большую модель.", "ASSERTION", epistemics.VerificationUnverified)
|
||||
requireKinds(t, result, capability.CapabilityKindVerifyFact)
|
||||
found := false
|
||||
for _, requirement := range result.Candidate.Requirements {
|
||||
if requirement.CapabilityKind == capability.CapabilityKindVerifyFact && requirement.EpistemicPrecondition == epistemics.VerificationUnverified {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected unverified verification precondition: %#v", result.Candidate.Requirements)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanningSendEmailPreservesAuthorityBoundary(t *testing.T) {
|
||||
result := planForTest(t, "Отправь Ивану письмо.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
requireKinds(t, result, capability.CapabilityKindSendEmail)
|
||||
if result.Goal.State == work.GoalCompleted || !strings.Contains(result.Reply, "требует отдельной capability/authority") {
|
||||
t.Fatalf("planning must not claim execution/completion: goal=%#v reply=%q", result.Goal, result.Reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanningFallbackPersistsWithoutCognition(t *testing.T) {
|
||||
result := planForTest(t, "Сделай что-нибудь полезное.", "DIRECTIVE", epistemics.VerificationUnknown)
|
||||
if result.Goal.ID == "" || result.Candidate.ID == "" || result.Plan.ID == "" {
|
||||
t.Fatalf("expected persisted planning identities: %#v", result)
|
||||
}
|
||||
if result.Candidate.Source != "deterministic-planner-v1" {
|
||||
t.Fatalf("expected deterministic fallback source: %s", result.Candidate.Source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathSimpleAnswerDoesNotRequireDurableGoalWorkPlan(t *testing.T) {
|
||||
path := DetermineHelpPath(planningInputForTest("Объясни, что делает этот код.", "QUESTION", epistemics.VerificationNotApplicable))
|
||||
if path.Kind != work.HelpPathCognition || path.NeedsDurableGoal || path.NeedsDurableWork || path.NeedsDurablePlan {
|
||||
t.Fatalf("simple help must not require durable planning: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathReminderCanUseDirectCapabilityWithoutWorkflow(t *testing.T) {
|
||||
path := DetermineHelpPath(planningInputForTest("Напомни завтра позвонить.", "DIRECTIVE", epistemics.VerificationNotApplicable))
|
||||
if path.Kind != work.HelpPathDirectCapability || path.NeedsDurableWork || len(path.SuggestedCapabilityKinds) != 1 || path.SuggestedCapabilityKinds[0] != capability.CapabilityKindScheduleReminder {
|
||||
t.Fatalf("reminder should be direct scheduling capability path: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathComplexWorkMaterializesDurablePlanning(t *testing.T) {
|
||||
path := DetermineHelpPath(planningInputForTest("Разберись, почему УПО тормозит.", "REQUEST_TO_INVESTIGATE", epistemics.VerificationNotApplicable))
|
||||
if path.Kind != work.HelpPathDurableWork || !path.NeedsDurableGoal || !path.NeedsDurableWork || !path.NeedsDurablePlan {
|
||||
t.Fatalf("complex investigation should need durable planning: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathCapabilityGapDoesNotBecomeCodexTask(t *testing.T) {
|
||||
path := DetermineHelpPath(planningInputForTest("Подключи интеграцию с неизвестным сервисом, которого сейчас нет.", "DIRECTIVE", epistemics.VerificationNotApplicable))
|
||||
if path.Kind != work.HelpPathCapabilityGap || !path.CapabilityGap {
|
||||
t.Fatalf("expected capability gap path: %#v", path)
|
||||
}
|
||||
if strings.Contains(strings.ToLower(path.Reply), "codex") {
|
||||
t.Fatalf("capability gap must not be represented as Codex task: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func planForTest(t *testing.T, message string, act string, verification epistemics.VerificationState) work.PlanningResult {
|
||||
t.Helper()
|
||||
result, err := BuildPlanningResult(planningInputForTest(message, act, verification), time.Unix(100, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ValidatePlanCandidate(result.Candidate); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func planningInputForTest(message string, act string, verification epistemics.VerificationState) work.PlanningInput {
|
||||
return work.PlanningInput{
|
||||
ConversationID: "conversation-1",
|
||||
UtteranceID: "utterance-1",
|
||||
IntentID: "intent-1",
|
||||
ActorID: identity.ActorID("actor-1"),
|
||||
PrincipalID: identity.PrincipalID("principal-1"),
|
||||
ProjectID: work.ProjectRef("upo"),
|
||||
ProjectDisplayName: "УПО",
|
||||
ProjectProvenance: work.ProjectProvenanceRef("EXPLICIT"),
|
||||
Message: message,
|
||||
SpeechAct: work.SpeechActRef(act),
|
||||
AssertionVerification: verification,
|
||||
ReceivedAtUnixMillis: time.Unix(100, 0).UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
func requireKinds(t *testing.T, result work.PlanningResult, kinds ...capability.CapabilityKind) {
|
||||
t.Helper()
|
||||
actual := map[capability.CapabilityKind]bool{}
|
||||
for _, requirement := range result.Candidate.Requirements {
|
||||
actual[requirement.CapabilityKind] = true
|
||||
}
|
||||
for _, kind := range kinds {
|
||||
if !actual[kind] {
|
||||
t.Fatalf("missing capability kind %s in %#v", kind, result.Candidate.Requirements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requireNoKind(t *testing.T, result work.PlanningResult, kind capability.CapabilityKind) {
|
||||
t.Helper()
|
||||
for _, requirement := range result.Candidate.Requirements {
|
||||
if requirement.CapabilityKind == kind {
|
||||
t.Fatalf("unexpected capability kind %s in %#v", kind, result.Candidate.Requirements)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user